Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/entities/polygon.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

463 lines
16 KiB
Python

# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Sequence, Optional, Union, TYPE_CHECKING, Iterator
from typing_extensions import Self
import abc
import copy
from ezdxf.audit import Auditor, AuditError
from ezdxf.lldxf import const
from ezdxf.lldxf.tags import Tags
from ezdxf import colors
from ezdxf.tools import pattern
from ezdxf.math import Vec3, Matrix44
from ezdxf.math.transformtools import OCSTransform
from .boundary_paths import BoundaryPaths
from .dxfns import SubclassProcessor, DXFNamespace
from .dxfgfx import DXFGraphic
from .gradient import Gradient
from .pattern import Pattern, PatternLine
from .dxfentity import DXFEntity
from .copy import default_copy
if TYPE_CHECKING:
from ezdxf import xref
RGB = colors.RGB
__all__ = ["DXFPolygon"]
PATH_CODES = {
10,
11,
12,
13,
40,
42,
50,
51,
42,
72,
73,
74,
92,
93,
94,
95,
96,
97,
330,
}
PATTERN_DEFINITION_LINE_CODES = {53, 43, 44, 45, 46, 79, 49}
class DXFPolygon(DXFGraphic):
"""Base class for the HATCH and the MPOLYGON entity."""
LOAD_GROUP_CODES: dict[int, Union[str, list[str]]] = {}
def __init__(self) -> None:
super().__init__()
self.paths = BoundaryPaths()
self.pattern: Optional[Pattern] = None
self.gradient: Optional[Gradient] = None
self.seeds: list[tuple[float, float]] = [] # not supported/exported by MPOLYGON
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
"""Copy paths, pattern, gradient, seeds."""
assert isinstance(entity, DXFPolygon)
entity.paths = copy.deepcopy(self.paths)
entity.pattern = copy.deepcopy(self.pattern)
entity.gradient = copy.deepcopy(self.gradient)
entity.seeds = copy.deepcopy(self.seeds)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
# Copy without subclass marker:
tags = Tags(processor.subclasses[2][1:])
# Removes boundary path data from tags:
tags = self.load_paths(tags)
# Removes gradient data from tags:
tags = self.load_gradient(tags)
# Removes pattern from tags:
tags = self.load_pattern(tags)
# Removes seeds from tags:
tags = self.load_seeds(tags)
# Load HATCH DXF attributes from remaining tags:
processor.fast_load_dxfattribs(
dxf, self.LOAD_GROUP_CODES, subclass=tags, recover=True
)
return dxf
def load_paths(self, tags: Tags) -> Tags:
# Find first group code 91 = count of loops, Spline data also contains
# group code 91!
try:
start_index = tags.tag_index(91)
except const.DXFValueError:
raise const.DXFStructureError(
f"{self.dxftype()}: Missing required DXF tag 'Number of "
f"boundary paths (loops)' (code=91)."
)
path_tags = tags.collect_consecutive_tags(PATH_CODES, start=start_index + 1)
if len(path_tags):
self.paths = BoundaryPaths.load_tags(path_tags)
end_index = start_index + len(path_tags) + 1
del tags[start_index:end_index]
return tags
def load_pattern(self, tags: Tags) -> Tags:
try:
# Group code 78 = Number of pattern definition lines
index = tags.tag_index(78)
except const.DXFValueError:
# No pattern definition lines found.
return tags
pattern_tags = tags.collect_consecutive_tags(
PATTERN_DEFINITION_LINE_CODES, start=index + 1
)
self.pattern = Pattern.load_tags(pattern_tags)
# Delete pattern data including length tag 78
del tags[index : index + len(pattern_tags) + 1]
return tags
def load_gradient(self, tags: Tags) -> Tags:
try:
index = tags.tag_index(450)
except const.DXFValueError:
# No gradient data present
return tags
# Gradient data is always at the end of the AcDbHatch subclass.
self.gradient = Gradient.load_tags(tags[index:]) # type: ignore
# Remove gradient data from tags
del tags[index:]
return tags
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
assert isinstance(clone, DXFPolygon)
assert clone.doc is not None
super().map_resources(clone, mapping)
db = clone.doc.entitydb
for path in clone.paths:
handles = [mapping.get_handle(h) for h in path.source_boundary_objects]
path.source_boundary_objects = [h for h in handles if h in db]
def load_seeds(self, tags: Tags) -> Tags:
return tags
@property
def has_solid_fill(self) -> bool:
"""``True`` if entity has a solid fill. (read only)"""
return bool(self.dxf.solid_fill)
@property
def has_pattern_fill(self) -> bool:
"""``True`` if entity has a pattern fill. (read only)"""
return not bool(self.dxf.solid_fill)
@property
def has_gradient_data(self) -> bool:
"""``True`` if entity has a gradient fill. A hatch with gradient fill
has also a solid fill. (read only)
"""
return bool(self.gradient)
@property
def bgcolor(self) -> Optional[RGB]:
"""
Set pattern fill background color as (r, g, b)-tuple, rgb values
in the range [0, 255] (read/write/del)
usage::
r, g, b = entity.bgcolor # get pattern fill background color
entity.bgcolor = (10, 20, 30) # set pattern fill background color
del entity.bgcolor # delete pattern fill background color
"""
try:
xdata_bgcolor = self.get_xdata("HATCHBACKGROUNDCOLOR")
except const.DXFValueError:
return None
color = xdata_bgcolor.get_first_value(1071, 0)
try:
return colors.int2rgb(int(color))
except ValueError: # invalid data type
return RGB(0, 0, 0)
@bgcolor.setter
def bgcolor(self, rgb: RGB) -> None:
color_value = (
colors.rgb2int(rgb) | -0b111110000000000000000000000000
) # it's magic
self.discard_xdata("HATCHBACKGROUNDCOLOR")
self.set_xdata("HATCHBACKGROUNDCOLOR", [(1071, color_value)])
@bgcolor.deleter
def bgcolor(self) -> None:
self.discard_xdata("HATCHBACKGROUNDCOLOR")
def set_gradient(
self,
color1: RGB = RGB(0, 0, 0),
color2: RGB = RGB(255, 255, 255),
rotation: float = 0.0,
centered: float = 0.0,
one_color: int = 0,
tint: float = 0.0,
name: str = "LINEAR",
) -> None:
"""Sets the gradient fill mode and removes all pattern fill related data, requires
DXF R2004 or newer. A gradient filled hatch is also a solid filled hatch.
Valid gradient type names are:
- "LINEAR"
- "CYLINDER"
- "INVCYLINDER"
- "SPHERICAL"
- "INVSPHERICAL"
- "HEMISPHERICAL"
- "INVHEMISPHERICAL"
- "CURVED"
- "INVCURVED"
Args:
color1: (r, g, b)-tuple for first color, rgb values as int in
the range [0, 255]
color2: (r, g, b)-tuple for second color, rgb values as int in
the range [0, 255]
rotation: rotation angle in degrees
centered: determines whether the gradient is centered or not
one_color: 1 for gradient from `color1` to tinted `color1`
tint: determines the tinted target `color1` for a one color
gradient. (valid range 0.0 to 1.0)
name: name of gradient type, default "LINEAR"
"""
if self.doc is not None and self.doc.dxfversion < const.DXF2004:
raise const.DXFVersionError("Gradient support requires DXF R2004")
if name and name not in const.GRADIENT_TYPES:
raise const.DXFValueError(f"Invalid gradient type name: {name}")
self.pattern = None
self.dxf.solid_fill = 1
self.dxf.pattern_name = "SOLID"
self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
gradient = Gradient()
gradient.color1 = color1
gradient.color2 = color2
gradient.one_color = one_color
gradient.rotation = rotation
gradient.centered = centered
gradient.tint = tint
gradient.name = name
self.gradient = gradient
def set_pattern_fill(
self,
name: str,
color: int = 7,
angle: float = 0.0,
scale: float = 1.0,
double: int = 0,
style: int = 1,
pattern_type: int = 1,
definition=None,
) -> None:
"""Sets the pattern fill mode and removes all gradient related data.
The pattern definition should be designed for a scale factor 1 and a rotation
angle of 0 degrees. The predefined hatch pattern like "ANSI33" are scaled
according to the HEADER variable $MEASUREMENT for ISO measurement (m, cm, ... ),
or imperial units (in, ft, ...), this replicates the behavior of BricsCAD.
Args:
name: pattern name as string
color: pattern color as :ref:`ACI`
angle: pattern rotation angle in degrees
scale: pattern scale factor
double: double size flag
style: hatch style (0 = normal; 1 = outer; 2 = ignore)
pattern_type: pattern type (0 = user-defined;
1 = predefined; 2 = custom)
definition: list of definition lines and a definition line is a
4-tuple [angle, base_point, offset, dash_length_items],
see :meth:`set_pattern_definition`
"""
self.gradient = None
self.dxf.solid_fill = 0
self.dxf.pattern_name = name
self.dxf.color = color
self.dxf.pattern_scale = float(scale)
self.dxf.pattern_angle = float(angle)
self.dxf.pattern_double = int(double)
self.dxf.hatch_style = style
self.dxf.pattern_type = pattern_type
if definition is None:
measurement = 1
if self.doc:
measurement = self.doc.header.get("$MEASUREMENT", measurement)
predefined_pattern = (
pattern.ISO_PATTERN if measurement else pattern.IMPERIAL_PATTERN
)
definition = predefined_pattern.get(name, predefined_pattern["ANSI31"])
self.set_pattern_definition(
definition,
factor=self.dxf.pattern_scale,
angle=self.dxf.pattern_angle,
)
def set_pattern_definition(
self, lines: Sequence, factor: float = 1, angle: float = 0
) -> None:
"""Setup pattern definition by a list of definition lines and the
definition line is a 4-tuple (angle, base_point, offset, dash_length_items).
The pattern definition should be designed for a pattern scale factor of 1 and
a pattern rotation angle of 0.
- angle: line angle in degrees
- base-point: (x, y) tuple
- offset: (dx, dy) tuple
- dash_length_items: list of dash items (item > 0 is a line,
item < 0 is a gap and item == 0.0 is a point)
Args:
lines: list of definition lines
factor: pattern scale factor
angle: rotation angle in degrees
"""
if factor != 1 or angle:
lines = pattern.scale_pattern(lines, factor=factor, angle=angle)
self.pattern = Pattern(
[PatternLine(line[0], line[1], line[2], line[3]) for line in lines]
)
def set_pattern_scale(self, scale: float) -> None:
"""Sets the pattern scale factor and scales the pattern definition.
The method always starts from the original base scale, the
:code:`set_pattern_scale(1)` call resets the pattern scale to the original
appearance as defined by the pattern designer, but only if the pattern attribute
:attr:`dxf.pattern_scale` represents the actual scale, it cannot
restore the original pattern scale from the pattern definition itself.
Args:
scale: pattern scale factor
"""
if not self.has_pattern_fill:
return
dxf = self.dxf
self.pattern.scale(factor=1.0 / dxf.pattern_scale * scale) # type: ignore
dxf.pattern_scale = scale
def set_pattern_angle(self, angle: float) -> None:
"""Sets the pattern rotation angle and rotates the pattern definition.
The method always starts from the original base rotation of 0, the
:code:`set_pattern_angle(0)` call resets the pattern rotation angle to the
original appearance as defined by the pattern designer, but only if the
pattern attribute :attr:`dxf.pattern_angle` represents the actual pattern
rotation, it cannot restore the original rotation angle from the
pattern definition itself.
Args:
angle: pattern rotation angle in degrees
"""
if not self.has_pattern_fill:
return
dxf = self.dxf
self.pattern.scale(angle=angle - dxf.pattern_angle) # type: ignore
dxf.pattern_angle = angle % 360.0
def transform(self, m: Matrix44) -> DXFPolygon:
"""Transform entity by transformation matrix `m` inplace."""
dxf = self.dxf
ocs = OCSTransform(dxf.extrusion, m)
elevation = Vec3(dxf.elevation).z
self.paths.transform(ocs, elevation=elevation)
dxf.elevation = ocs.transform_vertex(Vec3(0, 0, elevation)).replace(
x=0.0, y=0.0
)
dxf.extrusion = ocs.new_extrusion
if self.pattern:
# todo: non-uniform scaling
# take the scaling factor of the x-axis
factor = ocs.transform_length((self.dxf.pattern_scale, 0, 0))
angle = ocs.transform_deg_angle(self.dxf.pattern_angle)
# todo: non-uniform pattern scaling is not supported
self.pattern.scale(factor, angle)
self.dxf.pattern_scale = factor
self.dxf.pattern_angle = angle
self.post_transform(m)
return self
def triangulate(self, max_sagitta, min_segments=16) -> Iterator[Sequence[Vec3]]:
"""Triangulate the HATCH/MPOLYGON in OCS coordinates, Elevation and offset is
applied to all vertices.
Args:
max_sagitta: maximum distance from the center of the curve to the
center of the line segment between two approximation points to determine
if a segment should be subdivided.
min_segments: minimum segment count per Bézier curve
.. versionadded:: 1.1
"""
from ezdxf import path
elevation = Vec3(self.dxf.elevation)
if self.dxf.hasattr("offset"): # MPOLYGON
elevation += Vec3(self.dxf.offset) # offset in OCS?
boundary_paths = [path.from_hatch_boundary_path(p) for p in self.paths]
for vertices in path.triangulate(boundary_paths, max_sagitta, min_segments):
yield tuple(elevation + v for v in vertices)
def render_pattern_lines(self) -> Iterator[tuple[Vec3, Vec3]]:
"""Yields the pattern lines in WCS coordinates.
.. versionadded:: 1.1
"""
from ezdxf.render import hatching
if self.has_pattern_fill:
try:
yield from hatching.hatch_entity(self)
except hatching.HatchingError:
return
@abc.abstractmethod
def set_solid_fill(self, color: int = 7, style: int = 1, rgb: Optional[RGB] = None):
...
def audit(self, auditor: Auditor) -> None:
super().audit(auditor)
if not self.is_alive:
return
if not self.paths.is_valid():
auditor.fixed_error(
code=AuditError.INVALID_HATCH_BOUNDARY_PATH,
message=f"Deleted entity {str(self)} containing invalid boundary paths."
)
auditor.trash(self)