diff --git a/README.md b/README.md index c8a88e7..419ced4 100644 --- a/README.md +++ b/README.md @@ -277,8 +277,10 @@ below. Nothing is installed without someone clicking. **Grid** stepper sets how many columns this browser cuts the board into — 8 to 24, 12 by default. Finer lets tiles be smaller so more fit across (a 4K wall), coarser suits a small screen read close up; row height follows, - so tiles keep their shape and simply change size. It saves with the - layout, so a named layout comes back at the density it was arranged on. + so tiles keep their shape and simply change size. Going coarser, a tile + that no longer fits where it was moves to the first free spot rather than + onto another tile. It saves with the layout, so a named layout comes back + at the density it was arranged on. - Every tile has a **status dot**: green = live, amber = connecting, red = upstream unreachable (hover it for details). Tiles reconnect automatically. diff --git a/public/shell.js b/public/shell.js index 79de9f2..67c49dc 100644 --- a/public/shell.js +++ b/public/shell.js @@ -296,8 +296,9 @@ function cellHeightFor(cols) { } /** Cut the board into `cols` columns. Tiles keep their grid units — the - board gets denser or coarser and everything on it changes size with it; - only a tile now past the right edge is pulled back in. */ + board gets denser or coarser and everything on it changes size with it. + A tile now past the right edge is pulled back in, and one that would then + sit on another moves to the first free spot instead. */ function setGridCols(value, { save = true } = {}) { const next = normalizeGridCols(value); const changed = next !== COLS; @@ -305,9 +306,16 @@ function setGridCols(value, { save = true } = {}) { grid.style.gridTemplateColumns = `repeat(${COLS}, minmax(0, 1fr))`; document.documentElement.style.setProperty('--cell-h', cellHeightFor(COLS) + 'px'); if (changed) { - for (const tile of tiles) { + // A coarser board may not have room for every tile where it was. Settle + // them in reading order: pull a tile past the right edge back in, and + // give one that would then land on a tile already settled the first free + // spot instead — tiles never overlap here, any more than under a drag. + const settled = []; + for (const tile of tiles.slice().sort((a, b) => a.y - b.y || a.x - b.x)) { tile.w = Math.max(1, Math.min(COLS, tile.w)); tile.x = Math.max(0, Math.min(COLS - tile.w, tile.x)); + if (settled.some((s) => overlaps(tile, s))) Object.assign(tile, spotAmong(tile.w, tile.h, settled)); + settled.push(tile); applyRect(tile); notifyResize(tile); } @@ -386,6 +394,17 @@ function isFree(rect, excludeId) { return isFreeExcept(rect, [excludeId]); } +/** First spot, scanning top-to-bottom, left-to-right, clear of these tiles. */ +function spotAmong(w, h, others) { + for (let y = 0; y < 400; y += 1) { + for (let x = 0; x <= COLS - w; x += 1) { + const rect = { x, y, w, h }; + if (!others.some((o) => overlaps(rect, o))) return { x, y }; + } + } + return { x: 0, y: 0 }; +} + /** First free spot scanning top-to-bottom, left-to-right. */ function findSpot(w, h) { for (let y = 0; y < 200; y += 1) {