Improve the editor's selection tool (slightly WIP)
It now supports arbitrary regions! The tool itself still makes rectangles, but you can shift-drag to add to the selection. It also distinguishes visually between a floating selection and not, is more easily visible against certain tile backgrounds and at small zoom levels, and, I don't know, probably some other stuff.
This commit is contained in:
parent
48482b2a65
commit
7e0c1b0337
@ -1,5 +1,6 @@
|
|||||||
// Small helper classes used by the editor, often with their own UI for the SVG overlay.
|
// Small helper classes used by the editor, often with their own UI for the SVG overlay.
|
||||||
import { mk, mk_svg } from '../util.js';
|
import { DIRECTIONS } from '../defs.js';
|
||||||
|
import { BitVector, mk, mk_svg } from '../util.js';
|
||||||
|
|
||||||
export class SVGConnection {
|
export class SVGConnection {
|
||||||
constructor(sx, sy, dx, dy) {
|
constructor(sx, sy, dx, dy) {
|
||||||
@ -31,12 +32,12 @@ export class SVGConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO probably need to combine this with Selection somehow since it IS one, just not committed yet
|
export class PendingRectangularSelection {
|
||||||
export class PendingSelection {
|
|
||||||
constructor(owner) {
|
constructor(owner) {
|
||||||
this.owner = owner;
|
this.owner = owner;
|
||||||
this.element = mk_svg('rect.overlay-pending-selection');
|
this.element = mk_svg('rect.overlay-pending-selection');
|
||||||
this.owner.svg_group.append(this.element);
|
this.size_text = mk_svg('text.overlay-edit-tip');
|
||||||
|
this.owner.svg_group.append(this.element, this.size_text);
|
||||||
this.rect = null;
|
this.rect = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,15 +48,20 @@ export class PendingSelection {
|
|||||||
this.element.setAttribute('y', this.rect.y);
|
this.element.setAttribute('y', this.rect.y);
|
||||||
this.element.setAttribute('width', this.rect.width);
|
this.element.setAttribute('width', this.rect.width);
|
||||||
this.element.setAttribute('height', this.rect.height);
|
this.element.setAttribute('height', this.rect.height);
|
||||||
|
this.size_text.textContent = `${this.rect.width} × ${this.rect.height}`;
|
||||||
|
this.size_text.setAttribute('x', this.rect.x + this.rect.width / 2);
|
||||||
|
this.size_text.setAttribute('y', this.rect.y + this.rect.height / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
commit() {
|
commit() {
|
||||||
this.owner.set_from_rect(this.rect);
|
this.owner.add_rect(this.rect);
|
||||||
this.element.remove();
|
this.element.remove();
|
||||||
|
this.size_text.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
this.element.remove();
|
this.element.remove();
|
||||||
|
this.size_text.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,113 +71,315 @@ export class Selection {
|
|||||||
|
|
||||||
this.svg_group = mk_svg('g');
|
this.svg_group = mk_svg('g');
|
||||||
this.editor.svg_overlay.append(this.svg_group);
|
this.editor.svg_overlay.append(this.svg_group);
|
||||||
|
// Used for the floating preview and selection rings, which should all move together
|
||||||
|
this.selection_group = mk_svg('g');
|
||||||
|
this.svg_group.append(this.selection_group);
|
||||||
|
|
||||||
this.rect = null;
|
// Note that this is a set of the ORIGINAL coordinates of the selected cells. Moving a
|
||||||
this.element = mk_svg('rect.overlay-selection.overlay-transient');
|
// floated selection doesn't change this; instead it updates floated_offset
|
||||||
this.svg_group.append(this.element);
|
this.cells = new Set;
|
||||||
|
this.bbox = null;
|
||||||
|
// I want a black-and-white outline ring so it shows against any background, but the only
|
||||||
|
// way to do that in SVG is apparently to just duplicate the path
|
||||||
|
this.ring_bg_element = mk_svg('path.overlay-selection-background.overlay-transient');
|
||||||
|
this.ring_element = mk_svg('path.overlay-selection.overlay-transient');
|
||||||
|
this.selection_group.append(this.ring_bg_element, this.ring_element);
|
||||||
|
|
||||||
this.floated_cells = null;
|
this.floated_cells = null;
|
||||||
this.floated_element = null;
|
this.floated_element = null;
|
||||||
this.floated_canvas = null;
|
this.floated_canvas = null;
|
||||||
|
this.floated_offset = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get is_empty() {
|
get is_empty() {
|
||||||
return this.rect === null;
|
return this.cells.size === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get is_floating() {
|
get is_floating() {
|
||||||
return !! this.floated_cells;
|
return !! this.floated_cells;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get has_moved() {
|
||||||
|
return !! (this.floated_offset && (this.floated_offset[0] || this.floated_offset[0]));
|
||||||
|
}
|
||||||
|
|
||||||
contains(x, y) {
|
contains(x, y) {
|
||||||
// Empty selection means everything is selected?
|
// Empty selection means everything is selected?
|
||||||
if (this.rect === null)
|
if (this.is_empty)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return this.rect.left <= x && x < this.rect.right && this.rect.top <= y && y < this.rect.bottom;
|
if (this.floated_offset) {
|
||||||
|
x -= this.floated_offset[0];
|
||||||
|
y -= this.floated_offset[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cells.has(this.editor.stored_level.coords_to_scalar(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
create_pending() {
|
create_pending() {
|
||||||
return new PendingSelection(this);
|
return new PendingRectangularSelection(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_from_rect(rect) {
|
add_rect(rect) {
|
||||||
let old_rect = this.rect;
|
let old_cells = this.cells;
|
||||||
|
// TODO would be nice to only store the difference between the old/new sets of cells?
|
||||||
|
this.cells = new Set(this.cells);
|
||||||
|
|
||||||
this.editor._do(
|
this.editor._do(
|
||||||
() => this._set_from_rect(rect),
|
() => this._add_rect(rect),
|
||||||
() => {
|
() => {
|
||||||
if (old_rect) {
|
this._set_from_set(old_cells);
|
||||||
this._set_from_rect(old_rect);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this._clear();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_set_from_rect(rect) {
|
_add_rect(rect) {
|
||||||
this.rect = rect;
|
let stored_level = this.editor.stored_level;
|
||||||
this.element.classList.add('--visible');
|
for (let y = rect.top; y < rect.bottom; y++) {
|
||||||
this.element.setAttribute('x', this.rect.x);
|
for (let x = rect.left; x < rect.right; x++) {
|
||||||
this.element.setAttribute('y', this.rect.y);
|
this.cells.add(stored_level.coords_to_scalar(x, y));
|
||||||
this.element.setAttribute('width', this.rect.width);
|
}
|
||||||
this.element.setAttribute('height', this.rect.height);
|
}
|
||||||
|
|
||||||
|
if (! this.bbox) {
|
||||||
|
this.bbox = rect;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Just recreate it from scratch to avoid mixing old and new properties
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX wait what the hell is this doing here? why would we set_from_rect while floating, vs
|
||||||
|
// stamping it first?
|
||||||
if (this.floated_element) {
|
if (this.floated_element) {
|
||||||
|
console.log("what the hell is this doing here");
|
||||||
let tileset = this.editor.renderer.tileset;
|
let tileset = this.editor.renderer.tileset;
|
||||||
this.floated_canvas.width = rect.width * tileset.size_x;
|
this.floated_canvas.width = this.bbox.width * tileset.size_x;
|
||||||
this.floated_canvas.height = rect.height * tileset.size_y;
|
this.floated_canvas.height = this.bbox.height * tileset.size_y;
|
||||||
let foreign_obj = this.floated_element.querySelector('foreignObject');
|
let foreign_obj = this.floated_element.querySelector('foreignObject');
|
||||||
foreign_obj.setAttribute('width', this.floated_canvas.width);
|
foreign_obj.setAttribute('width', this.floated_canvas.width);
|
||||||
foreign_obj.setAttribute('height', this.floated_canvas.height);
|
foreign_obj.setAttribute('height', this.floated_canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._update_outline();
|
||||||
|
}
|
||||||
|
|
||||||
|
_set_from_set(cells) {
|
||||||
|
this.cells = cells;
|
||||||
|
|
||||||
|
// Recompute bbox
|
||||||
|
if (cells.size === 0) {
|
||||||
|
this.bbox = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let min_x = null;
|
||||||
|
let min_y = null;
|
||||||
|
let max_x = null;
|
||||||
|
let max_y = null;
|
||||||
|
for (let n of cells) {
|
||||||
|
let [x, y] = this.editor.stored_level.scalar_to_coords(n);
|
||||||
|
if (min_x === null) {
|
||||||
|
min_x = x;
|
||||||
|
min_y = y;
|
||||||
|
max_x = x;
|
||||||
|
max_y = y;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
min_x = Math.min(min_x, x);
|
||||||
|
max_x = Math.max(max_x, x);
|
||||||
|
min_y = Math.min(min_y, y);
|
||||||
|
max_y = Math.max(max_y, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bbox = new DOMRect(min_x, min_y, max_x - min_x + 1, max_y - min_y + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX ??? if (this.floated_element) {
|
||||||
|
|
||||||
|
this._update_outline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faster internal version of contains() that ignores the floating offset
|
||||||
|
_contains(x, y) {
|
||||||
|
let stored_level = this.editor.stored_level;
|
||||||
|
return stored_level.is_point_within_bounds(x, y) &&
|
||||||
|
this.cells.has(stored_level.coords_to_scalar(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
_update_outline() {
|
||||||
|
if (this.is_empty) {
|
||||||
|
this.ring_bg_element.classList.remove('--visible');
|
||||||
|
this.ring_element.classList.remove('--visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the borders between cells to an SVG path.
|
||||||
|
// I don't know an especially clever way to do this so I guess I'll just make it up. The
|
||||||
|
// basic idea is to start with the top-left highlighted cell, start tracing from its top
|
||||||
|
// left corner towards the right (which must be a border, because this is the top left
|
||||||
|
// selected cell, so nothing above it is selected), then just keep going until we get back
|
||||||
|
// to where we started. Then we... repeat.
|
||||||
|
// But how do we repeat? My tiny insight is that every island (including holes) must cross
|
||||||
|
// the top of at least one cell; the only alternatives are for it to be zero width or only
|
||||||
|
// exist in the bottom row, and either way that makes it zero area, which isn't allowed. So
|
||||||
|
// we only have to track and check the top edges of cells, and run through every cell in the
|
||||||
|
// grid in order, stopping to draw a new outline when we find a cell whose top edge we
|
||||||
|
// haven't yet examined (and whose top edge is in fact a border). We unfortunately need to
|
||||||
|
// examine cells outside the selection, too, so that we can identify holes. But we can
|
||||||
|
// restrict all of this to within the bbox, so that's nice.
|
||||||
|
// Also, note that we concern ourselves with /grid points/ here, which are intersections of
|
||||||
|
// grid lines, whereas the grid cells are the spaces between grid lines.
|
||||||
|
// TODO might be more efficient to store a list of horizontal spans instead of just cells,
|
||||||
|
// but of course this would be more complicated
|
||||||
|
let seen_tops = new BitVector(this.bbox.width * this.bbox.height);
|
||||||
|
// In clockwise order for ease of rotation, starting with right
|
||||||
|
let directions = [
|
||||||
|
[1, 0],
|
||||||
|
[0, 1],
|
||||||
|
[-1, 0],
|
||||||
|
[0, -1],
|
||||||
|
];
|
||||||
|
|
||||||
|
let segments = [];
|
||||||
|
for (let y = this.bbox.top; y < this.bbox.bottom; y++) {
|
||||||
|
for (let x = this.bbox.left; x < this.bbox.right; x++) {
|
||||||
|
if (seen_tops.get((x - this.bbox.left) + this.bbox.width * (y - this.bbox.top)))
|
||||||
|
// Already traced
|
||||||
|
continue;
|
||||||
|
if (this._contains(x, y) === this._contains(x, y - 1))
|
||||||
|
// Not a top border
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Start a new segment!
|
||||||
|
let gx = x;
|
||||||
|
let gy = y;
|
||||||
|
let dx = 1;
|
||||||
|
let dy = 0;
|
||||||
|
let d = 0;
|
||||||
|
|
||||||
|
let segment = [];
|
||||||
|
segments.push(segment);
|
||||||
|
segment.push([gx, gy]);
|
||||||
|
while (segment.length < 100) {
|
||||||
|
// At this point we know that d is a valid direction and we've just traced it
|
||||||
|
if (dx === 1) {
|
||||||
|
seen_tops.set((gx - this.bbox.left) + this.bbox.width * (gy - this.bbox.top));
|
||||||
|
}
|
||||||
|
else if (dx === -1) {
|
||||||
|
seen_tops.set((gx - 1 - this.bbox.left) + this.bbox.width * (gy - this.bbox.top));
|
||||||
|
}
|
||||||
|
gx += dx;
|
||||||
|
gy += dy;
|
||||||
|
|
||||||
|
if (gx === x && gy === y)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Now we're at a new point, so search for the next direction, starting from the left
|
||||||
|
// Again, this is clockwise order (tr, br, bl, tl), arranged so that direction D goes
|
||||||
|
// between cells D and D + 1
|
||||||
|
let neighbors = [
|
||||||
|
this._contains(gx, gy - 1),
|
||||||
|
this._contains(gx, gy),
|
||||||
|
this._contains(gx - 1, gy),
|
||||||
|
this._contains(gx - 1, gy - 1),
|
||||||
|
];
|
||||||
|
let new_d = (d + 1) % 4;
|
||||||
|
for (let i = 3; i <= 4; i++) {
|
||||||
|
let sd = (d + i) % 4;
|
||||||
|
if (neighbors[sd] !== neighbors[(sd + 1) % 4]) {
|
||||||
|
new_d = sd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (new_d !== d) {
|
||||||
|
// We're turning, so this is a new point
|
||||||
|
segment.push([gx, gy]);
|
||||||
|
d = new_d;
|
||||||
|
[dx, dy] = directions[d];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO do it again for the next region... but how do i tell where the next region is?
|
||||||
|
|
||||||
|
let pathdata = [];
|
||||||
|
for (let subpath of segments) {
|
||||||
|
let first = true;
|
||||||
|
for (let [x, y] of subpath) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
pathdata.push(`M${x},${y}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pathdata.push(`L${x},${y}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pathdata.push('z');
|
||||||
|
}
|
||||||
|
this.ring_bg_element.classList.add('--visible');
|
||||||
|
this.ring_bg_element.setAttribute('d', pathdata.join(' '));
|
||||||
|
this.ring_element.classList.add('--visible');
|
||||||
|
this.ring_element.setAttribute('d', pathdata.join(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
move_by(dx, dy) {
|
move_by(dx, dy) {
|
||||||
if (! this.rect)
|
if (this.is_empty)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.rect.x += dx;
|
if (! this.floated_cells) {
|
||||||
this.rect.y += dy;
|
console.error("Can't move a non-floating selection");
|
||||||
this.element.setAttribute('x', this.rect.x);
|
|
||||||
this.element.setAttribute('y', this.rect.y);
|
|
||||||
|
|
||||||
if (! this.floated_element)
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let bbox = this.rect;
|
this.floated_offset[0] += dx;
|
||||||
this.floated_element.setAttribute('transform', `translate(${bbox.x} ${bbox.y})`);
|
this.floated_offset[1] += dy;
|
||||||
|
this._update_floating_transform();
|
||||||
|
}
|
||||||
|
|
||||||
|
_update_floating_transform() {
|
||||||
|
let transform = `translate(${this.floated_offset[0]} ${this.floated_offset[1]})`;
|
||||||
|
this.selection_group.setAttribute('transform', transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
let rect = this.rect;
|
// FIXME behavior when floating is undefined
|
||||||
if (! rect)
|
if (this.is_empty)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
let old_cells = this.cells;
|
||||||
|
|
||||||
this.editor._do(
|
this.editor._do(
|
||||||
() => this._clear(),
|
() => this._clear(),
|
||||||
() => this._set_from_rect(rect),
|
() => {
|
||||||
|
this._set_from_set(old_cells);
|
||||||
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_clear() {
|
_clear() {
|
||||||
this.rect = null;
|
this.cells = new Set;
|
||||||
this.element.classList.remove('--visible');
|
this.bbox = null;
|
||||||
|
this.ring_bg_element.classList.remove('--visible');
|
||||||
|
this.ring_element.classList.remove('--visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO only used internally in one place?
|
||||||
*iter_coords() {
|
*iter_coords() {
|
||||||
if (! this.rect)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let stored_level = this.editor.stored_level;
|
let stored_level = this.editor.stored_level;
|
||||||
for (let y = this.rect.top; y < this.rect.bottom; y++) {
|
for (let n of this.cells) {
|
||||||
for (let x = this.rect.left; x < this.rect.right; x++) {
|
let [x, y] = stored_level.scalar_to_coords(n);
|
||||||
let n = stored_level.coords_to_scalar(x, y);
|
if (this.floated_offset) {
|
||||||
yield [x, y, n];
|
x += this.floated_offset[0];
|
||||||
|
y += this.floated_offset[1];
|
||||||
}
|
}
|
||||||
|
yield [x, y, n];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,90 +389,143 @@ export class Selection {
|
|||||||
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");
|
||||||
|
|
||||||
let floated_cells = [];
|
let floated_cells = new Map;
|
||||||
let tileset = this.editor.renderer.tileset;
|
|
||||||
let stored_level = this.editor.stored_level;
|
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()) {
|
for (let [x, y, n] of this.iter_coords()) {
|
||||||
let cell = stored_level.linear_cells[n];
|
let cell = stored_level.linear_cells[n];
|
||||||
if (copy) {
|
if (copy) {
|
||||||
floated_cells.push(cell.map(tile => tile ? {...tile} : null));
|
floated_cells.set(n, cell.map(tile => tile ? {...tile} : null));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
floated_cells.push(cell);
|
floated_cells.set(n, cell);
|
||||||
this.editor.replace_cell(cell, this.editor.make_blank_cell(x, y));
|
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.editor._do(
|
||||||
() => {
|
() => {
|
||||||
this.floated_canvas = canvas;
|
|
||||||
this.floated_element = floated_element;
|
|
||||||
this.floated_cells = floated_cells;
|
this.floated_cells = floated_cells;
|
||||||
this.svg_group.append(floated_element);
|
this.floated_offset = [0, 0];
|
||||||
|
this._init_floated_canvas();
|
||||||
|
this.ring_element.classList.add('--floating');
|
||||||
},
|
},
|
||||||
() => this._defloat(),
|
() => this._delete_floating(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create floated_canvas and floated_element, based on floated_cells
|
||||||
|
_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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
stamp_float(copy = false) {
|
stamp_float(copy = false) {
|
||||||
if (! this.floated_element)
|
if (! this.floated_element)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let stored_level = this.editor.stored_level;
|
let stored_level = this.editor.stored_level;
|
||||||
let i = 0;
|
for (let n of this.cells) {
|
||||||
for (let [x, y, n] of this.iter_coords()) {
|
let [x, y] = stored_level.scalar_to_coords(n);
|
||||||
let cell = this.floated_cells[i];
|
x += this.floated_offset[0];
|
||||||
|
y += this.floated_offset[1];
|
||||||
|
// If the selection is moved so that part of it is outside the level, skip that bit
|
||||||
|
if (! stored_level.is_point_within_bounds(x, y))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let cell = this.floated_cells.get(n);
|
||||||
if (copy) {
|
if (copy) {
|
||||||
cell = cell.map(tile => tile ? {...tile} : null);
|
cell = cell.map(tile => tile ? {...tile} : null);
|
||||||
}
|
}
|
||||||
cell.x = x;
|
cell.x = x;
|
||||||
cell.y = y;
|
cell.y = y;
|
||||||
this.editor.replace_cell(stored_level.linear_cells[n], cell);
|
|
||||||
i += 1;
|
let n2 = stored_level.coords_to_scalar(x, y);
|
||||||
|
this.editor.replace_cell(stored_level.linear_cells[n2], cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defloat() {
|
// Converts a floating selection back to a regular selection, including stamping it in place
|
||||||
|
commit_floating() {
|
||||||
|
// This is OK; we're idempotent
|
||||||
if (! this.floated_element)
|
if (! this.floated_element)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.stamp_float();
|
this.stamp_float();
|
||||||
|
|
||||||
let element = this.floated_element;
|
// Actually apply the offset, so we can be a regular selection again
|
||||||
let canvas = this.floated_canvas;
|
let old_cells = this.cells;
|
||||||
let cells = this.floated_cells;
|
let old_bbox = DOMRect.fromRect(this.bbox);
|
||||||
|
let new_cells = new Set;
|
||||||
|
let stored_level = this.editor.stored_level;
|
||||||
|
for (let n of old_cells) {
|
||||||
|
let [x, y] = stored_level.scalar_to_coords(n);
|
||||||
|
x += this.floated_offset[0];
|
||||||
|
y += this.floated_offset[1];
|
||||||
|
|
||||||
|
if (stored_level.is_point_within_bounds(x, y)) {
|
||||||
|
new_cells.add(stored_level.coords_to_scalar(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_floated_cells = this.floated_cells;
|
||||||
|
let old_floated_offset = this.floated_offset;
|
||||||
this.editor._do(
|
this.editor._do(
|
||||||
() => this._defloat(),
|
|
||||||
() => {
|
() => {
|
||||||
this.floated_cells = cells;
|
this._delete_floating();
|
||||||
this.floated_canvas = canvas;
|
this._set_from_set(new_cells);
|
||||||
this.floated_element = element;
|
},
|
||||||
this.svg_group.append(element);
|
() => {
|
||||||
|
// Don't use _set_from_set here; it's not designed for an offset float
|
||||||
|
this.cells = old_cells;
|
||||||
|
this.bbox = old_bbox;
|
||||||
|
this._update_outline();
|
||||||
|
|
||||||
|
this.floated_cells = old_floated_cells;
|
||||||
|
this.floated_offset = old_floated_offset;
|
||||||
|
this._init_floated_canvas();
|
||||||
|
this._update_floating_transform();
|
||||||
|
this.ring_element.classList.add('--floating');
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_defloat() {
|
_delete_floating() {
|
||||||
|
this.selection_group.removeAttribute('transform');
|
||||||
|
this.ring_element.classList.remove('--floating');
|
||||||
this.floated_element.remove();
|
this.floated_element.remove();
|
||||||
|
|
||||||
|
this.floated_cells = null;
|
||||||
|
this.floated_offset = null;
|
||||||
this.floated_element = null;
|
this.floated_element = null;
|
||||||
this.floated_canvas = null;
|
this.floated_canvas = null;
|
||||||
this.floated_cells = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redraw the selection canvas from scratch
|
// Redraw the selection canvas from scratch
|
||||||
@ -278,6 +539,7 @@ export class Selection {
|
|||||||
this.editor.renderer.draw_static_generic({
|
this.editor.renderer.draw_static_generic({
|
||||||
x0: 0, y0: 0,
|
x0: 0, y0: 0,
|
||||||
x1: this.rect.width, y1: this.rect.height,
|
x1: this.rect.width, y1: this.rect.height,
|
||||||
|
// FIXME this is now a set, not a flat list
|
||||||
cells: this.floated_cells,
|
cells: this.floated_cells,
|
||||||
width: this.rect.width,
|
width: this.rect.width,
|
||||||
ctx: this.floated_canvas.getContext('2d'),
|
ctx: this.floated_canvas.getContext('2d'),
|
||||||
|
|||||||
@ -16,6 +16,18 @@ import { SVGConnection, Selection } from './helpers.js';
|
|||||||
import * as mouseops from './mouseops.js';
|
import * as mouseops from './mouseops.js';
|
||||||
import { TILES_WITH_PROPS } from './tile-overlays.js';
|
import { TILES_WITH_PROPS } from './tile-overlays.js';
|
||||||
|
|
||||||
|
// FIXME some idle thoughts
|
||||||
|
// for adjust tool:
|
||||||
|
// - preview gray button (or click to actually do it)
|
||||||
|
// - preview wire reach
|
||||||
|
// - preview destination teleporter (or maybe order)
|
||||||
|
// - preview monster pathing
|
||||||
|
// - preview ice/ff routing (what about e.g. doublemaze)
|
||||||
|
// generally:
|
||||||
|
// - show wires that are initially powered
|
||||||
|
// - show traps that are initially closed
|
||||||
|
// - show implicit red/brown connections
|
||||||
|
// - selection and eyedropper should preserve red/brown button connections (somehow)
|
||||||
|
|
||||||
// Edited levels are stored as follows.
|
// Edited levels are stored as follows.
|
||||||
// StoredPack and StoredLevel both have an editor_metadata containing:
|
// StoredPack and StoredLevel both have an editor_metadata containing:
|
||||||
@ -76,12 +88,12 @@ export class Editor extends PrimaryView {
|
|||||||
mk_svg('marker', {id: 'overlay-arrowhead', markerWidth: 4, markerHeight: 4, refX: 3, refY: 2, orient: 'auto'},
|
mk_svg('marker', {id: 'overlay-arrowhead', markerWidth: 4, markerHeight: 4, refX: 3, refY: 2, orient: 'auto'},
|
||||||
mk_svg('polygon', {points: '0 0, 4 2, 0 4'}),
|
mk_svg('polygon', {points: '0 0, 4 2, 0 4'}),
|
||||||
),
|
),
|
||||||
),
|
mk_svg('filter', {id: 'overlay-filter-outline'},
|
||||||
mk_svg('filter', {id: 'overlay-filter-outline'},
|
mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated', operator: 'dilate', radius: 0.03125}),
|
||||||
mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated', operator: 'dilate', radius: 0.03125}),
|
mk_svg('feFlood', {'flood-color': '#0009', result: 'fill'}),
|
||||||
mk_svg('feFlood', {'flood-color': '#0009', result: 'fill'}),
|
mk_svg('feComposite', {'in': 'fill', in2: 'dilated', operator: 'in'}),
|
||||||
mk_svg('feComposite', {'in': 'fill', in2: 'dilated', operator: 'in'}),
|
mk_svg('feComposite', {'in': 'SourceGraphic'}),
|
||||||
mk_svg('feComposite', {'in': 'SourceGraphic'}),
|
),
|
||||||
),
|
),
|
||||||
this.connections_g,
|
this.connections_g,
|
||||||
);
|
);
|
||||||
@ -143,9 +155,9 @@ export class Editor extends PrimaryView {
|
|||||||
this.cancel_mouse_drag();
|
this.cancel_mouse_drag();
|
||||||
}
|
}
|
||||||
let new_rect = new DOMRect(0, 0, this.stored_level.size_x, this.stored_level.size_y);
|
let new_rect = new DOMRect(0, 0, this.stored_level.size_x, this.stored_level.size_y);
|
||||||
let old_rect = this.selection.rect;
|
if (this.selection.cells.size !== this.stored_level.size_x * this.stored_level.size_y) {
|
||||||
if (! (old_rect && old_rect.x === new_rect.x && old_rect.y === new_rect.y && old_rect.width === new_rect.width && old_rect.height === new_rect.height)) {
|
this.selection.clear();
|
||||||
this.selection.set_from_rect(new_rect);
|
this.selection.add_rect(new_rect);
|
||||||
this.commit_undo();
|
this.commit_undo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,7 +168,7 @@ export class Editor extends PrimaryView {
|
|||||||
this.cancel_mouse_drag();
|
this.cancel_mouse_drag();
|
||||||
}
|
}
|
||||||
if (! this.selection.is_empty) {
|
if (! this.selection.is_empty) {
|
||||||
this.selection.defloat();
|
this.selection.commit_floating();
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
this.commit_undo();
|
this.commit_undo();
|
||||||
}
|
}
|
||||||
@ -285,6 +297,13 @@ export class Editor extends PrimaryView {
|
|||||||
});
|
});
|
||||||
window.addEventListener('blur', () => {
|
window.addEventListener('blur', () => {
|
||||||
this.cancel_mouse_drag();
|
this.cancel_mouse_drag();
|
||||||
|
|
||||||
|
// Assume all modifiers are released
|
||||||
|
for (let mouse_op of this.mouse_ops) {
|
||||||
|
if (mouse_op) {
|
||||||
|
mouse_op.clear_modifiers();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('mouseleave', () => {
|
window.addEventListener('mouseleave', () => {
|
||||||
this.mouse_coords = null;
|
this.mouse_coords = null;
|
||||||
@ -1088,6 +1107,7 @@ export class Editor extends PrimaryView {
|
|||||||
|
|
||||||
this.zoom = zoom;
|
this.zoom = zoom;
|
||||||
this.renderer.canvas.style.setProperty('--scale', this.zoom);
|
this.renderer.canvas.style.setProperty('--scale', this.zoom);
|
||||||
|
this.svg_overlay.style.setProperty('--scale', this.zoom);
|
||||||
this.actual_viewport_el.classList.toggle('--crispy', this.zoom >= 1);
|
this.actual_viewport_el.classList.toggle('--crispy', this.zoom >= 1);
|
||||||
this.statusbar_zoom.textContent = `${this.zoom * 100}%`;
|
this.statusbar_zoom.textContent = `${this.zoom * 100}%`;
|
||||||
|
|
||||||
@ -1556,6 +1576,7 @@ export class Editor extends PrimaryView {
|
|||||||
old_w = w;
|
old_w = w;
|
||||||
if (swap_dimensions) {
|
if (swap_dimensions) {
|
||||||
[w, h] = [h, w];
|
[w, h] = [h, w];
|
||||||
|
// FIXME this will need more interesting rearranging
|
||||||
this.selection._set_from_rect(new DOMRect(
|
this.selection._set_from_rect(new DOMRect(
|
||||||
this.selection.rect.x, this.selection.rect.y, w, h));
|
this.selection.rect.x, this.selection.rect.y, w, h));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,15 @@ import { mk, mk_svg, walk_grid } from '../util.js';
|
|||||||
import { SVGConnection } from './helpers.js';
|
import { SVGConnection } from './helpers.js';
|
||||||
import { TILES_WITH_PROPS } from './tile-overlays.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
|
const MOUSE_BUTTON_MASKS = [1, 4, 2]; // MouseEvent.button/buttons are ordered differently
|
||||||
export class MouseOperation {
|
export class MouseOperation {
|
||||||
constructor(editor, physical_button) {
|
constructor(editor, physical_button) {
|
||||||
@ -127,6 +136,13 @@ export class MouseOperation {
|
|||||||
_update_modifiers(ev) {
|
_update_modifiers(ev) {
|
||||||
this.ctrl = ev.ctrlKey;
|
this.ctrl = ev.ctrlKey;
|
||||||
this.shift = ev.shiftKey;
|
this.shift = ev.shiftKey;
|
||||||
|
this.alt = ev.altKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_modifiers() {
|
||||||
|
this.ctrl = false;
|
||||||
|
this.shift = false;
|
||||||
|
this.alt = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
do_commit() {
|
do_commit() {
|
||||||
@ -508,16 +524,21 @@ export class FillOperation extends MouseOperation {
|
|||||||
|
|
||||||
|
|
||||||
// TODO also, delete
|
// TODO also, delete
|
||||||
// TODO also, non-rectangular selections
|
|
||||||
// TODO also, better marching ants, hard to see on gravel
|
// TODO also, better marching ants, hard to see on gravel
|
||||||
export class SelectOperation extends MouseOperation {
|
export class SelectOperation extends MouseOperation {
|
||||||
handle_press() {
|
handle_press() {
|
||||||
if (! this.editor.selection.is_empty && this.editor.selection.contains(this.click_cell_x, this.click_cell_y)) {
|
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
|
// Move existing selection
|
||||||
this.mode = 'float';
|
this.mode = 'float';
|
||||||
if (this.ctrl) {
|
this.make_copy = this.ctrl;
|
||||||
this.make_copy = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Create new selection
|
// Create new selection
|
||||||
@ -569,17 +590,31 @@ export class SelectOperation extends MouseOperation {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else { // create/extend
|
||||||
// If there's an existing floating selection (which isn't what we're operating on),
|
if (this.has_moved) {
|
||||||
// commit it before doing anything else
|
// Drag either creates or extends the selection
|
||||||
this.editor.selection.defloat();
|
// If there's an existing floating selection (which isn't what we're operating on),
|
||||||
if (! this.has_moved) {
|
// commit it before doing anything else
|
||||||
// Plain click clears selection
|
this.editor.selection.commit_floating();
|
||||||
this.pending_selection.discard();
|
|
||||||
this.editor.selection.clear();
|
if (this.mode === 'create') {
|
||||||
|
this.editor.selection.clear();
|
||||||
|
}
|
||||||
|
this.pending_selection.commit();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.pending_selection.commit();
|
// 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();
|
this.editor.commit_undo();
|
||||||
@ -592,6 +627,13 @@ export class SelectOperation extends MouseOperation {
|
|||||||
this.pending_selection.discard();
|
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 {
|
export class ForceFloorOperation extends MouseOperation {
|
||||||
@ -1166,10 +1208,7 @@ export class CameraOperation extends MouseOperation {
|
|||||||
this.editor.viewport_el.style.cursor = cursor;
|
this.editor.viewport_el.style.cursor = cursor;
|
||||||
|
|
||||||
// Create a text element to show the size while editing
|
// Create a text element to show the size while editing
|
||||||
this.size_text = mk_svg('text.overlay-edit-tip', {
|
this.size_text = mk_svg('text.overlay-edit-tip');
|
||||||
// Center it within the rectangle probably (x and y are set in _update_size_text)
|
|
||||||
'text-anchor': 'middle', 'dominant-baseline': 'middle',
|
|
||||||
});
|
|
||||||
this._update_size_text();
|
this._update_size_text();
|
||||||
this.editor.svg_overlay.append(this.size_text);
|
this.editor.svg_overlay.append(this.size_text);
|
||||||
}
|
}
|
||||||
|
|||||||
27
js/util.js
27
js/util.js
@ -337,6 +337,33 @@ export function* walk_grid(x0, y0, x1, y1, min_a, min_b, max_a, max_b) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Baby's first bit vector
|
||||||
|
export class BitVector {
|
||||||
|
constructor(size) {
|
||||||
|
this.array = new Uint32Array(Math.ceil(size / 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
get(bit) {
|
||||||
|
let i = Math.floor(bit / 32);
|
||||||
|
let b = bit % 32;
|
||||||
|
return (this.array[i] & (1 << b)) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(bit) {
|
||||||
|
let i = Math.floor(bit / 32);
|
||||||
|
let b = bit % 32;
|
||||||
|
this.array[i] |= (1 << b);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(bit) {
|
||||||
|
let i = Math.floor(bit / 32);
|
||||||
|
let b = bit % 32;
|
||||||
|
this.array[i] &= ~(1 << b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Root class to indirect over where we might get files from
|
// Root class to indirect over where we might get files from
|
||||||
// - a pool of uploaded in-memory files
|
// - a pool of uploaded in-memory files
|
||||||
// - a single uploaded zip file
|
// - a single uploaded zip file
|
||||||
|
|||||||
41
style.css
41
style.css
@ -2215,8 +2215,11 @@ svg.level-editor-overlay {
|
|||||||
/* allow clicks to go through us! */
|
/* allow clicks to go through us! */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
|
/* not used to shrink us (absolute positioning does that), just to make the stroke width a
|
||||||
|
* consistent size at any zoom level */
|
||||||
|
--scale: 1;
|
||||||
/* default svg properties */
|
/* default svg properties */
|
||||||
stroke-width: 0.0625;
|
stroke-width: calc(0.0625px / var(--scale));
|
||||||
fill: none;
|
fill: none;
|
||||||
}
|
}
|
||||||
svg.level-editor-overlay .overlay-transient {
|
svg.level-editor-overlay .overlay-transient {
|
||||||
@ -2226,24 +2229,33 @@ svg.level-editor-overlay .overlay-transient.--visible {
|
|||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
svg.level-editor-overlay rect.overlay-cursor {
|
svg.level-editor-overlay rect.overlay-cursor {
|
||||||
x-stroke: hsla(220, 100%, 60%, 0.5);
|
stroke: hsla(var(--main-hue), 100%, 90%, 0.75);
|
||||||
fill: hsla(220, 100%, 75%, 0.25);
|
fill: hsla(var(--main-hue), 100%, 75%, 0.25);
|
||||||
}
|
}
|
||||||
svg.level-editor-overlay rect.overlay-pending-selection {
|
svg.level-editor-overlay rect.overlay-pending-selection {
|
||||||
stroke: hsla(220, 100%, 60%, 0.5);
|
stroke: hsla(var(--selected-hue), 100%, 60%, 0.5);
|
||||||
fill: hsla(220, 100%, 75%, 0.25);
|
fill: hsla(var(--selected-hue), 100%, 75%, 0.25);
|
||||||
}
|
}
|
||||||
svg.level-editor-overlay rect.overlay-selection {
|
svg.level-editor-overlay path.overlay-selection-background {
|
||||||
stroke: #000c;
|
stroke: hsla(var(--selected-hue), 10%, 90%, 0.9);
|
||||||
fill: hsla(220, 0%, 75%, 0.25);
|
fill: none;
|
||||||
stroke-dasharray: 0.125, 0.125;
|
pointer-events: none;
|
||||||
animation: marching-ants 1s linear infinite;
|
}
|
||||||
|
svg.level-editor-overlay path.overlay-selection {
|
||||||
|
stroke: hsla(var(--selected-hue), 10%, 10%, 0.75);
|
||||||
|
fill: hsla(var(--selected-hue), 50%, 75%, 0.375);
|
||||||
|
stroke-width: calc(0.125px / var(--scale));
|
||||||
|
stroke-dasharray: calc(0.125px / var(--scale)), calc(0.125px / var(--scale));
|
||||||
|
animation: marching-ants 0.5s linear infinite;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
svg.level-editor-overlay path.overlay-selection.--floating {
|
||||||
|
stroke: hsla(var(--selected-hue), 80%, 50%, 0.75);
|
||||||
|
}
|
||||||
@keyframes marching-ants {
|
@keyframes marching-ants {
|
||||||
0% {
|
0% {
|
||||||
stroke-dashoffset: 0.25;
|
stroke-dashoffset: calc(0.25px / var(--scale));
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
stroke-dashoffset: 0;
|
stroke-dashoffset: 0;
|
||||||
@ -2269,8 +2281,11 @@ svg.level-editor-overlay text {
|
|||||||
font-size: 1px;
|
font-size: 1px;
|
||||||
}
|
}
|
||||||
svg.level-editor-overlay text.overlay-edit-tip {
|
svg.level-editor-overlay text.overlay-edit-tip {
|
||||||
|
/* Used for showing e.g. the size of a pending selection. Centered around its anchor */
|
||||||
stroke: none;
|
stroke: none;
|
||||||
fill: black;
|
fill: hsl(var(--selected-hue), 80%, 30%);
|
||||||
|
text-anchor: middle;
|
||||||
|
dominant-baseline: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-big-tooltip {
|
.editor-big-tooltip {
|
||||||
@ -2293,7 +2308,7 @@ svg.level-editor-overlay text.overlay-edit-tip {
|
|||||||
text-transform: none;
|
text-transform: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #d8d8d8;
|
color: #d8d8d8;
|
||||||
background: hsl(220, 10%, 20%);
|
background: hsl(var(--main-hue), 10%, 20%);
|
||||||
box-shadow: 0 1px 2px 1px #0004;
|
box-shadow: 0 1px 2px 1px #0004;
|
||||||
}
|
}
|
||||||
.editor-big-tooltip h3 {
|
.editor-big-tooltip h3 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user