Improve the connection tool somewhat; show implicit connections
For example, you can now make connections with the connection tool. Remarkable. Unfortunately, implicit connections aren't updated as you edit the level yet. Also came with some refactors for searching a level and whatnot.
This commit is contained in:
1 parent
5e2dfdd926
commit
3cf81b53ad
10 files changed
+324
-176
No files matched your search
+29
-8
@@ -4,12 +4,15 @@ import { BitVector, mk, mk_svg } from '../util.js';
|
||||
|
||||
export class SVGConnection {
|
||||
constructor(sx, sy, dx, dy) {
|
||||
this.source = mk_svg('circle.-source', {r: 0.5});
|
||||
this.source = mk_svg('circle.-source', {r: 0.5, cx: sx + 0.5, cy: sy + 0.5});
|
||||
this.line = mk_svg('line.-arrow', {});
|
||||
this.dest = mk_svg('rect.-dest', {width: 1, height: 1});
|
||||
this.dest = mk_svg('rect.-dest', {x: dx, y: dy, 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);
|
||||
this.sx = sx;
|
||||
this.sy = sy;
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
this._update_line_endpoints();
|
||||
}
|
||||
|
||||
set_source(sx, sy) {
|
||||
@@ -17,17 +20,35 @@ export class SVGConnection {
|
||||
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);
|
||||
this._update_line_endpoints();
|
||||
}
|
||||
|
||||
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);
|
||||
this._update_line_endpoints();
|
||||
}
|
||||
|
||||
_update_line_endpoints() {
|
||||
// Start the line at the edge of the circle, so, add 0.5 in the direction of the line
|
||||
let vx = this.dx - this.sx;
|
||||
let vy = this.dy - this.sy;
|
||||
let line_length = Math.sqrt(vx*vx + vy*vy);
|
||||
let trim_x = 0;
|
||||
let trim_y = 0;
|
||||
if (line_length >= 1) {
|
||||
trim_x = 0.5 * vx / line_length;
|
||||
trim_y = 0.5 * vy / line_length;
|
||||
}
|
||||
this.line.setAttribute('x1', this.sx + 0.5 + trim_x);
|
||||
this.line.setAttribute('y1', this.sy + 0.5 + trim_y);
|
||||
// Technically this isn't quite right, since the ending is a square and the arrowhead will
|
||||
// poke into it a bit from angles near 45°, but that requires a bit more trig than seems
|
||||
// worth it
|
||||
this.line.setAttribute('x2', this.dx + 0.5 - trim_x);
|
||||
this.line.setAttribute('y2', this.dy + 0.5 - trim_y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+97
-25
@@ -1,5 +1,6 @@
|
||||
import * as fflate from '../vendor/fflate.js';
|
||||
|
||||
import * as algorithms from '../algorithms.js';
|
||||
import { DIRECTIONS, LAYERS } from '../defs.js';
|
||||
import * as format_base from '../format-base.js';
|
||||
import * as c2g from '../format-c2g.js';
|
||||
@@ -81,7 +82,7 @@ export class Editor extends PrimaryView {
|
||||
this.renderer.show_facing = true;
|
||||
|
||||
// FIXME need this in load_level which is called even if we haven't been setup yet
|
||||
this.connections_g = mk_svg('g');
|
||||
this.connections_g = mk_svg('g', {'data-name': 'connections'});
|
||||
// This SVG draws vectors on top of the editor, like monster paths and button connections
|
||||
this.svg_overlay = mk_svg('svg.level-editor-overlay', {viewBox: '0 0 32 32'},
|
||||
mk_svg('defs',
|
||||
@@ -89,9 +90,13 @@ export class Editor extends PrimaryView {
|
||||
mk_svg('polygon', {points: '0 0, 4 2, 0 4'}),
|
||||
),
|
||||
mk_svg('filter', {id: 'overlay-filter-outline'},
|
||||
mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated', operator: 'dilate', radius: 0.03125}),
|
||||
mk_svg('feFlood', {'flood-color': '#0009', result: 'fill'}),
|
||||
mk_svg('feComposite', {'in': 'fill', in2: 'dilated', operator: 'in'}),
|
||||
this._filter_morphology_element = mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated', operator: 'dilate', radius: 0.03125}),
|
||||
this._filter_morphology_element2 = mk_svg('feMorphology', {'in': 'SourceAlpha', result: 'dilated2', operator: 'dilate', radius: 0.0625}),
|
||||
mk_svg('feFlood', {'flood-color': '#000'}),
|
||||
mk_svg('feComposite', {in2: 'dilated', operator: 'in', result: 'fill'}),
|
||||
mk_svg('feFlood', {'flood-color': '#fffc'}),
|
||||
mk_svg('feComposite', {in2: 'dilated2', operator: 'in', result: 'fill2'}),
|
||||
mk_svg('feComposite', {'in': 'fill', in2: 'fill2'}),
|
||||
mk_svg('feComposite', {'in': 'SourceGraphic'}),
|
||||
),
|
||||
),
|
||||
@@ -1043,16 +1048,16 @@ export class Editor extends PrimaryView {
|
||||
|
||||
// Load connections
|
||||
// TODO what if the source tile is not connectable?
|
||||
// TODO there's a has_custom_connections flag, is that important here or is it just because
|
||||
// i can't test an object as a bool
|
||||
this.connections_g.textContent = '';
|
||||
this.connections_elements = {};
|
||||
for (let [src, dest] of Object.entries(this.stored_level.custom_connections)) {
|
||||
this.connections_arrows = {};
|
||||
for (let [src, dest] of this.stored_level.custom_connections) {
|
||||
let [sx, sy] = this.stored_level.scalar_to_coords(src);
|
||||
let [dx, dy] = this.stored_level.scalar_to_coords(dest);
|
||||
let el = new SVGConnection(sx, sy, dx, dy).element;
|
||||
this.connections_elements[src] = el;
|
||||
this.connections_g.append(el);
|
||||
let arrow = new SVGConnection(sx, sy, dx, dy);
|
||||
this.connections_arrows[src] = arrow;
|
||||
arrow.element.setAttribute(
|
||||
'data-source', this.stored_level.linear_cells[src][LAYERS.terrain].type.name);
|
||||
this.connections_g.append(arrow.element);
|
||||
}
|
||||
// TODO why are these in connections_g lol
|
||||
for (let [i, region] of this.stored_level.camera_regions.entries()) {
|
||||
@@ -1060,6 +1065,9 @@ export class Editor extends PrimaryView {
|
||||
this.connections_g.append(el);
|
||||
}
|
||||
|
||||
// Load *implicit* connections
|
||||
this.recreate_implicit_connections();
|
||||
|
||||
this.renderer.set_level(stored_level);
|
||||
if (this.active) {
|
||||
this.redraw_entire_level();
|
||||
@@ -1112,6 +1120,9 @@ export class Editor extends PrimaryView {
|
||||
this.svg_overlay.style.setProperty('--scale', this.zoom);
|
||||
this.actual_viewport_el.classList.toggle('--crispy', this.zoom >= 1);
|
||||
this.statusbar_zoom.textContent = `${this.zoom * 100}%`;
|
||||
// The arrow outline isn't CSS, so we have to fix it manually
|
||||
this._filter_morphology_element.setAttribute('radius', 0.03125 / this.zoom);
|
||||
this._filter_morphology_element2.setAttribute('radius', 0.0625 / this.zoom);
|
||||
|
||||
let index = ZOOM_LEVELS.findIndex(el => el >= this.zoom);
|
||||
if (index < 0) {
|
||||
@@ -1737,35 +1748,96 @@ export class Editor extends PrimaryView {
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Connections (buttons to things they control)
|
||||
|
||||
// Create a connection between two cells and update the UI accordingly. If dest is null or
|
||||
// undefined, delete any existing connection instead.
|
||||
set_custom_connection(src, dest) {
|
||||
let prev = this.stored_level.custom_connections[src];
|
||||
let prev = this.stored_level.custom_connections.get(src);
|
||||
this._do(
|
||||
() => this._set_custom_connection(src, dest),
|
||||
() => this._set_custom_connection(src, prev),
|
||||
);
|
||||
}
|
||||
_set_custom_connection(src, dest) {
|
||||
if (this.connections_elements[src]) {
|
||||
this.connections_elements[src].remove();
|
||||
}
|
||||
|
||||
if ((dest ?? null) === null) {
|
||||
delete this.stored_level.custom_connections[src];
|
||||
delete this.connections_elements[src];
|
||||
if (this.connections_arrows[src]) {
|
||||
this.connections_arrows[src].element.remove();
|
||||
}
|
||||
this.stored_level.custom_connections.delete(src)
|
||||
delete this.connections_arrows[src];
|
||||
}
|
||||
else {
|
||||
this.stored_level.custom_connections[src] = dest;
|
||||
let el = new SVGConnection(
|
||||
...this.stored_level.scalar_to_coords(src),
|
||||
...this.stored_level.scalar_to_coords(dest),
|
||||
).element;
|
||||
this.connections_elements[src] = el;
|
||||
this.connections_g.append(el);
|
||||
this.stored_level.custom_connections.set(src, dest);
|
||||
|
||||
if (this.connections_arrows[src]) {
|
||||
this.connections_arrows[src].set_dest(
|
||||
...this.stored_level.scalar_to_coords(dest));
|
||||
}
|
||||
else {
|
||||
let arrow = new SVGConnection(
|
||||
...this.stored_level.scalar_to_coords(src),
|
||||
...this.stored_level.scalar_to_coords(dest));
|
||||
this.connections_arrows[src] = arrow;
|
||||
this.connections_g.append(arrow.element);
|
||||
}
|
||||
this.connections_arrows[src].element.setAttribute(
|
||||
'data-source', this.stored_level.linear_cells[src][LAYERS.terrain].type.name);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO also use this to indicate traps or flame jets that are initially toggled (trickier with
|
||||
// flame jets since that can make them look like they're the wrong tile...)
|
||||
recreate_implicit_connections() {
|
||||
this.implicit_connections = new Map;
|
||||
|
||||
for (let el of this.connections_g.querySelectorAll(':scope > .--implicit')) {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
for (let [n, cell] of this.stored_level.linear_cells.entries()) {
|
||||
if (this.stored_level.custom_connections.has(n))
|
||||
continue;
|
||||
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (! terrain.type.connects_to)
|
||||
continue;
|
||||
|
||||
let find_func;
|
||||
if (terrain.type.connect_order === 'forward') {
|
||||
find_func = algorithms.find_terrain_linear;
|
||||
}
|
||||
else if (terrain.type.connect_order === 'diamond') {
|
||||
find_func = algorithms.find_terrain_diamond;
|
||||
}
|
||||
|
||||
let target_cell = null;
|
||||
for (let [found_tile, found_cell] of find_func(this.stored_level, cell, terrain.type.connects_to)) {
|
||||
target_cell = found_cell;
|
||||
break;
|
||||
}
|
||||
if (target_cell === null)
|
||||
continue;
|
||||
|
||||
let svg = new SVGConnection(
|
||||
...this.stored_level.scalar_to_coords(n),
|
||||
target_cell.x, target_cell.y);
|
||||
this.implicit_connections.set(n, {
|
||||
index: this.stored_level.coords_to_scalar(target_cell.x, target_cell.y),
|
||||
svg_connection: svg,
|
||||
});
|
||||
svg.element.classList.add('--implicit');
|
||||
svg.element.setAttribute('data-source', terrain.type.name);
|
||||
this.connections_g.append(svg.element);
|
||||
}
|
||||
}
|
||||
|
||||
update_implicit_connection(cell, tile, previous_type) {
|
||||
// TODO actually this should also update explicit ones, if the source/dest types are changed
|
||||
// in such a way as to make the connection invalid
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Undo/redo
|
||||
|
||||
|
||||
+72
-4
@@ -17,6 +17,7 @@ import { TILES_WITH_PROPS } from './tile-overlays.js';
|
||||
// - no ice drawing tool
|
||||
// - cursor box shows with selection tool which seems inappropriate
|
||||
// - controls do not exactly stand out and are just plain text
|
||||
// - set trap as initially open? feels like a weird hack. but it does appear in cc2lp1
|
||||
const MOUSE_BUTTON_MASKS = [1, 4, 2]; // MouseEvent.button/buttons are ordered differently
|
||||
export class MouseOperation {
|
||||
constructor(editor, physical_button) {
|
||||
@@ -57,7 +58,7 @@ export class MouseOperation {
|
||||
// appropriate, and its position will be updated to match the position of the cursor
|
||||
set_cursor_element(el) {
|
||||
this.cursor_element = el;
|
||||
el.setAttribute('data-mouseop', this.constructor.name);
|
||||
el.setAttribute('data-name', this.constructor.name);
|
||||
if (! this.is_hover_visible) {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
@@ -834,14 +835,40 @@ export class TrackOperation extends MouseOperation {
|
||||
}
|
||||
|
||||
export class ConnectOperation extends MouseOperation {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// This is the SVGConnection structure but with only the source circle
|
||||
this.connectable_circle = mk_svg('circle.-source', {r: 0.5});
|
||||
this.connectable_cursor = mk_svg('g.overlay-connection', this.connectable_circle);
|
||||
this.connectable_cursor.style.display = 'none';
|
||||
// TODO how do i distinguish from existing ones
|
||||
this.connectable_cursor.style.stroke = 'lime';
|
||||
this.editor.svg_overlay.append(this.connectable_cursor);
|
||||
}
|
||||
|
||||
handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||||
let cell = this.cell(cell_x, cell_y);
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (terrain.type.connects_to) {
|
||||
this.connectable_cursor.style.display = '';
|
||||
this.connectable_circle.setAttribute('cx', cell_x + 0.5);
|
||||
this.connectable_circle.setAttribute('cy', cell_y + 0.5);
|
||||
}
|
||||
else {
|
||||
this.connectable_cursor.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
handle_press(x, y) {
|
||||
// TODO restrict to button/cloner unless holding shift
|
||||
// TODO what do i do when you erase a button/cloner? can i detect if you're picking it up?
|
||||
let src = this.editor.stored_level.coords_to_scalar(x, y);
|
||||
let cell = this.cell(x, y);
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
if (this.alt_mode) {
|
||||
// Auto connect using Lynx rules
|
||||
let cell = this.cell(x, y);
|
||||
let terrain = cell[LAYERS.terrain];
|
||||
// TODO just use the editor's existing implicits for this?
|
||||
let other = null;
|
||||
let swap = false;
|
||||
if (terrain.type.name === 'button_red') {
|
||||
@@ -870,8 +897,17 @@ export class ConnectOperation extends MouseOperation {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, this is the start of a drag
|
||||
if (! terrain.type.connects_to)
|
||||
return;
|
||||
|
||||
this.pending_cxn = new SVGConnection(x, y, x, y);
|
||||
this.pending_source = src;
|
||||
this.pending_type = terrain.type.name;
|
||||
this.editor.svg_overlay.append(this.pending_cxn.element);
|
||||
// Hide the normal cursor for the duration
|
||||
this.connectable_cursor.style.display = 'none';
|
||||
}
|
||||
// FIXME this is hella the sort of thing that should be on Editor, or in algorithms
|
||||
search_for(i0, name, dir) {
|
||||
@@ -896,14 +932,46 @@ export class ConnectOperation extends MouseOperation {
|
||||
}
|
||||
}
|
||||
handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
||||
if (! this.pending_cxn)
|
||||
return;
|
||||
|
||||
this.pending_cxn.set_dest(cell_x, cell_y);
|
||||
|
||||
let cell = this.cell(cell_x, cell_y);
|
||||
if (TILE_TYPES[this.pending_type].connects_to.has(cell[LAYERS.terrain].type.name)) {
|
||||
this.pending_target = this.editor.stored_level.coords_to_scalar(cell_x, cell_y);
|
||||
this.pending_cxn.element.style.opacity = 0.5;
|
||||
}
|
||||
else {
|
||||
this.pending_target = null;
|
||||
this.pending_cxn.element.style.opacity = '';
|
||||
}
|
||||
}
|
||||
commit_press() {
|
||||
// TODO
|
||||
if (! this.pending_cxn)
|
||||
return;
|
||||
|
||||
if (this.pending_target !== null) {
|
||||
this.editor.set_custom_connection(this.pending_source, this.pending_target);
|
||||
}
|
||||
|
||||
this.pending_cxn.element.remove();
|
||||
this.pending_cxn = null;
|
||||
}
|
||||
abort_press() {
|
||||
this.pending_cxn.element.remove();
|
||||
if (this.pending_cxn) {
|
||||
this.pending_cxn.element.remove();
|
||||
this.pending_cxn = null;
|
||||
}
|
||||
}
|
||||
cleanup_press() {
|
||||
}
|
||||
|
||||
do_destroy() {
|
||||
this.connectable_cursor.remove();
|
||||
super.do_destroy();
|
||||
}
|
||||
}
|
||||
export class WireOperation extends MouseOperation {
|
||||
handle_press() {
|
||||
|
||||
Reference in new issue
Block a user