801 lines
21 KiB
Python
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()
|