# Copyright (c) 2020-2022, Manfred Moitzi # License: MIT License from __future__ import annotations from typing import TYPE_CHECKING, cast, Sequence, Any from itertools import chain from ezdxf.entities import factory, MLineStyle from ezdxf.math import Vec3, OCS import logging if TYPE_CHECKING: from ezdxf.entities import MLine, DXFGraphic, Hatch, Line, Arc __all__ = ["virtual_entities"] logger = logging.getLogger("ezdxf") # The MLINE geometry stored in vertices, is the final geometry, # scaling factor, justification and MLineStyle settings are already # applied. def _dxfattribs(mline) -> dict[str, Any]: attribs = mline.graphic_properties() # True color value of MLINE is ignored by CAD applications: if "true_color" in attribs: del attribs["true_color"] return attribs def virtual_entities(mline: MLine) -> list[DXFGraphic]: """Yields 'virtual' parts of MLINE 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. """ def filling() -> Hatch: attribs = _dxfattribs(mline) attribs["color"] = style.dxf.fill_color attribs["elevation"] = Vec3(ocs.from_wcs(bottom_border[0])).replace( x=0.0, y=0.0 ) attribs["extrusion"] = mline.dxf.extrusion hatch = cast("Hatch", factory.new("HATCH", dxfattribs=attribs, doc=doc)) bulges: list[float] = [0.0] * (len(bottom_border) * 2) points = chain( Vec3.generate(ocs.points_from_wcs(bottom_border)), Vec3.generate(ocs.points_from_wcs(reversed(top_border))), ) if not closed: if style.get_flag_state(style.END_ROUND): bulges[len(bottom_border) - 1] = 1.0 if style.get_flag_state(style.START_ROUND): bulges[-1] = 1.0 lwpoints = ((v.x, v.y, bulge) for v, bulge in zip(points, bulges)) hatch.paths.add_polyline_path(lwpoints, is_closed=True) return hatch def start_cap() -> list[DXFGraphic]: entities: list[DXFGraphic] = [] if style.get_flag_state(style.START_SQUARE): entities.extend(create_miter(miter_points[0])) if style.get_flag_state(style.START_ROUND): entities.extend(round_caps(0, top_index, bottom_index)) if ( style.get_flag_state(style.START_INNER_ARC) and len(style.elements) > 3 ): start_index = ordered_indices[-2] end_index = ordered_indices[1] entities.extend(round_caps(0, start_index, end_index)) return entities def end_cap() -> list[DXFGraphic]: entities: list[DXFGraphic] = [] if style.get_flag_state(style.END_SQUARE): entities.extend(create_miter(miter_points[-1])) if style.get_flag_state(style.END_ROUND): entities.extend(round_caps(-1, bottom_index, top_index)) if ( style.get_flag_state(style.END_INNER_ARC) and len(style.elements) > 3 ): start_index = ordered_indices[1] end_index = ordered_indices[-2] entities.extend(round_caps(-1, start_index, end_index)) return entities def round_caps(miter_index: int, start_index: int, end_index: int): color1 = style.elements[start_index].color color2 = style.elements[end_index].color start = ocs.from_wcs(miter_points[miter_index][start_index]) end = ocs.from_wcs(miter_points[miter_index][end_index]) return _arc_caps(start, end, color1, color2) def _arc_caps( start: Vec3, end: Vec3, color1: int, color2: int ) -> Sequence[Arc]: attribs = _dxfattribs(mline) center = start.lerp(end) radius = (end - start).magnitude / 2.0 angle = (start - center).angle_deg attribs["center"] = center attribs["radius"] = radius attribs["color"] = color1 attribs["start_angle"] = angle attribs["end_angle"] = angle + (180 if color1 == color2 else 90) arc1 = cast("Arc", factory.new("ARC", dxfattribs=attribs, doc=doc)) if color1 == color2: return (arc1,) attribs["start_angle"] = angle + 90 attribs["end_angle"] = angle + 180 attribs["color"] = color2 arc2 = cast("Arc", factory.new("ARC", dxfattribs=attribs, doc=doc)) return arc1, arc2 def lines() -> list[Line]: prev = None _lines: list[Line] = [] attribs = _dxfattribs(mline) for miter in miter_points: if prev is not None: for index, element in enumerate(style.elements): attribs["start"] = prev[index] attribs["end"] = miter[index] attribs["color"] = element.color attribs["linetype"] = element.linetype _lines.append( cast( "Line", factory.new("LINE", dxfattribs=attribs, doc=doc), ) ) prev = miter return _lines def display_miter(): _lines = [] skip = set() skip.add(len(miter_points) - 1) if not closed: skip.add(0) for index, miter in enumerate(miter_points): if index not in skip: _lines.extend(create_miter(miter)) return _lines def create_miter(miter) -> list[Line]: _lines: list[Line] = [] attribs = _dxfattribs(mline) top = miter[top_index] bottom = miter[bottom_index] zero = bottom.lerp(top) element = style.elements[top_index] attribs["start"] = top attribs["end"] = zero attribs["color"] = element.color attribs["linetype"] = element.linetype _lines.append( cast("Line", factory.new("LINE", dxfattribs=attribs, doc=doc)) ) element = style.elements[bottom_index] attribs["start"] = bottom attribs["end"] = zero attribs["color"] = element.color attribs["linetype"] = element.linetype _lines.append( cast("Line", factory.new("LINE", dxfattribs=attribs, doc=doc)) ) return _lines entities: list[DXFGraphic] = [] if not mline.is_alive or mline.doc is None or len(mline.vertices) < 2: return entities style: MLineStyle = mline.style # type: ignore if style is None: return entities doc = mline.doc ocs = OCS(mline.dxf.extrusion) element_count = len(style.elements) closed = mline.is_closed ordered_indices = style.ordered_indices() bottom_index = ordered_indices[0] top_index = ordered_indices[-1] bottom_border: list[Vec3] = [] top_border: list[Vec3] = [] miter_points: list[list[Vec3]] = [] for vertex in mline.vertices: offsets = vertex.line_params if len(offsets) != element_count: logger.debug( f"Invalid line parametrization for vertex {len(miter_points)} " f"in {str(mline)}." ) return entities location = vertex.location miter_direction = vertex.miter_direction miter = [] for offset in offsets: try: length = offset[0] except IndexError: # DXFStructureError? length = 0 miter.append(location + miter_direction * length) miter_points.append(miter) top_border.append(miter[top_index]) bottom_border.append(miter[bottom_index]) if closed: miter_points.append(miter_points[0]) top_border.append(top_border[0]) bottom_border.append(bottom_border[0]) if not closed: entities.extend(start_cap()) entities.extend(lines()) if style.get_flag_state(style.MITER): entities.extend(display_miter()) if not closed: entities.extend(end_cap()) if style.get_flag_state(style.FILL): entities.insert(0, filling()) return entities