324 lines
12 KiB
Python
324 lines
12 KiB
Python
# Copyright (c) 2019-2022 Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING, Iterable, Optional
|
|
|
|
from ezdxf.lldxf import const
|
|
from ezdxf.lldxf import validator
|
|
from ezdxf.lldxf.attributes import (
|
|
DXFAttr,
|
|
DXFAttributes,
|
|
DefSubclass,
|
|
XType,
|
|
RETURN_DEFAULT,
|
|
group_code_mapping,
|
|
)
|
|
from ezdxf.lldxf.tags import Tags
|
|
from ezdxf.math import NULLVEC, Z_AXIS
|
|
from .boundary_paths import AbstractBoundaryPath
|
|
from .dxfentity import base_class
|
|
from .dxfgfx import acdb_entity
|
|
from .factory import register_entity
|
|
from .polygon import DXFPolygon
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.colors import RGB
|
|
from ezdxf.document import Drawing
|
|
from ezdxf.entities import DXFEntity
|
|
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
|
|
|
|
|
__all__ = ["Hatch"]
|
|
|
|
|
|
acdb_hatch = DefSubclass(
|
|
"AcDbHatch",
|
|
{
|
|
# This subclass can also represent a MPolygon, whatever this is, never seen
|
|
# such a MPolygon in the wild.
|
|
# x- and y-axis always equal 0, z-axis represents the elevation:
|
|
"elevation": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
|
"extrusion": DXFAttr(
|
|
210,
|
|
xtype=XType.point3d,
|
|
default=Z_AXIS,
|
|
validator=validator.is_not_null_vector,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Hatch pattern name:
|
|
"pattern_name": DXFAttr(2, default="SOLID"),
|
|
# HATCH: Solid fill flag:
|
|
# 0 = pattern fill
|
|
# 1 = solid fill
|
|
"solid_fill": DXFAttr(
|
|
70,
|
|
default=1,
|
|
validator=validator.is_integer_bool,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# HATCH: associativity flag
|
|
# 0 = non-associative
|
|
# 1 = associative
|
|
"associative": DXFAttr(
|
|
71,
|
|
default=0,
|
|
validator=validator.is_integer_bool,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# 91: Number of boundary paths (loops)
|
|
# following: Boundary path data. Repeats number of times specified by
|
|
# code 91. See Boundary Path Data
|
|
# Hatch style:
|
|
# 0 = Hatch “odd parity” area (Normal style)
|
|
# 1 = Hatch outermost area only (Outer style)
|
|
# 2 = Hatch through entire area (Ignore style)
|
|
"hatch_style": DXFAttr(
|
|
75,
|
|
default=const.HATCH_STYLE_NESTED,
|
|
validator=validator.is_in_integer_range(0, 3),
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Hatch pattern type:
|
|
# 0 = User-defined
|
|
# 1 = Predefined
|
|
# 2 = Custom
|
|
"pattern_type": DXFAttr(
|
|
76,
|
|
default=const.HATCH_TYPE_PREDEFINED,
|
|
validator=validator.is_in_integer_range(0, 3),
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Hatch pattern angle (pattern fill only) in degrees:
|
|
"pattern_angle": DXFAttr(52, default=0),
|
|
# Hatch pattern scale or spacing (pattern fill only):
|
|
"pattern_scale": DXFAttr(
|
|
41,
|
|
default=1,
|
|
validator=validator.is_not_zero,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Hatch pattern double flag (pattern fill only):
|
|
# 0 = not double
|
|
# 1 = double
|
|
"pattern_double": DXFAttr(
|
|
77,
|
|
default=0,
|
|
validator=validator.is_integer_bool,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# 78: Number of pattern definition lines
|
|
# following: Pattern line data. Repeats number of times specified by
|
|
# code 78. See Pattern Data
|
|
# Pixel size used to determine the density to perform various intersection
|
|
# and ray casting operations in hatch pattern computation for associative
|
|
# hatches and hatches created with the Flood method of hatching
|
|
"pixel_size": DXFAttr(47, optional=True),
|
|
# Number of seed points
|
|
"n_seed_points": DXFAttr(
|
|
98,
|
|
default=0,
|
|
validator=validator.is_greater_or_equal_zero,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# 10, 20: Seed point (in OCS) 2D point (multiple entries)
|
|
# 450 Indicates solid hatch or gradient; if solid hatch, the values for the
|
|
# remaining codes are ignored but must be present. Optional;
|
|
#
|
|
# if code 450 is in the file, then the following codes must be in the
|
|
# file: 451, 452, 453, 460, 461, 462, and 470.
|
|
# If code 450 is not in the file, then the following codes must not be
|
|
# in the file: 451, 452, 453, 460, 461, 462, and 470
|
|
#
|
|
# 0 = Solid hatch
|
|
# 1 = Gradient
|
|
#
|
|
# 451 Zero is reserved for future use
|
|
# 452 Records how colors were defined and is used only by dialog code:
|
|
#
|
|
# 0 = Two-color gradient
|
|
# 1 = Single-color gradient
|
|
#
|
|
# 453 Number of colors:
|
|
#
|
|
# 0 = Solid hatch
|
|
# 2 = Gradient
|
|
#
|
|
# 460 Rotation angle in radians for gradients (default = 0, 0)
|
|
# 461 Gradient definition; corresponds to the Centered option on the
|
|
# Gradient Tab of the Boundary Hatch and Fill dialog box. Each gradient
|
|
# has two definitions, shifted and non-shifted. A Shift value describes
|
|
# the blend of the two definitions that should be used. A value of 0.0
|
|
# means only the non-shifted version should be used, and a value of 1.0
|
|
# means that only the shifted version should be used.
|
|
#
|
|
# 462 Color tint value used by dialog code (default = 0, 0; range is 0.0 to
|
|
# 1.0). The color tint value is a gradient color and controls the degree
|
|
# of tint in the dialog when the Hatch group code 452 is set to 1.
|
|
#
|
|
# 463 Reserved for future use:
|
|
#
|
|
# 0 = First value
|
|
# 1 = Second value
|
|
#
|
|
# 470 String (default = LINEAR)
|
|
},
|
|
)
|
|
acdb_hatch_group_code = group_code_mapping(acdb_hatch)
|
|
|
|
|
|
@register_entity
|
|
class Hatch(DXFPolygon):
|
|
"""DXF HATCH entity"""
|
|
|
|
DXFTYPE = "HATCH"
|
|
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_hatch)
|
|
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
|
LOAD_GROUP_CODES = acdb_hatch_group_code
|
|
|
|
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
|
"""Export entity specific data as DXF tags."""
|
|
super().export_entity(tagwriter)
|
|
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_hatch.name)
|
|
self.dxf.export_dxf_attribs(
|
|
tagwriter,
|
|
[
|
|
"elevation",
|
|
"extrusion",
|
|
"pattern_name",
|
|
"solid_fill",
|
|
"associative",
|
|
],
|
|
)
|
|
self.paths.export_dxf(tagwriter, self.dxftype())
|
|
self.dxf.export_dxf_attribs(tagwriter, ["hatch_style", "pattern_type"])
|
|
if self.pattern:
|
|
self.dxf.export_dxf_attribs(
|
|
tagwriter, ["pattern_angle", "pattern_scale", "pattern_double"]
|
|
)
|
|
self.pattern.export_dxf(tagwriter)
|
|
self.dxf.export_dxf_attribs(tagwriter, ["pixel_size"])
|
|
self.export_seeds(tagwriter)
|
|
if self.gradient and tagwriter.dxfversion > const.DXF2000:
|
|
self.gradient.export_dxf(tagwriter)
|
|
|
|
def load_seeds(self, tags: Tags) -> Tags:
|
|
try:
|
|
start_index = tags.tag_index(98)
|
|
except const.DXFValueError:
|
|
return tags
|
|
seed_data = tags.collect_consecutive_tags(
|
|
{98, 10, 20}, start=start_index
|
|
)
|
|
|
|
# Remove seed data from tags:
|
|
del tags[start_index : start_index + len(seed_data) + 1]
|
|
|
|
# Just process vertices with group code 10
|
|
self.seeds = [value for code, value in seed_data if code == 10]
|
|
return tags
|
|
|
|
def export_seeds(self, tagwriter: AbstractTagWriter):
|
|
tagwriter.write_tag2(98, len(self.seeds))
|
|
for seed in self.seeds:
|
|
tagwriter.write_vertex(10, seed[:2])
|
|
|
|
def remove_dependencies(self, other: Optional[Drawing] = None) -> None:
|
|
"""Remove all dependencies from actual document. (internal API)"""
|
|
if not self.is_alive:
|
|
return
|
|
|
|
super().remove_dependencies()
|
|
self.remove_association()
|
|
|
|
def remove_association(self):
|
|
"""Remove associated path elements."""
|
|
if self.dxf.associative:
|
|
self.dxf.associative = 0
|
|
for path in self.paths:
|
|
path.source_boundary_objects = []
|
|
|
|
def set_solid_fill(
|
|
self, color: int = 7, style: int = 1, rgb: Optional[RGB] = None
|
|
):
|
|
"""Set the solid fill mode and removes all gradient and pattern fill related
|
|
data.
|
|
|
|
Args:
|
|
color: :ref:`ACI`, (0 = BYBLOCK; 256 = BYLAYER)
|
|
style: hatch style (0 = normal; 1 = outer; 2 = ignore)
|
|
rgb: true color value as (r, g, b)-tuple - has higher priority
|
|
than `color`. True color support requires DXF R2000.
|
|
|
|
"""
|
|
# remove existing gradient and pattern fill
|
|
self.gradient = None
|
|
self.pattern = None
|
|
self.dxf.solid_fill = 1
|
|
|
|
# if true color is present, the color attribute is ignored
|
|
self.dxf.color = color
|
|
self.dxf.hatch_style = style
|
|
self.dxf.pattern_name = "SOLID"
|
|
self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
|
|
if rgb is not None:
|
|
self.rgb = rgb
|
|
|
|
def associate(
|
|
self, path: AbstractBoundaryPath, entities: Iterable[DXFEntity]
|
|
):
|
|
"""Set association from hatch boundary `path` to DXF geometry `entities`.
|
|
|
|
A HATCH entity can be associative to a base geometry, this association
|
|
is **not** maintained nor verified by `ezdxf`, so if you modify the base
|
|
geometry the geometry of the boundary path is not updated and no
|
|
verification is done to check if the associated geometry matches
|
|
the boundary path, this opens many possibilities to create
|
|
invalid DXF files: USE WITH CARE!
|
|
|
|
"""
|
|
# I don't see this as a time critical operation, do as much checks as
|
|
# needed to avoid invalid DXF files.
|
|
if not self.is_alive:
|
|
raise const.DXFStructureError("HATCH entity is destroyed")
|
|
|
|
doc = self.doc
|
|
owner = self.dxf.owner
|
|
handle = self.dxf.handle
|
|
if doc is None or owner is None or handle is None:
|
|
raise const.DXFStructureError(
|
|
"virtual entity can not have associated entities"
|
|
)
|
|
|
|
for entity in entities:
|
|
if not entity.is_alive or entity.is_virtual:
|
|
raise const.DXFStructureError(
|
|
"associated entity is destroyed or a virtual entity"
|
|
)
|
|
if doc is not entity.doc:
|
|
raise const.DXFStructureError(
|
|
"associated entity is from a different document"
|
|
)
|
|
if owner != entity.dxf.owner:
|
|
raise const.DXFStructureError(
|
|
"associated entity is from a different layout"
|
|
)
|
|
|
|
path.source_boundary_objects.append(entity.dxf.handle)
|
|
entity.append_reactor_handle(handle)
|
|
self.dxf.associative = 1 if len(path.source_boundary_objects) else 0
|
|
|
|
def set_seed_points(self, points: Iterable[tuple[float, float]]) -> None:
|
|
"""Set seed points, `points` is an iterable of (x, y)-tuples.
|
|
I don't know why there can be more than one seed point.
|
|
All points in :ref:`OCS` (:attr:`Hatch.dxf.elevation` is the Z value)
|
|
|
|
"""
|
|
points = list(points)
|
|
if len(points) < 1:
|
|
raise const.DXFValueError(
|
|
"Argument points should be an iterable of 2D points and requires"
|
|
" at least one point."
|
|
)
|
|
self.seeds = list(points)
|
|
self.dxf.n_seed_points = len(self.seeds)
|