Push large tiles upwards back into the grid
This commit is contained in:
		
					parent
					
						
							
								cabad628b4
							
						
					
				
			
			
				commit
				
					
						8b8d6fd0e0
					
				
			
		
					 3 changed files with 199 additions and 19 deletions
				
			
		
							
								
								
									
										38
									
								
								src/array-utils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/array-utils.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2023 New Vector Ltd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Gets the index of the last element in the array to satsify the given
 | 
				
			||||||
 | 
					 * predicate.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					// TODO: remove this once TypeScript recognizes the existence of
 | 
				
			||||||
 | 
					// Array.prototype.findLastIndex
 | 
				
			||||||
 | 
					export function findLastIndex<T>(
 | 
				
			||||||
 | 
					  array: T[],
 | 
				
			||||||
 | 
					  predicate: (item: T) => boolean
 | 
				
			||||||
 | 
					): number | null {
 | 
				
			||||||
 | 
					  for (let i = array.length - 1; i >= 0; i--) {
 | 
				
			||||||
 | 
					    if (predicate(array[i])) return i;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Counts the number of elements in an array that satsify the given predicate.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const count = <T>(array: T[], predicate: (item: T) => boolean): number =>
 | 
				
			||||||
 | 
					  array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0);
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@ limitations under the License.
 | 
				
			||||||
import TinyQueue from "tinyqueue";
 | 
					import TinyQueue from "tinyqueue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { TileDescriptor } from "./TileDescriptor";
 | 
					import { TileDescriptor } from "./TileDescriptor";
 | 
				
			||||||
 | 
					import { count, findLastIndex } from "../array-utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * A 1×1 cell in a grid which belongs to a tile.
 | 
					 * A 1×1 cell in a grid which belongs to a tile.
 | 
				
			||||||
| 
						 | 
					@ -105,17 +106,6 @@ export function getPaths(dest: number, g: Grid): (number | null)[] {
 | 
				
			||||||
  return edges as (number | null)[];
 | 
					  return edges as (number | null)[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function findLastIndex<T>(
 | 
					 | 
				
			||||||
  array: T[],
 | 
					 | 
				
			||||||
  predicate: (item: T) => boolean
 | 
					 | 
				
			||||||
): number | null {
 | 
					 | 
				
			||||||
  for (let i = array.length - 1; i >= 0; i--) {
 | 
					 | 
				
			||||||
    if (predicate(array[i])) return i;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return null;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const findLast1By1Index = (g: Grid): number | null =>
 | 
					const findLast1By1Index = (g: Grid): number | null =>
 | 
				
			||||||
  findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1);
 | 
					  findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -209,11 +199,117 @@ function getNextGap(g: Grid): number | null {
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Gets the index of the origin of the tile to which the given cell belongs.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function getOrigin(g: Grid, index: number): number {
 | 
				
			||||||
 | 
					  const initialColumn = column(index, g);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (
 | 
				
			||||||
 | 
					    let i = index;
 | 
				
			||||||
 | 
					    i >= 0;
 | 
				
			||||||
 | 
					    i = column(i, g) === 0 ? i - g.columns + initialColumn : i - 1
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    const cell = g.cells[i];
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      cell !== undefined &&
 | 
				
			||||||
 | 
					      cell.origin &&
 | 
				
			||||||
 | 
					      inArea(index, i, areaEnd(i, cell.columns, cell.rows, g), g)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					      return i;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  throw new Error("Tile is broken");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Moves the tile at index "from" over to index "to", displacing other tiles
 | 
				
			||||||
 | 
					 * along the way.
 | 
				
			||||||
 | 
					 * Precondition: the destination area must consist of only 1×1 tiles.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function moveTile(g: Grid, from: number, to: number) {
 | 
				
			||||||
 | 
					  const tile = g.cells[from]!;
 | 
				
			||||||
 | 
					  const fromEnd = areaEnd(from, tile.columns, tile.rows, g);
 | 
				
			||||||
 | 
					  const toEnd = areaEnd(to, tile.columns, tile.rows, g);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const displacedTiles: Cell[] = [];
 | 
				
			||||||
 | 
					  forEachCellInArea(to, toEnd, g, (c, i) => {
 | 
				
			||||||
 | 
					    if (c !== undefined && !inArea(i, from, fromEnd, g)) displacedTiles.push(c);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const movingCells: Cell[] = [];
 | 
				
			||||||
 | 
					  forEachCellInArea(from, fromEnd, g, (c, i) => {
 | 
				
			||||||
 | 
					    movingCells.push(c!);
 | 
				
			||||||
 | 
					    g.cells[i] = undefined;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  forEachCellInArea(
 | 
				
			||||||
 | 
					    to,
 | 
				
			||||||
 | 
					    toEnd,
 | 
				
			||||||
 | 
					    g,
 | 
				
			||||||
 | 
					    (_c, i) => (g.cells[i] = movingCells.shift())
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  forEachCellInArea(
 | 
				
			||||||
 | 
					    from,
 | 
				
			||||||
 | 
					    fromEnd,
 | 
				
			||||||
 | 
					    g,
 | 
				
			||||||
 | 
					    (_c, i) => (g.cells[i] ??= displacedTiles.shift())
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Attempts to push a tile upwards by one row, displacing 1×1 tiles and shifting
 | 
				
			||||||
 | 
					 * enlarged tiles around when necessary.
 | 
				
			||||||
 | 
					 * @returns Whether the tile was actually pushed
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function pushTileUp(g: Grid, from: number): boolean {
 | 
				
			||||||
 | 
					  const tile = g.cells[from]!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // TODO: pushing large tiles sideways might be more successful in some
 | 
				
			||||||
 | 
					  // situations
 | 
				
			||||||
 | 
					  const cellsAboveAreDisplacable =
 | 
				
			||||||
 | 
					    from - g.columns >= 0 &&
 | 
				
			||||||
 | 
					    allCellsInArea(
 | 
				
			||||||
 | 
					      from - g.columns,
 | 
				
			||||||
 | 
					      from - g.columns + tile.columns - 1,
 | 
				
			||||||
 | 
					      g,
 | 
				
			||||||
 | 
					      (c, i) =>
 | 
				
			||||||
 | 
					        c === undefined ||
 | 
				
			||||||
 | 
					        (c.columns === 1 && c.rows === 1) ||
 | 
				
			||||||
 | 
					        pushTileUp(g, getOrigin(g, i))
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (cellsAboveAreDisplacable) {
 | 
				
			||||||
 | 
					    moveTile(g, from, from - g.columns);
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Backfill any gaps in the grid.
 | 
					 * Backfill any gaps in the grid.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function fillGaps(g: Grid): Grid {
 | 
					export function fillGaps(g: Grid): Grid {
 | 
				
			||||||
  const result: Grid = { ...g, cells: [...g.cells] };
 | 
					  const result: Grid = { ...g, cells: [...g.cells] };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // This will hopefully be the size of the grid after we're done here, assuming
 | 
				
			||||||
 | 
					  // that we can pack the large tiles tightly enough
 | 
				
			||||||
 | 
					  const idealLength = count(result.cells, (c) => c !== undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Step 1: Take any large tiles hanging off the bottom of the grid, and push
 | 
				
			||||||
 | 
					  // them upwards
 | 
				
			||||||
 | 
					  for (let i = result.cells.length - 1; i >= idealLength; i--) {
 | 
				
			||||||
 | 
					    const cell = result.cells[i];
 | 
				
			||||||
 | 
					    if (cell !== undefined && (cell.columns > 1 || cell.rows > 1)) {
 | 
				
			||||||
 | 
					      const originIndex =
 | 
				
			||||||
 | 
					        i - (cell.columns - 1) - result.columns * (cell.rows - 1);
 | 
				
			||||||
 | 
					      // If it's not possible to pack the large tiles any tighter, give up
 | 
				
			||||||
 | 
					      if (!pushTileUp(result, originIndex)) break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Step 2: Fill all 1×1 gaps
 | 
				
			||||||
  let gap = getNextGap(result);
 | 
					  let gap = getNextGap(result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (gap !== null) {
 | 
					  if (gap !== null) {
 | 
				
			||||||
| 
						 | 
					@ -263,9 +359,6 @@ export function fillGaps(g: Grid): Grid {
 | 
				
			||||||
    } while (gap !== null);
 | 
					    } while (gap !== null);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // TODO: If there are any large tiles on the last row, shuffle them back
 | 
					 | 
				
			||||||
  // upwards into a full row
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Shrink the array to remove trailing gaps
 | 
					  // Shrink the array to remove trailing gaps
 | 
				
			||||||
  const finalLength =
 | 
					  const finalLength =
 | 
				
			||||||
    (findLastIndex(result.cells, (c) => c !== undefined) ?? -1) + 1;
 | 
					    (findLastIndex(result.cells, (c) => c !== undefined) ?? -1) + 1;
 | 
				
			||||||
| 
						 | 
					@ -381,13 +474,18 @@ export function cycleTileSize(tileId: string, g: Grid): Grid {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Copy tiles from the original grid to the new one, with the new rows
 | 
					  // Copy tiles from the original grid to the new one, with the new rows
 | 
				
			||||||
  // inserted at the target location
 | 
					  // inserted at the target location
 | 
				
			||||||
  g.cells.forEach((c, src) => {
 | 
					  g.cells.forEach((c, from) => {
 | 
				
			||||||
    if (c?.origin && c.item.id !== tileId) {
 | 
					    if (c?.origin && c.item.id !== tileId) {
 | 
				
			||||||
      const offset =
 | 
					      const offset =
 | 
				
			||||||
        row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0;
 | 
					        row(from, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0;
 | 
				
			||||||
      forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => {
 | 
					      forEachCellInArea(
 | 
				
			||||||
 | 
					        from,
 | 
				
			||||||
 | 
					        areaEnd(from, c.columns, c.rows, g),
 | 
				
			||||||
 | 
					        g,
 | 
				
			||||||
 | 
					        (c, i) => {
 | 
				
			||||||
          gappyGrid.cells[i + offset] = c;
 | 
					          gappyGrid.cells[i + offset] = c;
 | 
				
			||||||
      });
 | 
					        }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -169,6 +169,50 @@ dddd
 | 
				
			||||||
iegh`
 | 
					iegh`
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					testFillGaps(
 | 
				
			||||||
 | 
					  "keeps a large tile from hanging off the bottom",
 | 
				
			||||||
 | 
					  `
 | 
				
			||||||
 | 
					abcd
 | 
				
			||||||
 | 
					efgh
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					ii  
 | 
				
			||||||
 | 
					ii`,
 | 
				
			||||||
 | 
					  `
 | 
				
			||||||
 | 
					abcd
 | 
				
			||||||
 | 
					iigh
 | 
				
			||||||
 | 
					iief`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					testFillGaps(
 | 
				
			||||||
 | 
					  "pushes a chain of large tiles upwards",
 | 
				
			||||||
 | 
					  `
 | 
				
			||||||
 | 
					abcd
 | 
				
			||||||
 | 
					e fg
 | 
				
			||||||
 | 
					hh  
 | 
				
			||||||
 | 
					hh  
 | 
				
			||||||
 | 
					 ii 
 | 
				
			||||||
 | 
					 ii`,
 | 
				
			||||||
 | 
					  `
 | 
				
			||||||
 | 
					hhcd
 | 
				
			||||||
 | 
					hhfg
 | 
				
			||||||
 | 
					aiib
 | 
				
			||||||
 | 
					eii`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					testFillGaps(
 | 
				
			||||||
 | 
					  "gives up on pushing large tiles upwards when not possible",
 | 
				
			||||||
 | 
					  `
 | 
				
			||||||
 | 
					aabb
 | 
				
			||||||
 | 
					aabb
 | 
				
			||||||
 | 
					cc  
 | 
				
			||||||
 | 
					cc`,
 | 
				
			||||||
 | 
					  `
 | 
				
			||||||
 | 
					aabb
 | 
				
			||||||
 | 
					aabb
 | 
				
			||||||
 | 
					cc  
 | 
				
			||||||
 | 
					cc`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function testCycleTileSize(
 | 
					function testCycleTileSize(
 | 
				
			||||||
  title: string,
 | 
					  title: string,
 | 
				
			||||||
  tileId: string,
 | 
					  tileId: string,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue