# Copyright (c) 2018-2024, Manfred Moitzi # License: MIT License from __future__ import annotations from typing import ( TYPE_CHECKING, Iterable, Optional, Sequence, Iterator, ) from typing_extensions import Self from collections import OrderedDict, namedtuple import math from ezdxf.audit import AuditError from ezdxf.entities.factory import register_entity from ezdxf.lldxf import const, validator from ezdxf.lldxf.attributes import ( DXFAttr, DXFAttributes, DefSubclass, XType, RETURN_DEFAULT, group_code_mapping, ) from ezdxf.lldxf.tags import Tags, group_tags from ezdxf.math import NULLVEC, X_AXIS, Y_AXIS, Z_AXIS, UVec, Vec3, UCS, OCS from .dxfentity import base_class, SubclassProcessor from .dxfobj import DXFObject from .dxfgfx import DXFGraphic, acdb_entity from .objectcollection import ObjectCollection from .copy import default_copy import logging if TYPE_CHECKING: from ezdxf.audit import Auditor from ezdxf.document import Drawing from ezdxf.entities import DXFNamespace, DXFEntity from ezdxf.layouts import BaseLayout from ezdxf.lldxf.tagwriter import AbstractTagWriter from ezdxf.math import Matrix44 from ezdxf.query import EntityQuery from ezdxf import xref __all__ = ["MLine", "MLineVertex", "MLineStyle", "MLineStyleCollection"] logger = logging.getLogger("ezdxf") # Usage example: CADKitSamples\Lock-Off.dxf def filter_close_vertices( vertices: Iterable[Vec3], abs_tol: float = 1e-12 ) -> Iterable[Vec3]: prev = None for vertex in vertices: if prev is None: yield vertex prev = vertex else: if not vertex.isclose(prev, abs_tol=abs_tol): yield vertex prev = vertex acdb_mline = DefSubclass( "AcDbMline", OrderedDict( { "style_name": DXFAttr(2, default="Standard"), "style_handle": DXFAttr(340), "scale_factor": DXFAttr( 40, default=1, validator=validator.is_not_zero, fixer=RETURN_DEFAULT, ), # Justification # 0 = Top (Right) # 1 = Zero (Center) # 2 = Bottom (Left) "justification": DXFAttr( 70, default=0, validator=validator.is_in_integer_range(0, 3), fixer=RETURN_DEFAULT, ), # Flags (bit-coded values): # 1 = Has at least one vertex (code 72 is greater than 0) # 2 = Closed # 4 = Suppress start caps # 8 = Suppress end caps "flags": DXFAttr(71, default=1), # Number of MLINE vertices "count": DXFAttr(72, xtype=XType.callback, getter="__len__"), # Number of elements in MLINESTYLE definition "style_element_count": DXFAttr(73, default=2), # start location in WCS! "start_location": DXFAttr( 10, xtype=XType.callback, getter="start_location" ), # Normal vector of the entity plane, but all vertices in WCS! "extrusion": DXFAttr( 210, xtype=XType.point3d, default=Z_AXIS, validator=validator.is_not_null_vector, fixer=RETURN_DEFAULT, ), # MLine data: # 11: vertex coordinates # Multiple entries; one entry for each vertex. # 12: Direction vector of segment starting at this vertex # Multiple entries; one for each vertex. # 13: Direction vector of miter at this vertex # Multiple entries: one for each vertex. # 74: Number of parameters for this element, # repeats for each element in segment # 41: Element parameters, # repeats based on previous code 74 # 75: Number of area fill parameters for this element, # repeats for each element in segment # 42: Area fill parameters, # repeats based on previous code 75 } ), ) acdb_mline_group_codes = group_code_mapping(acdb_mline) # For information about line- and fill parametrization see comments in class # MLineVertex(). # # The 2 group codes in mline entities and mlinestyle objects are redundant # fields. These groups should not be modified under any circumstances, although # it is safe to read them and use their values. The correct fields to modify # are as follows: # # Mline # The 340 group in the same object, which indicates the proper MLINESTYLE # object. # # Mlinestyle # The 3 group value in the MLINESTYLE dictionary, which precedes the 350 group # that has the handle or entity name of # the current mlinestyle. # Facts and assumptions not clearly defined by the DXF reference: # - the reference line is defined by the group code 11 points (fact) # - all line segments are parallel to the reference line (assumption) # - all line vertices are located in the same plane, the orientation of the plane # is defined by the extrusion vector (assumption) # - the scale factor is applied to to all geometries # - the start- and end angle (MLineStyle) is also applied to the first and last # miter direction vector # - the last two points mean: all geometries and direction vectors can be used # as stored in the DXF file no additional scaling or rotation is necessary # for the MLINE rendering. Disadvantage: minor changes of DXF attributes # require a refresh of the MLineVertices. # Ezdxf does not support the creation of line-break (gap) features, but will be # preserving this data if the MLINE stays unchanged. # Editing the MLINE entity by ezdxf removes the line-break features (gaps). class MLineVertex: def __init__(self) -> None: self.location: Vec3 = NULLVEC self.line_direction: Vec3 = X_AXIS self.miter_direction: Vec3 = Y_AXIS # Line parametrization (74/41) # ---------------------------- # The line parameterization is a list of float values. # The list may contain zero or more items. # # The first value (miter-offset) is the distance from the vertex # location along the miter direction vector to the point where the # line element's path intersects the miter vector. # # The next value (line-start-offset) is the distance along the line # direction from the miter/line path intersection point to the actual # start of the line element. # # The next value (dash-length) is the distance from the start of the # line element (dash) to the first break (or gap) in the line element. # The successive values continue to list the start and stop points of # the line element in this segment of the mline. # Linetypes do not affect the line parametrization. # # # 1. line element: [miter-offset, line-start-offset, dash, gap, dash, ...] # 2. line element: [...] # ... self.line_params: list[Sequence[float]] = [] """ The line parameterization is a list of float values. The list may contain zero or more items. """ # Fill parametrization (75/42) # ---------------------------- # # The fill parameterization is also a list of float values. # Similar to the line parametrization, it describes the # parametrization of the fill area for this mline segment. # The values are interpreted identically to the line parameters and when # taken as a whole for all line elements in the mline segment, they # define the boundary of the fill area for the mline segment. # # A common example of the use of the fill mechanism is when an # unfilled mline crosses over a filled mline and "mledit" is used to # cause the filled mline to appear unfilled in the crossing area. # This would result in two fill parameters for each line element in the # affected mline segment; one for the fill stop and one for the fill # start. # # [dash-length, gap-length, ...]? self.fill_params: list[Sequence[float]] = [] def __copy__(self) -> MLineVertex: vtx = self.__class__() vtx.location = self.location vtx.line_direction = self.line_direction vtx.miter_direction = self.miter_direction vtx.line_params = list(self.line_params) vtx.fill_params = list(self.fill_params) return vtx copy = __copy__ @classmethod def load(cls, tags: Tags) -> MLineVertex: vtx = MLineVertex() line_params: list[float] = [] line_params_count = 0 fill_params: list[float] = [] fill_params_count = 0 for code, value in tags: if code == 11: vtx.location = Vec3(value) elif code == 12: vtx.line_direction = Vec3(value) elif code == 13: vtx.miter_direction = Vec3(value) elif code == 74: line_params_count = value if line_params_count == 0: vtx.line_params.append(tuple()) else: line_params = [] elif code == 41: line_params.append(value) line_params_count -= 1 if line_params_count == 0: vtx.line_params.append(tuple(line_params)) line_params = [] elif code == 75: fill_params_count = value if fill_params_count == 0: vtx.fill_params.append(tuple()) else: fill_params = [] elif code == 42: fill_params.append(value) fill_params_count -= 1 if fill_params_count == 0: vtx.fill_params.append(tuple(fill_params)) return vtx def export_dxf(self, tagwriter: AbstractTagWriter): tagwriter.write_vertex(11, self.location) tagwriter.write_vertex(12, self.line_direction) tagwriter.write_vertex(13, self.miter_direction) for line_params, fill_params in zip(self.line_params, self.fill_params): tagwriter.write_tag2(74, len(line_params)) for param in line_params: tagwriter.write_tag2(41, param) tagwriter.write_tag2(75, len(fill_params)) for param in fill_params: tagwriter.write_tag2(42, param) @classmethod def new( cls, start: UVec, line_direction: UVec, miter_direction: UVec, line_params: Optional[Iterable[Sequence[float]]] = None, fill_params: Optional[Iterable[Sequence[float]]] = None, ) -> MLineVertex: vtx = MLineVertex() vtx.location = Vec3(start) vtx.line_direction = Vec3(line_direction) vtx.miter_direction = Vec3(miter_direction) vtx.line_params = list(line_params or []) vtx.fill_params = list(fill_params or []) if len(vtx.line_params) != len(vtx.fill_params): raise const.DXFValueError("Count mismatch of line- and fill parameters") return vtx def transform(self, m: Matrix44) -> MLineVertex: """Transform MLineVertex by transformation matrix `m` inplace.""" self.location = m.transform(self.location) self.line_direction = m.transform_direction(self.line_direction) self.miter_direction = m.transform_direction(self.miter_direction) return self @register_entity class MLine(DXFGraphic): DXFTYPE = "MLINE" DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mline) MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000 TOP = const.MLINE_TOP ZERO = const.MLINE_ZERO BOTTOM = const.MLINE_BOTTOM HAS_VERTICES = const.MLINE_HAS_VERTICES CLOSED = const.MLINE_CLOSED SUPPRESS_START_CAPS = const.MLINE_SUPPRESS_START_CAPS SUPPRESS_END_CAPS = const.MLINE_SUPPRESS_END_CAPS def __init__(self) -> None: super().__init__() # The MLINE geometry stored in vertices, is the final geometry, # scaling factor, justification and MLineStyle settings are already # applied. This is why the geometry has to be updated every time a # change is applied. self.vertices: list[MLineVertex] = [] def __len__(self): """Count of MLINE vertices.""" return len(self.vertices) def copy_data(self, entity: Self, copy_strategy=default_copy) -> None: assert isinstance(entity, MLine) entity.vertices = [v.copy() for v in self.vertices] def load_dxf_attribs( self, processor: Optional[SubclassProcessor] = None ) -> DXFNamespace: dxf = super().load_dxf_attribs(processor) if processor: tags = processor.fast_load_dxfattribs( dxf, acdb_mline_group_codes, 2, log=False ) self.load_vertices(tags) return dxf def load_vertices(self, tags: Tags) -> None: self.vertices.extend( MLineVertex.load(tags) for tags in group_tags(tags, splitcode=11) ) def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool: # Do not export MLines without vertices return len(self.vertices) > 1 # todo: check if line- and fill parametrization is compatible with # MLINE style, requires same count of elements! def export_entity(self, tagwriter: AbstractTagWriter) -> None: # ezdxf does not export MLINE entities without vertices, # see method preprocess_export() self.set_flag_state(self.HAS_VERTICES, True) super().export_entity(tagwriter) tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_mline.name) self.dxf.export_dxf_attribs(tagwriter, acdb_mline.attribs.keys()) self.export_vertices(tagwriter) def export_vertices(self, tagwriter: AbstractTagWriter) -> None: for vertex in self.vertices: vertex.export_dxf(tagwriter) def register_resources(self, registry: xref.Registry) -> None: """Register required resources to the resource registry.""" super().register_resources(registry) registry.add_handle(self.dxf.style_handle) def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None: """Translate resources from self to the copied entity.""" super().map_resources(clone, mapping) style = mapping.get_reference_of_copy(self.dxf.style_handle) if not isinstance(style, MLineStyle): assert clone.doc is not None style = clone.doc.mline_styles.get("Standard") if isinstance(style, MLineStyle): clone.dxf.style_handle = style.dxf.handle clone.dxf.style_name = style.dxf.name else: clone.dxf.style_handle = "0" clone.dxf.style_name = "Standard" @property def is_closed(self) -> bool: """Returns ``True`` if MLINE is closed. Compatibility interface to :class:`Polyline` """ return self.get_flag_state(self.CLOSED) def close(self, state: bool = True) -> None: """Get/set closed state of MLINE and update geometry accordingly. Compatibility interface to :class:`Polyline` """ state = bool(state) if state != self.is_closed: self.set_flag_state(self.CLOSED, state) self.update_geometry() @property def start_caps(self) -> bool: """Get/Set start caps state. ``True`` to enable start caps and ``False`` tu suppress start caps.""" return not self.get_flag_state(self.SUPPRESS_START_CAPS) @start_caps.setter def start_caps(self, value: bool) -> None: """Set start caps state.""" self.set_flag_state(self.SUPPRESS_START_CAPS, not bool(value)) @property def end_caps(self) -> bool: """Get/Set end caps state. ``True`` to enable end caps and ``False`` tu suppress start caps.""" return not self.get_flag_state(self.SUPPRESS_END_CAPS) @end_caps.setter def end_caps(self, value: bool) -> None: """Set start caps state.""" self.set_flag_state(self.SUPPRESS_END_CAPS, not bool(value)) def set_scale_factor(self, value: float) -> None: """Set the scale factor and update geometry accordingly.""" value = float(value) if not math.isclose(self.dxf.scale_factor, value): self.dxf.scale_factor = value self.update_geometry() def set_justification(self, value: int) -> None: """Set MLINE justification and update geometry accordingly. See :attr:`dxf.justification` for valid settings. """ value = int(value) if self.dxf.justification != value: self.dxf.justification = value self.update_geometry() @property def style(self) -> Optional[MLineStyle]: """Get associated MLINESTYLE.""" if self.doc is None: return None _style = self.doc.entitydb.get(self.dxf.style_handle) if _style is None: _style = self.doc.mline_styles.get(self.dxf.style_name) return _style # type: ignore def set_style(self, name: str) -> None: """Set MLINESTYLE by name and update geometry accordingly. The MLINESTYLE definition must exist. """ if self.doc is None: logger.debug("Can't change style of unbounded MLINE entity.") return try: style = self.doc.mline_styles[name] except const.DXFKeyError: raise const.DXFValueError(f"Undefined MLINE style: {name}") assert isinstance(style, MLineStyle) # Line- and fill parametrization depends on the count of # elements, a change in the number of elements triggers a # reset of the parametrization: old_style = self.style new_element_count = len(style.elements) reset = False if old_style is not None: # Do not trust the stored "style_element_count" value reset = len(old_style.elements) != new_element_count self.dxf.style_name = name self.dxf.style_handle = style.dxf.handle self.dxf.style_element_count = new_element_count if reset: self.update_geometry() def start_location(self) -> Vec3: """Returns the start location of the reference line. Callback function for :attr:`dxf.start_location`. """ if len(self.vertices): return self.vertices[0].location else: return NULLVEC def get_locations(self) -> list[Vec3]: """Returns the vertices of the reference line.""" return [v.location for v in self.vertices] def extend(self, vertices: Iterable[UVec]) -> None: """Append multiple vertices to the reference line. It is possible to work with 3D vertices, but all vertices have to be in the same plane and the normal vector of this plan is stored as extrusion vector in the MLINE entity. """ vertices = Vec3.list(vertices) if not vertices: return all_vertices = [] if len(self): all_vertices.extend(self.get_locations()) all_vertices.extend(vertices) self.generate_geometry(all_vertices) def update_geometry(self) -> None: """Regenerate the MLINE geometry based on current settings.""" self.generate_geometry(self.get_locations()) def generate_geometry(self, vertices: list[Vec3]) -> None: """Regenerate the MLINE geometry for new reference line defined by `vertices`. """ vertices = list(filter_close_vertices(vertices, abs_tol=1e-6)) if len(vertices) == 0: self.clear() return elif len(vertices) == 1: self.vertices = [MLineVertex.new(vertices[0], X_AXIS, Y_AXIS)] return style = self.style assert style is not None, "valid MLINE style required" if len(style.elements) == 0: raise const.DXFStructureError(f"No line elements defined in {str(style)}.") def miter(dir1: Vec3, dir2: Vec3): return ((dir1 + dir2) * 0.5).normalize().orthogonal() ucs = UCS.from_z_axis_and_point_in_xz( origin=vertices[0], point=vertices[1], axis=self.dxf.extrusion, ) # Transform given vertices into UCS and project them into the # UCS-xy-plane by setting the z-axis to 0: vertices = [v.replace(z=0.0) for v in ucs.points_from_wcs(vertices)] start_angle = style.dxf.start_angle end_angle = style.dxf.end_angle line_directions = [ (v2 - v1).normalize() for v1, v2 in zip(vertices, vertices[1:]) ] if self.is_closed: line_directions.append((vertices[0] - vertices[-1]).normalize()) closing_miter = miter(line_directions[0], line_directions[-1]) miter_directions = [closing_miter] else: closing_miter = None line_directions.append(line_directions[-1]) miter_directions = [line_directions[0].rotate_deg(start_angle)] for d1, d2 in zip(line_directions, line_directions[1:]): miter_directions.append(miter(d1, d2)) if closing_miter is None: miter_directions.pop() miter_directions.append(line_directions[-1].rotate_deg(end_angle)) else: miter_directions.append(closing_miter) self.vertices = [ MLineVertex.new(v, d, m) for v, d, m in zip(vertices, line_directions, miter_directions) ] self._update_parametrization() # reverse transformation into WCS for v in self.vertices: v.transform(ucs.matrix) def _update_parametrization(self): scale = self.dxf.scale_factor style = self.style justification = self.dxf.justification offsets = [e.offset for e in style.elements] min_offset = min(offsets) max_offset = max(offsets) shift = 0 if justification == self.TOP: shift = -max_offset elif justification == self.BOTTOM: shift = -min_offset for vertex in self.vertices: angle = vertex.line_direction.angle_between(vertex.miter_direction) try: stretch = scale / math.sin(angle) except ZeroDivisionError: stretch = 1.0 vertex.line_params = [ ((element.offset + shift) * stretch, 0.0) for element in style.elements ] vertex.fill_params = [tuple() for _ in style.elements] def clear(self) -> None: """Remove all MLINE vertices.""" self.vertices.clear() def remove_dependencies(self, other: Optional[Drawing] = None) -> None: """Remove all dependencies from current document. (internal API) """ if not self.is_alive: return super().remove_dependencies(other) self.dxf.style_handle = "0" if other: style = other.mline_styles.get(self.dxf.style_name) if style: self.dxf.style_handle = style.dxf.handle return self.dxf.style_name = "Standard" def transform(self, m: Matrix44) -> Self: """Transform MLINE entity by transformation matrix `m` inplace.""" for vertex in self.vertices: vertex.transform(m) self.dxf.extrusion = m.transform_direction(self.dxf.extrusion) scale = self.dxf.scale_factor scale_vec = m.transform_direction(Vec3(scale, scale, scale)) if math.isclose(scale_vec.x, scale_vec.y, abs_tol=1e-6) and math.isclose( scale_vec.y, scale_vec.z, abs_tol=1e-6 ): self.dxf.scale_factor = sum(scale_vec) / 3 # average error # None uniform scaling will not be applied to the scale_factor! self.update_geometry() self.post_transform(m) return self def __virtual_entities__(self) -> Iterator[DXFGraphic]: """Implements the SupportsVirtualEntities protocol. This protocol is for consistent internal usage and does not replace the method :meth:`virtual_entities`! """ from ezdxf.render.mline import virtual_entities for e in virtual_entities(self): e.set_source_of_copy(self) yield e def virtual_entities(self) -> Iterator[DXFGraphic]: """Yields virtual DXF primitives of the MLINE entity as LINE, ARC and HATCH entities. These entities are located at the original positions, but are not stored in the entity database, have no handle and are not assigned to any layout. """ return self.__virtual_entities__() def explode(self, target_layout: Optional[BaseLayout] = None) -> EntityQuery: """Explode the MLINE entity as LINE, ARC and HATCH entities into target layout, if target layout is ``None``, the target layout is the layout of the MLINE. This method destroys the source entity. Returns an :class:`~ezdxf.query.EntityQuery` container referencing all DXF primitives. Args: target_layout: target layout for DXF primitives, ``None`` for same layout as source entity. """ from ezdxf.explode import explode_entity return explode_entity(self, target_layout) def audit(self, auditor: Auditor) -> None: """Validity check.""" def reset_mline_style(name="Standard"): auditor.fixed_error( code=AuditError.RESET_MLINE_STYLE, message=f'Reset MLINESTYLE to "{name}" in {str(self)}.', dxf_entity=self, ) self.dxf.style_name = name style = doc.mline_styles.get(name) self.dxf.style_handle = style.dxf.handle super().audit(auditor) doc = auditor.doc if doc is None: return # Audit associated MLINESTYLE name and handle: style = doc.entitydb.get(self.dxf.style_handle) if not isinstance(style, MLineStyle): # handle is invalid, get style by name style = doc.mline_styles.get(self.dxf.style_name, None) if style is None: reset_mline_style() else: # fix MLINESTYLE handle: auditor.fixed_error( code=AuditError.INVALID_MLINESTYLE_HANDLE, message=f"Fixed invalid style handle in {str(self)}.", dxf_entity=self, ) self.dxf.style_handle = style.dxf.handle else: # update MLINESTYLE name silently self.dxf.style_name = style.dxf.name # Get current (maybe fixed) MLINESTYLE: style = self.style assert style is not None, "valid MLINE style required" # Update style element count silently: element_count = len(style.elements) self.dxf.style_element_count = element_count # Audit vertices: for vertex in self.vertices: if NULLVEC.isclose(vertex.line_direction): break if NULLVEC.isclose(vertex.miter_direction): break if len(vertex.line_params) != element_count: break # Ignore fill parameters. else: # no break return # Invalid vertices found: auditor.fixed_error( code=AuditError.INVALID_MLINE_VERTEX, message=f"Execute geometry update for {str(self)}.", dxf_entity=self, ) self.update_geometry() def ocs(self) -> OCS: # WCS entity which supports the "extrusion" attribute in a # different way! return OCS() acdb_mline_style = DefSubclass( "AcDbMlineStyle", { "name": DXFAttr(2, default="Standard"), # Flags (bit-coded): # 1 =Fill on # 2 = Display miters # 16 = Start square end (line) cap # 32 = Start inner arcs cap # 64 = Start round (outer arcs) cap # 256 = End square (line) cap # 512 = End inner arcs cap # 1024 = End round (outer arcs) cap "flags": DXFAttr(70, default=0), # Style description (string, 255 characters maximum): "description": DXFAttr(3, default=""), # Fill color (integer, default = 256): "fill_color": DXFAttr( 62, default=256, validator=validator.is_valid_aci_color, fixer=RETURN_DEFAULT, ), # Start angle (real, default is 90 degrees): "start_angle": DXFAttr(51, default=90), # End angle (real, default is 90 degrees): "end_angle": DXFAttr(52, default=90), # 71: Number of elements # 49: Element offset (real, no default). # Multiple entries can exist; one entry for each element # 62: Element color (integer, default = 0). # Multiple entries can exist; one entry for each element # 6: Element linetype (string, default = BYLAYER). # Multiple entries can exist; one entry for each element }, ) acdb_mline_style_group_codes = group_code_mapping(acdb_mline_style) MLineStyleElement = namedtuple("MLineStyleElement", "offset color linetype") class MLineStyleElements: def __init__(self, tags: Optional[Tags] = None): self.elements: list[MLineStyleElement] = [] if tags: for e in self.parse_tags(tags): data = MLineStyleElement( e.get("offset", 1.0), e.get("color", 0), e.get("linetype", "BYLAYER"), ) self.elements.append(data) def copy(self) -> MLineStyleElements: elements = MLineStyleElements() # new list of immutable data elements.elements = list(self.elements) return elements def __len__(self): return len(self.elements) def __getitem__(self, item): return self.elements[item] def __iter__(self): return iter(self.elements) def export_dxf(self, tagwriter: AbstractTagWriter): write_tag = tagwriter.write_tag2 write_tag(71, len(self.elements)) for offset, color, linetype in self.elements: write_tag(49, offset) write_tag(62, color) write_tag(6, linetype) def append(self, offset: float, color: int = 0, linetype: str = "BYLAYER") -> None: """Append a new line element. Args: offset: normal offset from the reference line: if justification is ``MLINE_ZERO``, positive values are above and negative values are below the reference line. color: :ref:`ACI` value linetype: linetype name """ self.elements.append( MLineStyleElement(float(offset), int(color), str(linetype)) ) @staticmethod def parse_tags(tags: Tags) -> Iterator[dict]: collector = None for code, value in tags: if code == 49: if collector is not None: yield collector collector = {"offset": value} elif code == 62: collector["color"] = value # type: ignore elif code == 6: collector["linetype"] = value # type: ignore if collector is not None: yield collector def ordered_indices(self) -> list[int]: offsets = [e.offset for e in self.elements] return [offsets.index(value) for value in sorted(offsets)] @register_entity class MLineStyle(DXFObject): DXFTYPE = "MLINESTYLE" DXFATTRIBS = DXFAttributes(base_class, acdb_mline_style) FILL = const.MLINESTYLE_FILL MITER = const.MLINESTYLE_MITER START_SQUARE = const.MLINESTYLE_START_SQUARE START_INNER_ARC = const.MLINESTYLE_START_INNER_ARC START_ROUND = const.MLINESTYLE_START_ROUND END_SQUARE = const.MLINESTYLE_END_SQUARE END_INNER_ARC = const.MLINESTYLE_END_INNER_ARC END_ROUND = const.MLINESTYLE_END_ROUND def __init__(self): super().__init__() self.elements = MLineStyleElements() def copy_data(self, entity: Self, copy_strategy=default_copy) -> None: assert isinstance(entity, MLineStyle) entity.elements = self.elements.copy() def load_dxf_attribs( self, processor: Optional[SubclassProcessor] = None ) -> DXFNamespace: dxf = super().load_dxf_attribs(processor) if processor: tags = processor.subclass_by_index(1) if tags is None: raise const.DXFStructureError( f"missing 'AcDbMLine' subclass in MLINE(#{dxf.handle})" ) try: # Find index of the count tag: index71 = tags.tag_index(71) except const.DXFValueError: # The count tag does not exist: DXF structure error? pass else: self.elements = MLineStyleElements(tags[index71 + 1 :]) # type: ignore # Remove processed tags: del tags[index71:] processor.fast_load_dxfattribs(dxf, acdb_mline_style_group_codes, tags) return dxf def export_entity(self, tagwriter: AbstractTagWriter) -> None: super().export_entity(tagwriter) tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_mline_style.name) self.dxf.export_dxf_attribs(tagwriter, acdb_mline_style.attribs.keys()) self.elements.export_dxf(tagwriter) def update_all(self): """Update all MLINE entities using this MLINESTYLE. The update is required if elements were added or removed or the offset of any element was changed. """ if self.doc: handle = self.dxf.handle mlines = (e for e in self.doc.entitydb.values() if e.dxftype() == "MLINE") for mline in mlines: if mline.dxf.style_handle == handle: mline.update_geometry() def ordered_indices(self) -> list[int]: return self.elements.ordered_indices() def audit(self, auditor: Auditor) -> None: super().audit(auditor) if len(self.elements) == 0: auditor.add_error( code=AuditError.INVALID_MLINESTYLE_ELEMENT_COUNT, message=f"No line elements defined in {str(self)}.", dxf_entity=self, ) def register_resources(self, registry: xref.Registry) -> None: """Register required resources to the resource registry.""" super().register_resources(registry) for element in self.elements: registry.add_linetype(element.linetype) def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None: """Translate resources from self to the copied entity.""" assert isinstance(clone, MLineStyle) super().map_resources(clone, mapping) self.elements.elements = [ MLineStyleElement( element.offset, element.color, mapping.get_linetype(element.linetype), ) for element in self.elements ] class MLineStyleCollection(ObjectCollection[MLineStyle]): def __init__(self, doc: Drawing): super().__init__(doc, dict_name="ACAD_MLINESTYLE", object_type="MLINESTYLE") self.create_required_entries() def create_required_entries(self) -> None: if "Standard" not in self: entity: MLineStyle = self.new("Standard") entity.elements.append(0.5, 256) entity.elements.append(-0.5, 256)