1262 lines
46 KiB
Python
1262 lines
46 KiB
Python
# Copyright (c) 2019-2024 Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Union,
|
|
Iterable,
|
|
Iterator,
|
|
Optional,
|
|
Callable,
|
|
cast,
|
|
)
|
|
from typing_extensions import Self
|
|
import enum
|
|
import math
|
|
import logging
|
|
|
|
from ezdxf.lldxf import const, validator
|
|
from ezdxf.lldxf.attributes import (
|
|
DXFAttr,
|
|
DXFAttributes,
|
|
DefSubclass,
|
|
XType,
|
|
RETURN_DEFAULT,
|
|
group_code_mapping,
|
|
VIRTUAL_TAG,
|
|
)
|
|
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000, DXF2018
|
|
from ezdxf.lldxf.types import DXFTag, dxftag
|
|
from ezdxf.lldxf.tags import (
|
|
Tags,
|
|
find_begin_and_end_of_encoded_xdata_tags,
|
|
NotFoundException,
|
|
)
|
|
|
|
from ezdxf.math import Vec3, Matrix44, OCS, UCS, NULLVEC, Z_AXIS, X_AXIS, UVec
|
|
from ezdxf.math.transformtools import transform_extrusion
|
|
from ezdxf.colors import rgb2int, RGB
|
|
from ezdxf.tools.text import (
|
|
split_mtext_string,
|
|
escape_dxf_line_endings,
|
|
fast_plain_mtext,
|
|
plain_mtext,
|
|
scale_mtext_inline_commands,
|
|
)
|
|
from . import factory
|
|
from .dxfentity import base_class, SubclassProcessor
|
|
from .dxfgfx import DXFGraphic, acdb_entity
|
|
from .xdata import XData
|
|
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.lldxf.tagwriter import AbstractTagWriter
|
|
from ezdxf.entitydb import EntityDB
|
|
from ezdxf import xref
|
|
|
|
__all__ = [
|
|
"MText",
|
|
"MTextColumns",
|
|
"ColumnType",
|
|
"acdb_mtext",
|
|
"acdb_mtext_group_codes",
|
|
"export_mtext_content",
|
|
]
|
|
logger = logging.getLogger("ezdxf")
|
|
|
|
BG_FILL_MASK = 1 + 2 + 16
|
|
|
|
acdb_mtext = DefSubclass(
|
|
"AcDbMText",
|
|
{
|
|
# virtual text attribute, the actual content is stored in
|
|
# multiple tags (1, 3, 3, ...)
|
|
"text": DXFAttr(
|
|
VIRTUAL_TAG,
|
|
xtype=XType.callback,
|
|
getter="_get_text",
|
|
setter="_set_text",
|
|
),
|
|
"insert": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
|
# Nominal (initial) text height
|
|
"char_height": DXFAttr(
|
|
40,
|
|
default=2.5,
|
|
validator=validator.is_greater_zero,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Reference column width
|
|
"width": DXFAttr(41, optional=True),
|
|
# Found in BricsCAD export:
|
|
"defined_height": DXFAttr(46, dxfversion="AC1021"),
|
|
# Attachment point: enum const.MTextEntityAlignment
|
|
# 1 = Top left
|
|
# 2 = Top center
|
|
# 3 = Top right
|
|
# 4 = Middle left
|
|
# 5 = Middle center
|
|
# 6 = Middle right
|
|
# 7 = Bottom left
|
|
# 8 = Bottom center
|
|
# 9 = Bottom right
|
|
"attachment_point": DXFAttr(
|
|
71,
|
|
default=1,
|
|
validator=validator.is_in_integer_range(1, 10),
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Flow direction: enum MTextFlowDirection
|
|
# 1 = Left to right
|
|
# 3 = Top to bottom
|
|
# 5 = By style (the flow direction is inherited from the associated
|
|
# text style)
|
|
"flow_direction": DXFAttr(
|
|
72,
|
|
default=1,
|
|
optional=True,
|
|
validator=validator.is_one_of({1, 3, 5}),
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Content text:
|
|
# group code 1: text
|
|
# group code 3: additional text (optional)
|
|
# Text style name:
|
|
"style": DXFAttr(
|
|
7,
|
|
default="Standard",
|
|
optional=True,
|
|
validator=validator.is_valid_table_name, # do not fix!
|
|
),
|
|
"extrusion": DXFAttr(
|
|
210,
|
|
xtype=XType.point3d,
|
|
default=Z_AXIS,
|
|
optional=True,
|
|
validator=validator.is_not_null_vector,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# x-axis direction vector (in WCS)
|
|
# If rotation and text_direction are present, text_direction wins
|
|
"text_direction": DXFAttr(
|
|
11,
|
|
xtype=XType.point3d,
|
|
default=X_AXIS,
|
|
optional=True,
|
|
validator=validator.is_not_null_vector,
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Horizontal width of the characters that make up the mtext entity.
|
|
# This value will always be equal to or less than the value of *width*,
|
|
# (read-only, ignored if supplied)
|
|
"rect_width": DXFAttr(42, optional=True),
|
|
# Vertical height of the mtext entity (read-only, ignored if supplied)
|
|
"rect_height": DXFAttr(43, optional=True),
|
|
# Text rotation in degrees - Error in DXF reference, which claims radians
|
|
"rotation": DXFAttr(50, default=0, optional=True),
|
|
# Line spacing style (optional): enum const.MTextLineSpacing
|
|
# 1 = At least (taller characters will override)
|
|
# 2 = Exact (taller characters will not override)
|
|
"line_spacing_style": DXFAttr(
|
|
73,
|
|
default=1,
|
|
optional=True,
|
|
validator=validator.is_one_of({1, 2}),
|
|
fixer=RETURN_DEFAULT,
|
|
),
|
|
# Line spacing factor (optional): Percentage of default (3-on-5) line
|
|
# spacing to be applied. Valid values range from 0.25 to 4.00
|
|
"line_spacing_factor": DXFAttr(
|
|
44,
|
|
default=1,
|
|
optional=True,
|
|
validator=validator.is_in_float_range(0.25, 4.00),
|
|
fixer=validator.fit_into_float_range(0.25, 4.00),
|
|
),
|
|
# Determines how much border there is around the text.
|
|
# (45) + (90) + (63) all three required, if one of them is used
|
|
"box_fill_scale": DXFAttr(45, dxfversion="AC1021"),
|
|
# background fill type flags: enum const.MTextBackgroundColor
|
|
# 0 = off
|
|
# 1 = color -> (63) < (421) or (431)
|
|
# 2 = drawing window color
|
|
# 3 = use background color (1 & 2)
|
|
# 16 = text frame ODA specification 20.4.46
|
|
# 2021-05-14: text frame only is supported bg_fill = 16,
|
|
# but scaling is always 1.5 and tags 45 + 63 are not present
|
|
"bg_fill": DXFAttr(
|
|
90,
|
|
dxfversion="AC1021",
|
|
validator=validator.is_valid_bitmask(BG_FILL_MASK),
|
|
fixer=validator.fix_bitmask(BG_FILL_MASK),
|
|
),
|
|
# background fill color as ACI, required even true color is used
|
|
"bg_fill_color": DXFAttr(
|
|
63,
|
|
dxfversion="AC1021",
|
|
validator=validator.is_valid_aci_color,
|
|
),
|
|
# 420-429? : background fill color as true color value, (63) also required
|
|
# but ignored
|
|
"bg_fill_true_color": DXFAttr(421, dxfversion="AC1021"),
|
|
# 430-439? : background fill color as color name ???, (63) also required
|
|
# but ignored
|
|
"bg_fill_color_name": DXFAttr(431, dxfversion="AC1021"),
|
|
# background fill color transparency - not used by AutoCAD/BricsCAD
|
|
"bg_fill_transparency": DXFAttr(441, dxfversion="AC1021"),
|
|
},
|
|
)
|
|
acdb_mtext_group_codes = group_code_mapping(acdb_mtext)
|
|
|
|
|
|
# -----------------------------------------------------------------------
|
|
# For more information go to docs/source/dxfinternals/entities/mtext.rst
|
|
# -----------------------------------------------------------------------
|
|
|
|
# MTEXT column support:
|
|
# MTEXT columns have the same appearance and handling for all DXF versions
|
|
# as a single MTEXT entity like in DXF R2018.
|
|
|
|
|
|
class ColumnType(enum.IntEnum):
|
|
NONE = 0
|
|
STATIC = 1
|
|
DYNAMIC = 2
|
|
|
|
|
|
class MTextColumns:
|
|
"""The column count is not stored explicit in the columns definition for
|
|
DXF versions R2018+.
|
|
|
|
If column_type is DYNAMIC and auto_height is True the column
|
|
count is defined by the content. The exact calculation of the column count
|
|
requires an accurate rendering of the MTEXT content like AutoCAD does!
|
|
|
|
If the column count is not defined, ezdxf tries to calculate the column
|
|
count from total_width, width and gutter_width, if these attributes are set
|
|
properly.
|
|
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self.column_type: ColumnType = ColumnType.STATIC
|
|
# The embedded object in R2018 does not store the column count for
|
|
# column type DYNAMIC and auto_height is True!
|
|
# For DXF < R2018 the column count, may not match the count of linked
|
|
# MTEXT entities!
|
|
self.count: int = 1
|
|
self.auto_height: bool = False
|
|
self.reversed_column_flow: bool = False
|
|
self.defined_height: float = 0.0
|
|
self.width: float = 0.0
|
|
self.gutter_width: float = 0.0
|
|
self.total_width: float = 0.0
|
|
self.total_height: float = 0.0
|
|
# Storage for handles of linked MTEXT entities at loading stage:
|
|
self.linked_handles: Optional[list[str]] = None
|
|
# Storage for linked MTEXT entities for DXF versions < R2018:
|
|
self.linked_columns: list[MText] = []
|
|
# R2018+: heights of all columns if auto_height is False
|
|
self.heights: list[float] = []
|
|
|
|
def deep_copy(self) -> MTextColumns:
|
|
columns = self.shallow_copy()
|
|
columns.linked_columns = [mtext.copy() for mtext in self.linked_columns]
|
|
return columns
|
|
|
|
def shallow_copy(self) -> MTextColumns:
|
|
columns = MTextColumns()
|
|
columns.count = self.count
|
|
columns.column_type = self.column_type
|
|
columns.auto_height = self.auto_height
|
|
columns.reversed_column_flow = self.reversed_column_flow
|
|
columns.defined_height = self.defined_height
|
|
columns.width = self.width
|
|
columns.gutter_width = self.gutter_width
|
|
columns.total_width = self.total_width
|
|
columns.total_height = self.total_height
|
|
columns.linked_columns = list(self.linked_columns)
|
|
columns.heights = list(self.heights)
|
|
return columns
|
|
|
|
@classmethod
|
|
def new_static_columns(
|
|
cls, count: int, width: float, gutter_width: float, height: float
|
|
) -> MTextColumns:
|
|
columns = cls()
|
|
columns.column_type = ColumnType.STATIC
|
|
columns.count = int(count)
|
|
columns.width = float(width)
|
|
columns.gutter_width = float(gutter_width)
|
|
columns.defined_height = float(height)
|
|
columns.update_total_width()
|
|
columns.update_total_height()
|
|
return columns
|
|
|
|
@classmethod
|
|
def new_dynamic_auto_height_columns(
|
|
cls, count: int, width: float, gutter_width: float, height: float
|
|
) -> MTextColumns:
|
|
columns = cls()
|
|
columns.column_type = ColumnType.DYNAMIC
|
|
columns.auto_height = True
|
|
columns.count = int(count)
|
|
columns.width = float(width)
|
|
columns.gutter_width = float(gutter_width)
|
|
columns.defined_height = float(height)
|
|
columns.update_total_width()
|
|
columns.update_total_height()
|
|
return columns
|
|
|
|
@classmethod
|
|
def new_dynamic_manual_height_columns(
|
|
cls, width: float, gutter_width: float, heights: Iterable[float]
|
|
) -> MTextColumns:
|
|
columns = cls()
|
|
columns.column_type = ColumnType.DYNAMIC
|
|
columns.auto_height = False
|
|
columns.width = float(width)
|
|
columns.gutter_width = float(gutter_width)
|
|
columns.defined_height = 0.0
|
|
columns.heights = list(heights)
|
|
columns.count = len(columns.heights)
|
|
columns.update_total_width()
|
|
columns.update_total_height()
|
|
return columns
|
|
|
|
def update_total_width(self):
|
|
count = self.count
|
|
if count > 0:
|
|
self.total_width = count * self.width + (count - 1) * self.gutter_width
|
|
else:
|
|
self.total_width = 0.0
|
|
|
|
def update_total_height(self):
|
|
if self.has_dynamic_manual_height:
|
|
self.total_height = max(self.heights)
|
|
else:
|
|
self.total_height = self.defined_height
|
|
|
|
@property
|
|
def has_dynamic_auto_height(self) -> bool:
|
|
return self.column_type == ColumnType.DYNAMIC and self.auto_height
|
|
|
|
@property
|
|
def has_dynamic_manual_height(self) -> bool:
|
|
return self.column_type == ColumnType.DYNAMIC and not self.auto_height
|
|
|
|
def link_columns(self, doc: Drawing):
|
|
# DXF R2018+ has no linked MTEXT entities.
|
|
if doc.dxfversion >= DXF2018 or not self.linked_handles:
|
|
return
|
|
db = doc.entitydb
|
|
assert db is not None, "entity database not initialized"
|
|
linked_columns = []
|
|
for handle in self.linked_handles:
|
|
mtext = cast("MText", db.get(handle))
|
|
if mtext:
|
|
linked_columns.append(mtext)
|
|
else:
|
|
logger.debug(f"Linked MTEXT column #{handle} does not exist.")
|
|
self.linked_handles = None
|
|
self.linked_columns = linked_columns
|
|
|
|
def transform(self, m: Matrix44, hscale: float = 1, vscale: float = 1):
|
|
self.width *= hscale
|
|
self.gutter_width *= hscale
|
|
self.total_width *= hscale
|
|
self.total_height *= vscale
|
|
self.defined_height *= vscale
|
|
self.heights = [h * vscale for h in self.heights]
|
|
for mtext in self.linked_columns:
|
|
mtext.transform(m)
|
|
|
|
def acad_mtext_column_info_xdata(self) -> Tags:
|
|
tags = Tags(
|
|
[
|
|
DXFTag(1000, "ACAD_MTEXT_COLUMN_INFO_BEGIN"),
|
|
DXFTag(1070, 75),
|
|
DXFTag(1070, int(self.column_type)),
|
|
DXFTag(1070, 79),
|
|
DXFTag(1070, int(self.auto_height)),
|
|
DXFTag(1070, 76),
|
|
DXFTag(1070, self.count),
|
|
DXFTag(1070, 78),
|
|
DXFTag(1070, int(self.reversed_column_flow)),
|
|
DXFTag(1070, 48),
|
|
DXFTag(1040, self.width),
|
|
DXFTag(1070, 49),
|
|
DXFTag(1040, self.gutter_width),
|
|
]
|
|
)
|
|
if self.has_dynamic_manual_height:
|
|
tags.extend([DXFTag(1070, 50), DXFTag(1070, len(self.heights))])
|
|
tags.extend(DXFTag(1040, height) for height in self.heights)
|
|
tags.append(DXFTag(1000, "ACAD_MTEXT_COLUMN_INFO_END"))
|
|
return tags
|
|
|
|
def acad_mtext_columns_xdata(self) -> Tags:
|
|
tags = Tags(
|
|
[
|
|
DXFTag(1000, "ACAD_MTEXT_COLUMNS_BEGIN"),
|
|
DXFTag(1070, 47),
|
|
DXFTag(1070, self.count), # incl. main MTEXT
|
|
]
|
|
)
|
|
tags.extend( # writes only (count - 1) handles!
|
|
DXFTag(1005, handle) for handle in self.mtext_handles()
|
|
)
|
|
tags.append(DXFTag(1000, "ACAD_MTEXT_COLUMNS_END"))
|
|
return tags
|
|
|
|
def mtext_handles(self) -> list[str]:
|
|
"""Returns a list of all linked MTEXT handles."""
|
|
if self.linked_handles:
|
|
return self.linked_handles
|
|
handles = []
|
|
for column in self.linked_columns:
|
|
if column.is_alive:
|
|
handle = column.dxf.handle
|
|
if handle is None:
|
|
raise const.DXFStructureError("Linked MTEXT column has no handle.")
|
|
handles.append(handle)
|
|
else:
|
|
raise const.DXFStructureError("Linked MTEXT column deleted!")
|
|
return handles
|
|
|
|
def acad_mtext_defined_height_xdata(self) -> Tags:
|
|
return Tags(
|
|
[
|
|
DXFTag(1000, "ACAD_MTEXT_DEFINED_HEIGHT_BEGIN"),
|
|
DXFTag(1070, 46),
|
|
DXFTag(1040, self.defined_height),
|
|
DXFTag(1000, "ACAD_MTEXT_DEFINED_HEIGHT_END"),
|
|
]
|
|
)
|
|
|
|
|
|
def load_columns_from_embedded_object(
|
|
dxf: DXFNamespace, embedded_obj: Tags
|
|
) -> MTextColumns:
|
|
columns = MTextColumns()
|
|
insert = dxf.get("insert") # mandatory attribute, but what if ...
|
|
text_direction = dxf.get("text_direction") # optional attribute
|
|
reference_column_width = dxf.get("width") # optional attribute
|
|
for code, value in embedded_obj:
|
|
# Update duplicated attributes if MTEXT attributes are not set:
|
|
if code == 10 and text_direction is None:
|
|
dxf.text_direction = Vec3(value)
|
|
# rotation is not needed anymore:
|
|
dxf.discard("rotation")
|
|
elif code == 11 and insert is None:
|
|
dxf.insert = Vec3(value)
|
|
elif code == 40 and reference_column_width is None:
|
|
dxf.width = value
|
|
elif code == 41:
|
|
# Column height if auto height is True.
|
|
columns.defined_height = value
|
|
# Keep in sync with DXF attribute:
|
|
dxf.defined_height = value
|
|
elif code == 42:
|
|
columns.total_width = value
|
|
elif code == 43:
|
|
columns.total_height = value
|
|
elif code == 44:
|
|
# All columns have the same width.
|
|
columns.width = value
|
|
elif code == 45:
|
|
# All columns have the same gutter width = space between columns.
|
|
columns.gutter_width = value
|
|
elif code == 71:
|
|
columns.column_type = ColumnType(value)
|
|
elif code == 72: # column height count
|
|
# The column height count can be 0 in some cases (dynamic & auto
|
|
# height) in DXF version R2018+.
|
|
columns.count = value
|
|
elif code == 73:
|
|
columns.auto_height = bool(value)
|
|
elif code == 74:
|
|
columns.reversed_column_flow = bool(value)
|
|
elif code == 46: # column heights
|
|
# The last column height is 0; takes the rest?
|
|
columns.heights.append(value)
|
|
|
|
# The column count is not defined explicit:
|
|
if columns.count == 0:
|
|
if columns.heights: # very unlikely
|
|
columns.count = len(columns.heights)
|
|
elif columns.total_width > 0:
|
|
# calculate column count from total_width
|
|
g = columns.gutter_width
|
|
wg = abs(columns.width + g)
|
|
if wg > 1e-6:
|
|
columns.count = int(round((columns.total_width + g) / wg))
|
|
return columns
|
|
|
|
|
|
def load_mtext_column_info(tags: Tags) -> Optional[MTextColumns]:
|
|
try: # has column info?
|
|
start, end = find_begin_and_end_of_encoded_xdata_tags(
|
|
"ACAD_MTEXT_COLUMN_INFO", tags
|
|
)
|
|
except NotFoundException:
|
|
return None
|
|
columns = MTextColumns()
|
|
height_count = 0
|
|
group_code = None
|
|
for code, value in tags[start + 1 : end]:
|
|
if height_count:
|
|
if code == 1040:
|
|
columns.heights.append(value)
|
|
height_count -= 1
|
|
continue
|
|
else: # error
|
|
logger.error("missing column heights in MTEXT entity")
|
|
height_count = 0
|
|
|
|
if group_code is None:
|
|
group_code = value
|
|
continue
|
|
|
|
if group_code == 75:
|
|
columns.column_type = ColumnType(value)
|
|
elif group_code == 79:
|
|
columns.auto_height = bool(value)
|
|
elif group_code == 76:
|
|
# column count, may not match the count of linked MTEXT entities!
|
|
columns.count = int(value)
|
|
elif group_code == 78:
|
|
columns.reversed_column_flow = bool(value)
|
|
elif group_code == 48:
|
|
columns.width = value
|
|
elif group_code == 49:
|
|
columns.gutter_width = value
|
|
elif group_code == 50:
|
|
height_count = int(value)
|
|
group_code = None
|
|
return columns
|
|
|
|
|
|
def load_mtext_linked_column_handles(tags: Tags) -> list[str]:
|
|
handles: list[str] = []
|
|
try:
|
|
start, end = find_begin_and_end_of_encoded_xdata_tags(
|
|
"ACAD_MTEXT_COLUMNS", tags
|
|
)
|
|
except NotFoundException:
|
|
return handles
|
|
for code, value in tags[start:end]:
|
|
if code == 1005:
|
|
handles.append(value)
|
|
return handles
|
|
|
|
|
|
def load_mtext_defined_height(tags: Tags) -> float:
|
|
# The defined height stored in the linked MTEXT entities, is not required:
|
|
#
|
|
# If all columns have the same height (static & dynamic auto height), the
|
|
# "defined_height" is stored in the main MTEXT, but the linked MTEXT entities
|
|
# also have a "ACAD_MTEXT_DEFINED_HEIGHT" group in the ACAD section of XDATA.
|
|
#
|
|
# If the columns have different heights (dynamic manual height), these
|
|
# height values are only stored in the main MTEXT. The linked MTEXT
|
|
# entities do not have an ACAD section at all.
|
|
|
|
height = 0.0
|
|
try:
|
|
start, end = find_begin_and_end_of_encoded_xdata_tags(
|
|
"ACAD_MTEXT_DEFINED_HEIGHT", tags
|
|
)
|
|
except NotFoundException:
|
|
return height
|
|
|
|
for code, value in tags[start:end]:
|
|
if code == 1040:
|
|
height = value
|
|
return height
|
|
|
|
|
|
def load_columns_from_xdata(dxf: DXFNamespace, xdata: XData) -> Optional[MTextColumns]:
|
|
# The ACAD section in XDATA of the main MTEXT entity stores all column
|
|
# related information:
|
|
if "ACAD" in xdata:
|
|
acad = xdata.get("ACAD")
|
|
else:
|
|
return None
|
|
|
|
name = f"MTEXT(#{dxf.get('handle')})"
|
|
try:
|
|
columns = load_mtext_column_info(acad)
|
|
except const.DXFStructureError:
|
|
logger.error(f"Invalid ACAD_MTEXT_COLUMN_INFO in {name}")
|
|
return None
|
|
|
|
if columns is None: # no columns defined
|
|
return None
|
|
|
|
try:
|
|
columns.linked_handles = load_mtext_linked_column_handles(acad)
|
|
except const.DXFStructureError:
|
|
logger.error(f"Invalid ACAD_MTEXT_COLUMNS in {name}")
|
|
|
|
columns.update_total_width()
|
|
if columns.heights: # dynamic columns, manual heights
|
|
# This is correct even if the last column is the tallest, which height
|
|
# is not known. The height of last column is always stored as 0.
|
|
columns.total_height = max(columns.heights)
|
|
else: # all columns have the same "defined" height
|
|
try:
|
|
columns.defined_height = load_mtext_defined_height(acad)
|
|
except const.DXFStructureError:
|
|
logger.error(f"Invalid ACAD_MTEXT_DEFINED_HEIGHT in {name}")
|
|
columns.total_height = columns.defined_height
|
|
|
|
return columns
|
|
|
|
|
|
def extract_mtext_text_frame_handles(xdata: XData) -> list[str]:
|
|
# Stores information about the text frame until DXF R2018.
|
|
# Newer CAD applications do not need that information nor the separated
|
|
# LWPOLYLINE as text frame entity.
|
|
handles: list[str] = []
|
|
if "ACAD" in xdata:
|
|
acad = xdata.get("ACAD")
|
|
else:
|
|
return handles
|
|
|
|
try:
|
|
start, end = find_begin_and_end_of_encoded_xdata_tags(
|
|
"ACAD_MTEXT_TEXT_BORDERS", acad
|
|
)
|
|
except NotFoundException:
|
|
return handles
|
|
|
|
for code, value in acad[start:end]:
|
|
# multiple handles to a LWPOLYLINE entity could be present:
|
|
if code == 1005:
|
|
handles.append(value)
|
|
|
|
# remove MTEXT_TEXT_BORDERS data
|
|
del acad[start:end]
|
|
if len(acad) < 2:
|
|
xdata.discard("ACAD")
|
|
return handles
|
|
|
|
|
|
@factory.register_entity
|
|
class MText(DXFGraphic):
|
|
"""DXF MTEXT entity"""
|
|
|
|
DXFTYPE = "MTEXT"
|
|
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mtext)
|
|
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.text: str = ""
|
|
# Linked MText columns do not have a MTextColumns() object!
|
|
self._columns: Optional[MTextColumns] = None
|
|
|
|
def _get_text(self):
|
|
"""Getter for virtual Mtext.dxf.text attribute.
|
|
|
|
The MText content is stored in multiple tags (1, 3, 3, ...) and cannot
|
|
be supported as a simple DXF tag. The virtual MText.dxf.text attribute
|
|
adds compatibility to other text based entities: TEXT, ATTRIB, ATTDEF
|
|
|
|
"""
|
|
return self.text
|
|
|
|
def _set_text(self, value):
|
|
"""Setter for virtual Mtext.dxf.text attribute."""
|
|
self.text = str(value)
|
|
|
|
@property
|
|
def columns(self) -> Optional[MTextColumns]:
|
|
"""Returns a copy of the column configuration."""
|
|
# The column configuration is deliberately not editable.
|
|
# Can't prevent access to _columns, but you are on your own if do this!
|
|
return self._columns.shallow_copy() if self._columns else None
|
|
|
|
@property
|
|
def has_columns(self) -> bool:
|
|
return self._columns is not None
|
|
|
|
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
|
assert isinstance(entity, MText)
|
|
entity.text = self.text
|
|
if self.has_columns:
|
|
# copies also the linked MTEXT column entities!
|
|
entity._columns = self._columns.deep_copy() # type: ignore
|
|
|
|
def load_dxf_attribs(
|
|
self, processor: Optional[SubclassProcessor] = None
|
|
) -> DXFNamespace:
|
|
dxf = super().load_dxf_attribs(processor)
|
|
if processor:
|
|
tags = processor.subclass_by_index(2)
|
|
if tags:
|
|
tags = Tags(self.load_mtext_content(tags))
|
|
processor.fast_load_dxfattribs(
|
|
dxf, acdb_mtext_group_codes, subclass=tags, recover=True
|
|
)
|
|
if processor.embedded_objects:
|
|
obj = processor.embedded_objects[0]
|
|
self._columns = load_columns_from_embedded_object(dxf, obj)
|
|
elif self.xdata:
|
|
self._columns = load_columns_from_xdata(dxf, self.xdata)
|
|
else:
|
|
raise const.DXFStructureError(
|
|
f"missing 'AcDbMText' subclass in MTEXT(#{dxf.handle})"
|
|
)
|
|
return dxf
|
|
|
|
def post_load_hook(self, doc: Drawing) -> Optional[Callable]:
|
|
def destroy_text_frame_entity():
|
|
entitydb = doc.entitydb
|
|
if entitydb:
|
|
for handle in extract_mtext_text_frame_handles(self.xdata):
|
|
text_frame = entitydb.get(handle)
|
|
if text_frame:
|
|
text_frame.destroy()
|
|
|
|
def unlink_mtext_columns_from_layout():
|
|
"""Unlinked MTEXT entities from layout entity space."""
|
|
layout = self.get_layout()
|
|
if layout is not None:
|
|
for mtext in self._columns.linked_columns:
|
|
layout.unlink_entity(mtext)
|
|
else:
|
|
for mtext in self._columns.linked_columns:
|
|
mtext.dxf.owner = None
|
|
|
|
super().post_load_hook(doc)
|
|
if self.xdata:
|
|
destroy_text_frame_entity()
|
|
|
|
if self.has_columns:
|
|
# Link columns, one MTEXT entity for each column, to the main MTEXT
|
|
# entity (DXF version <R2018).
|
|
self._columns.link_columns(doc) # type: ignore
|
|
return unlink_mtext_columns_from_layout
|
|
return None
|
|
|
|
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
|
|
"""Pre requirement check and pre-processing for export.
|
|
|
|
Returns False if MTEXT should not be exported at all.
|
|
|
|
(internal API)
|
|
"""
|
|
columns = self._columns
|
|
if columns and tagwriter.dxfversion < const.DXF2018:
|
|
if columns.count != len(columns.linked_columns) + 1:
|
|
logger.debug(f"{str(self)}: column count does not match linked columns")
|
|
# just log for debugging, because AutoCAD accept this!
|
|
if not all(column.is_alive for column in columns.linked_columns):
|
|
logger.debug(f"{str(self)}: contains destroyed linked columns")
|
|
return False
|
|
self.sync_common_attribs_of_linked_columns()
|
|
return True
|
|
|
|
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
|
super().export_dxf(tagwriter)
|
|
# Linked MTEXT entities are not stored in the layout entity space!
|
|
if self.has_columns and tagwriter.dxfversion < const.DXF2018:
|
|
self.export_linked_entities(tagwriter)
|
|
|
|
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
|
"""Export entity specific data as DXF tags."""
|
|
super().export_entity(tagwriter)
|
|
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_mtext.name)
|
|
self.dxf.export_dxf_attribs(
|
|
tagwriter,
|
|
[
|
|
"insert",
|
|
"char_height",
|
|
"width",
|
|
"defined_height",
|
|
"attachment_point",
|
|
"flow_direction",
|
|
],
|
|
)
|
|
export_mtext_content(self.text, tagwriter)
|
|
self.dxf.export_dxf_attribs(
|
|
tagwriter,
|
|
[
|
|
"style",
|
|
"extrusion",
|
|
"text_direction",
|
|
"rect_width",
|
|
"rect_height",
|
|
"rotation",
|
|
"line_spacing_style",
|
|
"line_spacing_factor",
|
|
"box_fill_scale",
|
|
"bg_fill",
|
|
"bg_fill_color",
|
|
"bg_fill_true_color",
|
|
"bg_fill_color_name",
|
|
"bg_fill_transparency",
|
|
],
|
|
)
|
|
columns = self._columns
|
|
if columns is None or columns.column_type == ColumnType.NONE:
|
|
return
|
|
|
|
if tagwriter.dxfversion >= DXF2018:
|
|
self.export_embedded_object(tagwriter)
|
|
else:
|
|
self.set_column_xdata()
|
|
self.set_linked_columns_xdata()
|
|
|
|
def load_mtext_content(self, tags: Tags) -> Iterator[DXFTag]:
|
|
tail = ""
|
|
parts = []
|
|
for tag in tags:
|
|
if tag.code == 1:
|
|
tail = tag.value
|
|
elif tag.code == 3:
|
|
parts.append(tag.value)
|
|
else:
|
|
yield tag
|
|
parts.append(tail)
|
|
self.text = escape_dxf_line_endings("".join(parts))
|
|
|
|
def export_embedded_object(self, tagwriter: AbstractTagWriter):
|
|
dxf = self.dxf
|
|
cols = self._columns
|
|
assert cols is not None
|
|
|
|
tagwriter.write_tag2(101, "Embedded Object")
|
|
tagwriter.write_tag2(70, 1) # unknown meaning
|
|
tagwriter.write_tag(dxftag(10, dxf.text_direction))
|
|
tagwriter.write_tag(dxftag(11, dxf.insert))
|
|
tagwriter.write_tag2(40, dxf.width) # repeated reference column width
|
|
tagwriter.write_tag2(41, cols.defined_height)
|
|
tagwriter.write_tag2(42, cols.total_width)
|
|
tagwriter.write_tag2(43, cols.total_height)
|
|
tagwriter.write_tag2(71, int(cols.column_type))
|
|
|
|
if cols.has_dynamic_auto_height:
|
|
count = 0
|
|
else:
|
|
count = cols.count
|
|
tagwriter.write_tag2(72, count)
|
|
|
|
tagwriter.write_tag2(44, cols.width)
|
|
tagwriter.write_tag2(45, cols.gutter_width)
|
|
tagwriter.write_tag2(73, int(cols.auto_height))
|
|
tagwriter.write_tag2(74, int(cols.reversed_column_flow))
|
|
for height in cols.heights:
|
|
tagwriter.write_tag2(46, height)
|
|
|
|
def export_linked_entities(self, tagwriter: AbstractTagWriter):
|
|
for mtext in self._columns.linked_columns: # type: ignore
|
|
if mtext.dxf.handle is None:
|
|
raise const.DXFStructureError("Linked MTEXT column has no handle.")
|
|
# Export linked columns as separated DXF entities:
|
|
mtext.export_dxf(tagwriter)
|
|
|
|
def sync_common_attribs_of_linked_columns(self):
|
|
common_attribs = self.dxfattribs(
|
|
drop={"handle", "insert", "rect_width", "rect_height"}
|
|
)
|
|
for mtext in self._columns.linked_columns:
|
|
mtext.update_dxf_attribs(common_attribs)
|
|
|
|
def set_column_xdata(self):
|
|
if self.xdata is None:
|
|
self.xdata = XData()
|
|
cols = self._columns
|
|
acad = cols.acad_mtext_column_info_xdata()
|
|
acad.extend(cols.acad_mtext_columns_xdata())
|
|
if not cols.has_dynamic_manual_height:
|
|
acad.extend(cols.acad_mtext_defined_height_xdata())
|
|
|
|
xdata = self.xdata
|
|
# Replace existing column data and therefore also removes
|
|
# ACAD_MTEXT_TEXT_BORDERS information!
|
|
xdata.discard("ACAD")
|
|
xdata.add("ACAD", acad)
|
|
|
|
def set_linked_columns_xdata(self):
|
|
cols = self._columns
|
|
for column in cols.linked_columns:
|
|
column.discard_xdata("ACAD")
|
|
if not cols.has_dynamic_manual_height:
|
|
tags = cols.acad_mtext_defined_height_xdata()
|
|
for column in cols.linked_columns:
|
|
column.set_xdata("ACAD", tags)
|
|
|
|
def get_rotation(self) -> float:
|
|
"""Returns the text rotation in degrees."""
|
|
if self.dxf.hasattr("text_direction"):
|
|
vector = self.dxf.text_direction
|
|
radians = math.atan2(vector[1], vector[0]) # ignores z-axis
|
|
rotation = math.degrees(radians)
|
|
else:
|
|
rotation = self.dxf.get("rotation", 0)
|
|
return rotation
|
|
|
|
def set_rotation(self, angle: float) -> MText:
|
|
"""Sets the attribute :attr:`rotation` to `angle` (in degrees) and deletes
|
|
:attr:`dxf.text_direction` if present.
|
|
"""
|
|
# text_direction has higher priority than rotation, therefore delete it
|
|
self.dxf.discard("text_direction")
|
|
self.dxf.rotation = angle
|
|
return self # fluent interface
|
|
|
|
def set_location(
|
|
self,
|
|
insert: UVec,
|
|
rotation: Optional[float] = None,
|
|
attachment_point: Optional[int] = None,
|
|
) -> MText:
|
|
"""Sets the attributes :attr:`dxf.insert`, :attr:`dxf.rotation` and
|
|
:attr:`dxf.attachment_point`, ``None`` for :attr:`dxf.rotation` or
|
|
:attr:`dxf.attachment_point` preserves the existing value.
|
|
|
|
"""
|
|
self.dxf.insert = Vec3(insert)
|
|
if rotation is not None:
|
|
self.set_rotation(rotation)
|
|
if attachment_point is not None:
|
|
self.dxf.attachment_point = attachment_point
|
|
return self # fluent interface
|
|
|
|
def set_bg_color(
|
|
self,
|
|
color: Union[int, str, RGB, None],
|
|
scale: float = 1.5,
|
|
text_frame=False,
|
|
):
|
|
"""Sets the background color as :ref:`ACI` value, as name string or as
|
|
(r, g, b) tuple.
|
|
|
|
Use the special color name ``canvas``, to set the background color to the canvas
|
|
background color. Remove the background filling by setting argument `color` to
|
|
``None``.
|
|
|
|
Args:
|
|
color: color as :ref:`ACI`, string, (r, g, b) tuple or ``None``
|
|
scale: determines how much border there is around the text, the
|
|
value is based on the text height, and should be in the range
|
|
of [1, 5], where 1 fits exact the MText entity.
|
|
text_frame: draw a text frame in text color if ``True``
|
|
|
|
"""
|
|
if 1 <= scale <= 5:
|
|
self.dxf.box_fill_scale = scale
|
|
else:
|
|
raise ValueError("argument scale has to be in range from 1 to 5.")
|
|
|
|
text_frame = const.MTEXT_TEXT_FRAME if text_frame else 0
|
|
if color is None:
|
|
self.dxf.discard("bg_fill")
|
|
self.dxf.discard("box_fill_scale")
|
|
self.dxf.discard("bg_fill_color")
|
|
self.dxf.discard("bg_fill_true_color")
|
|
self.dxf.discard("bg_fill_color_name")
|
|
if text_frame:
|
|
# special case, text frame only with scaling factor = 1.5
|
|
self.dxf.bg_fill = 16
|
|
elif color == "canvas": # special case for use background color
|
|
self.dxf.bg_fill = const.MTEXT_BG_CANVAS_COLOR | text_frame
|
|
self.dxf.bg_fill_color = 0 # required but ignored
|
|
else:
|
|
self.dxf.bg_fill = const.MTEXT_BG_COLOR | text_frame
|
|
if isinstance(color, int):
|
|
self.dxf.bg_fill_color = color
|
|
elif isinstance(color, str):
|
|
self.dxf.bg_fill_color = 0 # required but ignored
|
|
self.dxf.bg_fill_color_name = color
|
|
elif isinstance(color, tuple):
|
|
self.dxf.bg_fill_color = 0 # required but ignored
|
|
self.dxf.bg_fill_true_color = rgb2int(color)
|
|
return self # fluent interface
|
|
|
|
def __iadd__(self, text: str) -> MText:
|
|
"""Append `text` to existing content (:attr:`text` attribute)."""
|
|
self.text += text
|
|
return self
|
|
|
|
append = __iadd__
|
|
|
|
def get_text_direction(self) -> Vec3:
|
|
"""Returns the horizontal text direction as :class:`~ezdxf.math.Vec3`
|
|
object, even if only the text rotation is defined.
|
|
|
|
"""
|
|
dxf = self.dxf
|
|
# "text_direction" has higher priority than "rotation"
|
|
if dxf.hasattr("text_direction"):
|
|
return dxf.text_direction
|
|
if dxf.hasattr("rotation"):
|
|
# MTEXT is not an OCS entity, but I don't know how else to convert
|
|
# a rotation angle for an entity just defined by an extrusion vector.
|
|
# It's correct for the most common case: extrusion=(0, 0, 1)
|
|
return OCS(dxf.extrusion).to_wcs(Vec3.from_deg_angle(dxf.rotation))
|
|
return X_AXIS
|
|
|
|
def convert_rotation_to_text_direction(self):
|
|
"""Convert text rotation into text direction and discard text rotation."""
|
|
dxf = self.dxf
|
|
if dxf.hasattr("rotation"):
|
|
if not dxf.hasattr("text_direction"):
|
|
dxf.text_direction = self.get_text_direction()
|
|
dxf.discard("rotation")
|
|
|
|
def ucs(self) -> UCS:
|
|
"""Returns the :class:`~ezdxf.math.UCS` of the :class:`MText` entity,
|
|
defined by the insert location (origin), the text direction or rotation
|
|
(x-axis) and the extrusion vector (z-axis).
|
|
|
|
"""
|
|
dxf = self.dxf
|
|
return UCS(
|
|
origin=dxf.insert,
|
|
ux=self.get_text_direction(),
|
|
uz=dxf.extrusion,
|
|
)
|
|
|
|
def transform(self, m: Matrix44) -> MText:
|
|
"""Transform the MTEXT entity by transformation matrix `m` inplace."""
|
|
dxf = self.dxf
|
|
old_extrusion = Vec3(dxf.extrusion)
|
|
new_extrusion, _ = transform_extrusion(old_extrusion, m)
|
|
self.convert_rotation_to_text_direction()
|
|
|
|
old_text_direction = Vec3(dxf.text_direction)
|
|
new_text_direction = m.transform_direction(old_text_direction)
|
|
|
|
old_vertical_direction = old_extrusion.cross(old_text_direction)
|
|
old_char_height = float(dxf.char_height)
|
|
old_char_height_vec = old_vertical_direction.normalize(old_char_height)
|
|
new_char_height_vec = m.transform_direction(old_char_height_vec)
|
|
oblique = new_text_direction.angle_between(new_char_height_vec)
|
|
new_char_height = new_char_height_vec.magnitude * math.sin(oblique)
|
|
dxf.char_height = new_char_height
|
|
if (
|
|
not math.isclose(old_char_height, new_char_height)
|
|
and abs(old_char_height) > 1e-12
|
|
):
|
|
factor = new_char_height / old_char_height
|
|
# Column content is transformed by the sub-entities itself!
|
|
self.text = scale_mtext_inline_commands(self.text, factor)
|
|
|
|
if dxf.hasattr("width"):
|
|
width_vec = old_text_direction.normalize(dxf.width)
|
|
dxf.width = m.transform_direction(width_vec).magnitude
|
|
|
|
dxf.insert = m.transform(dxf.insert)
|
|
dxf.text_direction = new_text_direction
|
|
dxf.extrusion = new_extrusion
|
|
if self.has_columns:
|
|
hscale = m.transform_direction(old_text_direction.normalize()).magnitude
|
|
vscale = m.transform_direction(old_vertical_direction.normalize()).magnitude
|
|
self._columns.transform(m, hscale, vscale) # type: ignore
|
|
self.post_transform(m)
|
|
return self
|
|
|
|
def plain_text(self, split=False, fast=True) -> Union[list[str], str]:
|
|
"""Returns the text content without inline formatting codes.
|
|
|
|
The "fast" mode is accurate if the DXF content was created by
|
|
reliable (and newer) CAD applications like AutoCAD or BricsCAD.
|
|
The "accurate" mode is for some rare cases where the content was
|
|
created by older CAD applications or unreliable DXF libraries and CAD
|
|
applications.
|
|
|
|
Args:
|
|
split: split content text at line breaks if ``True`` and
|
|
returns a list of strings without line endings
|
|
fast: uses the "fast" mode to extract the plain MTEXT content if
|
|
``True`` or the "accurate" mode if set to ``False``
|
|
|
|
"""
|
|
if fast:
|
|
return fast_plain_mtext(self.text, split=split)
|
|
else:
|
|
return plain_mtext(self.text, split=split)
|
|
|
|
def all_columns_plain_text(self, split=False) -> Union[list[str], str]:
|
|
"""Returns the text content of all columns without inline formatting
|
|
codes.
|
|
|
|
Args:
|
|
split: split content text at line breaks if ``True`` and
|
|
returns a list of strings without line endings
|
|
|
|
"""
|
|
|
|
def merged_content():
|
|
content = [fast_plain_mtext(self.text, split=False)]
|
|
if self.has_columns:
|
|
for c in self._columns.linked_columns:
|
|
content.append(c.plain_text(split=False))
|
|
return "".join(content)
|
|
|
|
def split_content():
|
|
content = fast_plain_mtext(self.text, split=True)
|
|
if self.has_columns:
|
|
if content and content[-1] == "":
|
|
content.pop()
|
|
for c in self._columns.linked_columns:
|
|
content.extend(c.plain_text(split=True))
|
|
if content and content[-1] == "":
|
|
content.pop()
|
|
return content
|
|
|
|
if split:
|
|
return split_content()
|
|
else:
|
|
return merged_content()
|
|
|
|
def all_columns_raw_content(self) -> str:
|
|
"""Returns the text content of all columns as a single string
|
|
including the inline formatting codes.
|
|
|
|
"""
|
|
content = [self.text]
|
|
if self.has_columns:
|
|
for column in self._columns.linked_columns: # type: ignore
|
|
content.append(column.text)
|
|
return "".join(content)
|
|
|
|
def audit(self, auditor: Auditor):
|
|
"""Validity check."""
|
|
if not self.is_alive:
|
|
return
|
|
if self.dxf.owner is not None:
|
|
# Kills linked columns, because owner (None) does not exist!
|
|
super().audit(auditor)
|
|
else: # linked columns: owner is None
|
|
# TODO: special audit for linked columns
|
|
pass
|
|
auditor.check_text_style(self)
|
|
# TODO: audit column structure
|
|
|
|
def destroy(self) -> None:
|
|
if not self.is_alive:
|
|
return
|
|
|
|
if self.has_columns:
|
|
for column in self._columns.linked_columns: # type: ignore
|
|
column.destroy()
|
|
|
|
del self._columns
|
|
super().destroy()
|
|
|
|
# Linked MTEXT columns are not the same structure as
|
|
# POLYLINE & INSERT with sub-entities and SEQEND :(
|
|
def add_sub_entities_to_entitydb(self, db: EntityDB) -> None:
|
|
"""Add linked columns (MTEXT) entities to entity database `db`,
|
|
called from EntityDB. (internal API)
|
|
|
|
"""
|
|
if self.is_alive and self._columns:
|
|
doc = self.doc
|
|
for column in self._columns.linked_columns:
|
|
if column.is_alive and column.is_virtual:
|
|
column.doc = doc
|
|
db.add(column)
|
|
|
|
def process_sub_entities(self, func: Callable[[DXFEntity], None]):
|
|
"""Call `func` for linked columns. (internal API)"""
|
|
if self.is_alive and self._columns:
|
|
for entity in self._columns.linked_columns:
|
|
if entity.is_alive:
|
|
func(entity)
|
|
|
|
def setup_columns(self, columns: MTextColumns, linked: bool = False) -> None:
|
|
assert columns.column_type != ColumnType.NONE
|
|
assert columns.count > 0, "one or more columns required"
|
|
assert columns.width > 0, "column width has to be > 0"
|
|
assert columns.gutter_width >= 0, "gutter width has to be >= 0"
|
|
|
|
if self.has_columns:
|
|
raise const.DXFStructureError("Column setup already exist.")
|
|
self._columns = columns
|
|
self.dxf.width = columns.width
|
|
self.dxf.defined_height = columns.defined_height
|
|
if columns.total_height < 1e-6:
|
|
columns.total_height = columns.defined_height
|
|
if columns.total_width < 1e-6:
|
|
columns.update_total_width()
|
|
if linked:
|
|
self._create_linked_columns()
|
|
|
|
def _create_linked_columns(self) -> None:
|
|
"""Create linked MTEXT columns for DXF versions before R2018."""
|
|
# creates virtual MTEXT entities
|
|
dxf = self.dxf
|
|
attribs = self.dxfattribs(drop={"handle", "owner"})
|
|
doc = self.doc
|
|
cols = self._columns
|
|
assert cols is not None
|
|
|
|
insert = dxf.get("insert", Vec3())
|
|
default_direction = Vec3.from_deg_angle(dxf.get("rotation", 0))
|
|
text_direction = Vec3(dxf.get("text_direction", default_direction))
|
|
offset = text_direction.normalize(cols.width + cols.gutter_width)
|
|
linked_columns = cols.linked_columns
|
|
for _ in range(cols.count - 1):
|
|
insert += offset
|
|
column = MText.new(dxfattribs=attribs, doc=doc)
|
|
column.dxf.insert = insert
|
|
linked_columns.append(column)
|
|
|
|
def remove_dependencies(self, other: Optional[Drawing] = None) -> None:
|
|
if not self.is_alive:
|
|
return
|
|
|
|
super().remove_dependencies()
|
|
style_exist = bool(other) and self.dxf.style in other.styles # type: ignore
|
|
if not style_exist:
|
|
self.dxf.style = "Standard"
|
|
|
|
if self.has_columns:
|
|
for column in self._columns.linked_columns: # type: ignore
|
|
column.remove_dependencies(other)
|
|
|
|
def ocs(self) -> OCS:
|
|
# WCS entity which supports the "extrusion" attribute in a
|
|
# different way!
|
|
return OCS()
|
|
|
|
def register_resources(self, registry: xref.Registry) -> None:
|
|
"""Register required resources to the resource registry."""
|
|
super().register_resources(registry)
|
|
if self.dxf.hasattr("style"):
|
|
registry.add_text_style(self.dxf.style)
|
|
if self._columns:
|
|
for mtext in self._columns.linked_columns:
|
|
mtext.register_resources(registry)
|
|
|
|
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
|
"""Translate resources from self to the copied entity."""
|
|
assert isinstance(clone, MText)
|
|
super().map_resources(clone, mapping)
|
|
|
|
if clone.dxf.hasattr("style"):
|
|
clone.dxf.style = mapping.get_text_style(clone.dxf.style)
|
|
if self._columns and clone._columns:
|
|
for col_self, col_clone in zip(
|
|
self._columns.linked_columns, clone._columns.linked_columns
|
|
):
|
|
col_self.map_resources(col_clone, mapping)
|
|
|
|
|
|
def export_mtext_content(text, tagwriter: AbstractTagWriter) -> None:
|
|
txt = escape_dxf_line_endings(text)
|
|
str_chunks = split_mtext_string(txt, size=250)
|
|
if len(str_chunks) == 0:
|
|
str_chunks.append("")
|
|
while len(str_chunks) > 1:
|
|
tagwriter.write_tag2(3, str_chunks.pop(0))
|
|
tagwriter.write_tag2(1, str_chunks[0])
|