From b82e112cbc2a64110f4449ba63e32b3e39be869e Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Thu, 9 May 2024 21:00:10 -0600 Subject: [PATCH] Teach the connect tool to move destinations and delete connections --- js/editor/editordefs.js | 19 ++-- js/editor/mouseops.js | 187 ++++++++++++++++++++++++++-------------- style.css | 3 + 3 files changed, 134 insertions(+), 75 deletions(-) diff --git a/js/editor/editordefs.js b/js/editor/editordefs.js index a97613c..a1a10ab 100644 --- a/js/editor/editordefs.js +++ b/js/editor/editordefs.js @@ -212,21 +212,16 @@ export const TOOLS = { connect: { icon: 'icons/tool-connect.png', name: "Connect", - // XXX shouldn't you be able to drag the destination? - // TODO mod + right click for RRO or diamond alg? ah but we only have ctrl available - // ok lemme think then - // left drag: create a new connection (supported connections only) - // ctrl-click: erase all connections - // shift-drag: create a new connection (arbitrary cells) - // right drag: move a connection endpoint - // ctrl-right drag: move the other endpoint (if a cell is both source and dest) desc: [ "Set up CC1-style clone and trap connections.", - "(WIP)", - "NOTE: Not supported in the real CC2!", + "(Supported in CC1 and LL, but not CC2!)", "", - "[mouse2] Auto link using Lynx/CC2 rules", - //desc: "Set up CC1-style clone and trap connections.\nNOTE: Not supported in CC2!\nLeft drag: link button with valid target\nCtrl-click: erase link\nRight click: auto link using Lynx rules", + "[mouse1] Connect a button to a mechanism", + "[mouse1] Move existing connections", + "[ctrl] [mouse1] Delete connection", + "[shift] [mouse1] Allow connecting to any cell", + "(not recommended)", + "[mouse2] Auto link a button using Lynx/CC2 rules", ].join("\n"), uses_layers: false, op1: mouseops.ConnectOperation, diff --git a/js/editor/mouseops.js b/js/editor/mouseops.js index a336758..0a00cfa 100644 --- a/js/editor/mouseops.js +++ b/js/editor/mouseops.js @@ -1362,31 +1362,54 @@ export class ConnectOperation extends MouseOperation { 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); + this.source_circle = mk_svg('circle.-source', {cx: 0.5, cy: 0.5, r: 0.5}); + this.source_cursor = mk_svg('g.overlay-connection.overlay-transient.--cursor', this.source_circle); + this.target_square = mk_svg('rect.-target', {x: 0, y: 0, width: 1, height: 1}); + this.target_cursor = mk_svg('g.overlay-connection.overlay-transient.--cursor', this.target_square); + this.editor.svg_overlay.append(this.source_cursor, this.target_cursor); } handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) { + if (cell_x === this.prev_cell_x && cell_y === this.prev_cell_y && ! this.hover_stale) + return; + + this.hover_stale = false; 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); + this.source_cursor.classList.add('--visible'); + this.source_cursor.setAttribute('transform', `translate(${cell_x} ${cell_y})`); + this.target_cursor.classList.remove('--visible'); } else { - this.connectable_cursor.style.display = 'none'; + this.pending_sources = []; + this.pending_cxns = []; + let pt = this.editor.coords_to_scalar(cell_x, cell_y); + this.pending_original_target = pt; + for (let [src, dest] of this.editor.stored_level.custom_connections) { + if (dest === pt) { + // Just take an arbitrary type as pending I guess + this.pending_type = this.editor.stored_level.linear_cells[src][LAYERS.terrain].type.name; + this.pending_sources.push(src); + this.pending_cxns.push(this.editor.connections_arrows.get(src)); + } + } + + this.source_cursor.classList.remove('--visible'); + if (this.pending_sources.length > 0) { + this.target_cursor.classList.add('--visible'); + this.target_cursor.setAttribute('transform', `translate(${cell_x} ${cell_y})`); + } + else { + this.target_cursor.classList.remove('--visible'); + } } } 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.coords_to_scalar(x, y); + let pt = this.editor.coords_to_scalar(x, y); let cell = this.cell(x, y); let terrain = cell[LAYERS.terrain]; if (this.alt_mode) { @@ -1395,104 +1418,142 @@ export class ConnectOperation extends MouseOperation { let other = null; let swap = false; if (terrain.type.name === 'button_red') { - other = this.search_for(src, 'cloner', 1); + other = this.search_for(cell, 'cloner', 1); } else if (terrain.type.name === 'cloner') { - other = this.search_for(src, 'button_red', -1); + other = this.search_for(cell, 'button_red', -1); swap = true; } else if (terrain.type.name === 'button_brown') { - other = this.search_for(src, 'trap', 1); + other = this.search_for(cell, 'trap', 1); } else if (terrain.type.name === 'trap') { - other = this.search_for(src, 'button_brown', -1); + other = this.search_for(cell, 'button_brown', -1); swap = true; } if (other !== null) { if (swap) { - this.editor.set_custom_connection(other, src); + this.editor.set_custom_connection(other, pt); } else { - this.editor.set_custom_connection(src, other); + this.editor.set_custom_connection(pt, other); } this.editor.commit_undo(); } 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) { - let l = this.editor.stored_level.linear_cells.length; - let i = i0; - while (true) { - i += dir; - if (i < 0) { - i += l; - } - else if (i >= l) { - i -= l; - } - if (i === i0) - return null; - - let cell = this.editor.stored_level.linear_cells[i]; - let tile = cell[LAYERS.terrain]; - if (tile.type.name === name) { - return i; - } + // Otherwise, this is the start of a drag, which could be one of two things... + if (terrain.type.connects_to) { + // This is a source, and we're dragging to a destination + let cxn = new SVGConnection(x, y, x, y); + this.pending_cxns = [cxn]; + this.pending_sources = [pt]; + this.pending_type = terrain.type.name; + this.pending_original_target = null; + this.editor.svg_overlay.append(cxn.element); } + else { + // This /might/ be a destination (or a stack of them) + if (this.pending_sources.length === 0) + return; + } + + if (this.ctrl) { + // Forget the drag, delete whatever was clicked + for (let src of this.pending_sources) { + this.editor.set_custom_connection(src, null); + } + this.editor.commit_undo(); + this.cleanup_press(); + return; + } + + // Hide the normal cursors for the duration + this.source_cursor.classList.remove('--visible'); + this.target_cursor.classList.remove('--visible'); + } + search_for(start_cell, name, dir) { + for (let [_, cell] of algorithms.find_terrain_linear( + this.editor.stored_level, start_cell, new Set([name]), dir < 0)) + { + return this.editor.coords_to_scalar(cell.x, cell.y); + } + return null; } handle_drag(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) { - if (! this.pending_cxn) + if (this.pending_cxns.length === 0) return; - this.pending_cxn.set_dest(cell_x, cell_y); + for (let cxn of this.pending_cxns) { + 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)) { + if (this.shift || + TILE_TYPES[this.pending_type].connects_to.has(cell[LAYERS.terrain].type.name)) + { this.pending_target = this.editor.coords_to_scalar(cell_x, cell_y); - this.pending_cxn.element.style.opacity = 0.5; + for (let cxn of this.pending_cxns) { + cxn.element.style.opacity = ''; + } } else { this.pending_target = null; - this.pending_cxn.element.style.opacity = ''; + for (let cxn of this.pending_cxns) { + cxn.element.style.opacity = 0.25; + } } } commit_press() { - // TODO - if (! this.pending_cxn) + if (this.pending_cxns.length === 0) return; - if (this.pending_target !== null) { - this.editor.set_custom_connection(this.pending_source, this.pending_target); + if (this.pending_target === null) { + this.abort_press(); + return; } - this.pending_cxn.element.remove(); - this.pending_cxn = null; + for (let src of this.pending_sources) { + this.editor.set_custom_connection(src, this.pending_target); + } + this.editor.commit_undo(); + if (this.pending_original_target !== null) { + // If we were moving a target, then the connections we were altering were real ones, so + // we need to clear them here to avoid having them removed in cleanup + this.pending_cxns = []; + } } abort_press() { - if (this.pending_cxn) { - this.pending_cxn.element.remove(); - this.pending_cxn = null; + if (this.pending_original_target !== null) { + // If we were moving a target, then the connections we were altering were real ones, so + // set them back to where they were + let [x, y] = this.editor.scalar_to_coords(this.pending_original_target); + for (let cxn of this.pending_cxns) { + cxn.element.style.opacity = ''; + cxn.set_dest(x, y); + } + this.pending_cxns = []; } } cleanup_press() { + for (let cxn of this.pending_cxns) { + cxn.element.remove(); + } + this.pending_cxns = []; + + this.hover_stale = true; + this.rehover(); + } + + handle_refresh() { + this.hover_stale = true; } do_destroy() { - this.connectable_cursor.remove(); + this.source_cursor.remove(); + this.target_cursor.remove(); super.do_destroy(); } } diff --git a/style.css b/style.css index c04961f..d7edac1 100644 --- a/style.css +++ b/style.css @@ -2313,6 +2313,9 @@ svg.level-editor-overlay g.overlay-connection { stroke: #e4e4e4; filter: url(#overlay-filter-outline); } +svg.level-editor-overlay g.overlay-connection.--cursor { + stroke: hsl(90, 90%, 40%); +} svg.level-editor-overlay g.overlay-connection[data-source=button_red] { stroke: hsl(0, 90%, 60%); }