Source code for brickalize.model

# brickalize/model.py
"""
Defines the BrickModel class, which represents the assembled structure
of bricks in layers.
"""

# Import necessary classes from other modules within the package
from .bricks import Brick   # To access its default height constant `Brick.height`

# No other external libraries like numpy, open3d, etc., are directly used
# within the BrickModel class logic itself. They are used by classes
# that operate on or visualize the BrickModel.


[docs] class BrickModel:
[docs] def __init__(self, layer_height:float = Brick.height): """ Initializes an empty brick model. Args: layer_height (float): Raises: TypeError: if layer_height is not a float. ValueError: if the layer_height is not larger than 0 """ self.__verify_inputs(layer_height) self.__layers = {} self.__layer_height = layer_height
def __verify_inputs(self, layer_height:float): if not isinstance(layer_height, float): raise TypeError("Layer height must be a float.") if layer_height <= 0: raise ValueError("Layer height must be non-zero and positive.") @property def layer_height(self) -> float: """The height of each layer in the model.""" return self.__layer_height @property def layers(self) -> dict[int, list[dict]]: """A dictionary containing all layers and the bricks in each layer.""" return self.__layers.copy() @property def is_empty(self) -> bool: """bool: True if the model contains no bricks, False otherwise.""" return not bool(self.__layers) # Check if the layers dictionary is empty @property def size(self) -> tuple[int, int, int]: """The size of the model in bricks (x, y, z).""" if len(self.__layers) == 0: return (0, 0, 0) x_min, y_min, z_min = self.min x_max, y_max, z_max = self.max return (int(x_max - x_min), int(y_max - y_min), int(z_max - z_min + 1)) @property def min(self) -> tuple[int, int, int]: """The minimum x, y, z coordinates of the model in bricks.""" if len(self.__layers) == 0: return (0, 0, 0) x_min = min([min([brick["position"][0] for brick in layer]) for layer in self.__layers.values()]) y_min = min([min([brick["position"][1] for brick in layer]) for layer in self.__layers.values()]) z_min = min(self.__layers.keys()) return (x_min, y_min, z_min) @property def max(self) -> tuple[int, int, int]: """The maximum x, y, z coordinates of the model in bricks.""" if len(self.__layers) == 0: return (0, 0, 0) x_max = max([max([brick["position"][0] + brick["size"][0] for brick in layer]) for layer in self.__layers.values()]) y_max = max([max([brick["position"][1] + brick["size"][1] for brick in layer]) for layer in self.__layers.values()]) z_max = max(self.__layers.keys()) return (x_max, y_max, z_max)
[docs] def place_brick(self, oriented_brick:tuple[tuple[int, int], bool], position:tuple[int, int, int]) -> bool: """ Attempt to place a brick in the model at a specified position. Args: oriented_brick (tuple[tuple[int, int], bool]): tuple containing a tuple with the size in x and y direction and a bool that is True if the brick is support. ((x, y), is_support) position (tuple[int, int, int]): The x, y and z coordinates, for the left, front, bottom corner, respectively Returns: bool: Whether the placement was succesful or not Raises: TypeError: If the inputs are invalid """ # Verify the inputs if not isinstance(oriented_brick, tuple) and not isinstance(oriented_brick, list): raise TypeError("oriented_brick must be a tuple or list.") if (not isinstance(oriented_brick[0], tuple) and not isinstance(oriented_brick[0], list)) or not isinstance(oriented_brick[0][0], int) or not isinstance(oriented_brick[0][1], int): raise TypeError("oriented_brick must contain the size in (x,y) format at index 0 (tuple[int]).") if (not isinstance(position, tuple) and not isinstance(position, list)) or not isinstance(position[0], int) or not isinstance(position[1], int) or not isinstance(position[2], int): raise TypeError("position must contain the coordinates in (x,y,z) format (tuple[int]).") if not isinstance(oriented_brick[1], bool): raise TypeError("oriented_brick must contain a bool at index 1.") z = position[2] positioned_brick = {"size":(oriented_brick[0][0], oriented_brick[0][1]), "position":(position[0], position[1]), "support":oriented_brick[1]} if z in self.__layers: for brick in self.__layers[z]: if self.__is_overlap(positioned_brick, brick): return False self.__layers[z].append(positioned_brick) return True else: self.__layers[z] = [positioned_brick] return True
[docs] def remove_brick(self, position:tuple[int, int, int]) -> bool: """ Attempt to remove a brick in the model at a specified position. Args: position (tuple[int, int, int]): The x, y and z coordinates, for the left, front, bottom corner, respectively Returns: bool: Whether the placement was succesful or not Raises: TypeError: If the inputs are invalid """ # Verify the inputs if (not isinstance(position, tuple) and not isinstance(position, list)) or not isinstance(position[0], int) or not isinstance(position[1], int) or not isinstance(position[2], int): raise TypeError("position must contain the coordinates in (x,y,z) format (tuple[int]).") x,y,z = position if z in self.__layers: brick_count = len(self.__layers[z]) for brick in range(brick_count): if self.__layers[z][brick]["position"] == (x,y): if brick_count == 1: self.__layers.pop(z) else: self.__layers[z].pop(brick) return True return False
[docs] def normalize(self) -> None: """ Normalize the model to start from (0,0,0) in the bottom left corner. This will change the position of all bricks in the model. """ # Check if the model is already normalized if self.is_empty or self.min == (0, 0, 0): return x_min, y_min, z_min = self.min for z in self.__layers.keys(): for brick in self.__layers[z]: brick["position"] = (brick["position"][0] - x_min, brick["position"][1] - y_min) self.__layers = {z-z_min:self.__layers[z] for z in self.__layers.keys()}
def __is_overlap(self, brick1:dict, brick2:dict) -> bool: """ Check if two bricks overlap based on their size and position. Bricks must contain at least: {"size":(x,y), "position":(x,y)} """ x_size1, y_size1 = brick1["size"] x_size2, y_size2 = brick2["size"] x1, y1 = brick1["position"] x2, y2 = brick2["position"] if (x1 + x_size1 > x2 and x2 + x_size2 > x1 and y1 + y_size1 > y2 and y2 + y_size2 > y1): return True return False def __str__(self): result = "" # Sort layers from bottom to top for z in sorted(self.__layers.keys()): result += f"Layer {z}:\n" for brick in self.__layers[z]: brick_size = brick["size"] brick_position = brick["position"] result += f" {'Support' if brick['support'] else 'Building'} brick ({brick_size[0]} x {brick_size[1]}) at: ({brick_position[0]}, {brick_position[1]}) \n" return result