1463 lines
55 KiB
Python
1463 lines
55 KiB
Python
# Copyright (c) 2018-2024, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING, Union, Optional, Iterable, Any, Iterator
|
|
from typing_extensions import Self
|
|
import copy
|
|
import logging
|
|
import math
|
|
from collections import namedtuple
|
|
|
|
from ezdxf.lldxf import const
|
|
from ezdxf.lldxf.types import cast_value
|
|
from ezdxf.lldxf.attributes import (
|
|
DXFAttr,
|
|
DXFAttributes,
|
|
DefSubclass,
|
|
XType,
|
|
group_code_mapping,
|
|
)
|
|
from ezdxf.lldxf.tags import Tags
|
|
from ezdxf.lldxf.const import EXT_MAX_STR_LEN
|
|
from ezdxf.math import (
|
|
Vec3,
|
|
NULLVEC,
|
|
X_AXIS,
|
|
Y_AXIS,
|
|
Z_AXIS,
|
|
Matrix44,
|
|
WCSTransform,
|
|
OCSTransform,
|
|
NonUniformScalingError,
|
|
)
|
|
from ezdxf import colors
|
|
from ezdxf.proxygraphic import ProxyGraphicError
|
|
from ezdxf.tools.text import safe_string, scale_mtext_inline_commands
|
|
from ezdxf.tools.handle import safe_handle
|
|
|
|
from .dxfentity import base_class, SubclassProcessor
|
|
from .dxfobj import DXFObject
|
|
from .dxfgfx import DXFGraphic, acdb_entity
|
|
|
|
from .factory import register_entity
|
|
from .objectcollection import ObjectCollection
|
|
from .copy import default_copy
|
|
|
|
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.lldxf.types import DXFTag
|
|
from ezdxf.query import EntityQuery
|
|
from ezdxf import xref
|
|
|
|
__all__ = [
|
|
"MultiLeader",
|
|
"MLeader",
|
|
"MLeaderStyle",
|
|
"MLeaderStyleCollection",
|
|
"MLeaderContext",
|
|
"MTextData",
|
|
"BlockData",
|
|
"LeaderData",
|
|
"LeaderLine",
|
|
"ArrowHeadData",
|
|
"AttribData",
|
|
]
|
|
logger = logging.getLogger("ezdxf")
|
|
|
|
|
|
# DXF Examples:
|
|
# "D:\source\dxftest\CADKitSamples\house design for two family with common staircasedwg.dxf"
|
|
# "D:\source\dxftest\CADKitSamples\house design.dxf"
|
|
|
|
# How to render MLEADER: https://atlight.github.io/formats/dxf-leader.html
|
|
# DXF reference:
|
|
# http://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-72D20B8C-0F5E-4993-BEB7-0FCF94F32BE0
|
|
|
|
acdb_mleader = DefSubclass(
|
|
"AcDbMLeader",
|
|
{
|
|
"version": DXFAttr(270, default=2),
|
|
"style_handle": DXFAttr(340),
|
|
# Theory: Take properties from MLEADERSTYLE,
|
|
# except explicit overridden here:
|
|
"property_override_flags": DXFAttr(90),
|
|
# Bit coded flags from ODA DWG specs pg.158:
|
|
# 1 << 0 = leader_type
|
|
# 1 << 1 = leader_line_color
|
|
# 1 << 2 = leader_linetype_handle
|
|
# 1 << 3 = leader_lineweight
|
|
# 1 << 4 = has_landing
|
|
# 1 << 5 = landing_gap_size ???
|
|
# 1 << 6 = has_dogleg
|
|
# 1 << 7 = dogleg_length
|
|
# 1 << 8 = arrow_head_handle
|
|
# 1 << 9 = arrow_head_size
|
|
# 1 << 10 = content_type
|
|
# 1 << 11 = text_style_handle
|
|
# 1 << 12 = text_left_attachment_type (of MTEXT)
|
|
# 1 << 13 = text_angle_type (of MTEXT)
|
|
# 1 << 14 = text_alignment_type (of MTEXT)
|
|
# 1 << 15 = text_color (of MTEXT)
|
|
# 1 << 16 = ??? Text height (of MTEXT) ???
|
|
# 1 << 17 = has_text_frame
|
|
# 1 << 18 = ??? Enable use of default MTEXT (from MLEADERSTYLE)
|
|
# 1 << 19 = block_record_handle
|
|
# 1 << 20 = block_color
|
|
# 1 << 21 = block_scale_vector
|
|
# 1 << 22 = block_rotation
|
|
# 1 << 23 = block_connection_type
|
|
# 1 << 24 = ??? Scale ???
|
|
# 1 << 25 = text_right_attachment_type (of MTEXT)
|
|
# 1 << 26 = ??? Text switch alignment type (of MTEXT) ???
|
|
# 1 << 27 = text_attachment_direction (of MTEXT)
|
|
# 1 << 28 = text_top_attachment_type (of MTEXT)
|
|
# 1 << 29 = Text_bottom_attachment_type (of MTEXT)
|
|
# leader_type:
|
|
# 0 = invisible
|
|
# 1 = straight line leader
|
|
# 2 = spline leader
|
|
"leader_type": DXFAttr(170, default=1),
|
|
"leader_line_color": DXFAttr(91, default=colors.BY_BLOCK_RAW_VALUE),
|
|
"leader_linetype_handle": DXFAttr(341),
|
|
"leader_lineweight": DXFAttr(171, default=const.LINEWEIGHT_BYBLOCK),
|
|
"has_landing": DXFAttr(290, default=1),
|
|
"has_dogleg": DXFAttr(291, default=1),
|
|
"dogleg_length": DXFAttr(41, default=8), # depend on $MEASUREMENT?
|
|
# no handle is default arrow 'closed filled':
|
|
"arrow_head_handle": DXFAttr(342),
|
|
# unscaled arrow head size:
|
|
"arrow_head_size": DXFAttr(42, default=4), # depend on $MEASUREMENT?
|
|
"content_type": DXFAttr(172, default=2),
|
|
# 0 = None
|
|
# 1 = Block content
|
|
# 2 = MTEXT content
|
|
# 3 = TOLERANCE content
|
|
# Text Content:
|
|
"text_style_handle": DXFAttr(343),
|
|
"text_left_attachment_type": DXFAttr(173, default=1),
|
|
# Values 0-8 are used for the left/right attachment
|
|
# point (attachment direction is horizontal)
|
|
# Attachment point is:
|
|
# 0 = top of top text line
|
|
# 1 = middle of top text line
|
|
# 2 = middle of whole text
|
|
# 3 = middle of bottom text line
|
|
# 4 = bottom of bottom text line
|
|
# 5 = bottom of bottom text line & underline bottom text line
|
|
# 6 = bottom of top text line & underline top text line
|
|
# 7 = bottom of top text line
|
|
# 8 = bottom of top text line & underline all text lines
|
|
"text_right_attachment_type": DXFAttr(95, default=1), # like 173
|
|
"text_angle_type": DXFAttr(174, default=1),
|
|
# 0 = text angle is equal to last leader line segment angle
|
|
# 1 = text is horizontal
|
|
# 2 = text angle is equal to last leader line segment angle, but potentially
|
|
# rotated by 180 degrees so the right side is up for readability.
|
|
"text_alignment_type": DXFAttr(175, default=2),
|
|
"text_color": DXFAttr(92, default=colors.BY_BLOCK_RAW_VALUE),
|
|
"has_text_frame": DXFAttr(292, default=0),
|
|
# Block Content:
|
|
"block_record_handle": DXFAttr(344),
|
|
"block_color": DXFAttr(93, default=colors.BY_BLOCK_RAW_VALUE), # raw color
|
|
"block_scale_vector": DXFAttr(10, xtype=XType.point3d, default=Vec3(1, 1, 1)),
|
|
"block_rotation": DXFAttr(43, default=0), # in radians!!!
|
|
"block_connection_type": DXFAttr(176, default=0),
|
|
# 0 = center extents
|
|
# 1 = insertion point
|
|
"is_annotative": DXFAttr(293, default=0),
|
|
# REPEAT "arrow_heads": DXF R2007+
|
|
# arrow_head_index: 94, ???
|
|
# arrow_head_handle: 345
|
|
# END "arrow heads"
|
|
# REPEAT "block attribs" (ATTDEF): DXF R2007+
|
|
# attrib_handle: 330
|
|
# attrib_index: 177, sequential index of the label in the collection
|
|
# attrib_width: 44
|
|
# attrib_text: 302, collision with group code (302, "LEADER{") in context data
|
|
# END "block attribs"
|
|
# Text Content:
|
|
"is_text_direction_negative": DXFAttr(294, default=0, dxfversion=const.DXF2007),
|
|
"text_IPE_align": DXFAttr(178, default=0, dxfversion=const.DXF2007),
|
|
"text_attachment_point": DXFAttr(179, default=1, dxfversion=const.DXF2007),
|
|
# 1 = left
|
|
# 2 = center
|
|
# 3 = right
|
|
"scale": DXFAttr(45, default=1, dxfversion=const.DXF2007),
|
|
# This defines whether the leaders attach to the left/right of the content
|
|
# block/text, or attach to the top/bottom:
|
|
# 0 = horizontal
|
|
# 1 = vertical
|
|
"text_attachment_direction": DXFAttr(271, default=0, dxfversion=const.DXF2010),
|
|
# like 173, but
|
|
# 9 = center
|
|
# 10= underline and center
|
|
"text_bottom_attachment_type": DXFAttr(
|
|
272, default=9, dxfversion=const.DXF2010
|
|
),
|
|
# like 173, but
|
|
# 9 = center
|
|
# 10= overline and center
|
|
"text_top_attachment_type": DXFAttr(273, default=9, dxfversion=const.DXF2010),
|
|
"leader_extend_to_text": DXFAttr(295, default=0, dxfversion=const.DXF2013),
|
|
},
|
|
)
|
|
# The text frame shape is stored in XDATA except for the default rectangle:
|
|
# 1001 ACAD
|
|
# 1070 <type>
|
|
# 2 = rounded rectangle
|
|
# 3 = parallelogram
|
|
# 4 = triangle
|
|
# 5 = square
|
|
# 6 = pentagon
|
|
# 7 = hexagon
|
|
# 8 = octagon
|
|
# 9 = circle
|
|
# 10= ellipse
|
|
|
|
acdb_mleader_group_codes = group_code_mapping(acdb_mleader)
|
|
CONTEXT_STR = "CONTEXT_DATA{"
|
|
LEADER_STR = "LEADER{"
|
|
LEADER_LINE_STR = "LEADER_LINE{"
|
|
START_CONTEXT_DATA = 300
|
|
END_CONTEXT_DATA = 301
|
|
START_LEADER = 302
|
|
END_LEADER = 303
|
|
START_LEADER_LINE = 304
|
|
END_LEADER_LINE = 305
|
|
|
|
|
|
def compile_context_tags(
|
|
data: list[DXFTag], stop_code: int
|
|
) -> list[Union[DXFTag, list]]:
|
|
def build_structure(tag: DXFTag, stop: int) -> list[Union[DXFTag, list]]:
|
|
collector = [tag]
|
|
tag = next(tags)
|
|
while tag.code != stop:
|
|
if tag.code == START_LEADER:
|
|
collector.append(build_structure(tag, END_LEADER)) # type: ignore
|
|
# Group code 304 is used also for MTEXT content, therefore always
|
|
# test for group code and value string:
|
|
elif tag.code == START_LEADER_LINE and tag.value == LEADER_LINE_STR:
|
|
collector.append(build_structure(tag, END_LEADER_LINE)) # type: ignore
|
|
else:
|
|
collector.append(tag)
|
|
tag = next(tags)
|
|
return collector # type: ignore
|
|
|
|
tags = iter(data)
|
|
return build_structure(next(tags), stop_code)
|
|
|
|
|
|
ArrowHeadData = namedtuple("ArrowHeadData", "index, handle")
|
|
AttribData = namedtuple("AttribData", "handle, index, width, text")
|
|
|
|
|
|
@register_entity
|
|
class MultiLeader(DXFGraphic):
|
|
DXFTYPE = "MULTILEADER"
|
|
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mleader)
|
|
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.context = MLeaderContext()
|
|
self.arrow_heads: list[ArrowHeadData] = []
|
|
self.block_attribs: list[AttribData] = []
|
|
|
|
@property
|
|
def has_mtext_content(self) -> bool:
|
|
"""``True`` if MULTILEADER has MTEXT content."""
|
|
return self.context.mtext is not None
|
|
|
|
def get_mtext_content(self) -> str:
|
|
"""Get MTEXT content as string, return "" if MULTILEADER has
|
|
BLOCK content.
|
|
"""
|
|
mtext = self.context.mtext
|
|
if mtext is not None:
|
|
return mtext.default_content
|
|
return ""
|
|
|
|
def set_mtext_content(self, text: str):
|
|
"""Set MTEXT content as string, does nothing if MULTILEADER has
|
|
BLOCK content.
|
|
"""
|
|
mtext = self.context.mtext
|
|
if mtext is not None:
|
|
mtext.default_content = safe_string(text, EXT_MAX_STR_LEN)
|
|
|
|
@property
|
|
def has_block_content(self) -> bool:
|
|
"""``True`` if MULTILEADER has BLOCK content."""
|
|
return self.context.block is not None
|
|
|
|
def get_block_content(self) -> dict[str, str]:
|
|
"""Get BLOCK attributes as dictionary of (tag, value) pairs.
|
|
Returns an empty dictionary if MULTILEADER has MTEXT content.
|
|
"""
|
|
assert self.doc is not None, "valid DXF document required"
|
|
entitydb = self.doc.entitydb
|
|
tags: dict[str, str] = dict()
|
|
for attr in self.block_attribs:
|
|
attdef = entitydb.get(attr.handle)
|
|
if attdef is not None:
|
|
tags[attdef.dxf.tag] = attr.text
|
|
return tags
|
|
|
|
def set_block_content(self, content: dict[str, str]):
|
|
"""Set BLOCK attributes by a dictionary of (tag, value) pairs.
|
|
Does nothing if MULTILEADER has MTEXT content.
|
|
"""
|
|
assert self.doc is not None, "valid DXF document required"
|
|
entitydb = self.doc.entitydb
|
|
tags: dict[str, str] = dict()
|
|
block_attribs = self.block_attribs
|
|
for index, attr in enumerate(block_attribs):
|
|
attdef = entitydb.get(attr.handle)
|
|
if attdef is not None:
|
|
tag = attdef.dxf.tag
|
|
new_text = safe_string(content.get(tag), EXT_MAX_STR_LEN)
|
|
if new_text is not None:
|
|
block_attribs[index] = attr._replace(text=new_text)
|
|
return tags
|
|
|
|
def copy_data(self, entity: DXFEntity, copy_strategy=default_copy) -> None:
|
|
"""Copy leaders"""
|
|
assert isinstance(entity, MultiLeader)
|
|
entity.context = copy.deepcopy(self.context)
|
|
entity.arrow_heads = copy.deepcopy(self.arrow_heads)
|
|
entity.block_attribs = copy.deepcopy(self.block_attribs)
|
|
|
|
def load_dxf_attribs(
|
|
self, processor: Optional[SubclassProcessor] = None
|
|
) -> DXFNamespace:
|
|
dxf = super().load_dxf_attribs(processor)
|
|
if processor is None:
|
|
return dxf
|
|
tags = processor.subclass_by_index(2)
|
|
if tags:
|
|
context = self.extract_context_data(tags)
|
|
if context:
|
|
try:
|
|
self.context = self.load_context(context)
|
|
except const.DXFStructureError:
|
|
logger.info(
|
|
f"Context structure error in entity MULTILEADER(#{dxf.handle})"
|
|
)
|
|
else:
|
|
raise const.DXFStructureError(
|
|
f"missing 'AcDbMLeader' subclass in MULTILEADER(#{dxf.handle})"
|
|
)
|
|
|
|
self.arrow_heads = self.extract_arrow_heads(tags)
|
|
self.block_attribs = self.extract_block_attribs(tags)
|
|
|
|
processor.fast_load_dxfattribs(
|
|
dxf, acdb_mleader_group_codes, subclass=tags, recover=True
|
|
)
|
|
return dxf
|
|
|
|
@staticmethod
|
|
def extract_context_data(tags: Tags) -> list[DXFTag]:
|
|
start, end = None, None
|
|
context_data = []
|
|
for index, tag in enumerate(tags):
|
|
if tag.code == START_CONTEXT_DATA:
|
|
start = index
|
|
elif tag.code == END_CONTEXT_DATA:
|
|
end = index + 1
|
|
|
|
if start and end:
|
|
context_data = tags[start:end]
|
|
# Remove context data!
|
|
del tags[start:end]
|
|
return context_data
|
|
|
|
@staticmethod
|
|
def load_context(data: list[DXFTag]) -> MLeaderContext:
|
|
try:
|
|
context = compile_context_tags(data, END_CONTEXT_DATA)
|
|
except StopIteration:
|
|
raise const.DXFStructureError
|
|
else:
|
|
return MLeaderContext.load(context)
|
|
|
|
@staticmethod
|
|
def extract_arrow_heads(data: Tags) -> list[ArrowHeadData]:
|
|
def store_head():
|
|
heads.append(
|
|
ArrowHeadData(
|
|
collector.get(94, 0), # arrow head index
|
|
collector.get(345, "0"), # arrow head handle
|
|
)
|
|
)
|
|
collector.clear()
|
|
|
|
heads: list[ArrowHeadData] = []
|
|
try:
|
|
start = data.tag_index(94)
|
|
except const.DXFValueError:
|
|
return heads
|
|
|
|
end = start
|
|
collector = dict()
|
|
for code, value in data.collect_consecutive_tags({94, 345}, start):
|
|
end += 1
|
|
collector[code] = value
|
|
if code == 345:
|
|
store_head()
|
|
|
|
# Remove processed tags:
|
|
del data[start:end]
|
|
return heads
|
|
|
|
@staticmethod
|
|
def extract_block_attribs(data: Tags) -> list[AttribData]:
|
|
def store_attrib():
|
|
attribs.append(
|
|
AttribData(
|
|
collector.get(330, "0"), # ATTDEF handle
|
|
collector.get(177, 0), # ATTDEF index
|
|
collector.get(44, 1.0), # ATTDEF width
|
|
collector.get(302, ""), # ATTDEF text (content)
|
|
)
|
|
)
|
|
collector.clear()
|
|
|
|
attribs: list[AttribData] = []
|
|
try:
|
|
start = data.tag_index(330)
|
|
except const.DXFValueError:
|
|
return attribs
|
|
|
|
end = start
|
|
collector: dict[int, Any] = dict()
|
|
for code, value in data.collect_consecutive_tags({330, 177, 44, 302}, start):
|
|
end += 1
|
|
if code == 330 and len(collector):
|
|
store_attrib()
|
|
collector[code] = value
|
|
if len(collector):
|
|
store_attrib()
|
|
|
|
# Remove processed tags:
|
|
del data[start:end]
|
|
return attribs
|
|
|
|
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
|
|
if self.context.is_valid:
|
|
return True
|
|
else:
|
|
logger.debug(f"Ignore {str(self)} at DXF export, invalid context data.")
|
|
return False
|
|
|
|
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
|
def write_handle_if_exist(code: int, name: str):
|
|
handle = dxf.get(name)
|
|
if handle is not None:
|
|
write_tag2(code, handle)
|
|
|
|
super().export_entity(tagwriter)
|
|
dxf = self.dxf
|
|
version = tagwriter.dxfversion
|
|
write_tag2 = tagwriter.write_tag2
|
|
|
|
write_tag2(100, acdb_mleader.name)
|
|
write_tag2(270, dxf.version)
|
|
self.context.export_dxf(tagwriter)
|
|
|
|
# Export common MLEADER tags:
|
|
# Don't use dxf.export_dxf_attribs() - all attributes should be written
|
|
# even if equal to the default value:
|
|
write_tag2(340, dxf.style_handle)
|
|
write_tag2(90, dxf.property_override_flags)
|
|
write_tag2(170, dxf.leader_type)
|
|
write_tag2(91, dxf.leader_line_color)
|
|
write_tag2(341, dxf.leader_linetype_handle)
|
|
write_tag2(171, dxf.leader_lineweight)
|
|
write_tag2(290, dxf.has_landing)
|
|
write_tag2(291, dxf.has_dogleg)
|
|
write_tag2(41, dxf.dogleg_length)
|
|
# arrow_head_handle is None for default arrow 'closed filled':
|
|
write_handle_if_exist(342, "arrow_head_handle")
|
|
write_tag2(42, dxf.arrow_head_size)
|
|
write_tag2(172, dxf.content_type)
|
|
write_tag2(343, dxf.text_style_handle) # mandatory!
|
|
write_tag2(173, dxf.text_left_attachment_type)
|
|
write_tag2(95, dxf.text_right_attachment_type)
|
|
write_tag2(174, dxf.text_angle_type)
|
|
write_tag2(175, dxf.text_alignment_type)
|
|
write_tag2(92, dxf.text_color)
|
|
write_tag2(292, dxf.has_text_frame)
|
|
|
|
write_handle_if_exist(344, "block_record_handle")
|
|
write_tag2(93, dxf.block_color)
|
|
tagwriter.write_vertex(10, dxf.block_scale_vector)
|
|
write_tag2(43, dxf.block_rotation)
|
|
write_tag2(176, dxf.block_connection_type)
|
|
write_tag2(293, dxf.is_annotative)
|
|
if version >= const.DXF2007:
|
|
self.export_arrow_heads(tagwriter)
|
|
self.export_block_attribs(tagwriter)
|
|
write_tag2(294, dxf.is_text_direction_negative)
|
|
write_tag2(178, dxf.text_IPE_align)
|
|
write_tag2(179, dxf.text_attachment_point)
|
|
write_tag2(45, dxf.scale)
|
|
|
|
if version >= const.DXF2010:
|
|
write_tag2(271, dxf.text_attachment_direction)
|
|
write_tag2(272, dxf.text_bottom_attachment_type)
|
|
write_tag2(273, dxf.text_top_attachment_type)
|
|
|
|
if version >= const.DXF2013:
|
|
write_tag2(295, dxf.leader_extend_to_text)
|
|
|
|
def export_arrow_heads(self, tagwriter: AbstractTagWriter) -> None:
|
|
for index, handle in self.arrow_heads:
|
|
tagwriter.write_tag2(94, index)
|
|
tagwriter.write_tag2(345, handle)
|
|
|
|
def export_block_attribs(self, tagwriter: AbstractTagWriter) -> None:
|
|
for attrib in self.block_attribs:
|
|
tagwriter.write_tag2(330, attrib.handle)
|
|
tagwriter.write_tag2(177, attrib.index)
|
|
tagwriter.write_tag2(44, attrib.width)
|
|
tagwriter.write_tag2(302, safe_string(attrib.text, EXT_MAX_STR_LEN))
|
|
|
|
def register_resources(self, registry: xref.Registry) -> None:
|
|
"""Register required resources to the resource registry."""
|
|
super().register_resources(registry)
|
|
dxf = self.dxf
|
|
registry.add_handle(dxf.style_handle)
|
|
registry.add_handle(dxf.leader_linetype_handle)
|
|
registry.add_handle(dxf.arrow_head_handle)
|
|
registry.add_handle(dxf.text_style_handle)
|
|
registry.add_handle(dxf.block_record_handle)
|
|
for arrow_head in self.arrow_heads:
|
|
registry.add_handle(arrow_head.handle)
|
|
# block attdef entities are included in the block definition!
|
|
self.context.register_resources(registry)
|
|
|
|
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
|
"""Translate resources from self to the copied entity."""
|
|
assert isinstance(clone, MultiLeader)
|
|
super().map_resources(clone, mapping)
|
|
dxf = self.dxf
|
|
clone_dxf = clone.dxf
|
|
clone_dxf.style_handle = mapping.get_handle(dxf.style_handle)
|
|
clone_dxf.leader_linetype_handle = mapping.get_handle(
|
|
dxf.leader_linetype_handle
|
|
)
|
|
clone_dxf.arrow_head_handle = mapping.get_handle(dxf.arrow_head_handle)
|
|
clone_dxf.text_style_handle = mapping.get_handle(dxf.text_style_handle)
|
|
clone_dxf.block_record_handle = mapping.get_handle(dxf.block_record_handle)
|
|
clone.map_arrow_head_handles(mapping)
|
|
clone.map_block_attrib_handles(mapping)
|
|
clone.context.map_resources(mapping)
|
|
|
|
def map_arrow_head_handles(self, mapping: xref.ResourceMapper):
|
|
self.arrow_heads = [
|
|
arrow._replace(handle=mapping.get_handle(arrow.handle))
|
|
for arrow in self.arrow_heads
|
|
]
|
|
|
|
def map_block_attrib_handles(self, mapping: xref.ResourceMapper):
|
|
self.block_attribs = [
|
|
attrib._replace(handle=mapping.get_handle(attrib.handle))
|
|
for attrib in self.block_attribs
|
|
]
|
|
|
|
def virtual_entities(self) -> Iterator[DXFGraphic]:
|
|
"""Yields the graphical representation of MULTILEADER as virtual DXF
|
|
primitives.
|
|
|
|
These entities are located at the original location, 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 MULTILEADER as DXF primitives into target layout,
|
|
if target layout is ``None``, the target layout is the layout of the
|
|
source entity.
|
|
|
|
Returns an :class:`~ezdxf.query.EntityQuery` container with all DXF
|
|
primitives.
|
|
|
|
Args:
|
|
target_layout: target layout for the DXF primitives, ``None`` for
|
|
same layout as the source entity.
|
|
|
|
"""
|
|
from ezdxf.explode import explode_entity
|
|
|
|
return explode_entity(self, target_layout)
|
|
|
|
def __virtual_entities__(self) -> Iterator[DXFGraphic]:
|
|
"""Support for "VirtualEntities" protocol."""
|
|
from ezdxf.render import mleader
|
|
|
|
try:
|
|
return mleader.virtual_entities(self, proxy_graphic=True)
|
|
except ProxyGraphicError:
|
|
return mleader.virtual_entities(self, proxy_graphic=False)
|
|
|
|
def __referenced_blocks__(self) -> Iterable[str]:
|
|
"""Support for "ReferencedBlocks" protocol."""
|
|
# 1. overridden MLEADERSTYLE attributes
|
|
for name in ("block_record_handle", "arrow_head_handle"):
|
|
handle = self.dxf.get(name, None)
|
|
if handle is not None:
|
|
yield handle
|
|
|
|
# 2. arrows heads
|
|
for arrow_head in self.arrow_heads:
|
|
yield arrow_head.handle
|
|
|
|
# 3. MLeader context
|
|
if self.context.block is not None:
|
|
handle = self.context.block.block_record_handle
|
|
if handle is not None:
|
|
yield handle
|
|
|
|
def transform(self, m: Matrix44) -> MultiLeader:
|
|
"""Transform the MULTILEADER entity by transformation matrix `m` inplace.
|
|
|
|
Non-uniform scaling is not supported.
|
|
|
|
Args:
|
|
m: transformation :class:`~ezdxf.math.Matrix44`
|
|
|
|
Raises:
|
|
NonUniformScalingError: for non-uniform scaling
|
|
|
|
"""
|
|
dxf = self.dxf
|
|
context = self.context
|
|
|
|
wcs = WCSTransform(m)
|
|
if not wcs.has_uniform_xy_scaling:
|
|
# caller has to catch this exception and explode the MULTILEADER
|
|
raise NonUniformScalingError(
|
|
"MULTILEADER does not support non-uniform scaling"
|
|
)
|
|
if abs(context.plane_x_axis.z) > 1e-12 or abs(context.plane_y_axis.z) > 1e-12:
|
|
# check only if not parallel to xy-plane
|
|
if not wcs.has_uniform_xyz_scaling:
|
|
# caller has to catch this exception and explode the MULTILEADER
|
|
raise NonUniformScalingError(
|
|
"MULTILEADER does not support non-uniform scaling"
|
|
)
|
|
context.transform(wcs)
|
|
# copy redundant attributes from sub-structures:
|
|
dxf.arrow_head_size = context.arrow_head_size
|
|
dxf.scale = context.scale
|
|
dxf.dogleg_length *= wcs.uniform_scale
|
|
if context.block:
|
|
dxf.block_rotation = context.block.rotation
|
|
dxf.block_scale_vector = context.block.scale
|
|
|
|
# ArrowHeadData: no transformation needed
|
|
# AttribData: no transformation needed
|
|
self.post_transform(m)
|
|
|
|
# The proxy graphic is stored in absolute WCS coordinates and does not
|
|
# represent the new transformed geometry.
|
|
self.update_proxy_graphic()
|
|
return self
|
|
|
|
def update_proxy_graphic(self):
|
|
# Maybe this is supported in the future - its very unlikeLy!
|
|
self.proxy_graphic = None
|
|
|
|
|
|
@register_entity
|
|
class MLeader(MultiLeader): # same entity different name
|
|
DXFTYPE = "MLEADER"
|
|
|
|
|
|
class MLeaderContext:
|
|
ATTRIBS = {
|
|
40: "scale",
|
|
10: "base_point",
|
|
41: "char_height",
|
|
140: "arrow_head_size",
|
|
145: "landing_gap_size",
|
|
174: "left_attachment",
|
|
175: "right_attachment",
|
|
176: "text_align_type",
|
|
177: "attachment_type",
|
|
110: "plane_origin",
|
|
111: "plane_x_axis",
|
|
112: "plane_y_axis",
|
|
297: "plane_normal_reversed",
|
|
272: "top_attachment",
|
|
273: "bottom_attachment",
|
|
}
|
|
|
|
def __init__(self) -> None:
|
|
self.leaders: list[LeaderData] = []
|
|
self.scale: float = 1.0 # overall scale
|
|
|
|
# MTEXT base point: is not the MTEXT insertion point!
|
|
# HORIZONTAL leader attachment:
|
|
# the "base_point" is always the start point of the leader on the LEFT side
|
|
# of the MTEXT, regardless of alignment and which side leaders are attached.
|
|
# VERTICAL leader attachment:
|
|
# the "base_point" is always the start point of the leader on the BOTTOM
|
|
# side of the MTEXT, regardless of alignment and which side leaders are
|
|
# attached.
|
|
|
|
# BLOCK base point: is not the BLOCK insertion point!
|
|
# HORIZONTAL leader attachment:
|
|
# Strange results, setting the "base_point" to the left center of the BLOCK,
|
|
# regardless which side leaders are attached, seems to be reasonable.
|
|
# VERTICAL leader attachment: not supported by BricsCAD
|
|
self.base_point: Vec3 = NULLVEC
|
|
self.char_height = 4.0 # scaled char height!
|
|
self.arrow_head_size = 4.0
|
|
self.landing_gap_size = 2.0
|
|
self.left_attachment = 1
|
|
self.right_attachment = 1
|
|
self.text_align_type = 0 # 0=left, 1=center, 2=right
|
|
self.attachment_type = 0 # 0=content extents, 1=insertion point
|
|
self.mtext: Optional[MTextData] = None
|
|
self.block: Optional[BlockData] = None
|
|
self.plane_origin: Vec3 = NULLVEC
|
|
self.plane_x_axis: Vec3 = X_AXIS
|
|
self.plane_y_axis: Vec3 = Y_AXIS
|
|
self.plane_normal_reversed: int = 0
|
|
self.top_attachment = 9
|
|
self.bottom_attachment = 9
|
|
|
|
@classmethod
|
|
def load(cls, context: list[Union[DXFTag, list]]) -> MLeaderContext:
|
|
assert context[0] == (START_CONTEXT_DATA, CONTEXT_STR)
|
|
ctx = cls()
|
|
content = None
|
|
for tag in context:
|
|
if isinstance(tag, list): # Leader()
|
|
ctx.leaders.append(LeaderData.load(tag))
|
|
continue
|
|
# parse context tags
|
|
code, value = tag
|
|
if content:
|
|
if content.parse(code, value):
|
|
continue
|
|
else:
|
|
content = None
|
|
|
|
if code == 290 and value == 1:
|
|
content = MTextData()
|
|
ctx.mtext = content
|
|
elif code == 296 and value == 1:
|
|
content = BlockData() # type: ignore
|
|
ctx.block = content
|
|
else:
|
|
name = MLeaderContext.ATTRIBS.get(code)
|
|
if name:
|
|
ctx.__setattr__(name, cast_value(code, value))
|
|
return ctx
|
|
|
|
def register_resources(self, registry: xref.Registry) -> None:
|
|
"""Register required resources to the resource registry."""
|
|
if self.mtext is not None:
|
|
self.mtext.register_resources(registry)
|
|
if self.block is not None:
|
|
self.block.register_resources(registry)
|
|
|
|
def map_resources(self, mapping: xref.ResourceMapper) -> None:
|
|
"""Translate resources from self to the copied entity."""
|
|
if self.mtext is not None:
|
|
self.mtext.map_resources(mapping)
|
|
if self.block is not None:
|
|
self.block.map_resources(mapping)
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return True
|
|
|
|
@property
|
|
def plane_z_axis(self) -> Vec3:
|
|
z_axis = self.plane_x_axis.cross(self.plane_y_axis).normalize()
|
|
if self.plane_normal_reversed:
|
|
z_axis = -z_axis
|
|
return z_axis
|
|
|
|
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
|
write_tag2 = tagwriter.write_tag2
|
|
write_vertex = tagwriter.write_vertex
|
|
write_tag2(START_CONTEXT_DATA, CONTEXT_STR)
|
|
# All MLeaderContext tags:
|
|
write_tag2(40, self.scale)
|
|
write_vertex(10, self.base_point)
|
|
write_tag2(41, self.char_height)
|
|
write_tag2(140, self.arrow_head_size)
|
|
write_tag2(145, self.landing_gap_size)
|
|
write_tag2(174, self.left_attachment)
|
|
write_tag2(175, self.right_attachment)
|
|
write_tag2(176, self.text_align_type)
|
|
write_tag2(177, self.attachment_type)
|
|
|
|
if self.mtext:
|
|
write_tag2(290, 1) # has mtext content
|
|
self.mtext.export_dxf(tagwriter)
|
|
else:
|
|
write_tag2(290, 0)
|
|
|
|
if self.block:
|
|
write_tag2(296, 1) # has block content
|
|
self.block.export_dxf(tagwriter)
|
|
else:
|
|
write_tag2(296, 0)
|
|
|
|
write_vertex(110, self.plane_origin)
|
|
write_vertex(111, self.plane_x_axis)
|
|
write_vertex(112, self.plane_y_axis)
|
|
write_tag2(297, self.plane_normal_reversed)
|
|
|
|
# Export Leader and LiederLine objects:
|
|
for leader in self.leaders:
|
|
leader.export_dxf(tagwriter)
|
|
|
|
# Additional MLeaderContext tags:
|
|
if tagwriter.dxfversion >= const.DXF2010:
|
|
write_tag2(272, self.top_attachment)
|
|
write_tag2(273, self.bottom_attachment)
|
|
write_tag2(END_CONTEXT_DATA, "}")
|
|
|
|
def transform(self, wcs: WCSTransform) -> None:
|
|
m = wcs.m
|
|
scale = wcs.uniform_scale
|
|
self.scale *= scale
|
|
self.base_point = m.transform(self.base_point)
|
|
self.char_height *= scale
|
|
self.arrow_head_size *= scale
|
|
self.landing_gap_size *= scale
|
|
self.plane_origin = m.transform(self.plane_origin) # confirmed by BricsCAD
|
|
self.plane_x_axis = m.transform_direction(self.plane_x_axis, normalize=True)
|
|
self.plane_y_axis = m.transform_direction(self.plane_y_axis, normalize=True)
|
|
self.plane_normal_reversed = 0
|
|
z_axis = m.transform_direction(Z_AXIS, normalize=True)
|
|
if z_axis.isclose(-self.plane_z_axis): # reversed z-axis?
|
|
self.plane_normal_reversed = 1
|
|
|
|
for leader in self.leaders:
|
|
leader.transform(wcs)
|
|
if self.mtext is not None:
|
|
self.mtext.transform(wcs)
|
|
if self.block is not None:
|
|
self.block.transform(wcs)
|
|
|
|
def set_scale(self, value: float):
|
|
try:
|
|
conversion_factor = value / self.scale
|
|
except ZeroDivisionError:
|
|
return
|
|
self.scale = value
|
|
self.char_height *= conversion_factor
|
|
self.arrow_head_size *= conversion_factor
|
|
self.landing_gap_size *= conversion_factor
|
|
|
|
|
|
class MTextData:
|
|
ATTRIBS = {
|
|
304: "default_content",
|
|
11: "extrusion",
|
|
340: "style_handle",
|
|
12: "insert",
|
|
13: "text_direction",
|
|
42: "rotation",
|
|
43: "width",
|
|
44: "defined_height",
|
|
45: "line_spacing_factor",
|
|
170: "line_spacing_style",
|
|
90: "color",
|
|
171: "alignment",
|
|
172: "flow_direction",
|
|
91: "bg_color",
|
|
141: "bg_scale_factor",
|
|
92: "bg_transparency",
|
|
291: "use_window_bg_color",
|
|
292: "has_bg_fill",
|
|
173: "column_type",
|
|
293: "use_auto_height",
|
|
142: "column_width",
|
|
143: "column_gutter_width",
|
|
294: "column_flow_reversed",
|
|
144: "column_sizes", # multiple values
|
|
295: "use_word_break",
|
|
}
|
|
|
|
def __init__(self) -> None:
|
|
self.default_content: str = ""
|
|
self.extrusion: Vec3 = Z_AXIS
|
|
self.style_handle: str = "0" # handle of TextStyle() table entry
|
|
self.insert: Vec3 = NULLVEC
|
|
self.text_direction: Vec3 = X_AXIS # text direction
|
|
self.rotation: float = 0.0 # in radians!
|
|
self.width: float = 0.0 # MTEXT width, not scaled
|
|
self.defined_height: float = 0.0 # defined column height, not scaled
|
|
self.line_spacing_factor: float = 1.0
|
|
self.line_spacing_style: int = 1 # 1=at least, 2=exactly
|
|
self.color: int = colors.BY_BLOCK_RAW_VALUE
|
|
self.alignment: int = 1 # 1=left, 2=center, 3=right
|
|
self.flow_direction: int = 1 # 1=horiz, 3=vert, 6=by style
|
|
self.bg_color: int = colors.WINDOW_BG_RAW_VALUE
|
|
self.bg_scale_factor: float = 1.5
|
|
self.bg_transparency: int = 0
|
|
self.use_window_bg_color: int = 0
|
|
self.has_bg_fill: int = 0
|
|
self.column_type: int = 0 # unknown values
|
|
self.use_auto_height: int = 0
|
|
self.column_width: float = 0.0 # not scaled
|
|
self.column_gutter_width: float = 0.0 # not scaled
|
|
self.column_flow_reversed: int = 0
|
|
self.column_sizes: list[float] = [] # heights?, not scaled
|
|
self.use_word_break: int = 1
|
|
|
|
def register_resources(self, registry: xref.Registry) -> None:
|
|
"""Register required resources to the resource registry."""
|
|
registry.add_handle(self.style_handle)
|
|
|
|
def map_resources(self, mapping: xref.ResourceMapper) -> None:
|
|
"""Translate resources from self to the copied entity."""
|
|
self.style_handle = mapping.get_handle(self.style_handle)
|
|
|
|
def parse(self, code: int, value) -> bool:
|
|
# return True if data belongs to mtext else False (end of mtext section)
|
|
if code == 144:
|
|
self.column_sizes.append(float(value))
|
|
return True
|
|
attrib = MTextData.ATTRIBS.get(code)
|
|
if attrib:
|
|
self.__setattr__(attrib, cast_value(code, value))
|
|
return bool(attrib)
|
|
|
|
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
|
write_tag2 = tagwriter.write_tag2
|
|
write_vertex = tagwriter.write_vertex
|
|
write_tag2(304, safe_string(self.default_content, EXT_MAX_STR_LEN))
|
|
write_vertex(11, self.extrusion)
|
|
write_tag2(340, safe_handle(self.style_handle))
|
|
write_vertex(12, self.insert)
|
|
write_vertex(13, self.text_direction)
|
|
write_tag2(42, self.rotation)
|
|
write_tag2(43, self.width)
|
|
write_tag2(44, self.defined_height)
|
|
write_tag2(45, self.line_spacing_factor)
|
|
write_tag2(170, self.line_spacing_style)
|
|
write_tag2(90, self.color)
|
|
write_tag2(171, self.alignment)
|
|
write_tag2(172, self.flow_direction)
|
|
write_tag2(91, self.bg_color)
|
|
write_tag2(141, self.bg_scale_factor)
|
|
write_tag2(92, self.bg_transparency)
|
|
write_tag2(291, self.use_window_bg_color)
|
|
write_tag2(292, self.has_bg_fill)
|
|
write_tag2(173, self.column_type)
|
|
write_tag2(293, self.use_auto_height)
|
|
write_tag2(142, self.column_width)
|
|
write_tag2(143, self.column_gutter_width)
|
|
write_tag2(294, self.column_flow_reversed)
|
|
for size in self.column_sizes:
|
|
write_tag2(144, size)
|
|
write_tag2(295, self.use_word_break)
|
|
|
|
def transform(self, wcs: WCSTransform) -> None:
|
|
# MTEXT is not really an OCS entity!
|
|
m = wcs.m
|
|
ocs = OCSTransform(self.extrusion, m) # source extrusion!
|
|
self.extrusion = ocs.new_extrusion # process like an OCS entity!
|
|
self.insert = m.transform(self.insert) # WCS
|
|
self.text_direction = wcs.m.transform_direction( # WCS
|
|
self.text_direction, normalize=True
|
|
)
|
|
# don't use rotation ;)
|
|
self.rotation = ocs.transform_angle(self.rotation)
|
|
scale = wcs.uniform_scale
|
|
if math.isclose(scale, 1.0) or abs(scale) <= 1e-12:
|
|
return
|
|
self.width *= scale
|
|
self.defined_height *= scale
|
|
self.column_width *= scale
|
|
self.column_gutter_width *= scale
|
|
self.column_sizes = [size * scale for size in self.column_sizes]
|
|
self.default_content = scale_mtext_inline_commands(
|
|
self.default_content, scale
|
|
)
|
|
|
|
def apply_conversion_factor(self, conversion_factor: float):
|
|
# conversion_factor: convert from an old scaling to a new scaling
|
|
if math.isclose(conversion_factor, 1.0) or abs(conversion_factor) <= 1e-12:
|
|
return
|
|
self.width *= conversion_factor
|
|
self.defined_height *= conversion_factor
|
|
self.column_width *= conversion_factor
|
|
self.column_gutter_width *= conversion_factor
|
|
self.column_sizes = [h * conversion_factor for h in self.column_sizes]
|
|
self.default_content = scale_mtext_inline_commands(
|
|
self.default_content, conversion_factor
|
|
)
|
|
|
|
|
|
class BlockData:
|
|
ATTRIBS = {
|
|
341: "block_record_handle",
|
|
14: "extrusion",
|
|
15: "insert",
|
|
16: "scale",
|
|
46: "rotation",
|
|
93: "color",
|
|
}
|
|
|
|
def __init__(self) -> None:
|
|
self.block_record_handle: Optional[str] = None
|
|
self.extrusion: Vec3 = Z_AXIS
|
|
self.insert: Vec3 = NULLVEC
|
|
self.scale: Vec3 = Vec3(1, 1, 1)
|
|
self.rotation: float = 0 # in radians!
|
|
self.color: int = colors.BY_BLOCK_RAW_VALUE
|
|
# The transformation matrix is stored in transposed order
|
|
# of ezdxf.math.Matrix44()!
|
|
self._matrix: list[float] = [] # group code 47 x 16
|
|
|
|
def register_resources(self, registry: xref.Registry) -> None:
|
|
"""Register required resources to the resource registry."""
|
|
registry.add_handle(self.block_record_handle)
|
|
|
|
def map_resources(self, mapping: xref.ResourceMapper) -> None:
|
|
"""Translate resources from self to the copied entity."""
|
|
assert self.block_record_handle is not None
|
|
self.block_record_handle = mapping.get_handle(self.block_record_handle)
|
|
|
|
@property
|
|
def matrix44(self) -> Matrix44:
|
|
if len(self._matrix) == 16:
|
|
m = Matrix44(self._matrix)
|
|
else:
|
|
m = Matrix44()
|
|
m.transpose()
|
|
return m
|
|
|
|
@matrix44.setter
|
|
def matrix44(self, m: Matrix44) -> None:
|
|
m = m.copy()
|
|
m.transpose()
|
|
self._matrix = list(m)
|
|
|
|
def parse(self, code: int, value) -> bool:
|
|
attrib = BlockData.ATTRIBS.get(code)
|
|
if attrib:
|
|
self.__setattr__(attrib, cast_value(code, value))
|
|
elif code == 47:
|
|
self._matrix.append(float(value))
|
|
else:
|
|
return False
|
|
# return True if data belongs to block else False (end of block section)
|
|
return True
|
|
|
|
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
|
write_tag2 = tagwriter.write_tag2
|
|
write_vertex = tagwriter.write_vertex
|
|
if self.block_record_handle:
|
|
write_tag2(341, self.block_record_handle)
|
|
else:
|
|
# Do not write None, but "0" is also not valid!
|
|
# DXF structure error should be detected before export.
|
|
write_tag2(341, "0")
|
|
write_vertex(14, self.extrusion)
|
|
write_vertex(15, self.insert)
|
|
write_vertex(16, self.scale)
|
|
write_tag2(46, self.rotation)
|
|
write_tag2(93, self.color)
|
|
m = self._matrix
|
|
if len(m) != 16:
|
|
# The content of this matrix is not used for block transformation,
|
|
# not by AutoCAD nor by BricsCAD, but BricsCAD gets irritated
|
|
# if this matrix is missing. AutoCAD is fine without the presence
|
|
# of this matrix.
|
|
m = list(Matrix44()) # identity matrix
|
|
for value in m:
|
|
write_tag2(47, value)
|
|
|
|
def transform(self, wcs: WCSTransform) -> None:
|
|
# INSERT is an OCS entity, but the insert point is WCS!!!
|
|
m = wcs.m
|
|
ocs = OCSTransform(self.extrusion, m) # source extrusion!
|
|
self.extrusion = ocs.new_extrusion
|
|
self.insert = m.transform(self.insert) # WCS coordinates!!!
|
|
self.scale = ocs.transform_scale_vector(self.scale)
|
|
self.rotation = ocs.transform_angle(self.rotation)
|
|
self.matrix44 = self.matrix44 * m
|
|
|
|
def apply_conversion_factor(self, conversion_factor: float):
|
|
# conversion_factor: convert from an old scaling to a new scaling
|
|
self.scale *= conversion_factor
|
|
|
|
|
|
class LeaderData:
|
|
def __init__(self) -> None:
|
|
self.lines: list[LeaderLine] = []
|
|
# has_last_leader_line:
|
|
# in AutoCAD the leader is invisible if set to 0
|
|
# BricsCAD ignores this flag
|
|
self.has_last_leader_line: int = 0 # group code 290,
|
|
self.has_dogleg_vector: int = 0 # group code 291
|
|
self.last_leader_point: Vec3 = NULLVEC # group code (10, 20, 30) in WCS
|
|
self.dogleg_vector: Vec3 = X_AXIS # group code (11, 21, 31) in WCS
|
|
self.dogleg_length: float = 1.0 # group code 40
|
|
self.index: int = 0 # group code 90
|
|
|
|
# 0=horizontal; 1=vertical
|
|
self.attachment_direction: int = 0 # group code 271, R2010+
|
|
self.breaks: list[Vec3] = [] # group code 12, 13 - multiple breaks possible!
|
|
|
|
@property
|
|
def has_horizontal_attachment(self) -> bool:
|
|
return not bool(self.attachment_direction)
|
|
|
|
@classmethod
|
|
def load(cls, context: list[Union[DXFTag, list]]):
|
|
assert context[0] == (START_LEADER, LEADER_STR)
|
|
leader = cls()
|
|
for tag in context:
|
|
if isinstance(tag, list): # LeaderLine()
|
|
leader.lines.append(LeaderLine.load(tag))
|
|
continue
|
|
|
|
code, value = tag
|
|
if code == 290:
|
|
leader.has_last_leader_line = value
|
|
elif code == 291:
|
|
leader.has_dogleg_vector = value
|
|
elif code == 10:
|
|
leader.last_leader_point = Vec3(value)
|
|
elif code == 11:
|
|
leader.dogleg_vector = Vec3(value)
|
|
elif code == 40:
|
|
leader.dogleg_length = value
|
|
elif code == 90:
|
|
leader.index = value
|
|
elif code == 271:
|
|
leader.attachment_direction = value
|
|
elif code in (12, 13):
|
|
leader.breaks.append(Vec3(value))
|
|
|
|
return leader
|
|
|
|
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
|
write_tag2 = tagwriter.write_tag2
|
|
write_vertex = tagwriter.write_vertex
|
|
|
|
write_tag2(START_LEADER, LEADER_STR)
|
|
write_tag2(290, self.has_last_leader_line)
|
|
write_tag2(291, self.has_dogleg_vector)
|
|
write_vertex(10, self.last_leader_point)
|
|
write_vertex(11, self.dogleg_vector)
|
|
|
|
code = 0
|
|
for vertex in self.breaks:
|
|
# write alternate group code 12 and 13
|
|
write_vertex(12 + code, vertex)
|
|
code = 1 - code
|
|
write_tag2(90, self.index)
|
|
write_tag2(40, self.dogleg_length)
|
|
|
|
# Export leader lines:
|
|
for line in self.lines:
|
|
line.export_dxf(tagwriter)
|
|
|
|
if tagwriter.dxfversion >= const.DXF2010:
|
|
write_tag2(271, self.attachment_direction)
|
|
write_tag2(END_LEADER, "}")
|
|
|
|
def transform(self, wcs: WCSTransform) -> None:
|
|
m = wcs.m
|
|
self.last_leader_point = m.transform(self.last_leader_point)
|
|
try:
|
|
dog_leg = m.transform_direction(
|
|
self.dogleg_vector.normalize(self.dogleg_length)
|
|
)
|
|
except ZeroDivisionError: # dogleg_vector is NULL
|
|
dog_leg = m.transform_direction(Vec3(self.dogleg_length, 0, 0))
|
|
self.dogleg_vector = dog_leg.normalize()
|
|
self.dogleg_length = dog_leg.magnitude
|
|
self.breaks = list(m.transform_vertices(self.breaks))
|
|
for leader_line in self.lines:
|
|
leader_line.transform(wcs)
|
|
|
|
|
|
class LeaderLine:
|
|
def __init__(self) -> None:
|
|
self.vertices: list[Vec3] = [] # WCS coordinates
|
|
self.breaks: list[Union[int, Vec3]] = []
|
|
# Breaks: 90, 11, 12, [11, 12, ...] [, 90, 11, 12 [11, 12, ...]]
|
|
# group code 90 = break index
|
|
# group code 11 = start vertex of break
|
|
# group code 12 = end vertex of break
|
|
# multiple breaks per index possible
|
|
self.index: int = 0 # group code 91
|
|
self.color: int = colors.BY_BLOCK_RAW_VALUE # group code 92
|
|
# R2010+: override properties see ODA DWG pg. 214-215
|
|
|
|
@classmethod
|
|
def load(cls, tags: list[DXFTag]):
|
|
assert tags[0] == (START_LEADER_LINE, LEADER_LINE_STR)
|
|
line = LeaderLine()
|
|
vertices = line.vertices
|
|
breaks = []
|
|
for code, value in tags:
|
|
if code == 10:
|
|
vertices.append(Vec3(value))
|
|
elif code in (90, 11, 12):
|
|
breaks.append(cast_value(code, value))
|
|
elif code == 91:
|
|
line.index = value
|
|
elif code == 92:
|
|
line.color = value
|
|
if breaks:
|
|
line.breaks = breaks
|
|
return line
|
|
|
|
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
|
write_tag2 = tagwriter.write_tag2
|
|
write_vertex = tagwriter.write_vertex
|
|
|
|
write_tag2(START_LEADER_LINE, LEADER_LINE_STR)
|
|
for vertex in self.vertices:
|
|
write_vertex(10, vertex)
|
|
if self.breaks:
|
|
code = 0
|
|
for value in self.breaks:
|
|
if isinstance(value, int):
|
|
# break index
|
|
write_tag2(90, value)
|
|
else:
|
|
# 11 .. start vertex of break
|
|
# 12 .. end vertex of break
|
|
write_vertex(11 + code, value)
|
|
code = 1 - code
|
|
write_tag2(91, self.index)
|
|
write_tag2(92, self.color)
|
|
write_tag2(END_LEADER_LINE, "}")
|
|
|
|
def transform(self, wcs: WCSTransform) -> None:
|
|
m = wcs.m
|
|
self.vertices = list(m.transform_vertices(self.vertices))
|
|
if self.breaks:
|
|
breaks: list[Union[int, Vec3]] = []
|
|
for value in self.breaks:
|
|
if isinstance(value, Vec3):
|
|
breaks.append(m.transform(value))
|
|
else: # break index
|
|
breaks.append(value)
|
|
self.breaks = breaks
|
|
|
|
|
|
acdb_mleader_style = DefSubclass(
|
|
"AcDbMLeaderStyle",
|
|
{
|
|
"unknown1": DXFAttr(179, default=2),
|
|
"content_type": DXFAttr(170, default=2),
|
|
"draw_mleader_order_type": DXFAttr(171, default=1),
|
|
"draw_leader_order_type": DXFAttr(172, default=0),
|
|
"max_leader_segments_points": DXFAttr(90, default=2),
|
|
"first_segment_angle_constraint": DXFAttr(40, default=0.0),
|
|
"second_segment_angle_constraint": DXFAttr(41, default=0.0),
|
|
# leader_type:
|
|
# 0 = invisible
|
|
# 1 = straight line leader
|
|
# 2 = spline leader
|
|
"leader_type": DXFAttr(173, default=1),
|
|
"leader_line_color": DXFAttr(91, default=colors.BY_BLOCK_RAW_VALUE),
|
|
"leader_linetype_handle": DXFAttr(340),
|
|
"leader_lineweight": DXFAttr(92, default=-2),
|
|
"has_landing": DXFAttr(290, default=1),
|
|
"landing_gap_size": DXFAttr(42, default=2.0),
|
|
"has_dogleg": DXFAttr(291, default=1),
|
|
"dogleg_length": DXFAttr(43, default=8),
|
|
"name": DXFAttr(3, default="Standard"), # description text
|
|
# no handle is default arrow 'closed filled':
|
|
"arrow_head_handle": DXFAttr(341),
|
|
"arrow_head_size": DXFAttr(44, default=4),
|
|
"default_text_content": DXFAttr(300, default=""),
|
|
"text_style_handle": DXFAttr(342),
|
|
"text_left_attachment_type": DXFAttr(174, default=1),
|
|
"text_angle_type": DXFAttr(175, default=1),
|
|
"text_alignment_type": DXFAttr(176, default=0),
|
|
"text_right_attachment_type": DXFAttr(178, default=1),
|
|
"text_color": DXFAttr(93, default=colors.BY_BLOCK_RAW_VALUE),
|
|
"char_height": DXFAttr(45, default=4),
|
|
"has_text_frame": DXFAttr(292, default=0),
|
|
"text_align_always_left": DXFAttr(297, default=0),
|
|
"align_space": DXFAttr(46, default=4),
|
|
"has_block_scaling": DXFAttr(293),
|
|
"block_record_handle": DXFAttr(343),
|
|
"block_color": DXFAttr(94, default=colors.BY_BLOCK_RAW_VALUE),
|
|
"block_scale_x": DXFAttr(47, default=1),
|
|
"block_scale_y": DXFAttr(49, default=1),
|
|
"block_scale_z": DXFAttr(140, default=1),
|
|
"has_block_rotation": DXFAttr(294, default=1),
|
|
"block_rotation": DXFAttr(141, default=0),
|
|
"block_connection_type": DXFAttr(177, default=0),
|
|
"scale": DXFAttr(142, default=1),
|
|
"overwrite_property_value": DXFAttr(295, default=0),
|
|
"is_annotative": DXFAttr(296, default=0),
|
|
"break_gap_size": DXFAttr(143, default=3.75),
|
|
# 0 = Horizontal; 1 = Vertical:
|
|
"text_attachment_direction": DXFAttr(271, default=0),
|
|
# 9 = Center; 10 = Underline and Center:
|
|
"text_bottom_attachment_type": DXFAttr(272, default=9),
|
|
# 9 = Center; 10 = Overline and Center:
|
|
"text_top_attachment_type": DXFAttr(273, default=9),
|
|
"unknown2": DXFAttr(298, optional=True), # boolean flag ?
|
|
},
|
|
)
|
|
acdb_mleader_style_group_codes = group_code_mapping(acdb_mleader_style)
|
|
|
|
MLEADER_STYLE_HANDLE_ATTRIBS = (
|
|
"leader_linetype_handle",
|
|
"arrow_head_handle",
|
|
"text_style_handle",
|
|
"block_record_handle",
|
|
)
|
|
|
|
|
|
@register_entity
|
|
class MLeaderStyle(DXFObject):
|
|
DXFTYPE = "MLEADERSTYLE"
|
|
DXFATTRIBS = DXFAttributes(base_class, acdb_mleader_style)
|
|
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
|
|
|
def load_dxf_attribs(
|
|
self, processor: Optional[SubclassProcessor] = None
|
|
) -> DXFNamespace:
|
|
dxf = super().load_dxf_attribs(processor)
|
|
if processor:
|
|
processor.fast_load_dxfattribs(
|
|
dxf, acdb_mleader_style_group_codes, subclass=1
|
|
)
|
|
return dxf
|
|
|
|
def register_resources(self, registry: xref.Registry) -> None:
|
|
super().register_resources(registry)
|
|
dxf = self.dxf
|
|
for attrib_name in MLEADER_STYLE_HANDLE_ATTRIBS:
|
|
registry.add_handle(dxf.get(attrib_name, "0"))
|
|
|
|
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
|
super().map_resources(clone, mapping)
|
|
dxf = self.dxf
|
|
for attrib_name in MLEADER_STYLE_HANDLE_ATTRIBS:
|
|
if dxf.hasattr(attrib_name):
|
|
clone.dxf.set(attrib_name, mapping.get_handle(dxf.get(attrib_name)))
|
|
else:
|
|
clone.dxf.discard(attrib_name)
|
|
|
|
def set_mtext_style(self, name: str) -> None:
|
|
assert self.doc is not None, "valid DXF document required"
|
|
style = self.doc.styles.get(name)
|
|
if style is not None:
|
|
self.dxf.text_style_handle = style.dxf.handle
|
|
else:
|
|
raise ValueError(f"text style '{name}' does not exist")
|
|
|
|
def set_leader_properties(
|
|
self,
|
|
color: Union[int, colors.RGB] = colors.BYBLOCK,
|
|
linetype: str = "BYBLOCK",
|
|
lineweight: int = const.LINEWEIGHT_BYBLOCK,
|
|
leader_type: int = 1,
|
|
):
|
|
assert self.doc is not None, "valid DXF document required"
|
|
self.dxf.leader_line_color = colors.encode_raw_color(color)
|
|
linetype_ = self.doc.linetypes.get(linetype)
|
|
if linetype_ is None:
|
|
raise ValueError(f"invalid linetype name '{linetype}'")
|
|
self.dxf.leader_linetype_handle = linetype_.dxf.handle
|
|
self.dxf.leader_lineweight = int(lineweight)
|
|
self.dxf.leader_type = int(leader_type)
|
|
|
|
def set_arrow_head(self, name: str = ""):
|
|
from ezdxf.render.arrows import ARROWS
|
|
|
|
assert self.doc is not None, "valid DXF document required"
|
|
if name:
|
|
self.dxf.arrow_head_handle = ARROWS.arrow_handle(self.doc.blocks, name)
|
|
else:
|
|
# empty string is the default "closed filled" arrow,
|
|
# no handle needed
|
|
del self.dxf.arrow_head_handle
|
|
|
|
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
|
super().export_entity(tagwriter)
|
|
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_mleader_style.name)
|
|
self.dxf.export_dxf_attribs(tagwriter, acdb_mleader_style.attribs.keys())
|
|
|
|
def __referenced_blocks__(self) -> Iterable[str]:
|
|
"""Support for "ReferencedBlocks" protocol."""
|
|
for name in ("block_record_handle", "arrow_head_handle"):
|
|
handle = self.dxf.get(name, None)
|
|
if handle is not None:
|
|
yield handle
|
|
|
|
def audit(self, auditor: Auditor) -> None:
|
|
from ezdxf.audit import AuditError
|
|
|
|
if not self.is_alive:
|
|
return
|
|
super().audit(auditor)
|
|
entitydb = auditor.entitydb
|
|
dxf = self.dxf
|
|
name = str(self)
|
|
handle = dxf.get("text_style_handle", None)
|
|
if handle is not None and handle not in entitydb:
|
|
standard = auditor.doc.styles.get("Standard")
|
|
if standard is not None:
|
|
self.dxf.text_style_handle = standard.dxf.handle
|
|
auditor.fixed_error(
|
|
AuditError.UNDEFINED_TEXT_STYLE,
|
|
f"{name}: text_style_handle={handle} is not valid, replaced by "
|
|
f"'Standard' text style",
|
|
self,
|
|
)
|
|
else:
|
|
logger.warning("required text style 'Standard' does not exist")
|
|
self.dxf.discard("text_style_handle")
|
|
auditor.fixed_error(
|
|
AuditError.UNDEFINED_TEXT_STYLE,
|
|
f"{name}: removed invalid text_style_handle={handle}",
|
|
self,
|
|
)
|
|
for attrib in ("arrow_head_handle", "block_record_handle"):
|
|
handle = dxf.get(attrib)
|
|
if handle is None or handle == "0": # legit "undefined values"
|
|
continue
|
|
if handle not in entitydb:
|
|
dxf.discard(attrib)
|
|
auditor.fixed_error(
|
|
AuditError.UNDEFINED_BLOCK,
|
|
f"{name}: removed invalid {attrib}={handle}",
|
|
self,
|
|
)
|
|
|
|
|
|
class MLeaderStyleCollection(ObjectCollection[MLeaderStyle]):
|
|
def __init__(self, doc: Drawing):
|
|
super().__init__(doc, dict_name="ACAD_MLEADERSTYLE", object_type="MLEADERSTYLE")
|
|
self.create_required_entries()
|
|
|
|
def create_required_entries(self) -> None:
|
|
if "Standard" not in self:
|
|
mleader_style = self.new("Standard")
|
|
# set standard text style
|
|
text_style = self.doc.styles.get("Standard")
|
|
mleader_style.dxf.text_style_handle = text_style.dxf.handle
|