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 {
// 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,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() {
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_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,
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_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);
}
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) {
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() {
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?
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({
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'),
// 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)
}

View File

@ -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;
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;
}
old_cells = this.stored_level.linear_cells;
this.stored_level.linear_cells = new_cells;
}
else {
if (! this.selection.is_empty) {
// 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;
this.selection._rearrange_cells(old_w, convert_coords, upgrade_tile);
return;
}
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];
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) {
// 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();
}
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.
_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.