Distinguish regions by clicking on cell borders

41 views Asked by At

The code below has an issue identifying regions enclosed within a border.

If I surround all the "A" cells within a perimeter, it is identified as its own region; but if I enclose the "B" region first, it is not identified as its own region.

const COLOR_PALETTE = ['#FDD', '#DFD', '#DDF', '#FFD', '#DBF', '#BFD', '#FDB', '#BDF'];
const CLICK_DISTANCE_THRESHOLD = 5;
const DATA = [
  ['A', 'A', 'A', 'B'],
  ['C', 'C', 'A', 'B'],
  ['C', 'D', 'B', 'B'],
  ['C', 'D', 'D', 'D']
];

const $grid = $('#grid');

$grid.append($('<tbody>')
  .append(DATA.map((row, rowIndex) => $('<tr>')
    .append(row.map((val, colIndex) => $('<td>')
      .text(val).data('coords', [rowIndex, colIndex]))))));

$grid.on('click', 'td', onBorderClick);

function onBorderClick(event) {
    const $target = $(this);
  const rect = this.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  // Determine the closest border and toggle it
  if (x < CLICK_DISTANCE_THRESHOLD) {
    $target.toggleClass('selected-left');
    $target.prev().toggleClass('selected-right');
  } else if (x > rect.width - CLICK_DISTANCE_THRESHOLD) {
    $target.toggleClass('selected-right');
    $target.next().toggleClass('selected-left');
  } else if (y < CLICK_DISTANCE_THRESHOLD) {
    $target.toggleClass('selected-top');
    $target.parent().prev().children().eq($target.index()).toggleClass('selected-bottom');
  } else if (y > rect.height - CLICK_DISTANCE_THRESHOLD) {
    $target.toggleClass('selected-bottom');
    $target.parent().next().children().eq($target.index()).toggleClass('selected-top');
  }

  distinguishRegions();
}

function distinguishRegions() {
  // Clear previous regions
  $grid.find('td').css('background-color', '').removeData('region').removeAttr('data-region');
  let regionId = 0;
  $grid.find('td').each(function() {
    const $cell = $(this);
    if (!$cell.data('region')) {
      regionId++;
      floodFill($cell, regionId);
    }
  });
}

function floodFill($cell, regionId) {
  const queue = [$cell];
  while (queue.length > 0) {
    const $current = queue.shift();
    if ($current.length === 0 || $current.data('region')) continue;

    $current.data('region', regionId)
        .attr('data-region', regionId)
        .css('background-color', getRandomColor(regionId));

    const [r, c] = $current.data('coords');
    const neighbors = [
      !$current.hasClass('selected-top') && getCell(r - 1, c),
      !$current.hasClass('selected-bottom') && getCell(r + 1, c),
      !$current.hasClass('selected-left') && getCell(r, c - 1),
      !$current.hasClass('selected-right') && getCell(r, c + 1),
    ];

    queue.push(...neighbors.filter($n => $n && !$n.data('region')));
  }
}

function getCell(rowIndex, colIndex) {
  return $grid.find(`tr:eq(${rowIndex})`).find(`td:eq(${colIndex})`);
}

function getRandomColor(index) {
  return COLOR_PALETTE[index % COLOR_PALETTE.length];
}
table {
  border: 2px solid grey;
  border-collapse: collapse;
  cursor: pointer;
}

td {
  width: 2rem;
  height: 2rem;
  border: 2px dotted grey;
  text-align: center;
}

.selected-left { border-left: 2px solid red; }
.selected-right { border-right: 2px solid red; }
.selected-top { border-top: 2px solid red; }
.selected-bottom { border-bottom: 2px solid red; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<table id="grid"></table>

1

There are 1 answers

0
Mr. Polywhirl On

I ended up initializing the outer borders so that the entire grid begins as a single region.

const COLOR_PALETTE = ['#FDD', '#DFD', '#DDF', '#FFD', '#DBF', '#BFD', '#FDB', '#BDF'];
const CLICK_THRESHOLD = 5;
const SAMPLE_DATA = [
  ['A', 'A', 'A', 'B'],
  ['C', 'C', 'A', 'B'],
  ['C', 'D', 'B', 'B'],
  ['C', 'D', 'D', 'D']
];

(function($) {
  $.fn.fromMatrix = function(matrix) {
    return this
      .empty()
      .append($('<tbody>')
        .append(matrix.map((row, rowIndex) => $('<tr>')
          .append(row.map((val, colIndex) => $('<td>')
            .text(val))))));
  }
})(jQuery);

$('.grid').each(function() {
  const $grid = $(this).fromMatrix(SAMPLE_DATA);
  initializeBorders($grid);
});

$(document).on('click', '.grid td', onBorderClick);

function initializeBorders($table) {
    $table.find('tr').each(function(rowIndex, tr) {
    $(tr).find('td').each(function(colIndex, td) {
      const $cell = $(td).data('coords', [rowIndex, colIndex]);
      const neighbors = getNeighbors($table, rowIndex, colIndex);
      if (!neighbors.left.length) $cell.addClass('selected-left');
      if (!neighbors.right.length) $cell.addClass('selected-right');
      if (!neighbors.top.length) $cell.addClass('selected-top');
      if (!neighbors.bottom.length) $cell.addClass('selected-bottom');
    });
  });
}

function onBorderClick(event) {
  const $cell = $(this);
  const $table = $cell.closest('table');
  const { top, left, width, height } = this.getBoundingClientRect();
  const x = event.clientX - left, y = event.clientY - top;
  toggleBorder($cell, x, y, width, height);
  distinguishRegions($table);
}

function getNeighbors($table, rowIndex, colIndex) {
  return {
    top:  getCell($table, rowIndex - 1, colIndex),
    right: getCell($table, rowIndex, colIndex + 1),
    bottom: getCell($table, rowIndex + 1, colIndex),
    left: getCell($table, rowIndex, colIndex - 1)
  };
}

function toggleBorder($cell, x, y, width, height) {
  if (x < CLICK_THRESHOLD) toggleLeft($cell);
  else if (x > width - CLICK_THRESHOLD) toggleRight($cell);
  else if (y < CLICK_THRESHOLD) toggleTop($cell);
  else if (y > height - CLICK_THRESHOLD) toggleBottom($cell);
}

function toggleLeft($cell) {
  $cell.toggleClass('selected-left');
  $cell.prev().toggleClass('selected-right');
}

function toggleRight($cell) {
  $cell.toggleClass('selected-right');
  $cell.next().toggleClass('selected-left');
}

function toggleTop($cell) {
  $cell.toggleClass('selected-top');
  $cell.parent().prev().children().eq($cell.index()).toggleClass('selected-bottom');
}

function toggleBottom($cell) {
  $cell.toggleClass('selected-bottom');
  $cell.parent().next().children().eq($cell.index()).toggleClass('selected-top');
}

function distinguishRegions($table) {
  const $cells = $table.find('td');
  // Clear previous regions
  $cells.css('background-color', '').removeData('region').removeAttr('data-region');
  let regionId = 0;
  $cells.each(function() {
    const $cell = $(this);
    if (!$cell.data('region')) {
      regionId++;
      floodFill($table, $cell, regionId);
    }
  });
}

function floodFill($table, $cell, regionId) {
  const queue = [$cell];
  while (queue.length > 0) {
    const $current = queue.shift();
    if ($current.length === 0 || $current.data('region')) continue;

    $current.data('region', regionId)
      .attr('data-region', regionId)
      .css('background-color', getColorByRegion(regionId - 1));

    const [rowIndex, colIndex] = $current.data('coords');
    const neighbors = getNeighbors($table, rowIndex, colIndex);
    const adjacencies = [
      !$current.hasClass('selected-top') && neighbors.top,
      !$current.hasClass('selected-bottom') && neighbors.bottom,
      !$current.hasClass('selected-left') && neighbors.left,
      !$current.hasClass('selected-right') && neighbors.right,
    ];

    queue.push(...adjacencies.filter($n => $n && !$n.data('region')));
  }
}

function getCell($table, rowIndex, colIndex) {
  const $rows = $table.find('tr');
  if (rowIndex < 0 || rowIndex >= $rows.length) return $();
  const $cells = $rows.eq(rowIndex).find('td');
  if (colIndex < 0 || colIndex >= $cells.length) return $();
  return $cells.eq(colIndex);
}

function getColorByRegion(regionIndex) {
  return COLOR_PALETTE[regionIndex % COLOR_PALETTE.length];
}
html, body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2rem;
}

table {
  border-collapse: collapse;
  cursor: pointer;
}

td {
  width: 2rem;
  height: 2rem;
  border: 2px dotted grey;
  text-align: center;
}

.selected-left { border-left: 2px solid black; }
.selected-right { border-right: 2px solid black; }
.selected-top { border-top: 2px solid black; }
.selected-bottom { border-bottom: 2px solid black; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<table class="grid"></table>
<table class="grid"></table>
<table class="grid"></table>