Files
blender-portable-repo/scripts/addons/contactsheet/geo.py
T
2026-03-17 14:58:51 -06:00

801 lines
21 KiB
Python

# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import math
from typing import Union, Optional, List
from enum import Enum
class Align(Enum):
"""
Enum class that represents different alignment options.
"""
NO = 1
CENTER = 2
TOP = 3
BOTTOM = 4
class Point:
"""
Class that represents a point with 2 coordinates.
"""
def __init__(self, x: int, y: int):
self._x = int(x)
self._y = int(y)
@property
def x(self) -> int:
return self._x
@property
def y(self) -> int:
return self._y
def __add__(self, other: Point):
return Point(self.x + other.x, self.y + other.y)
def __iadd__(self, other: Point):
self.x + other.x
self.y + other.y
return self
def __sub__(self, other: Point):
return Point(self.x - other.x, self.y - other.y)
def __isub__(self, other: Point):
self.x - other.x
self.y - other.y
return self
def __repr__(self) -> str:
return f"Point(x: {self.x}, y: {self.y})"
class RectCoords:
"""
Class that represents the coordinates of a Rectangle.
Instances of this class are returned by the Rectangle class.
The individual coordinate can be retrieved with
(x1, x2, y1, y2) or (top_left, top_right, bot_left, bot_right).
"""
def __init__(self, x1: Point, x2: Point, y1: Point, y2: Point):
self._x1 = x1
self._x2 = x2
self._y1 = y1
self._y2 = y2
@property
def x1(self) -> Point:
return self._x1
@property
def x2(self) -> Point:
return self._x2
@property
def y1(self) -> Point:
return self._y1
@property
def y2(self) -> Point:
return self._y2
@property
def top_left(self) -> Point:
return self._x1
@property
def top_right(self) -> Point:
return self._x2
@property
def bot_left(self) -> Point:
return self._y1
@property
def bot_right(self) -> Point:
return self._y2
class Rectangle:
"""
Class that represents a rectangle. It makes heavy use of private functions so
this class can be easily re-implemented to work with Blender sequence strips.
"""
def __init__(self, x: int, y: int, width: int, height: int):
self._width: int = int(width)
self._height: int = int(height)
self._x: int = int(x)
self._y: int = int(y)
self._orig_width: int = self._width
self._orig_height: int = self._height
self._orig_x: int = self._x
self._orig_y: int = self._y
self._scale_x: float = 1.0
self._scale_y: float = 1.0
# X.
@property
def x(self) -> int:
return self._get_x()
def _get_x(self) -> int:
return self._x
@x.setter
def x(self, value: int) -> None:
return self._set_x(value)
def _set_x(self, value: int) -> None:
self._x = int(value)
@property
def orig_x(self) -> int:
return self._get_orig_x()
def _get_orig_x(self) -> int:
return self._orig_x
# Y.
@property
def y(self) -> int:
return self._get_y()
def _get_y(self) -> int:
return self._y
@y.setter
def y(self, value: int) -> None:
return self._set_y(value)
def _set_y(self, value: int) -> None:
self._y = int(value)
@property
def orig_y(self) -> int:
return self._get_orig_y()
def _get_orig_y(self) -> int:
return self._orig_y
# Width.
@property
def width(self) -> int:
return self._get_width()
def _get_width(self) -> int:
return self._width
@width.setter
def width(self, value: int) -> None:
return self._set_width(value)
def _set_width(self, value: int) -> None:
self._width = int(value)
@property
def orig_width(self) -> int:
return self._get_orig_width()
def _get_orig_width(self) -> int:
return self._orig_width
# Height.
@property
def height(self) -> int:
return self._get_height()
def _get_height(self) -> int:
return self._height
@height.setter
def height(self, value: int) -> None:
return self._set_height(value)
def _set_height(self, value: int) -> None:
self._height = int(value)
@property
def orig_height(self) -> int:
return self._get_orig_height()
def _get_orig_height(self) -> int:
return self._orig_height
# Scale.
@property
def scale_x(self):
return self._get_scale_x()
def _get_scale_x(self):
return self._scale_x
@scale_x.setter
def scale_x(self, factor: float) -> None:
return self._set_scale_x(factor)
def _set_scale_x(self, factor: float) -> None:
new_width = self.width * float(factor)
self.x += self.width / 2 - new_width / 2
self.width = new_width
@property
def scale_y(self):
return self._get_scale_y()
def _get_scale_y(self):
return self._scale_y
@scale_y.setter
def scale_y(self, factor: float) -> None:
return self._set_scale_y(factor)
def _set_scale_y(self, factor: float) -> None:
new_height = self.height * float(factor)
self.y += self.height / 2 - new_height / 2
self.height = new_height
def scale(self, factor: float) -> None:
self.scale_x *= factor
self.scale_y *= factor
# Aspect.
@property
def aspect_ratio(self) -> float:
return self.width / self.height
# Area.
@property
def area(self) -> int:
return self.width * self.height
# Center.
@property
def center(self) -> Point:
center_x = int(self.x + (0.5 * self.width))
center_y = int(self.y + (0.5 * self.height))
return Point(center_x, center_y)
# Position.
@property
def position(self) -> Point:
return Point(self.x, self.y)
@position.setter
def position(self, pos: Point) -> None:
self.x = pos.x
self.y = pos.y
# Coords.
@property
def coords(self) -> RectCoords:
top_left = self.position
top_right = Point(self.x + self.width, self.y)
bot_left = Point(self.x, self.y + self.height)
bot_right = Point(self.x + self.width, self.y + self.height)
return RectCoords(top_left, top_right, bot_left, bot_right)
# Functions.
def fit_to_rect(
self,
rect: Rectangle,
keep_aspect: bool = True,
align: Align = Align.CENTER,
keep_offset: bool = False,
):
# If self.aspect_ratio > rect.aspect_ratio:
# -> fit self by width
# else fit bei height.
# Width height.
if keep_aspect:
# Fit by width.
if self.aspect_ratio > rect.aspect_ratio:
scale_fac = rect.width / self.width
self.width = rect.width
self.height = int(self.height * scale_fac)
# Fit by height.
elif self.aspect_ratio < rect.aspect_ratio:
scale_fac = rect.height / self.height
self.height = rect.height
self.width = int(self.width * scale_fac)
else:
# Copy width and height.
self.width = rect.width
self.height = rect.height
# Position.
if keep_offset:
self.position += self.position - rect.position
else:
self.position = rect.position
# Fit by width.
if self.aspect_ratio > rect.aspect_ratio:
if align == Align.NO:
pass
if align == Align.CENTER:
height_diff = rect.height - self.height
self.y += int(height_diff / 2)
elif align == Align.TOP:
self.y == rect.y
elif align == Align.BOTTOM:
height_diff = rect.height - self.height
self.y = rect.y + height_diff
# Fit by height.
elif self.aspect_ratio < rect.aspect_ratio:
width_diff = rect.width - self.width
if align == Align.NO:
pass
if align == Align.CENTER:
self.x += int(width_diff / 2)
elif align == Align.TOP:
self.x == rect.x
elif align == Align.BOTTOM:
self.x = rect.x + width_diff
def reset_transform(self):
self.scale_x = 1
self.scale_y = 1
self.x = self.orig_x
self.y = self.orig_y
self.width = self.orig_width
self.height = self.orig_height
def copy(self) -> Rectangle:
return Rectangle(self.x, self.y, self.width, self.height)
def __repr__(self) -> str:
return f"Rectangle(x: {self.x}, y: {self.y}, width: {self.width}, height: {self.height})"
@property
def valid(self) -> bool:
return bool(self.width and self.height)
class NestedRectangle(Rectangle):
"""
A Class that inherits from Rectangle and holds a child inside. The child can be anything
that supports the public interface of the Rectangle class. Can also be another instance of
a NestedRectangle.
"""
def __init__(
self,
x: int,
y: int,
width: int,
height: int,
child: Optional[Union[Rectangle, NestedRectangle]] = None,
keep_aspect: bool = True,
align: Align = Align.CENTER,
keep_offset: bool = False,
):
super().__init__(x, y, width, height)
# If child was not supplied on init make child same dimensions as parent.
if child == None:
child = Rectangle(0, 0, self.width, self.height)
self._child = child
self._keep_aspect = keep_aspect
self._align = align
self._keep_offset = keep_offset
self._child.fit_to_rect(
self.get_rect(),
keep_aspect=keep_aspect,
align=align,
keep_offset=keep_offset,
)
def fit_to_rect(
self,
rect: Rectangle,
keep_aspect: bool = True,
align: Align = Align.CENTER,
keep_offset: bool = False,
):
super().fit_to_rect(
rect, keep_aspect=keep_aspect, align=align, keep_offset=keep_offset
)
self.child.fit_to_rect(
self.get_rect(),
keep_aspect=keep_aspect,
align=align,
keep_offset=keep_offset,
)
def get_rect(self) -> Rectangle:
return Rectangle(self.x, self.y, self.width, self.height)
@property
def child(self) -> Rectangle:
return self._child
def set_child(
self,
child: Union[Rectangle, NestedRectangle],
keep_aspect: bool = True,
align: Align = Align.CENTER,
keep_offset: bool = False,
) -> None:
self._child = child
self._child.fit_to_rect(
self.get_rect(),
keep_aspect=keep_aspect,
align=align,
keep_offset=keep_offset,
)
self._keep_aspect = keep_aspect
self._align = align
self._keep_offset = keep_offset
def copy(self) -> NestedRectangle:
return NestedRectangle(
self.x,
self.y,
self.width,
self.height,
self.child.copy(),
keep_aspect=self._keep_aspect,
align=self._align,
)
def _set_x(self, value: int) -> None:
offset = self.child.x - self._x
self._x = int(value)
self.child.x = int(value + offset)
def _set_y(self, value: int) -> None:
offset = self.child.y - self._y
self._y = int(value)
self.child.y = int(value + offset)
def _set_width(self, value: int) -> None:
self._width = int(value)
self.child.fit_to_rect(
self.get_rect(), self._keep_aspect, self._align, keep_offset=True
)
def _set_height(self, value: int) -> None:
self._height = int(value)
self.child.fit_to_rect(
self.get_rect(), self._keep_aspect, self._align, keep_offset=True
)
def _set_scale_x(self, factor: float) -> None:
super()._set_scale_x(factor)
self.child.fit_to_rect(
self.get_rect(),
keep_aspect=self._keep_aspect,
align=self._align,
keep_offset=self._keep_offset,
)
def _set_scale_y(self, factor: float) -> None:
super()._set_scale_y(factor)
self.child.fit_to_rect(
self.get_rect(),
keep_aspect=self._keep_aspect,
align=self._align,
keep_offset=self._keep_offset,
)
@property
def keep_aspect(self) -> bool:
return self._keep_aspect
@keep_aspect.setter
def keep_aspect(self, value: bool):
self._keep_aspect = value
@property
def align(self) -> Align:
return self._align
@align.setter
def align(self, value: Align) -> None:
self._align = value
self.child.fit_to_rect(
self.get_rect(),
keep_aspect=self._keep_aspect,
align=self._align,
keep_offset=self._keep_offset,
)
@property
def keep_offset(self) -> bool:
return self._keep_offset
@keep_offset.setter
def keep_offset(self, value: bool):
self._keep_offset = value
class Cell(NestedRectangle):
"""
A Class that inherits from NestedRectangle and holds another NestedRectangle inside.
It's specifically bound to this hierarchy so it works well inside the Grid Class.
"""
def __init__(
self,
x: int,
y: int,
width: int,
height: int,
child: Optional[NestedRectangle] = None,
keep_aspect: bool = True,
align: Align = Align.CENTER,
):
# If child was not supplied on init make child same dimensions as parent.
if child == None:
child = NestedRectangle(0, 0, width, height)
# Init self.
super().__init__(
x, y, width, height, child, keep_aspect=keep_aspect, align=align
)
def copy(self) -> Cell:
return Cell(
self.x,
self.y,
self.width,
self.height,
self.child.copy(),
keep_aspect=self._keep_aspect,
align=self._align,
)
def clear_content(self) -> None:
self.set_child(NestedRectangle(0, 0, self.width, self.height))
class Grid(Rectangle):
def __init__(
self,
x: int,
y: int,
width: int,
height: int,
row_count: int,
coll_count: int,
cell_templ: Optional[Cell] = None,
keep_aspect: bool = True,
align: Align = Align.CENTER,
) -> None:
super().__init__(x, y, width, height)
self._keep_aspect: bool = keep_aspect
self._align: Align = align
# If cell_templ was not supplied on init make cell that has same dimensions as row / coll.
if cell_templ == None:
cell_templ: Cell = Cell(
0, 0, int(self.height / row_count), int(self.width / coll_count)
)
# Init rows, colls, cells.
self.rows: List[Rectangle] = []
self.colls: List[Rectangle] = []
self.cells: List[List[Cell]] = []
self._init_grid(
row_count,
coll_count,
cell_templ,
keep_aspect,
align,
)
def _init_grid(
self,
row_count: int,
coll_count: int,
cell_templ: Cell,
keep_aspect: bool,
align: Align,
) -> None:
self._init_rows(row_count)
self._init_colls(coll_count)
self._init_cells(cell_templ, keep_aspect, align)
def _init_rows(self, row_count: int) -> None:
row_height: int = int(self.height / row_count)
self.rows = [
Rectangle(self.x, self.y + (row_height * row_idx), self.width, row_height)
for row_idx in range(row_count)
]
def _init_colls(self, coll_count: int) -> None:
coll_width: int = int(self.width / coll_count)
self.colls = [
Rectangle(self.x + (coll_width * coll_idx), self.y, coll_width, self.height)
for coll_idx in range(coll_count)
]
def _init_cells(self, cell_templ: Cell, keep_aspect: bool, align: Align) -> None:
# TODO: cell.child.keep_aspect = keep_aspect.
self.cells.clear()
# If cell_templ was supplied make sure it has the right dimension to fit in grid.
cell_templ.width = self.coll_width
cell_templ.height = self.row_height
for row_index, row in enumerate(self.rows):
cell_y = row.y
self.cells.append([])
for coll in self.colls:
cell_x = coll.x
# Make copy to have each cell individual instance.
cell_instance = cell_templ.copy()
cell_instance.position = Point(cell_x, cell_y)
self.cells[row_index].append(cell_instance)
@classmethod
def from_content(
csl,
x: int,
y: int,
width: int,
height: int,
content: List[NestedRectangle],
row_count: Optional[int] = None,
keep_aspect: bool = True,
align: Align = Align.CENTER,
) -> Grid:
"""
Creates a grid based of a content list. Calculates optimal amount of rows and colls.
"""
if not row_count:
# Calculate by how much images need to be scaled in order to fit.. (won't be perfect).
available_area = width * height
content_area_orig = content[0].child.area
content_area = available_area / len(content)
scale_factor = math.sqrt(content_area / content_area_orig)
content_size = (
content[0].child.width * scale_factor,
content[0].child.height * scale_factor,
)
row_count = math.ceil(width / content_size[0])
coll_count = math.ceil(len(content) / row_count)
else:
# Row count should not be bigger as nr of cells.
row_count = min(row_count, len(content))
coll_count = math.ceil(len(content) / row_count) # 13 / 3 = 4.3 -> 4
grid = Grid(
x,
y,
width,
height,
row_count,
coll_count,
keep_aspect=keep_aspect,
align=align,
)
grid.place_content(content, keep_aspect=keep_aspect, align=align)
return grid
def get_cells_for_row(self, row_index: int) -> List[Cell]:
return self.cells[row_index]
def get_cell(self, row_index: int, coll_index: int) -> Cell:
return self.cells[row_index][coll_index]
def get_cells_all(self) -> List[Cell]:
cells: List[Cell] = []
for row_idx in range(self.row_count()):
cells.extend(self.get_cells_for_row(row_idx))
return cells
@property
def row_height(self) -> int:
return int(self.height / self.row_count())
@property
def coll_width(self) -> int:
return int(self.width / self.coll_count())
def row_count(self) -> int:
return len(self.rows)
def coll_count(self) -> int:
return len(self.colls)
def place_content(
self,
content_list: List[NestedRectangle],
keep_aspect: bool = True,
align: Align = Align.CENTER,
keep_offset: bool = False,
clear_cells: bool = True,
):
"""
Fills up all available cells with the content from given list.
Will clear remaining empty cells.
"""
counter: int = 0
for row_idx in range(self.row_count()):
for cell in self.get_cells_for_row(row_idx):
try:
content = content_list[counter]
except IndexError:
if clear_cells:
cell.clear_content()
else:
# Switch child of cell.
cell.set_child(
content,
keep_aspect=keep_aspect,
align=align,
keep_offset=keep_offset,
)
counter += 1
def place_content_in_cell(
self,
row_index: int,
coll_index: int,
content: NestedRectangle,
keep_aspect: bool = True,
align: Align = Align.CENTER,
keep_offset: bool = False,
):
cell = self.get_cell(row_index, coll_index)
# Switch child of cell.
cell.set_child(
content,
keep_aspect=keep_aspect,
align=align,
keep_offset=keep_offset,
)
def scale_content(self, factor: float):
for cell in self.get_cells_all():
cell.child.scale_x *= factor
cell.child.scale_y *= factor
def scale_content_x(self, factor: float):
for cell in self.get_cells_all():
cell.child.scale_x *= factor
def scale_content_y(self, factor: float):
for cell in self.get_cells_all():
cell.child.scale_y *= factor
def reset_content_transforms(self):
for cell in self.get_cells_all():
cell.reset_transform()