Fix transforming selection + add more transforms

This commit is contained in:
Eevee (Evelyn Woods) 2024-04-17 02:24:06 -06:00
parent ed5f76221b
commit 2439048f59
2 changed files with 210 additions and 111 deletions

View File

@ -157,11 +157,12 @@ export class Selection {
} }
else { else {
// Just recreate it from scratch to avoid mixing old and new properties // 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( this.bbox = new DOMRect(
Math.min(this.bbox.x, rect.x), new_x, new_y,
Math.min(this.bbox.y, rect.y), Math.max(this.bbox.right, rect.right) - new_x,
Math.max(this.bbox.right, rect.right) - this.bbox.x, Math.max(this.bbox.bottom, rect.bottom) - new_y);
Math.max(this.bbox.bottom, rect.bottom) - this.bbox.y);
} }
this._update_outline(); this._update_outline();
@ -398,28 +399,18 @@ export class Selection {
this.ring_element.classList.remove('--visible'); 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 // Convert this selection into a floating selection, plucking all the selected cells from the
// level and replacing them with blank cells. // 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");
return;
}
let floated_cells = new Map; let floated_cells = new Map;
let stored_level = this.editor.stored_level; 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]; let cell = stored_level.linear_cells[n];
if (copy) { if (copy) {
floated_cells.set(n, cell.map(tile => tile ? {...tile} : null)); floated_cells.set(n, cell.map(tile => tile ? {...tile} : null));
@ -441,38 +432,33 @@ 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() { _init_floated_canvas() {
let tileset = this.editor.renderer.tileset; let tileset = this.editor.renderer.tileset;
this.floated_canvas = mk('canvas', { if (! this.floated_canvas) {
width: this.bbox.width * tileset.size_x, this.floated_canvas = mk('canvas');
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,
});
} }
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', { this.floated_element = mk_svg('g', mk_svg('foreignObject', {
x: 0, x: 0,
y: 0, y: 0,
width: this.floated_canvas.width,
height: this.floated_canvas.height,
transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`, transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`,
}, this.floated_canvas)); }, this.floated_canvas));
this.floated_element.setAttribute('transform', `translate(${this.bbox.x} ${this.bbox.y})`);
// This goes first, so the selection ring still appears on top // This goes first, so the selection ring still appears on top
this.selection_group.prepend(this.floated_element); 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})`);
}
stamp_float(copy = false) { stamp_float(copy = false) {
if (! this.floated_element) if (! this.floated_element)
@ -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() { _delete_floating() {
this.selection_group.removeAttribute('transform'); this.selection_group.removeAttribute('transform');
this.ring_element.classList.remove('--floating'); this.ring_element.classList.remove('--floating');
@ -561,20 +606,22 @@ export class Selection {
if (! this.floated_canvas) if (! this.floated_canvas)
return; return;
// FIXME uhoh, how do i actually do this? we have no renderer of our own, we have a let ctx = this.floated_canvas.getContext('2d');
// separate canvas, and all the renderer stuff expects to get ahold of a level. i guess for (let n of this.cells) {
// refactor it to draw a block of cells? let [x, y] = this.editor.stored_level.scalar_to_coords(n);
this.editor.renderer.draw_static_generic({ this.editor.renderer.draw_static_generic({
x0: 0, y0: 0, // Incredibly stupid hack for just drawing one cell
x1: this.rect.width, y1: this.rect.height, x0: 0, x1: 0,
// FIXME this is now a set, not a flat list y0: 0, y1: 0,
cells: this.floated_cells, width: 1,
width: this.rect.width, cells: [this.floated_cells.get(n)],
ctx: this.floated_canvas.getContext('2d'), 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) // TODO make more stuff respect this (more things should go through Editor for undo reasons anyway)
} }

View File

@ -455,18 +455,27 @@ export class Editor extends PrimaryView {
this.redo(); this.redo();
}); });
let edit_items = [ let edit_items = [
["Rotate CCW", () => { ["Rotate left", () => {
this.rotate_level_left(); this.rotate_level_left();
}], }],
["Rotate CW", () => { ["Rotate right", () => {
this.rotate_level_right(); this.rotate_level_right();
}], }],
["Mirror", () => { ["Rotate 180°", () => {
this.rotate_level_right();
}],
["Mirror horizontally", () => {
this.mirror_level(); this.mirror_level();
}], }],
["Flip", () => { ["Flip vertically", () => {
this.flip_level(); 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.edit_menu = new MenuOverlay(
this.conductor, this.conductor,
@ -1324,12 +1333,29 @@ export class Editor extends PrimaryView {
return this._transform_tile( return this._transform_tile(
tile, include_faux_adjustments ? 'adjust_forward' : null, 'rotate_right', 'right'); 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) { mirror_tile(tile) {
return this._transform_tile(tile, null, 'mirror', 'mirrored'); return this._transform_tile(tile, null, 'mirror', 'mirrored');
} }
flip_tile(tile) { flip_tile(tile) {
return this._transform_tile(tile, null, 'flip', 'flipped'); 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() { rotate_palette_left() {
this.palette_rotation_index += 1; 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 // (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 // completely reversible, and undo is done by performing the opposite transform rather than
// reloading a copy of a previous state.) // reloading a copy of a previous state.)
_rearrange_cells(swap_dimensions, downgrade_coords, upgrade_tile) { _rearrange_cells(swap_dimensions, convert_coords, upgrade_tile) {
let old_cells, old_w;
let w, h;
let new_cells = []; let new_cells = [];
if (this.selection.is_empty) { let w = this.stored_level.size_x;
// Do it to the whole level let h = this.stored_level.size_y;
w = this.stored_level.size_x; let old_w = w;
h = this.stored_level.size_y; let old_h = h;
old_w = w;
if (swap_dimensions) { if (swap_dimensions) {
[w, h] = [h, w]; [w, h] = [h, w];
this.stored_level.size_x = w; this.stored_level.size_x = w;
this.stored_level.size_y = h; this.stored_level.size_y = h;
} }
old_cells = this.stored_level.linear_cells;
this.stored_level.linear_cells = new_cells; if (! this.selection.is_empty) {
}
else {
// Do it to the selection // Do it to the selection
w = this.selection.rect.width; this.selection._rearrange_cells(old_w, convert_coords, upgrade_tile);
h = this.selection.rect.height; return;
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;
} }
for (let y = 0; y < h; y++) { let old_cells = this.stored_level.linear_cells;
for (let x = 0; x < w; x++) { for (let y = 0; y < old_h; y++) {
let [old_x, old_y] = downgrade_coords(x, y, w, h); for (let x = 0; x < old_w; x++) {
let cell = old_cells[old_y * old_w + old_x]; let [x2, y2] = convert_coords(x, y, w, h);
let cell = old_cells[y * old_w + x];
for (let tile of cell) { for (let tile of cell) {
if (tile) { if (tile) {
upgrade_tile(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() { rotate_level_right() {
@ -1622,6 +1641,11 @@ export class Editor extends PrimaryView {
); );
} }
rotate_level_180() { rotate_level_180() {
this._do_transform(
false,
() => this._rotate_level_180(),
() => this._rotate_level_180(),
);
} }
mirror_level() { mirror_level() {
this._do_transform( this._do_transform(
@ -1637,6 +1661,20 @@ export class Editor extends PrimaryView {
() => this._flip_level(), () => 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) { _do_transform(affects_size, redo, undo) {
// FIXME apply transform to connections if appropriate, somehow, ?? i don't even know how // FIXME apply transform to connections if appropriate, somehow, ?? i don't even know how
// those interact with floating selection yet :S // those interact with floating selection yet :S
@ -1656,36 +1694,36 @@ export class Editor extends PrimaryView {
this.commit_undo(); this.commit_undo();
} }
_post_transform_cleanup(affects_size) { _post_transform_cleanup(affects_size) {
if (this.selection.is_empty) { // The selection takes care of redrawing itself
if (affects_size) { 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(); this.update_after_size_change();
} }
else {
this.redraw_entire_level();
}
}
else {
// FIXME what if it affects size?
this.selection.redraw();
}
}
// TODO mirror diagonally?
// Internal-use versions of the above. These DO NOT create undo entries. // Internal-use versions of the above. These DO NOT create undo entries.
_rotate_level_left() { _rotate_level_left() {
this._rearrange_cells( this._rearrange_cells(
true, true,
(x, y, w, h) => [h - 1 - y, x], (x, y, w, h) => [y, w - 1 - x],
tile => this.rotate_tile_left(tile, false), tile => this.rotate_tile_left(tile, false),
); );
} }
_rotate_level_right() { _rotate_level_right() {
this._rearrange_cells( this._rearrange_cells(
true, true,
(x, y, w, h) => [y, w - 1 - x], (x, y, w, h) => [h - 1 - y, x],
tile => this.rotate_tile_right(tile, false), 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() { _mirror_level() {
this._rearrange_cells( this._rearrange_cells(
false, false,
@ -1700,6 +1738,20 @@ export class Editor extends PrimaryView {
tile => this.flip_tile(tile), 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 // Create a connection between two cells and update the UI accordingly. If dest is null or
// undefined, delete any existing connection instead. // undefined, delete any existing connection instead.