291 lines
9.1 KiB
JavaScript
291 lines
9.1 KiB
JavaScript
// Small helper classes used by the editor, often with their own UI for the SVG overlay.
|
|
import { mk, mk_svg } from '../util.js';
|
|
|
|
export class SVGConnection {
|
|
constructor(sx, sy, dx, dy) {
|
|
this.source = mk_svg('circle.-source', {r: 0.5});
|
|
this.line = mk_svg('line.-arrow', {});
|
|
this.dest = mk_svg('rect.-dest', {width: 1, height: 1});
|
|
this.element = mk_svg('g.overlay-connection', this.source, this.line, this.dest);
|
|
this.set_source(sx, sy);
|
|
this.set_dest(dx, dy);
|
|
}
|
|
|
|
set_source(sx, sy) {
|
|
this.sx = sx;
|
|
this.sy = sy;
|
|
this.source.setAttribute('cx', sx + 0.5);
|
|
this.source.setAttribute('cy', sy + 0.5);
|
|
this.line.setAttribute('x1', sx + 0.5);
|
|
this.line.setAttribute('y1', sy + 0.5);
|
|
}
|
|
|
|
set_dest(dx, dy) {
|
|
this.dx = dx;
|
|
this.dy = dy;
|
|
this.line.setAttribute('x2', dx + 0.5);
|
|
this.line.setAttribute('y2', dy + 0.5);
|
|
this.dest.setAttribute('x', dx);
|
|
this.dest.setAttribute('y', dy);
|
|
}
|
|
}
|
|
|
|
|
|
// TODO probably need to combine this with Selection somehow since it IS one, just not committed yet
|
|
export class PendingSelection {
|
|
constructor(owner) {
|
|
this.owner = owner;
|
|
this.element = mk_svg('rect.overlay-pending-selection');
|
|
this.owner.svg_group.append(this.element);
|
|
this.rect = null;
|
|
}
|
|
|
|
set_extrema(x0, y0, x1, y1) {
|
|
this.rect = new DOMRect(Math.min(x0, x1), Math.min(y0, y1), Math.abs(x0 - x1) + 1, Math.abs(y0 - y1) + 1);
|
|
this.element.classList.add('--visible');
|
|
this.element.setAttribute('x', this.rect.x);
|
|
this.element.setAttribute('y', this.rect.y);
|
|
this.element.setAttribute('width', this.rect.width);
|
|
this.element.setAttribute('height', this.rect.height);
|
|
}
|
|
|
|
commit() {
|
|
this.owner.set_from_rect(this.rect);
|
|
this.element.remove();
|
|
}
|
|
|
|
discard() {
|
|
this.element.remove();
|
|
}
|
|
}
|
|
|
|
export class Selection {
|
|
constructor(editor) {
|
|
this.editor = editor;
|
|
|
|
this.svg_group = mk_svg('g');
|
|
this.editor.svg_overlay.append(this.svg_group);
|
|
|
|
this.rect = null;
|
|
this.element = mk_svg('rect.overlay-selection.overlay-transient');
|
|
this.svg_group.append(this.element);
|
|
|
|
this.floated_cells = null;
|
|
this.floated_element = null;
|
|
this.floated_canvas = null;
|
|
}
|
|
|
|
get is_empty() {
|
|
return this.rect === null;
|
|
}
|
|
|
|
get is_floating() {
|
|
return !! this.floated_cells;
|
|
}
|
|
|
|
contains(x, y) {
|
|
// Empty selection means everything is selected?
|
|
if (this.rect === null)
|
|
return true;
|
|
|
|
return this.rect.left <= x && x < this.rect.right && this.rect.top <= y && y < this.rect.bottom;
|
|
}
|
|
|
|
create_pending() {
|
|
return new PendingSelection(this);
|
|
}
|
|
|
|
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.element.classList.add('--visible');
|
|
this.element.setAttribute('x', this.rect.x);
|
|
this.element.setAttribute('y', this.rect.y);
|
|
this.element.setAttribute('width', this.rect.width);
|
|
this.element.setAttribute('height', this.rect.height);
|
|
|
|
if (this.floated_element) {
|
|
let tileset = this.editor.renderer.tileset;
|
|
this.floated_canvas.width = rect.width * tileset.size_x;
|
|
this.floated_canvas.height = rect.height * tileset.size_y;
|
|
let foreign_obj = this.floated_element.querySelector('foreignObject');
|
|
foreign_obj.setAttribute('width', this.floated_canvas.width);
|
|
foreign_obj.setAttribute('height', this.floated_canvas.height);
|
|
}
|
|
}
|
|
|
|
move_by(dx, dy) {
|
|
if (! this.rect)
|
|
return;
|
|
|
|
this.rect.x += dx;
|
|
this.rect.y += dy;
|
|
this.element.setAttribute('x', this.rect.x);
|
|
this.element.setAttribute('y', this.rect.y);
|
|
|
|
if (! this.floated_element)
|
|
return;
|
|
|
|
let bbox = this.rect;
|
|
this.floated_element.setAttribute('transform', `translate(${bbox.x} ${bbox.y})`);
|
|
}
|
|
|
|
clear() {
|
|
let rect = this.rect;
|
|
if (! rect)
|
|
return;
|
|
|
|
this.editor._do(
|
|
() => this._clear(),
|
|
() => this._set_from_rect(rect),
|
|
false,
|
|
);
|
|
}
|
|
|
|
_clear() {
|
|
this.rect = null;
|
|
this.element.classList.remove('--visible');
|
|
}
|
|
|
|
*iter_coords() {
|
|
if (! this.rect)
|
|
return;
|
|
|
|
let stored_level = this.editor.stored_level;
|
|
for (let y = this.rect.top; y < this.rect.bottom; y++) {
|
|
for (let x = this.rect.left; x < this.rect.right; x++) {
|
|
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) {
|
|
if (this.floated_cells)
|
|
console.error("Trying to float a selection that's already floating");
|
|
|
|
let floated_cells = [];
|
|
let tileset = this.editor.renderer.tileset;
|
|
let stored_level = this.editor.stored_level;
|
|
let bbox = this.rect;
|
|
let canvas = mk('canvas', {width: bbox.width * tileset.size_x, height: bbox.height * tileset.size_y});
|
|
let ctx = canvas.getContext('2d');
|
|
ctx.drawImage(
|
|
this.editor.renderer.canvas,
|
|
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);
|
|
for (let [x, y, n] of this.iter_coords()) {
|
|
let cell = stored_level.linear_cells[n];
|
|
if (copy) {
|
|
floated_cells.push(cell.map(tile => tile ? {...tile} : null));
|
|
}
|
|
else {
|
|
floated_cells.push(cell);
|
|
this.editor.replace_cell(cell, this.editor.make_blank_cell(x, y));
|
|
}
|
|
}
|
|
let floated_element = mk_svg('g', mk_svg('foreignObject', {
|
|
x: 0, y: 0,
|
|
width: canvas.width, height: canvas.height,
|
|
transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`,
|
|
}, canvas));
|
|
floated_element.setAttribute('transform', `translate(${bbox.x} ${bbox.y})`);
|
|
|
|
// FIXME far more memory efficient to recreate the canvas in the redo, rather than hold onto
|
|
// it forever
|
|
this.editor._do(
|
|
() => {
|
|
this.floated_canvas = canvas;
|
|
this.floated_element = floated_element;
|
|
this.floated_cells = floated_cells;
|
|
this.svg_group.append(floated_element);
|
|
},
|
|
() => this._defloat(),
|
|
);
|
|
}
|
|
|
|
stamp_float(copy = false) {
|
|
if (! this.floated_element)
|
|
return;
|
|
|
|
let stored_level = this.editor.stored_level;
|
|
let i = 0;
|
|
for (let [x, y, n] of this.iter_coords()) {
|
|
let cell = this.floated_cells[i];
|
|
if (copy) {
|
|
cell = cell.map(tile => tile ? {...tile} : null);
|
|
}
|
|
cell.x = x;
|
|
cell.y = y;
|
|
this.editor.replace_cell(stored_level.linear_cells[n], cell);
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
defloat() {
|
|
if (! this.floated_element)
|
|
return;
|
|
|
|
this.stamp_float();
|
|
|
|
let element = this.floated_element;
|
|
let canvas = this.floated_canvas;
|
|
let cells = this.floated_cells;
|
|
this.editor._do(
|
|
() => this._defloat(),
|
|
() => {
|
|
this.floated_cells = cells;
|
|
this.floated_canvas = canvas;
|
|
this.floated_element = element;
|
|
this.svg_group.append(element);
|
|
},
|
|
false,
|
|
);
|
|
}
|
|
|
|
_defloat() {
|
|
this.floated_element.remove();
|
|
this.floated_element = null;
|
|
this.floated_canvas = null;
|
|
this.floated_cells = null;
|
|
}
|
|
|
|
// Redraw the selection canvas from scratch
|
|
redraw() {
|
|
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,
|
|
cells: this.floated_cells,
|
|
width: this.rect.width,
|
|
ctx: this.floated_canvas.getContext('2d'),
|
|
});
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|