928 lines
32 KiB
Python
928 lines
32 KiB
Python
# Copyright (c) 2010-2022, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Iterable,
|
|
Iterator,
|
|
Optional,
|
|
Sequence,
|
|
TYPE_CHECKING,
|
|
TypeVar,
|
|
)
|
|
from copy import deepcopy
|
|
|
|
from ezdxf.lldxf import const
|
|
from ezdxf.enums import (
|
|
MTextEntityAlignment,
|
|
MAP_MTEXT_ALIGN_TO_FLAGS,
|
|
)
|
|
from ezdxf.addons import MTextSurrogate
|
|
from ezdxf.math import UVec, Vec2
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.layouts import BlockLayout
|
|
from ezdxf.eztypes import GenericLayoutType
|
|
|
|
DEFAULT_TABLE_BG_LAYER = "TABLEBACKGROUND"
|
|
DEFAULT_TABLE_FG_LAYER = "TABLECONTENT"
|
|
DEFAULT_TABLE_GRID_LAYER = "TABLEGRID"
|
|
DEFAULT_TEXT_STYLE = "STANDARD"
|
|
DEFAULT_CELL_HEIGHT = 1.0
|
|
DEFAULT_CELL_WIDTH = 2.5
|
|
DEFAULT_CELL_CHAR_HEIGHT = 0.7
|
|
DEFAULT_CELL_LINE_SPACING = 1.5
|
|
DEFAULT_CELL_X_SCALE = 1.0
|
|
DEFAULT_CELL_Y_SCALE = 1.0
|
|
DEFAULT_CELL_TEXTCOLOR = const.BYLAYER
|
|
DEFAULT_CELL_BG_COLOR = None
|
|
DEFAULT_CELL_X_MARGIN = 0.1
|
|
DEFAULT_CELL_Y_MARGIN = 0.1
|
|
DEFAULT_BORDER_COLOR = 5
|
|
DEFAULT_BORDER_LINETYPE = "BYLAYER"
|
|
DEFAULT_BORDER_STATUS = True
|
|
DEFAULT_BORDER_PRIORITY = 50
|
|
|
|
|
|
T = TypeVar("T", bound="Cell")
|
|
|
|
|
|
class TablePainter:
|
|
"""The TablePainter class renders tables build from DXF primitives.
|
|
|
|
The TablePainter instance contains all the data cells.
|
|
|
|
Args:
|
|
insert: insert location as or :class:`~ezdxf.math.UVec`
|
|
nrows: row count
|
|
ncols: column count
|
|
cell_width: default cell width in drawing units
|
|
cell_height: default cell height in drawing units
|
|
default_grid: draw a grid of solid lines if ``True``, otherwise
|
|
draw only explicit defined borders, the default grid has a
|
|
priority of 50.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
insert: UVec,
|
|
nrows: int,
|
|
ncols: int,
|
|
cell_width=DEFAULT_CELL_WIDTH,
|
|
cell_height=DEFAULT_CELL_HEIGHT,
|
|
default_grid=True,
|
|
):
|
|
self.insert = Vec2(insert)
|
|
self.nrows: int = nrows
|
|
self.ncols: int = ncols
|
|
self.row_heights: list[float] = [cell_height] * nrows
|
|
self.col_widths: list[float] = [cell_width] * ncols
|
|
self.bg_layer_name: str = DEFAULT_TABLE_BG_LAYER
|
|
self.fg_layer_name: str = DEFAULT_TABLE_FG_LAYER
|
|
self.grid_layer_name: str = DEFAULT_TABLE_GRID_LAYER
|
|
self.styles: dict[str, CellStyle] = {"default": CellStyle()}
|
|
if not default_grid:
|
|
default_style = self.get_cell_style("default")
|
|
default_style.set_border_status(False, False, False, False)
|
|
|
|
self._cells: dict[tuple[int, int], Cell] = {} # data cells
|
|
self.frames: list[Frame] = [] # border frame objects
|
|
self.empty_cell = Cell(self) # represents all empty cells
|
|
|
|
def set_col_width(self, index: int, value: float):
|
|
"""Set column width in drawing units of the given column index.
|
|
|
|
Args:
|
|
index: zero based column index
|
|
value: new column width in drawing units
|
|
|
|
"""
|
|
self.col_widths[index] = float(value)
|
|
|
|
def set_row_height(self, index: int, value: float):
|
|
"""Set row height in drawing units of the given row index.
|
|
|
|
Args:
|
|
index: zero based row index
|
|
value: new row height in drawing units
|
|
"""
|
|
self.row_heights[index] = float(value)
|
|
|
|
def text_cell(
|
|
self,
|
|
row: int,
|
|
col: int,
|
|
text: str,
|
|
span: tuple[int, int] = (1, 1),
|
|
style="default",
|
|
) -> TextCell:
|
|
"""Factory method to create a new text cell at location (row, col),
|
|
with `text` as content, the `text` can be a line breaks ``'\\n'``.
|
|
The final cell can spread over several cells defined by the argument
|
|
`span`.
|
|
|
|
"""
|
|
cell = TextCell(self, text, style=style, span=span)
|
|
return self.set_cell(row, col, cell)
|
|
|
|
def block_cell(
|
|
self,
|
|
row: int,
|
|
col: int,
|
|
blockdef: BlockLayout,
|
|
span: tuple[int, int] = (1, 1),
|
|
attribs=None,
|
|
style="default",
|
|
) -> BlockCell:
|
|
"""Factory method to Create a new block cell at position (row, col).
|
|
|
|
Content is a block reference inserted by an INSERT entity,
|
|
attributes will be added if the block definition contains ATTDEF.
|
|
Assignments are defined by attribs-key to attdef-tag association.
|
|
|
|
Example: attribs = {'num': 1} if an ATTDEF with tag=='num' in
|
|
the block definition exists, an attrib with text=str(1) will be
|
|
created and added to the insert entity.
|
|
|
|
The cell spans over 'span' cells and has the cell style with the
|
|
name 'style'.
|
|
"""
|
|
if attribs is None:
|
|
attribs = {}
|
|
cell = BlockCell(
|
|
self, blockdef, style=style, attribs=attribs, span=span
|
|
)
|
|
return self.set_cell(row, col, cell)
|
|
|
|
@property
|
|
def table_width(self) -> float:
|
|
"""Returns the total table width."""
|
|
return sum(self.col_widths)
|
|
|
|
@property
|
|
def table_height(self) -> float:
|
|
"""Returns the total table height."""
|
|
return sum(self.row_heights)
|
|
|
|
def set_cell(self, row: int, col: int, cell: T) -> T:
|
|
"""Insert a cell at position (row, col)."""
|
|
row, col = self.validate_index(row, col)
|
|
self._cells[row, col] = cell
|
|
return cell
|
|
|
|
def get_cell(self, row: int, col: int) -> Cell:
|
|
"""Get cell at location (row, col)."""
|
|
row, col = self.validate_index(row, col)
|
|
try:
|
|
return self._cells[row, col]
|
|
except KeyError:
|
|
return self.empty_cell # empty cell with default style
|
|
|
|
def validate_index(self, row: int, col: int) -> tuple[int, int]:
|
|
row = int(row)
|
|
col = int(col)
|
|
if row < 0 or row >= self.nrows or col < 0 or col >= self.ncols:
|
|
raise IndexError("cell index out of range")
|
|
return row, col
|
|
|
|
def frame(
|
|
self,
|
|
row: int,
|
|
col: int,
|
|
width: int = 1,
|
|
height: int = 1,
|
|
style="default",
|
|
) -> Frame:
|
|
"""Creates a frame around the give cell area, starting at (row, col) and
|
|
covering `width` columns and `height` rows. The `style` argument is the
|
|
name of a :class:`CellStyle`.
|
|
"""
|
|
frame = Frame(self, pos=(row, col), span=(height, width), style=style)
|
|
self.frames.append(frame)
|
|
return frame
|
|
|
|
def new_cell_style(self, name: str, **kwargs) -> CellStyle:
|
|
"""Factory method to create a new :class:`CellStyle` object, overwrites
|
|
an already existing cell style.
|
|
|
|
Args:
|
|
name: style name as string
|
|
kwargs: see attributes of class :class:`CellStyle`
|
|
|
|
"""
|
|
assert (
|
|
isinstance(name, str) and name != ""
|
|
), "name has to be a non-empty string"
|
|
style: CellStyle = deepcopy(self.get_cell_style("default"))
|
|
style.update(kwargs)
|
|
self.styles[name] = style
|
|
return style
|
|
|
|
@staticmethod
|
|
def new_border_style(
|
|
color: int = const.BYLAYER,
|
|
status=True,
|
|
priority: int = 100,
|
|
linetype: str = "BYLAYER",
|
|
lineweight: int = const.LINEWEIGHT_BYLAYER,
|
|
) -> BorderStyle:
|
|
"""Factory method to create a new border style.
|
|
|
|
Args:
|
|
status: ``True`` for visible, ``False`` for invisible
|
|
color: :ref:`ACI`
|
|
linetype: linetype name, default is "BYLAYER"
|
|
lineweight: lineweight as int, default is by layer
|
|
priority: drawing priority, higher priorities cover lower priorities
|
|
|
|
"""
|
|
border_style = BorderStyle()
|
|
border_style.color = color
|
|
border_style.linetype = linetype
|
|
border_style.lineweight = lineweight
|
|
border_style.status = status
|
|
border_style.priority = priority
|
|
return border_style
|
|
|
|
def get_cell_style(self, name: str) -> CellStyle:
|
|
"""Get cell style by name."""
|
|
return self.styles[name]
|
|
|
|
def iter_visible_cells(
|
|
self, visibility_map: VisibilityMap
|
|
) -> Iterator[tuple[int, int, Cell]]:
|
|
"""Iterate over all visible cells"""
|
|
return (
|
|
(row, col, self.get_cell(row, col)) for row, col in visibility_map
|
|
)
|
|
|
|
def render(self, layout: GenericLayoutType, insert: Optional[UVec] = None):
|
|
"""Render table to layout."""
|
|
insert_backup = self.insert
|
|
if insert is not None:
|
|
self.insert = Vec2(insert)
|
|
visibility_map = VisibilityMap(self)
|
|
grid = Grid(self)
|
|
for row, col, cell in self.iter_visible_cells(visibility_map):
|
|
grid.render_cell_background(layout, row, col, cell)
|
|
grid.render_cell_content(layout, row, col, cell)
|
|
grid.render_lines(layout, visibility_map)
|
|
self.insert = insert_backup
|
|
|
|
|
|
class VisibilityMap:
|
|
"""Stores the visibility of the table cells."""
|
|
|
|
def __init__(self, table: TablePainter):
|
|
"""Create the visibility map for table."""
|
|
self.table: TablePainter = table
|
|
self._hidden_cells: set[tuple[int, int]] = set()
|
|
self._create_visibility_map()
|
|
|
|
def _create_visibility_map(self):
|
|
"""Set visibility for all existing cells."""
|
|
for row, col in iter(self):
|
|
cell = self.table.get_cell(row, col)
|
|
self._set_span_visibility(row, col, cell.span)
|
|
|
|
def _set_span_visibility(self, row: int, col: int, span: tuple[int, int]):
|
|
"""Set the visibility of the given cell.
|
|
|
|
The cell itself is visible, all other cells in the span-range
|
|
(tuple: width, height) are invisible, they are covered by the
|
|
main cell (row, col).
|
|
"""
|
|
|
|
if span != (1, 1):
|
|
nrows, ncols = span
|
|
for rowx in range(nrows):
|
|
for colx in range(ncols):
|
|
# switch all cells in span range to invisible
|
|
self.hide(row + rowx, col + colx)
|
|
# switch content cell visible
|
|
self.show(row, col)
|
|
|
|
def show(self, row: int, col: int):
|
|
"""Show cell (row, col)."""
|
|
try:
|
|
self._hidden_cells.remove((row, col))
|
|
except KeyError:
|
|
pass
|
|
|
|
def hide(self, row: int, col: int) -> None:
|
|
"""Hide cell (row, col)."""
|
|
self._hidden_cells.add((row, col))
|
|
|
|
def iter_all_cells(self) -> Iterator[tuple[int, int]]:
|
|
"""Iterate over all cell indices, yields (row, col) tuples."""
|
|
for row in range(self.table.nrows):
|
|
for col in range(self.table.ncols):
|
|
yield row, col
|
|
|
|
def is_visible_cell(self, row: int, col: int) -> bool:
|
|
"""True if cell (row, col) is visible, else False."""
|
|
return (row, col) not in self._hidden_cells
|
|
|
|
def __iter__(self) -> Iterator[tuple[int, int]]:
|
|
"""Iterate over all visible cells."""
|
|
return (
|
|
(row, col)
|
|
for (row, col) in self.iter_all_cells()
|
|
if self.is_visible_cell(row, col)
|
|
)
|
|
|
|
|
|
class CellStyle:
|
|
"""Cell style object.
|
|
|
|
.. important::
|
|
|
|
Always instantiate new styles by the factory method:
|
|
:meth:`TablePainter.new_cell_style`
|
|
|
|
"""
|
|
|
|
def __init__(self, data: Optional[dict[str, Any]] = None):
|
|
# text style is ignored by block cells
|
|
self.text_style = "STANDARD"
|
|
# text height in drawing units, ignored by block cells
|
|
self.char_height = DEFAULT_CELL_CHAR_HEIGHT
|
|
# line spacing in percent = char_height * line_spacing, ignored by block cells
|
|
self.line_spacing = DEFAULT_CELL_LINE_SPACING
|
|
# text stretching factor (width factor) or block reference x-scaling factor
|
|
self.scale_x = DEFAULT_CELL_X_SCALE
|
|
# block reference y-axis scaling factor, ignored by text cells
|
|
self.scale_y = DEFAULT_CELL_Y_SCALE
|
|
# dxf color index, ignored by block cells
|
|
self.text_color = DEFAULT_CELL_TEXTCOLOR
|
|
# text or block rotation in degrees
|
|
self.rotation = 0.0
|
|
# Letters are stacked top-to-bottom, but not rotated
|
|
self.stacked = False
|
|
# text and block alignment, see ezdxf.enums.MTextEntityAlignment
|
|
self.align = MTextEntityAlignment.TOP_CENTER
|
|
# left and right cell margin in drawing units
|
|
self.margin_x = DEFAULT_CELL_X_MARGIN
|
|
# top and bottom cell margin in drawing units
|
|
self.margin_y = DEFAULT_CELL_Y_MARGIN
|
|
# background color, dxf color index, ignored by block cells
|
|
self.bg_color = DEFAULT_CELL_BG_COLOR
|
|
# left border style
|
|
self.left = BorderStyle()
|
|
# top border style
|
|
self.top = BorderStyle()
|
|
# right border style
|
|
self.right = BorderStyle()
|
|
# bottom border style
|
|
self.bottom = BorderStyle()
|
|
if data:
|
|
self.update(data)
|
|
|
|
def __getitem__(self, k: str) -> Any:
|
|
return self.__dict__[k]
|
|
|
|
def __setitem__(self, k: str, v: Any):
|
|
if k in self.__dict__:
|
|
self.__dict__.__setitem__(k, v)
|
|
else:
|
|
raise KeyError(f"invalid attribute name: {k}")
|
|
|
|
def update(self, data: dict[str, Any]):
|
|
for k, v in data.items():
|
|
self.__setitem__(k, v)
|
|
assert isinstance(
|
|
self.align, MTextEntityAlignment
|
|
), "enum ezdxf.enums.MTextEntityAlignment for text alignments required"
|
|
|
|
def set_border_status(self, left=True, right=True, top=True, bottom=True):
|
|
"""Set status of all cell borders at once."""
|
|
self.left.status = left
|
|
self.right.status = right
|
|
self.top.status = top
|
|
self.bottom.status = bottom
|
|
|
|
def set_border_style(
|
|
self, style: BorderStyle, left=True, right=True, top=True, bottom=True
|
|
):
|
|
"""Set border styles of all cell borders at once."""
|
|
for border, status in (
|
|
("left", left),
|
|
("right", right),
|
|
("top", top),
|
|
("bottom", bottom),
|
|
):
|
|
if status:
|
|
self[border] = style
|
|
|
|
@staticmethod
|
|
def get_default_border_style() -> BorderStyle:
|
|
return BorderStyle()
|
|
|
|
def get_text_align_flags(self) -> tuple[int, int]:
|
|
return MAP_MTEXT_ALIGN_TO_FLAGS[self.align]
|
|
|
|
|
|
class BorderStyle:
|
|
"""Border style class.
|
|
|
|
.. important::
|
|
|
|
Always instantiate new border styles by the factory method:
|
|
:meth:`TablePainter.new_border_style`
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
status: bool = DEFAULT_BORDER_STATUS,
|
|
color: int = DEFAULT_BORDER_COLOR,
|
|
linetype: str = DEFAULT_BORDER_LINETYPE,
|
|
lineweight=const.LINEWEIGHT_BYLAYER,
|
|
priority: int = DEFAULT_BORDER_PRIORITY,
|
|
):
|
|
# border status, True for visible, False for hidden
|
|
self.status = status
|
|
# ACI
|
|
self.color = color
|
|
# linetype name, BYLAYER if None
|
|
self.linetype = linetype
|
|
# lineweight
|
|
self.lineweight = lineweight
|
|
# drawing priority, higher values cover lower values
|
|
self.priority = priority
|
|
|
|
|
|
class Grid:
|
|
"""Grid contains the graphical representation of the table."""
|
|
|
|
def __init__(self, table: TablePainter):
|
|
self.table: TablePainter = table
|
|
# contains the x-axis coords of the grid lines between the data columns.
|
|
self.col_pos: list[float] = self._calc_col_pos()
|
|
# contains the y-axis coords of the grid lines between the data rows.
|
|
self.row_pos: list[float] = self._calc_row_pos()
|
|
|
|
# _x_borders contains the horizontal border elements, list of border styles
|
|
# get index with _border_index(row, col), which means the border element
|
|
# above row, col, and row-indices are [0 .. nrows+1], nrows+1 for the
|
|
# grid line below the last row; list contains only the border style with
|
|
# the highest priority.
|
|
self._x_borders: list[BorderStyle] = [] # created in _init_borders
|
|
|
|
# _y_borders: same as _x_borders but for the vertical borders,
|
|
# col-indices are [0 .. ncols+1], ncols+1 for the last grid line right
|
|
# of the last column
|
|
self._y_borders: list[BorderStyle] = [] # created in _init_borders
|
|
# border style to delete borders inside of merged cells
|
|
self.no_border = BorderStyle(
|
|
status=False, priority=999, linetype="BYLAYER", color=0
|
|
)
|
|
|
|
def _init_borders(self, x_border: BorderStyle, y_border: BorderStyle):
|
|
"""Init the _hborders with <hborder> and _vborders with <vborder>."""
|
|
# <border_count> has more elements than necessary, but it unifies the
|
|
# index calculation for _vborders and _hborders.
|
|
# exact values are:
|
|
# x_border_count = ncols * (nrows+1), hindex = ncols * <row> + <col>
|
|
# y_border_count = nrows * (ncols+1), vindex = (ncols+1) * <row> + <col>
|
|
border_count: int = (self.table.nrows + 1) * (self.table.ncols + 1)
|
|
self._x_borders = [x_border] * border_count
|
|
self._y_borders = [y_border] * border_count
|
|
|
|
def _border_index(self, row: int, col: int) -> int:
|
|
"""Calculate linear index for border arrays _x_borders and _y_borders."""
|
|
return row * (self.table.ncols + 1) + col
|
|
|
|
def set_x_border(self, row: int, col: int, border_style: BorderStyle):
|
|
"""Set <border_style> for the horizontal border element above
|
|
<row>, <col>.
|
|
"""
|
|
return self._set_border_style(self._x_borders, row, col, border_style)
|
|
|
|
def set_y_border(self, row: int, col: int, border_style: BorderStyle):
|
|
"""Set <border_style> for the vertical border element left of
|
|
<row>, <col>.
|
|
"""
|
|
return self._set_border_style(self._y_borders, row, col, border_style)
|
|
|
|
def _set_border_style(
|
|
self,
|
|
borders: list[BorderStyle],
|
|
row: int,
|
|
col: int,
|
|
border_style: BorderStyle,
|
|
):
|
|
"""Set <border_style> for <row>, <col> in <borders>."""
|
|
border_index = self._border_index(row, col)
|
|
actual_borderstyle = borders[border_index]
|
|
if border_style.priority >= actual_borderstyle.priority:
|
|
borders[border_index] = border_style
|
|
|
|
def get_x_border(self, row: int, col: int) -> BorderStyle:
|
|
"""Get the horizontal border element above <row>, <col>.
|
|
Last grid line (below <nrows>) is the element above of <nrows+1>.
|
|
"""
|
|
return self._get_border(self._x_borders, row, col)
|
|
|
|
def get_y_border(self, row: int, col: int) -> BorderStyle:
|
|
"""Get the vertical border element left of <row>, <col>.
|
|
Last grid line (right of <ncols>) is the element left of <ncols+1>.
|
|
"""
|
|
return self._get_border(self._y_borders, row, col)
|
|
|
|
def _get_border(
|
|
self, borders: list[BorderStyle], row: int, col: int
|
|
) -> BorderStyle:
|
|
"""Get border element at <row>, <col> from <borders>."""
|
|
return borders[self._border_index(row, col)]
|
|
|
|
def _calc_col_pos(self) -> list[float]:
|
|
"""Calculate the x-axis coords of the grid lines between the columns."""
|
|
col_pos: list[float] = []
|
|
start_x: float = self.table.insert.x
|
|
sum_fields(start_x, self.table.col_widths, col_pos.append)
|
|
return col_pos
|
|
|
|
def _calc_row_pos(self) -> list[float]:
|
|
"""Calculate the y-axis coords of the grid lines between the rows."""
|
|
row_pos: list[float] = []
|
|
start_y: float = self.table.insert.y
|
|
sum_fields(start_y, self.table.row_heights, row_pos.append, -1.0)
|
|
return row_pos
|
|
|
|
def cell_coords(
|
|
self, row: int, col: int, span: tuple[int, int]
|
|
) -> tuple[float, float, float, float]:
|
|
"""Get the coordinates of the cell <row>,<col> as absolute drawing units.
|
|
|
|
:return: a tuple (left, right, top, bottom)
|
|
"""
|
|
top = self.row_pos[row]
|
|
bottom = self.row_pos[row + span[0]]
|
|
left = self.col_pos[col]
|
|
right = self.col_pos[col + span[1]]
|
|
return left, right, top, bottom
|
|
|
|
def render_cell_background(
|
|
self, layout: GenericLayoutType, row: int, col: int, cell: Cell
|
|
):
|
|
"""Render the cell background for (row, col) as SOLID entity."""
|
|
style = cell.style
|
|
if style.bg_color is None:
|
|
return
|
|
# get cell coords in absolute drawing units
|
|
left, right, top, bottom = self.cell_coords(row, col, cell.span)
|
|
layout.add_solid(
|
|
points=((left, top), (left, bottom), (right, top), (right, bottom)),
|
|
dxfattribs={
|
|
"color": style.bg_color,
|
|
"layer": self.table.bg_layer_name,
|
|
},
|
|
)
|
|
|
|
def render_cell_content(
|
|
self, layout: GenericLayoutType, row: int, col: int, cell: Cell
|
|
):
|
|
"""Render the cell content for <row>,<col> into layout object."""
|
|
# get cell coords in absolute drawing units
|
|
coords = self.cell_coords(row, col, cell.span)
|
|
cell.render(layout, coords, self.table.fg_layer_name)
|
|
|
|
def render_lines(self, layout: GenericLayoutType, vm: VisibilityMap):
|
|
"""Render all grid lines into layout object."""
|
|
# Init borders with default_style top- and left border.
|
|
default_style = self.table.get_cell_style("default")
|
|
x_border = default_style.top
|
|
y_border = default_style.left
|
|
self._init_borders(x_border, y_border)
|
|
self._set_frames(self.table.frames)
|
|
self._set_borders(self.table.iter_visible_cells(vm))
|
|
self._render_borders(layout, self.table)
|
|
|
|
def _set_borders(self, visible_cells: Iterable[tuple[int, int, Cell]]):
|
|
"""Set borders of the visible cells."""
|
|
for row, col, cell in visible_cells:
|
|
bottom_row = row + cell.span[0]
|
|
right_col = col + cell.span[1]
|
|
self._set_rect_borders(row, bottom_row, col, right_col, cell.style)
|
|
self._set_inner_borders(
|
|
row, bottom_row, col, right_col, self.no_border
|
|
)
|
|
|
|
def _set_inner_borders(
|
|
self,
|
|
top_row: int,
|
|
bottom_row: int,
|
|
left_col: int,
|
|
right_col: int,
|
|
border_style: BorderStyle,
|
|
):
|
|
"""Set `border_style` to the inner borders of the rectangle (top_row,
|
|
bottom_row, ...)
|
|
"""
|
|
if bottom_row - top_row > 1:
|
|
for col in range(left_col, right_col):
|
|
for row in range(top_row + 1, bottom_row):
|
|
self.set_x_border(row, col, border_style)
|
|
if right_col - left_col > 1:
|
|
for row in range(top_row, bottom_row):
|
|
for col in range(left_col + 1, right_col):
|
|
self.set_y_border(row, col, border_style)
|
|
|
|
def _set_rect_borders(
|
|
self,
|
|
top_row: int,
|
|
bottom_row: int,
|
|
left_col: int,
|
|
right_col: int,
|
|
style: CellStyle,
|
|
):
|
|
"""Set border `style` to the rectangle (top_row, bottom_row, ...)
|
|
|
|
The values describing the grid lines between the cells, see doc-strings
|
|
for methods set_x_border() and set_y_border() and see comments for
|
|
self._x_borders and self._y_borders.
|
|
"""
|
|
for col in range(left_col, right_col):
|
|
self.set_x_border(top_row, col, style.top)
|
|
self.set_x_border(bottom_row, col, style.bottom)
|
|
for row in range(top_row, bottom_row):
|
|
self.set_y_border(row, left_col, style.left)
|
|
self.set_y_border(row, right_col, style.right)
|
|
|
|
def _set_frames(self, frames: Iterable[Frame]):
|
|
"""Set borders for all defined frames."""
|
|
for frame in frames:
|
|
top_row = frame.pos[0]
|
|
left_col = frame.pos[1]
|
|
bottom_row = top_row + frame.span[0]
|
|
right_col = left_col + frame.span[1]
|
|
self._set_rect_borders(
|
|
top_row, bottom_row, left_col, right_col, frame.style
|
|
)
|
|
|
|
def _render_borders(self, layout: GenericLayoutType, table: TablePainter):
|
|
"""Render the grid lines as LINE entities into layout object."""
|
|
|
|
def render_line(start: Vec2, end: Vec2, style: BorderStyle):
|
|
"""Render the LINE entity into layout object."""
|
|
if style.status:
|
|
layout.add_line(
|
|
start=start,
|
|
end=end,
|
|
dxfattribs={
|
|
"layer": layer,
|
|
"color": style.color,
|
|
"linetype": style.linetype,
|
|
"lineweight": style.lineweight,
|
|
},
|
|
)
|
|
|
|
def render_x_borders():
|
|
"""Draw the horizontal grid lines."""
|
|
for row in range(table.nrows + 1):
|
|
y = self.row_pos[row]
|
|
for col in range(table.ncols):
|
|
left = self.col_pos[col]
|
|
right = self.col_pos[col + 1]
|
|
style = self.get_x_border(row, col)
|
|
render_line(Vec2(left, y), Vec2(right, y), style)
|
|
|
|
def render_y_borders():
|
|
"""Draw the vertical grid lines."""
|
|
for col in range(table.ncols + 1):
|
|
x = self.col_pos[col]
|
|
for row in range(table.nrows):
|
|
top = self.row_pos[row]
|
|
bottom = self.row_pos[row + 1]
|
|
style = self.get_y_border(row, col)
|
|
render_line(Vec2(x, top), Vec2(x, bottom), style)
|
|
|
|
layer = table.grid_layer_name
|
|
render_x_borders()
|
|
render_y_borders()
|
|
|
|
|
|
class Frame:
|
|
"""Represent a rectangle cell area enclosed by borderlines.
|
|
|
|
Args:
|
|
table: the assigned data table
|
|
pos: tuple (row, col), border goes left and top of pos
|
|
span: count of cells that Frame covers, border goes right and below of this cells
|
|
style: style name as string
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
table: TablePainter,
|
|
pos: tuple[int, int] = (0, 0),
|
|
span: tuple[int, int] = (1, 1),
|
|
style="default",
|
|
):
|
|
self.table = table
|
|
self.pos = pos
|
|
self.span = span
|
|
self.stylename = style
|
|
|
|
@property
|
|
def style(self) -> CellStyle:
|
|
return self.table.get_cell_style(self.stylename)
|
|
|
|
|
|
class Cell:
|
|
"""Base class for table cells.
|
|
|
|
Args:
|
|
table: assigned data table
|
|
style: style name as string
|
|
span: tuple(spanrows, spancols), count of cells that cell covers
|
|
|
|
A cell doesn't know its own position in the data table, because a cell can
|
|
be used multiple times in the same or in different tables, therefore the
|
|
cell itself can not determine if the cell-range reaches beyond the table
|
|
borders.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
table: TablePainter,
|
|
style="default",
|
|
span: tuple[int, int] = (1, 1),
|
|
):
|
|
self.table = table
|
|
self.stylename = style
|
|
# span values has to be >= 1
|
|
self.span = span
|
|
|
|
@property
|
|
def span(self) -> tuple[int, int]:
|
|
"""Get/set table span parameters."""
|
|
return self._span
|
|
|
|
@span.setter
|
|
def span(self, value: tuple[int, int]):
|
|
"""Ensures that span values are >= 1 in each direction."""
|
|
self._span = (max(1, value[0]), max(1, value[1]))
|
|
|
|
@property
|
|
def style(self) -> CellStyle:
|
|
"""Returns the associated :class:`CellStyle`."""
|
|
return self.table.get_cell_style(self.stylename)
|
|
|
|
def render(
|
|
self, layout: GenericLayoutType, coords: Sequence[float], layer: str
|
|
):
|
|
"""Renders the cell content into the given `layout`."""
|
|
pass
|
|
|
|
def get_workspace_coords(self, coords: Sequence[float]) -> Sequence[float]:
|
|
"""Reduces the cell-coords about the margin_x and the margin_y values."""
|
|
margin_x = self.style.margin_x
|
|
margin_y = self.style.margin_y
|
|
return (
|
|
coords[0] + margin_x, # left
|
|
coords[1] - margin_x, # right
|
|
coords[2] - margin_y, # top
|
|
coords[3] + margin_y, # bottom
|
|
)
|
|
|
|
|
|
CustomCell = Cell
|
|
|
|
|
|
class TextCell(Cell):
|
|
"""Implements a cell type containing a multi-line text. Uses the
|
|
:class:`~ezdxf.addons.MTextSurrogate` add-on to render the multi-line
|
|
text, therefore the content of these cells is compatible to DXF R12.
|
|
|
|
Args:
|
|
table: assigned data table
|
|
text: multi line text, lines separated by the new line character ``"\\n"``
|
|
style: cell style name as string
|
|
span: tuple(rows, cols) area of cells to cover
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
table: TablePainter,
|
|
text: str,
|
|
style="default",
|
|
span: tuple[int, int] = (1, 1),
|
|
):
|
|
super(TextCell, self).__init__(table, style, span)
|
|
self.text = text
|
|
|
|
def render(
|
|
self, layout: GenericLayoutType, coords: Sequence[float], layer: str
|
|
):
|
|
"""Text cell.
|
|
|
|
Args:
|
|
layout: target layout
|
|
coords: tuple of border-coordinates: left, right, top, bottom
|
|
layer: target layer name as string
|
|
|
|
"""
|
|
if not len(self.text):
|
|
return
|
|
|
|
left, right, top, bottom = self.get_workspace_coords(coords)
|
|
style = self.style
|
|
h_align, v_align = style.get_text_align_flags()
|
|
rotated = self.style.rotation
|
|
text = self.text
|
|
if style.stacked:
|
|
rotated = 0.0
|
|
text = "\n".join((char for char in self.text.replace("\n", " ")))
|
|
xpos = (left, float(left + right) / 2.0, right)[h_align]
|
|
ypos = (bottom, float(bottom + top) / 2.0, top)[v_align - 1]
|
|
mtext = MTextSurrogate(
|
|
text,
|
|
(xpos, ypos),
|
|
line_spacing=self.style.line_spacing,
|
|
style=self.style.text_style,
|
|
char_height=self.style.char_height,
|
|
rotation=rotated,
|
|
width_factor=self.style.scale_x,
|
|
align=style.align,
|
|
color=self.style.text_color,
|
|
layer=layer,
|
|
)
|
|
mtext.render(layout)
|
|
|
|
|
|
class BlockCell(Cell):
|
|
"""Implements a cell type containing a block reference.
|
|
|
|
Args:
|
|
table: table object
|
|
blockdef: :class:`ezdxf.layouts.BlockLayout` instance
|
|
attribs: BLOCK attributes as (tag, value) dictionary
|
|
style: cell style name as string
|
|
span: tuple(rows, cols) area of cells to cover
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
table: TablePainter,
|
|
blockdef: BlockLayout,
|
|
style="default",
|
|
attribs=None,
|
|
span: tuple[int, int] = (1, 1),
|
|
):
|
|
if attribs is None:
|
|
attribs = {}
|
|
super(BlockCell, self).__init__(table, style, span)
|
|
self.block_name = blockdef.name # dxf block name!
|
|
self.attribs = attribs
|
|
|
|
def render(
|
|
self, layout: GenericLayoutType, coords: Sequence[float], layer: str
|
|
):
|
|
"""Create the cell content as INSERT-entity with trailing ATTRIB-Entities.
|
|
|
|
Args:
|
|
layout: target layout
|
|
coords: tuple of border-coordinates : left, right, top, bottom
|
|
layer: target layer name as string
|
|
|
|
"""
|
|
left, right, top, bottom = self.get_workspace_coords(coords)
|
|
style = self.style
|
|
h_align, v_align = style.get_text_align_flags()
|
|
xpos = (left, float(left + right) / 2.0, right)[h_align]
|
|
ypos = (bottom, float(bottom + top) / 2.0, top)[v_align - 1]
|
|
layout.add_auto_blockref(
|
|
name=self.block_name,
|
|
insert=(xpos, ypos),
|
|
values=self.attribs,
|
|
dxfattribs={
|
|
"xscale": style.scale_x,
|
|
"yscale": style.scale_y,
|
|
"rotation": style.rotation,
|
|
"layer": layer,
|
|
},
|
|
)
|
|
|
|
|
|
def sum_fields(
|
|
start_value: float,
|
|
fields: list[float],
|
|
append: Callable[[float], None],
|
|
sign: float = 1.0,
|
|
):
|
|
"""Adds step-by-step the fields-values, starting with <start_value>,
|
|
and appends the resulting values to another object with the
|
|
append-method.
|
|
"""
|
|
position = start_value
|
|
append(position)
|
|
for element in fields:
|
|
position += element * sign
|
|
append(position)
|