Fix transforming selection + add more transforms
This commit is contained in:
parent
ed5f76221b
commit
2439048f59
@ -157,11 +157,12 @@ export class Selection {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Just recreate it from scratch to avoid mixing old and new properties
|
// Just recreate it from scratch to avoid mixing old and new properties
|
||||||
|
let new_x = Math.min(this.bbox.x, rect.x);
|
||||||
|
let new_y = Math.min(this.bbox.y, rect.y);
|
||||||
this.bbox = new DOMRect(
|
this.bbox = new DOMRect(
|
||||||
Math.min(this.bbox.x, rect.x),
|
new_x, new_y,
|
||||||
Math.min(this.bbox.y, rect.y),
|
Math.max(this.bbox.right, rect.right) - new_x,
|
||||||
Math.max(this.bbox.right, rect.right) - this.bbox.x,
|
Math.max(this.bbox.bottom, rect.bottom) - new_y);
|
||||||
Math.max(this.bbox.bottom, rect.bottom) - this.bbox.y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._update_outline();
|
this._update_outline();
|
||||||
@ -398,28 +399,18 @@ export class Selection {
|
|||||||
this.ring_element.classList.remove('--visible');
|
this.ring_element.classList.remove('--visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO only used internally in one place?
|
|
||||||
*iter_coords() {
|
|
||||||
let stored_level = this.editor.stored_level;
|
|
||||||
for (let n of this.cells) {
|
|
||||||
let [x, y] = stored_level.scalar_to_coords(n);
|
|
||||||
if (this.floated_offset) {
|
|
||||||
x += this.floated_offset[0];
|
|
||||||
y += this.floated_offset[1];
|
|
||||||
}
|
|
||||||
yield [x, y, n];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert this selection into a floating selection, plucking all the selected cells from the
|
// Convert this selection into a floating selection, plucking all the selected cells from the
|
||||||
// level and replacing them with blank cells.
|
// level and replacing them with blank cells.
|
||||||
enfloat(copy = false) {
|
enfloat(copy = false) {
|
||||||
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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let floated_cells = new Map;
|
let floated_cells = new Map;
|
||||||
let stored_level = this.editor.stored_level;
|
let stored_level = this.editor.stored_level;
|
||||||
for (let [x, y, n] of this.iter_coords()) {
|
for (let n of this.cells) {
|
||||||
|
let [x, y] = stored_level.scalar_to_coords(n);
|
||||||
let cell = stored_level.linear_cells[n];
|
let cell = stored_level.linear_cells[n];
|
||||||
if (copy) {
|
if (copy) {
|
||||||
floated_cells.set(n, cell.map(tile => tile ? {...tile} : null));
|
floated_cells.set(n, cell.map(tile => tile ? {...tile} : null));
|
||||||
@ -441,38 +432,33 @@ export class Selection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create floated_canvas and floated_element, based on floated_cells
|
// Create floated_canvas and floated_element, based on floated_cells, or update them if they
|
||||||
|
// already exist
|
||||||
_init_floated_canvas() {
|
_init_floated_canvas() {
|
||||||
let tileset = this.editor.renderer.tileset;
|
let tileset = this.editor.renderer.tileset;
|
||||||
this.floated_canvas = mk('canvas', {
|
if (! this.floated_canvas) {
|
||||||
width: this.bbox.width * tileset.size_x,
|
this.floated_canvas = mk('canvas');
|
||||||
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_canvas.width = this.bbox.width * tileset.size_x;
|
||||||
|
this.floated_canvas.height = this.bbox.height * tileset.size_y;
|
||||||
|
this.redraw();
|
||||||
|
|
||||||
|
if (! this.floated_element) {
|
||||||
this.floated_element = mk_svg('g', mk_svg('foreignObject', {
|
this.floated_element = mk_svg('g', mk_svg('foreignObject', {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: this.floated_canvas.width,
|
|
||||||
height: this.floated_canvas.height,
|
|
||||||
transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`,
|
transform: `scale(${1/tileset.size_x} ${1/tileset.size_y})`,
|
||||||
}, this.floated_canvas));
|
}, 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 goes first, so the selection ring still appears on top
|
||||||
this.selection_group.prepend(this.floated_element);
|
this.selection_group.prepend(this.floated_element);
|
||||||
}
|
}
|
||||||
|
let foreign = this.floated_element.querySelector('foreignObject');
|
||||||
|
foreign.setAttribute('width', this.floated_canvas.width);
|
||||||
|
foreign.setAttribute('height', this.floated_canvas.height);
|
||||||
|
|
||||||
|
// The canvas only covers our bbox, so it needs to start where the bbox does
|
||||||
|
this.floated_element.setAttribute('transform', `translate(${this.bbox.x} ${this.bbox.y})`);
|
||||||
|
}
|
||||||
|
|
||||||
stamp_float(copy = false) {
|
stamp_float(copy = false) {
|
||||||
if (! this.floated_element)
|
if (! this.floated_element)
|
||||||
@ -545,6 +531,65 @@ export class Selection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modifies the cells (and their arrangement) within a floating selection
|
||||||
|
_rearrange_cells(original_width, convert_coords, upgrade_tile) {
|
||||||
|
if (! this.floated_cells)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let new_cells = new Set;
|
||||||
|
let new_floated_cells = new Map;
|
||||||
|
let w = this.editor.stored_level.size_x;
|
||||||
|
let h = this.editor.stored_level.size_y;
|
||||||
|
for (let n of this.cells) {
|
||||||
|
// Alas this needs manually computing since the level may have changed size
|
||||||
|
let x = n % original_width;
|
||||||
|
let y = Math.floor(n / original_width);
|
||||||
|
let [x2, y2] = convert_coords(x, y, w, h);
|
||||||
|
let n2 = x2 + w * y2;
|
||||||
|
let cell = this.floated_cells.get(n);
|
||||||
|
cell.x = x2;
|
||||||
|
cell.y = y2;
|
||||||
|
for (let tile of cell) {
|
||||||
|
if (tile) {
|
||||||
|
upgrade_tile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_cells.add(n2);
|
||||||
|
new_floated_cells.set(n2, cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the old and new centers of the bboxes so the transform can be center-relative
|
||||||
|
let [cx0, cy0] = convert_coords(
|
||||||
|
Math.floor(this.bbox.x + this.bbox.width / 2),
|
||||||
|
Math.floor(this.bbox.y + this.bbox.height / 2),
|
||||||
|
w, h);
|
||||||
|
|
||||||
|
// Alter the bbox by just transforming two opposite corners
|
||||||
|
let [x1, y1] = convert_coords(this.bbox.left, this.bbox.top, w, h);
|
||||||
|
let [x2, y2] = convert_coords(this.bbox.right - 1, this.bbox.bottom - 1, w, h);
|
||||||
|
let xs = [x1, x2];
|
||||||
|
let ys = [y1, y2];
|
||||||
|
xs.sort((a, b) => a - b);
|
||||||
|
ys.sort((a, b) => a - b);
|
||||||
|
this.bbox = new DOMRect(xs[0], ys[0], xs[1] - xs[0] + 1, ys[1] - ys[0] + 1);
|
||||||
|
|
||||||
|
// Now make it center-relative by shifting the offsets
|
||||||
|
let [cx1, cy1] = convert_coords(
|
||||||
|
Math.floor(this.bbox.x + this.bbox.width / 2),
|
||||||
|
Math.floor(this.bbox.y + this.bbox.height / 2),
|
||||||
|
w, h);
|
||||||
|
this.floated_offset[0] += cx1 - cx0;
|
||||||
|
this.floated_offset[1] += cy1 - cy0;
|
||||||
|
this._update_floating_transform();
|
||||||
|
|
||||||
|
// No need for undo; this is undone by performing the reverse operation
|
||||||
|
this.cells = new_cells;
|
||||||
|
this.floated_cells = new_floated_cells;
|
||||||
|
this._init_floated_canvas();
|
||||||
|
|
||||||
|
this._update_outline();
|
||||||
|
}
|
||||||
|
|
||||||
_delete_floating() {
|
_delete_floating() {
|
||||||
this.selection_group.removeAttribute('transform');
|
this.selection_group.removeAttribute('transform');
|
||||||
this.ring_element.classList.remove('--floating');
|
this.ring_element.classList.remove('--floating');
|
||||||
@ -561,20 +606,22 @@ export class Selection {
|
|||||||
if (! this.floated_canvas)
|
if (! this.floated_canvas)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// FIXME uhoh, how do i actually do this? we have no renderer of our own, we have a
|
let ctx = this.floated_canvas.getContext('2d');
|
||||||
// separate canvas, and all the renderer stuff expects to get ahold of a level. i guess
|
for (let n of this.cells) {
|
||||||
// refactor it to draw a block of cells?
|
let [x, y] = this.editor.stored_level.scalar_to_coords(n);
|
||||||
this.editor.renderer.draw_static_generic({
|
this.editor.renderer.draw_static_generic({
|
||||||
x0: 0, y0: 0,
|
// Incredibly stupid hack for just drawing one cell
|
||||||
x1: this.rect.width, y1: this.rect.height,
|
x0: 0, x1: 0,
|
||||||
// FIXME this is now a set, not a flat list
|
y0: 0, y1: 0,
|
||||||
cells: this.floated_cells,
|
width: 1,
|
||||||
width: this.rect.width,
|
cells: [this.floated_cells.get(n)],
|
||||||
ctx: this.floated_canvas.getContext('2d'),
|
ctx: ctx,
|
||||||
|
destx: x - this.bbox.left,
|
||||||
|
desty: y - this.bbox.top,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
// TODO make more stuff respect this (more things should go through Editor for undo reasons anyway)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -455,18 +455,27 @@ export class Editor extends PrimaryView {
|
|||||||
this.redo();
|
this.redo();
|
||||||
});
|
});
|
||||||
let edit_items = [
|
let edit_items = [
|
||||||
["Rotate CCW", () => {
|
["Rotate left", () => {
|
||||||
this.rotate_level_left();
|
this.rotate_level_left();
|
||||||
}],
|
}],
|
||||||
["Rotate CW", () => {
|
["Rotate right", () => {
|
||||||
this.rotate_level_right();
|
this.rotate_level_right();
|
||||||
}],
|
}],
|
||||||
["Mirror", () => {
|
["Rotate 180°", () => {
|
||||||
|
this.rotate_level_right();
|
||||||
|
}],
|
||||||
|
["Mirror horizontally", () => {
|
||||||
this.mirror_level();
|
this.mirror_level();
|
||||||
}],
|
}],
|
||||||
["Flip", () => {
|
["Flip vertically", () => {
|
||||||
this.flip_level();
|
this.flip_level();
|
||||||
}],
|
}],
|
||||||
|
["Pivot around main diagonal", () => {
|
||||||
|
this.pivot_level_main();
|
||||||
|
}],
|
||||||
|
["Pivot around anti diagonal", () => {
|
||||||
|
this.pivot_level_anti();
|
||||||
|
}],
|
||||||
];
|
];
|
||||||
this.edit_menu = new MenuOverlay(
|
this.edit_menu = new MenuOverlay(
|
||||||
this.conductor,
|
this.conductor,
|
||||||
@ -1324,12 +1333,29 @@ export class Editor extends PrimaryView {
|
|||||||
return this._transform_tile(
|
return this._transform_tile(
|
||||||
tile, include_faux_adjustments ? 'adjust_forward' : null, 'rotate_right', 'right');
|
tile, include_faux_adjustments ? 'adjust_forward' : null, 'rotate_right', 'right');
|
||||||
}
|
}
|
||||||
|
rotate_tile_180(tile) {
|
||||||
|
let changed = this.rotate_tile_right(tile);
|
||||||
|
changed ||= this.rotate_tile_right(tile);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
mirror_tile(tile) {
|
mirror_tile(tile) {
|
||||||
return this._transform_tile(tile, null, 'mirror', 'mirrored');
|
return this._transform_tile(tile, null, 'mirror', 'mirrored');
|
||||||
}
|
}
|
||||||
flip_tile(tile) {
|
flip_tile(tile) {
|
||||||
return this._transform_tile(tile, null, 'flip', 'flipped');
|
return this._transform_tile(tile, null, 'flip', 'flipped');
|
||||||
}
|
}
|
||||||
|
pivot_tile_main(tile) {
|
||||||
|
// A flip along the main diagonal is equivalent to a right turn, then a horizontal mirror
|
||||||
|
let changed = this.rotate_tile_right(tile);
|
||||||
|
changed ||= this.mirror_tile(tile);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
pivot_tile_anti(tile) {
|
||||||
|
// A flip along the anti-diagonal is equivalent to a left turn, then a horizontal mirror
|
||||||
|
let changed = this.rotate_tile_left(tile);
|
||||||
|
changed ||= this.mirror_tile(tile);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
rotate_palette_left() {
|
rotate_palette_left() {
|
||||||
this.palette_rotation_index += 1;
|
this.palette_rotation_index += 1;
|
||||||
@ -1561,50 +1587,43 @@ export class Editor extends PrimaryView {
|
|||||||
// (These don't save undo entries for individual tiles, either, because they're expected to be
|
// (These don't save undo entries for individual tiles, either, because they're expected to be
|
||||||
// completely reversible, and undo is done by performing the opposite transform rather than
|
// completely reversible, and undo is done by performing the opposite transform rather than
|
||||||
// reloading a copy of a previous state.)
|
// reloading a copy of a previous state.)
|
||||||
_rearrange_cells(swap_dimensions, downgrade_coords, upgrade_tile) {
|
_rearrange_cells(swap_dimensions, convert_coords, upgrade_tile) {
|
||||||
let old_cells, old_w;
|
|
||||||
let w, h;
|
|
||||||
let new_cells = [];
|
let new_cells = [];
|
||||||
if (this.selection.is_empty) {
|
let w = this.stored_level.size_x;
|
||||||
// Do it to the whole level
|
let h = this.stored_level.size_y;
|
||||||
w = this.stored_level.size_x;
|
let old_w = w;
|
||||||
h = this.stored_level.size_y;
|
let old_h = h;
|
||||||
old_w = w;
|
|
||||||
if (swap_dimensions) {
|
if (swap_dimensions) {
|
||||||
[w, h] = [h, w];
|
[w, h] = [h, w];
|
||||||
this.stored_level.size_x = w;
|
this.stored_level.size_x = w;
|
||||||
this.stored_level.size_y = h;
|
this.stored_level.size_y = h;
|
||||||
}
|
}
|
||||||
old_cells = this.stored_level.linear_cells;
|
|
||||||
this.stored_level.linear_cells = new_cells;
|
if (! this.selection.is_empty) {
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Do it to the selection
|
// Do it to the selection
|
||||||
w = this.selection.rect.width;
|
this.selection._rearrange_cells(old_w, convert_coords, upgrade_tile);
|
||||||
h = this.selection.rect.height;
|
return;
|
||||||
old_w = w;
|
|
||||||
if (swap_dimensions) {
|
|
||||||
[w, h] = [h, w];
|
|
||||||
// FIXME this will need more interesting rearranging
|
|
||||||
this.selection._set_from_rect(new DOMRect(
|
|
||||||
this.selection.rect.x, this.selection.rect.y, w, h));
|
|
||||||
}
|
|
||||||
old_cells = this.selection.floated_cells;
|
|
||||||
this.selection.floated_cells = new_cells;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let y = 0; y < h; y++) {
|
let old_cells = this.stored_level.linear_cells;
|
||||||
for (let x = 0; x < w; x++) {
|
for (let y = 0; y < old_h; y++) {
|
||||||
let [old_x, old_y] = downgrade_coords(x, y, w, h);
|
for (let x = 0; x < old_w; x++) {
|
||||||
let cell = old_cells[old_y * old_w + old_x];
|
let [x2, y2] = convert_coords(x, y, w, h);
|
||||||
|
let cell = old_cells[y * old_w + x];
|
||||||
for (let tile of cell) {
|
for (let tile of cell) {
|
||||||
if (tile) {
|
if (tile) {
|
||||||
upgrade_tile(tile);
|
upgrade_tile(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new_cells.push(cell);
|
let n2 = this.stored_level.coords_to_scalar(x2, y2);
|
||||||
|
if (new_cells[n2]) {
|
||||||
|
console.error("Tile transformation overwriting the same cell twice:", x2, y2);
|
||||||
|
}
|
||||||
|
new_cells[n2] = cell;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.stored_level.linear_cells = new_cells;
|
||||||
}
|
}
|
||||||
|
|
||||||
rotate_level_right() {
|
rotate_level_right() {
|
||||||
@ -1622,6 +1641,11 @@ export class Editor extends PrimaryView {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
rotate_level_180() {
|
rotate_level_180() {
|
||||||
|
this._do_transform(
|
||||||
|
false,
|
||||||
|
() => this._rotate_level_180(),
|
||||||
|
() => this._rotate_level_180(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
mirror_level() {
|
mirror_level() {
|
||||||
this._do_transform(
|
this._do_transform(
|
||||||
@ -1637,6 +1661,20 @@ export class Editor extends PrimaryView {
|
|||||||
() => this._flip_level(),
|
() => this._flip_level(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
pivot_level_main() {
|
||||||
|
this._do_transform(
|
||||||
|
true,
|
||||||
|
() => this._pivot_level_main(),
|
||||||
|
() => this._pivot_level_main(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pivot_level_anti() {
|
||||||
|
this._do_transform(
|
||||||
|
true,
|
||||||
|
() => this._pivot_level_anti(),
|
||||||
|
() => this._pivot_level_anti(),
|
||||||
|
);
|
||||||
|
}
|
||||||
_do_transform(affects_size, redo, undo) {
|
_do_transform(affects_size, redo, undo) {
|
||||||
// FIXME apply transform to connections if appropriate, somehow, ?? i don't even know how
|
// FIXME apply transform to connections if appropriate, somehow, ?? i don't even know how
|
||||||
// those interact with floating selection yet :S
|
// those interact with floating selection yet :S
|
||||||
@ -1656,36 +1694,36 @@ export class Editor extends PrimaryView {
|
|||||||
this.commit_undo();
|
this.commit_undo();
|
||||||
}
|
}
|
||||||
_post_transform_cleanup(affects_size) {
|
_post_transform_cleanup(affects_size) {
|
||||||
if (this.selection.is_empty) {
|
// The selection takes care of redrawing itself
|
||||||
if (affects_size) {
|
if (! this.selection.is_empty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// We do basically the same work regardless of whether the size changed, so just do it
|
||||||
this.update_after_size_change();
|
this.update_after_size_change();
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
this.redraw_entire_level();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// FIXME what if it affects size?
|
|
||||||
this.selection.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO mirror diagonally?
|
|
||||||
|
|
||||||
// Internal-use versions of the above. These DO NOT create undo entries.
|
// Internal-use versions of the above. These DO NOT create undo entries.
|
||||||
_rotate_level_left() {
|
_rotate_level_left() {
|
||||||
this._rearrange_cells(
|
this._rearrange_cells(
|
||||||
true,
|
true,
|
||||||
(x, y, w, h) => [h - 1 - y, x],
|
(x, y, w, h) => [y, w - 1 - x],
|
||||||
tile => this.rotate_tile_left(tile, false),
|
tile => this.rotate_tile_left(tile, false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_rotate_level_right() {
|
_rotate_level_right() {
|
||||||
this._rearrange_cells(
|
this._rearrange_cells(
|
||||||
true,
|
true,
|
||||||
(x, y, w, h) => [y, w - 1 - x],
|
(x, y, w, h) => [h - 1 - y, x],
|
||||||
tile => this.rotate_tile_right(tile, false),
|
tile => this.rotate_tile_right(tile, false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
_rotate_level_180() {
|
||||||
|
this._rearrange_cells(
|
||||||
|
true,
|
||||||
|
(x, y, w, h) => [w - 1 - x, h - 1 - y],
|
||||||
|
tile => this.rotate_tile_180(tile, false),
|
||||||
|
);
|
||||||
|
}
|
||||||
_mirror_level() {
|
_mirror_level() {
|
||||||
this._rearrange_cells(
|
this._rearrange_cells(
|
||||||
false,
|
false,
|
||||||
@ -1700,6 +1738,20 @@ export class Editor extends PrimaryView {
|
|||||||
tile => this.flip_tile(tile),
|
tile => this.flip_tile(tile),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
_pivot_level_main() {
|
||||||
|
this._rearrange_cells(
|
||||||
|
true,
|
||||||
|
(x, y, w, h) => [y, x],
|
||||||
|
tile => this.pivot_tile_main(tile),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_pivot_level_anti() {
|
||||||
|
this._rearrange_cells(
|
||||||
|
true,
|
||||||
|
(x, y, w, h) => [w - 1 - y, h - 1 - x],
|
||||||
|
tile => this.pivot_tile_anti(tile),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Create a connection between two cells and update the UI accordingly. If dest is null or
|
// Create a connection between two cells and update the UI accordingly. If dest is null or
|
||||||
// undefined, delete any existing connection instead.
|
// undefined, delete any existing connection instead.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user