Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
62 / 62 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
| Placement | |
100.00% |
62 / 62 |
|
100.00% |
6 / 6 |
31 | |
100.00% |
1 / 1 |
| flow | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
| place | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
| overlap | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| rows | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
6 | |||
| overlaps | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
| free | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Cosray\Block; |
| 6 | |
| 7 | /** |
| 8 | * Positions on a blocks grid. A block on the field's grid sits at the |
| 9 | * column and row it starts at; the reading order follows from the |
| 10 | * positions, row by row and left to right, and rows are kept in it. A block without a position |
| 11 | * (an import, a programmatic write) is placed below the placed ones, |
| 12 | * flowing in order the way the browser's sparse row flow places spans. |
| 13 | * The same flow gave the blocks of a grid saved before positions |
| 14 | * existed their place, indent included. |
| 15 | */ |
| 16 | final class Placement |
| 17 | { |
| 18 | /** |
| 19 | * Where the sparse row flow puts each block: at the first spot from |
| 20 | * the cursor where its indent and span fit, the box past the indent. |
| 21 | * Spans wider than the grid are narrowed to it. |
| 22 | * |
| 23 | * @param list<array{colspan: int, rowspan: int, indent?: int}> $spans |
| 24 | * @return list<array{col: int, row: int}> |
| 25 | */ |
| 26 | public static function flow(array $spans, int $columns): array |
| 27 | { |
| 28 | $columns = max(1, $columns); |
| 29 | $taken = []; |
| 30 | [$top, $left] = [0, 0]; |
| 31 | $result = []; |
| 32 | |
| 33 | foreach ($spans as $span) { |
| 34 | $colspan = max(1, min($columns, $span['colspan'])); |
| 35 | $rowspan = max(1, $span['rowspan']); |
| 36 | $width = min($columns, $colspan + max(0, $span['indent'] ?? 0)); |
| 37 | |
| 38 | for ($row = $top, $col = $left;; $row++, $col = 0) { |
| 39 | for (; ($col + $width) <= $columns; $col++) { |
| 40 | if (self::free($taken, $row, $col, $width, $rowspan)) { |
| 41 | break 2; |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | for ($r = $row; $r < ($row + $rowspan); $r++) { |
| 47 | for ($c = $col; $c < ($col + $width); $c++) { |
| 48 | $taken["{$r}:{$c}"] = true; |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | [$top, $left] = [$row, $col + $width]; |
| 53 | $result[] = ['col' => $col + $width - $colspan + 1, 'row' => $row + 1]; |
| 54 | } |
| 55 | |
| 56 | return $result; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Every layout with a position: placed ones keep theirs, the others |
| 61 | * flow below them in order. |
| 62 | * |
| 63 | * @param list<Layout> $layouts |
| 64 | * @return list<Layout> |
| 65 | */ |
| 66 | public static function place(array $layouts, int $columns): array |
| 67 | { |
| 68 | $bottom = 0; |
| 69 | $loose = []; |
| 70 | |
| 71 | foreach ($layouts as $index => $layout) { |
| 72 | if ($layout->placed()) { |
| 73 | $bottom = max($bottom, $layout->row + $layout->rowspan - 1); |
| 74 | } else { |
| 75 | $loose[$index] = ['colspan' => $layout->colspan, 'rowspan' => $layout->rowspan]; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | $spots = self::flow(array_values($loose), $columns); |
| 80 | |
| 81 | foreach (array_keys($loose) as $position => $index) { |
| 82 | $layouts[$index] = $layouts[$index]->at($spots[$position]['col'], $bottom + $spots[$position]['row']); |
| 83 | } |
| 84 | |
| 85 | return array_values($layouts); |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * The index of the first layout overlapping one before it, null when |
| 90 | * none do. Layouts without a position take no cells. |
| 91 | * |
| 92 | * @param list<Layout> $layouts |
| 93 | */ |
| 94 | public static function overlap(array $layouts): ?int |
| 95 | { |
| 96 | foreach ($layouts as $index => $layout) { |
| 97 | for ($other = 0; $other < $index; $other++) { |
| 98 | if (self::overlaps($layouts[$other], $layout)) { |
| 99 | return $index; |
| 100 | } |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | return null; |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * Stored rows with every top-level layout clamped to the grid and |
| 109 | * placed, in reading order; rows that are no arrays go last. |
| 110 | * |
| 111 | * @param array<array-key, mixed> $rows |
| 112 | * @return list<mixed> |
| 113 | */ |
| 114 | public static function rows(array $rows, int $columns, int $min): array |
| 115 | { |
| 116 | $rows = array_values($rows); |
| 117 | $layouts = []; |
| 118 | |
| 119 | foreach ($rows as $index => $row) { |
| 120 | if (is_array($row)) { |
| 121 | $layouts[$index] = Layout::normalize($row['layout'] ?? null, $columns, $min); |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | $placed = array_combine(array_keys($layouts), self::place(array_values($layouts), $columns)); |
| 126 | $order = array_keys($rows); |
| 127 | |
| 128 | usort($order, static fn(int $a, int $b): int => isset($placed[$a], $placed[$b]) |
| 129 | ? [$placed[$a]->row, $placed[$a]->col, $a] <=> [$placed[$b]->row, $placed[$b]->col, $b] |
| 130 | : [isset($placed[$b]), $a] <=> [isset($placed[$a]), $b]); |
| 131 | |
| 132 | return array_map(static function (int $index) use ($rows, $placed): mixed { |
| 133 | $row = $rows[$index]; |
| 134 | |
| 135 | if (isset($placed[$index]) && is_array($row)) { |
| 136 | $row['layout'] = $placed[$index]->array(); |
| 137 | } |
| 138 | |
| 139 | return $row; |
| 140 | }, $order); |
| 141 | } |
| 142 | |
| 143 | private static function overlaps(Layout $a, Layout $b): bool |
| 144 | { |
| 145 | return ( |
| 146 | $a->placed() |
| 147 | && $b->placed() |
| 148 | && $a->col < ($b->col + $b->colspan) |
| 149 | && $b->col < ($a->col + $a->colspan) |
| 150 | && $a->row < ($b->row + $b->rowspan) |
| 151 | && $b->row < ($a->row + $a->rowspan) |
| 152 | ); |
| 153 | } |
| 154 | |
| 155 | /** @param array<string, true> $taken */ |
| 156 | private static function free(array $taken, int $top, int $left, int $width, int $height): bool |
| 157 | { |
| 158 | for ($row = $top; $row < ($top + $height); $row++) { |
| 159 | for ($col = $left; $col < ($left + $width); $col++) { |
| 160 | if (isset($taken["{$row}:{$col}"])) { |
| 161 | return false; |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | return true; |
| 167 | } |
| 168 | } |