geometry.py
5.9 kB · python · 154 lines
1import numpy as np2from copy import deepcopy3from typing import List, TYPE_CHECKING4from mrlypy.core.errors import MrlyError56if TYPE_CHECKING:7 from .models import Cell2d89# PUBLIC - IMMUTABLE1011def merge_2d(cells: List["Cell2d"], width: int, height: int) -> "Cell2d":12 from .models import Cell2d13 if not cells:14 raise MrlyError("Cannot merge an empty list of cells.")15 if len(cells) != width * height:16 raise MrlyError(f"Expected {width * height} cells, got {len(cells)}")17 first_cell = cells[0]18 cell_width, cell_height = first_cell.width, first_cell.height19 total_width = width * cell_width20 total_height = height * cell_height21 new_cell = Cell2d(width=total_width, height=total_height)22 for i, cell in enumerate(cells):23 if cell.width != cell_width or cell.height != cell_height:24 raise MrlyError("All cells in a merge operation must have the same dimensions.")25 x = i % width26 y = i // width27 start_x = x * cell_width28 end_x = start_x + cell_width29 start_y = y * cell_height30 end_y = start_y + cell_height31 if cell._types is not None:32 new_cell.types[start_y:end_y, start_x:end_x] = cell.types33 if cell._colors is not None:34 new_cell.colors[start_y:end_y, start_x:end_x] = cell.colors35 if cell._tags is not None:36 new_cell.tags[start_y:end_y, start_x:end_x] = cell.tags37 return new_cell3839def combine_2d(cell_1: "Cell2d", cell_2: "Cell2d") -> "Cell2d":40 from .models import Cell2d41 new_types = np.kron(cell_1.types, cell_2.types).astype(np.uint8)42 return Cell2d(types=new_types)4344def magic_2d(cells: List["Cell2d"]) -> "Cell2d":45 if len(cells) < 2:46 raise MrlyError("Magic composition requires at least two cells.")47 new_cell = combine_2d(cells[0], cells[1])48 for i in range(2, len(cells)):49 new_cell = combine_2d(new_cell, cells[i])50 return new_cell5152def special_2d(mask: np.ndarray, cell: "Cell2d") -> "Cell2d":53 height, width = mask.shape54 new_cells = []55 for y in range(height):56 for x in range(width):57 rotation_index = mask[y, x]58 if not (0 <= rotation_index <= 3):59 raise MrlyError(f"Invalid rotation value '{rotation_index}'. Must be 0, 1, 2, or 3.")60 new_cell = cell.copy().rotate(rotation_index)61 new_cells.append(new_cell)62 return merge_2d(new_cells, width, height)6364def mosaic_2d(mask: np.ndarray, cells: List["Cell2d"]) -> "Cell2d":65 height, width = mask.shape66 new_cells = []67 for y in range(height):68 for x in range(width):69 cell_index = mask[y, x]70 new_cell = cells[cell_index].copy()71 new_cells.append(new_cell)72 return merge_2d(new_cells, width, height)7374# PRIVATE - MUTABLE7576def invert_2d(cell: "Cell2d") -> "Cell2d":77 if cell._types is not None:78 cell.types = 1 - cell.types79 return cell8081def pad_2d(cell: "Cell2d", count: int = 1, value: int = 0) -> "Cell2d":82 if cell._types is not None:83 cell.types = np.pad(cell.types, count, mode="constant", constant_values=value)84 if cell._colors is not None:85 cell.colors = np.pad(cell.colors, count, mode="constant", constant_values=value)86 if cell._tags is not None:87 cell.tags = np.pad(cell.tags, count, mode="constant", constant_values=value)88 return cell8990def rotate_2d(cell: "Cell2d", k: int = 1) -> "Cell2d":91 if k % 4 == 0:92 return cell93 if cell._types is not None:94 cell.types = np.rot90(cell.types, k)95 if cell._colors is not None:96 cell.colors = np.rot90(cell.colors, k, axes=(1, 0))97 if cell._tags is not None:98 cell.tags = np.rot90(cell.tags, k)99 return cell100101def fractal_2d(cell: "Cell2d", level: int) -> "Cell2d":102 if level < 1:103 raise MrlyError("Fractal level must be at least 1.")104 if level == 1:105 return cell106 new_types = cell.types107 for _ in range(1, level):108 new_types = np.kron(new_types, cell.types)109 cell.types = new_types.astype(np.uint8)110 cell.colors = None111 cell.tags = None112 return cell113114def tile_2d(cell: "Cell2d", width: int, height: int) -> "Cell2d":115 if cell._types is not None:116 cell.types = np.tile(cell.types, (height, width))117 if cell._colors is not None:118 cell.colors = np.tile(cell.colors, (height, width, 1))119 if cell._tags is not None:120 cell.tags = np.tile(cell.tags, (height, width))121 return cell122123def layers_2d(cell: "Cell2d", dtype: np.dtype = np.dtype(np.uint8)) -> "Cell2d":124 height, width = cell.height, cell.width125 y_indices, x_indices = np.indices((height, width))126 center_y = (height - 1) / 2127 center_x = (width - 1) / 2128 distance_y = np.floor(np.abs(y_indices - center_y))129 distance_x = np.floor(np.abs(x_indices - center_x))130 tags = np.maximum(distance_x, distance_y)131 cell.tags = tags.astype(dtype)132 return cell133134def neighbors_2d(cell: "Cell2d", mask: np.ndarray, target: int = 1, mode: str = "constant", dtype: np.dtype = np.dtype(np.uint8)) -> "Cell2d":135 mask_height, mask_width = mask.shape136 if mask_height % 2 == 0 or mask_width % 2 == 0:137 raise MrlyError("Neighborhood (mask) dimensions must be odd.")138 if target not in [0, 1]:139 raise MrlyError("Bit to count (target) must be 0 or 1.")140 bit = (cell.types == target).astype(dtype)141 if mode not in ["constant", "wrap"]:142 raise MrlyError("Boundary (mode) must be 'constant' or 'wrap'.")143 py = mask_height // 2144 px = mask_width // 2145 pad_bits = np.pad(bit, pad_width=((py, py), (px, px)), mode=mode)146 neighbor_counts = np.zeros_like(cell.types, dtype=dtype)147 for r in range(mask_height):148 for c in range(mask_width):149 if mask[r, c] == 1:150 start_row, end_row = r, r + cell.types.shape[0]151 start_col, end_col = c, c + cell.types.shape[1]152 neighbor_counts += pad_bits[start_row:end_row, start_col:end_col]153 cell.tags = neighbor_counts.astype(dtype)154 return cell