Teach the connect tool to move destinations and delete connections
This commit is contained in:
parent
2fa84e0477
commit
b82e112cbc
@ -212,21 +212,16 @@ export const TOOLS = {
|
|||||||
connect: {
|
connect: {
|
||||||
icon: 'icons/tool-connect.png',
|
icon: 'icons/tool-connect.png',
|
||||||
name: "Connect",
|
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: [
|
desc: [
|
||||||
"Set up CC1-style clone and trap connections.",
|
"Set up CC1-style clone and trap connections.",
|
||||||
"(WIP)",
|
"(Supported in CC1 and LL, but not CC2!)",
|
||||||
"NOTE: Not supported in the real CC2!",
|
|
||||||
"",
|
"",
|
||||||
"[mouse2] Auto link using Lynx/CC2 rules",
|
"[mouse1] Connect a button to a mechanism",
|
||||||
//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] 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"),
|
].join("\n"),
|
||||||
uses_layers: false,
|
uses_layers: false,
|
||||||
op1: mouseops.ConnectOperation,
|
op1: mouseops.ConnectOperation,
|
||||||
|
|||||||
@ -1362,31 +1362,54 @@ export class ConnectOperation extends MouseOperation {
|
|||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
// This is the SVGConnection structure but with only the source circle
|
// This is the SVGConnection structure but with only the source circle
|
||||||
this.connectable_circle = mk_svg('circle.-source', {r: 0.5});
|
this.source_circle = mk_svg('circle.-source', {cx: 0.5, cy: 0.5, r: 0.5});
|
||||||
this.connectable_cursor = mk_svg('g.overlay-connection', this.connectable_circle);
|
this.source_cursor = mk_svg('g.overlay-connection.overlay-transient.--cursor', this.source_circle);
|
||||||
this.connectable_cursor.style.display = 'none';
|
this.target_square = mk_svg('rect.-target', {x: 0, y: 0, width: 1, height: 1});
|
||||||
// TODO how do i distinguish from existing ones
|
this.target_cursor = mk_svg('g.overlay-connection.overlay-transient.--cursor', this.target_square);
|
||||||
this.connectable_cursor.style.stroke = 'lime';
|
this.editor.svg_overlay.append(this.source_cursor, this.target_cursor);
|
||||||
this.editor.svg_overlay.append(this.connectable_cursor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_hover(client_x, client_y, frac_cell_x, frac_cell_y, cell_x, cell_y) {
|
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 cell = this.cell(cell_x, cell_y);
|
||||||
let terrain = cell[LAYERS.terrain];
|
let terrain = cell[LAYERS.terrain];
|
||||||
if (terrain.type.connects_to) {
|
if (terrain.type.connects_to) {
|
||||||
this.connectable_cursor.style.display = '';
|
this.source_cursor.classList.add('--visible');
|
||||||
this.connectable_circle.setAttribute('cx', cell_x + 0.5);
|
this.source_cursor.setAttribute('transform', `translate(${cell_x} ${cell_y})`);
|
||||||
this.connectable_circle.setAttribute('cy', cell_y + 0.5);
|
this.target_cursor.classList.remove('--visible');
|
||||||
}
|
}
|
||||||
else {
|
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) {
|
handle_press(x, y) {
|
||||||
// TODO restrict to button/cloner unless holding shift
|
// 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?
|
// 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 cell = this.cell(x, y);
|
||||||
let terrain = cell[LAYERS.terrain];
|
let terrain = cell[LAYERS.terrain];
|
||||||
if (this.alt_mode) {
|
if (this.alt_mode) {
|
||||||
@ -1395,104 +1418,142 @@ export class ConnectOperation extends MouseOperation {
|
|||||||
let other = null;
|
let other = null;
|
||||||
let swap = false;
|
let swap = false;
|
||||||
if (terrain.type.name === 'button_red') {
|
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') {
|
else if (terrain.type.name === 'cloner') {
|
||||||
other = this.search_for(src, 'button_red', -1);
|
other = this.search_for(cell, 'button_red', -1);
|
||||||
swap = true;
|
swap = true;
|
||||||
}
|
}
|
||||||
else if (terrain.type.name === 'button_brown') {
|
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') {
|
else if (terrain.type.name === 'trap') {
|
||||||
other = this.search_for(src, 'button_brown', -1);
|
other = this.search_for(cell, 'button_brown', -1);
|
||||||
swap = true;
|
swap = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (other !== null) {
|
if (other !== null) {
|
||||||
if (swap) {
|
if (swap) {
|
||||||
this.editor.set_custom_connection(other, src);
|
this.editor.set_custom_connection(other, pt);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.editor.set_custom_connection(src, other);
|
this.editor.set_custom_connection(pt, other);
|
||||||
}
|
}
|
||||||
this.editor.commit_undo();
|
this.editor.commit_undo();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, this is the start of a drag
|
// Otherwise, this is the start of a drag, which could be one of two things...
|
||||||
if (! terrain.type.connects_to)
|
if (terrain.type.connects_to) {
|
||||||
return;
|
// This is a source, and we're dragging to a destination
|
||||||
|
let cxn = new SVGConnection(x, y, x, y);
|
||||||
this.pending_cxn = new SVGConnection(x, y, x, y);
|
this.pending_cxns = [cxn];
|
||||||
this.pending_source = src;
|
this.pending_sources = [pt];
|
||||||
this.pending_type = terrain.type.name;
|
this.pending_type = terrain.type.name;
|
||||||
this.editor.svg_overlay.append(this.pending_cxn.element);
|
this.pending_original_target = null;
|
||||||
// Hide the normal cursor for the duration
|
this.editor.svg_overlay.append(cxn.element);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
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;
|
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);
|
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_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 {
|
else {
|
||||||
this.pending_target = null;
|
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() {
|
commit_press() {
|
||||||
// TODO
|
if (this.pending_cxns.length === 0)
|
||||||
if (! this.pending_cxn)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.pending_target !== null) {
|
if (this.pending_target === null) {
|
||||||
this.editor.set_custom_connection(this.pending_source, this.pending_target);
|
this.abort_press();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pending_cxn.element.remove();
|
for (let src of this.pending_sources) {
|
||||||
this.pending_cxn = null;
|
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() {
|
abort_press() {
|
||||||
if (this.pending_cxn) {
|
if (this.pending_original_target !== null) {
|
||||||
this.pending_cxn.element.remove();
|
// If we were moving a target, then the connections we were altering were real ones, so
|
||||||
this.pending_cxn = null;
|
// 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() {
|
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() {
|
do_destroy() {
|
||||||
this.connectable_cursor.remove();
|
this.source_cursor.remove();
|
||||||
|
this.target_cursor.remove();
|
||||||
super.do_destroy();
|
super.do_destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2313,6 +2313,9 @@ svg.level-editor-overlay g.overlay-connection {
|
|||||||
stroke: #e4e4e4;
|
stroke: #e4e4e4;
|
||||||
filter: url(#overlay-filter-outline);
|
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] {
|
svg.level-editor-overlay g.overlay-connection[data-source=button_red] {
|
||||||
stroke: hsl(0, 90%, 60%);
|
stroke: hsl(0, 90%, 60%);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user