refactor: excel parse
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
# Copyright (c) 2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Sequence, NamedTuple, Any, Iterator
|
||||
from typing_extensions import Self
|
||||
import abc
|
||||
import copy
|
||||
import enum
|
||||
import math
|
||||
from .deps import (
|
||||
Vec2,
|
||||
Path,
|
||||
colors,
|
||||
Matrix44,
|
||||
BoundingBox2d,
|
||||
)
|
||||
from .properties import Properties, Pen
|
||||
from ezdxf.npshapes import NumpyPath2d, NumpyPoints2d
|
||||
|
||||
# Page coordinates are always plot units:
|
||||
# 1 plot unit (plu) = 0.025mm
|
||||
# 40 plu = 1mm
|
||||
# 1016 plu = 1 inch
|
||||
# 3.39 plu = 1 dot @300 dpi
|
||||
# positive x-axis is horizontal from left to right
|
||||
# positive y-axis is vertical from bottom to top
|
||||
|
||||
|
||||
class Backend(abc.ABC):
|
||||
"""Abstract base class for implementing a low level output backends."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def draw_polyline(self, properties: Properties, points: Sequence[Vec2]) -> None:
|
||||
"""Draws a polyline from a sequence `points`. The input coordinates are page
|
||||
coordinates in plot units. The `points` sequence can contain 0 or more
|
||||
points!
|
||||
|
||||
Args:
|
||||
properties: display :class:`Properties` for the polyline
|
||||
points: sequence of :class:`ezdxf.math.Vec2` instances
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def draw_paths(
|
||||
self, properties: Properties, paths: Sequence[Path], filled: bool
|
||||
) -> None:
|
||||
"""Draws filled or outline paths from the sequence of `paths`. The input coordinates
|
||||
are page coordinates in plot units. The `paths` sequence can contain 0 or more
|
||||
single :class:`~ezdxf.path.Path` instances. Draws outline paths if
|
||||
Properties.FillType is NONE and filled paths otherwise.
|
||||
|
||||
Args:
|
||||
properties: display :class:`Properties` for the filled polygon
|
||||
paths: sequence of single :class:`ezdxf.path.Path` instances
|
||||
filled: draw filled paths if ``True`` otherwise outline paths
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class RecordType(enum.Enum):
|
||||
POLYLINE = enum.auto()
|
||||
FILLED_PATHS = enum.auto()
|
||||
OUTLINE_PATHS = enum.auto()
|
||||
|
||||
|
||||
class DataRecord(NamedTuple):
|
||||
type: RecordType
|
||||
property_hash: int
|
||||
data: Any
|
||||
|
||||
|
||||
class Recorder(Backend):
|
||||
"""The :class:`Recorder` class records the output of the :class:`Plotter` class.
|
||||
|
||||
All input coordinates are page coordinates:
|
||||
|
||||
- 1 plot unit (plu) = 0.025mm
|
||||
- 40 plu = 1 mm
|
||||
- 1016 plu = 1 inch
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._records: list[DataRecord] = []
|
||||
self._properties: dict[int, Properties] = {}
|
||||
self._pens: Sequence[Pen] = []
|
||||
|
||||
def player(self) -> Player:
|
||||
"""Returns a :class:`Player` instance with the original recordings. Make a copy
|
||||
of this player to protect the original recordings from being modified::
|
||||
|
||||
safe_player = recorder.player().copy()
|
||||
|
||||
"""
|
||||
return Player(self._records, self._properties)
|
||||
|
||||
def draw_polyline(self, properties: Properties, points: Sequence[Vec2]) -> None:
|
||||
self.store(RecordType.POLYLINE, properties, NumpyPoints2d(points))
|
||||
|
||||
def draw_paths(
|
||||
self, properties: Properties, paths: Sequence[Path], filled: bool
|
||||
) -> None:
|
||||
data = tuple(map(NumpyPath2d, paths))
|
||||
record_type = RecordType.FILLED_PATHS if filled else RecordType.OUTLINE_PATHS
|
||||
self.store(record_type, properties, data)
|
||||
|
||||
def store(self, record_type: RecordType, properties: Properties, args) -> None:
|
||||
prop_hash = properties.hash()
|
||||
if prop_hash not in self._properties:
|
||||
self._properties[prop_hash] = properties.copy()
|
||||
self._records.append(DataRecord(record_type, prop_hash, args))
|
||||
if len(self._pens) != len(properties.pen_table):
|
||||
self._pens = list(properties.pen_table.values())
|
||||
|
||||
|
||||
class Player:
|
||||
"""This class replays the recordings of the :class:`Recorder` class on another
|
||||
backend. The class can modify the recorded output.
|
||||
"""
|
||||
|
||||
def __init__(self, records: list[DataRecord], properties: dict[int, Properties]):
|
||||
self._records: list[DataRecord] = records
|
||||
self._properties: dict[int, Properties] = properties
|
||||
self._bbox = BoundingBox2d()
|
||||
|
||||
def __copy__(self) -> Self:
|
||||
"""Returns a new :class:`Player` instance with a copy of recordings."""
|
||||
records = copy.deepcopy(self._records)
|
||||
player = self.__class__(records, self._properties)
|
||||
player._bbox = self._bbox.copy()
|
||||
return player
|
||||
|
||||
copy = __copy__
|
||||
|
||||
def recordings(self) -> Iterator[tuple[RecordType, Properties, Any]]:
|
||||
"""Yields all recordings as `(RecordType, Properties, Data)` tuples.
|
||||
|
||||
The content of the `Data` field is determined by the enum :class:`RecordType`:
|
||||
|
||||
- :attr:`RecordType.POLYLINE` returns a :class:`NumpyPoints2d` instance
|
||||
- :attr:`RecordType.FILLED_POLYGON` returns a tuple of :class:`NumpyPath2d` instances
|
||||
|
||||
"""
|
||||
props = self._properties
|
||||
for record in self._records:
|
||||
yield record.type, props[record.property_hash], record.data
|
||||
|
||||
def bbox(self) -> BoundingBox2d:
|
||||
"""Returns the bounding box of all recorded polylines and polygons as
|
||||
:class:`~ezdxf.math.BoundingBox2d`.
|
||||
"""
|
||||
if not self._bbox.has_data:
|
||||
self.update_bbox()
|
||||
return self._bbox
|
||||
|
||||
def update_bbox(self) -> None:
|
||||
points: list[Vec2] = []
|
||||
for record in self._records:
|
||||
if record.type == RecordType.POLYLINE:
|
||||
points.extend(record.data.extents())
|
||||
else:
|
||||
for path in record.data:
|
||||
points.extend(path.extents())
|
||||
self._bbox = BoundingBox2d(points)
|
||||
|
||||
def replay(self, backend: Backend) -> None:
|
||||
"""Replay the recording on another backend."""
|
||||
current_props = Properties()
|
||||
props = self._properties
|
||||
for record in self._records:
|
||||
current_props = props.get(record.property_hash, current_props)
|
||||
if record.type == RecordType.POLYLINE:
|
||||
backend.draw_polyline(current_props, record.data.vertices())
|
||||
else:
|
||||
paths = [p.to_path2d() for p in record.data]
|
||||
backend.draw_paths(
|
||||
current_props, paths, filled=record.type == RecordType.FILLED_PATHS
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> None:
|
||||
"""Transforms the recordings by a transformation matrix `m` of type
|
||||
:class:`~ezdxf.math.Matrix44`.
|
||||
"""
|
||||
for record in self._records:
|
||||
if record.type == RecordType.POLYLINE:
|
||||
record.data.transform_inplace(m)
|
||||
else:
|
||||
for path in record.data:
|
||||
path.transform_inplace(m)
|
||||
|
||||
if self._bbox.has_data:
|
||||
# fast, but maybe inaccurate update
|
||||
self._bbox = BoundingBox2d(m.fast_2d_transform(self._bbox.rect_vertices()))
|
||||
|
||||
def sort_filled_paths(self) -> None:
|
||||
"""Sort filled paths by descending luminance (from light to dark).
|
||||
|
||||
This also changes the plot order in the way that all filled paths are plotted
|
||||
before polylines and outline paths.
|
||||
"""
|
||||
fillings = []
|
||||
outlines = []
|
||||
current = Properties()
|
||||
props = self._properties
|
||||
for record in self._records:
|
||||
if record.type == RecordType.FILLED_PATHS:
|
||||
current = props.get(record.property_hash, current)
|
||||
key = colors.luminance(current.resolve_fill_color())
|
||||
fillings.append((key, record))
|
||||
else:
|
||||
outlines.append(record)
|
||||
|
||||
fillings.sort(key=lambda r: r[0], reverse=True)
|
||||
records = [sort_rec[1] for sort_rec in fillings]
|
||||
records.extend(outlines)
|
||||
self._records = records
|
||||
|
||||
|
||||
def placement_matrix(
|
||||
bbox: BoundingBox2d, sx: float = 1.0, sy: float = 1.0, rotation: float = 0.0
|
||||
) -> Matrix44:
|
||||
"""Returns a matrix to place the bbox in the first quadrant of the coordinate
|
||||
system (+x, +y).
|
||||
"""
|
||||
if abs(sx) < 1e-9:
|
||||
sx = 1.0
|
||||
if abs(sy) < 1e-9:
|
||||
sy = 1.0
|
||||
m = Matrix44.scale(sx, sy, 1.0)
|
||||
if rotation:
|
||||
m @= Matrix44.z_rotate(math.radians(rotation))
|
||||
corners = m.fast_2d_transform(bbox.rect_vertices())
|
||||
tx, ty = BoundingBox2d(corners).extmin
|
||||
return m @ Matrix44.translate(-tx, -ty, 0)
|
||||
Reference in New Issue
Block a user