This commit is contained in:
Christian Anetzberger
2026-01-22 20:23:51 +01:00
commit a197de9456
4327 changed files with 1235205 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
# Copyright (c) 2011-2021 Manfred Moitzi
# License: MIT License
from . import factory
# basic classes
from .xdict import ExtensionDict
from .xdata import XData
from .appdata import AppData, Reactors
from .dxfentity import DXFEntity, DXFTagStorage
from .dxfgfx import DXFGraphic, SeqEnd, is_graphic_entity, get_font_name
from .dxfobj import DXFObject, is_dxf_object
from .dxfns import DXFNamespace, SubclassProcessor
# register management structures
from .dxfclass import DXFClass
from .table import TableHead
# register table entries
from .ltype import Linetype
from .layer import Layer, LayerOverrides
from .textstyle import Textstyle
from .dimstyle import DimStyle
from .view import View
from .vport import VPort
from .ucs import UCSTableEntry
from .appid import AppID
from .blockrecord import BlockRecord
# register DXF objects R2000
from .acad_proxy_entity import ACADProxyEntity
from .dxfobj import XRecord, Placeholder, VBAProject, SortEntsTable
from .dictionary import Dictionary, DictionaryVar, DictionaryWithDefault
from .layout import DXFLayout
from .idbuffer import IDBuffer
from .sun import Sun
from .material import Material, MaterialCollection
from .oleframe import OLE2Frame
from .spatial_filter import SpatialFilter
# register DXF objects R2007
from .visualstyle import VisualStyle
# register entities R12
from .line import Line
from .point import Point
from .circle import Circle
from .arc import Arc
from .shape import Shape
from .solid import Solid, Face3d, Trace
from .text import Text
from .subentity import LinkedEntities, entity_linker
from .insert import Insert
from .block import Block, EndBlk
from .polyline import Polyline, Polyface, Polymesh, MeshVertexCache
from .attrib import Attrib, AttDef, copy_attrib_as_text
from .dimension import *
from .dimstyleoverride import DimStyleOverride
from .viewport import Viewport
# register graphical entities R2000
from .lwpolyline import LWPolyline
from .ellipse import Ellipse
from .xline import XLine, Ray
from .mtext import MText
from .mtext_columns import *
from .spline import Spline
from .mesh import Mesh, MeshData
from .boundary_paths import *
from .gradient import *
from .pattern import *
from .hatch import *
from .mpolygon import MPolygon
from .image import Image, ImageDef, Wipeout
from .underlay import (
Underlay,
UnderlayDefinition,
PdfUnderlay,
DgnUnderlay,
DwfUnderlay,
)
from .leader import Leader
from .tolerance import Tolerance
from .helix import Helix
from .acis import (
Body,
Solid3d,
Region,
Surface,
ExtrudedSurface,
LoftedSurface,
RevolvedSurface,
SweptSurface,
)
from .mline import MLine, MLineVertex, MLineStyle, MLineStyleCollection
from .mleader import MLeader, MLeaderStyle, MLeaderStyleCollection, MultiLeader
# register graphical entities R2004
# register graphical entities R2007
from .light import Light
from .acad_table import AcadTableBlockContent, acad_table_to_block
# register graphical entities R2010
from .geodata import GeoData
# register graphical entities R2013
# register graphical entities R2018

View File

@@ -0,0 +1,147 @@
# Copyright (c) 2021-2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Iterator
from ezdxf.lldxf import const
from ezdxf.lldxf.tags import Tags
from ezdxf.query import EntityQuery
from .dxfentity import SubclassProcessor
from .dxfgfx import DXFGraphic
from . import factory
from .copy import default_copy, CopyNotSupported
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.entities import DXFNamespace
from ezdxf.layouts import BaseLayout
# Group Codes of AcDbProxyEntity
# https://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-89A690F9-E859-4D57-89EA-750F3FB76C6B
# 100 AcDbProxyEntity
# 90 Proxy entity class ID (always 498)
# 91 Application entity's class ID. Class IDs are based on the order of
# the class in the CLASSES section. The first class is given the ID of
# 500, the next is 501, and so on
#
# 92 Size of graphics data in bytes < R2010; R2010+ = 160
# 310 Binary graphics data (multiple entries can appear) (optional)
#
# 96 Size of unknown data in bytes < R2010; R2010+ = 162
# 311 Binary entity data (multiple entries can appear) (optional)
#
# 93 Size of entity data in bits <R2010; R2010+ = 161
# 310 Binary entity data (multiple entries can appear) (optional)
#
# 330 or 340 or 350 or 360 - An object ID (multiple entries can appear) (optional)
# 94 0 (indicates end of object ID section)
# 95 Object drawing format when it becomes a proxy (a 32-bit unsigned integer):
# Low word is AcDbDwgVersion
# High word is MaintenanceReleaseVersion
# 70 Original custom object data format:
# 0 = DWG format
# 1 = DXF format
@factory.register_entity
class ACADProxyEntity(DXFGraphic):
"""READ ONLY ACAD_PROXY_ENTITY CLASS! DO NOT MODIFY!"""
DXFTYPE = "ACAD_PROXY_ENTITY"
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
def __init__(self) -> None:
super().__init__()
self.acdb_proxy_entity: Optional[Tags] = None
def copy(self, copy_strategy=default_copy):
raise CopyNotSupported(f"Copying of {self.dxftype()} not supported.")
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
self.acdb_proxy_entity = processor.subclass_by_index(2)
self.load_proxy_graphic()
return dxf
def load_proxy_graphic(self) -> None:
if self.acdb_proxy_entity is None:
return
for length_code in (92, 160):
proxy_graphic = load_proxy_data(self.acdb_proxy_entity, length_code, 310)
if proxy_graphic:
self.proxy_graphic = proxy_graphic
return
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
# Proxy graphic is stored in AcDbProxyEntity and not as usual in
# AcDbEntity!
preserve_proxy_graphic = self.proxy_graphic
self.proxy_graphic = None
super().export_dxf(tagwriter)
self.proxy_graphic = preserve_proxy_graphic
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags. (internal API)"""
# Base class and AcDbEntity export is done by parent class
super().export_entity(tagwriter)
if self.acdb_proxy_entity is not None:
tagwriter.write_tags(self.acdb_proxy_entity)
# XDATA export is done by the parent class
def __virtual_entities__(self) -> Iterator[DXFGraphic]:
"""Implements the SupportsVirtualEntities protocol."""
from ezdxf.proxygraphic import ProxyGraphic
if self.proxy_graphic:
for e in ProxyGraphic(self.proxy_graphic, doc=self.doc).virtual_entities():
e.set_source_of_copy(self)
yield e
def virtual_entities(self) -> Iterator[DXFGraphic]:
"""Yields proxy graphic as "virtual" entities."""
return self.__virtual_entities__()
def explode(self, target_layout: Optional[BaseLayout] = None) -> EntityQuery:
"""Explodes the proxy graphic for the ACAD_PROXY_ENTITY into the target layout,
if target layout is ``None``, the layout of the ACAD_PROXY_ENTITY will be used.
This method destroys the source ACAD_PROXY_ENTITY entity.
Args:
target_layout: target layout for exploded entities, ``None`` for
same layout as the source ACAD_PROXY_ENTITY.
Returns:
:class:`~ezdxf.query.EntityQuery` container referencing all exploded
DXF entities.
"""
if target_layout is None:
target_layout = self.get_layout()
if target_layout is None:
raise const.DXFStructureError(
"ACAD_PROXY_ENTITY without layout assignment, specify target layout"
)
entities: list[DXFGraphic] = list(self.__virtual_entities__())
for e in entities:
target_layout.add_entity(e)
self.destroy()
return EntityQuery(entities)
def load_proxy_data(
tags: Tags, length_code: int, data_code: int = 310
) -> Optional[bytes]:
try:
index = tags.tag_index(length_code)
except const.DXFValueError:
return None
binary_data = []
for code, value in tags[index + 1 :]:
if code == data_code:
binary_data.append(value)
else:
break # at first tag with group code != data_code
return b"".join(binary_data)

View File

@@ -0,0 +1,477 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional, Iterator
from typing_extensions import Self
import copy
from ezdxf.math import Vec3, Matrix44
from ezdxf.lldxf.tags import Tags, group_tags
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
group_code_mapping,
)
from ezdxf.lldxf import const
from ezdxf.entities import factory
from .dxfentity import base_class, SubclassProcessor, DXFEntity, DXFTagStorage
from .dxfgfx import DXFGraphic, acdb_entity
from .dxfobj import DXFObject
from .objectcollection import ObjectCollection
from .copy import default_copy
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.document import Drawing
__all__ = [
"AcadTable",
"AcadTableBlockContent",
"acad_table_to_block",
"read_acad_table_content",
]
acdb_block_reference = DefSubclass(
"AcDbBlockReference",
{
# Block name: an anonymous block begins with a *T value
"geometry": DXFAttr(2),
# Insertion point:
"insert": DXFAttr(10, xtype=XType.point3d, default=Vec3(0, 0, 0)),
},
)
acdb_block_reference_group_codes = group_code_mapping(acdb_block_reference)
acdb_table = DefSubclass(
"AcDbTable",
{
# Table data version number: 0 = 2010
"version": DXFAttr(280),
# Hard of the TABLESTYLE object:
"table_style_id": DXFAttr(342),
# Handle of the associated anonymous BLOCK containing the graphical
# representation:
"block_record_handle": DXFAttr(343),
# Horizontal direction vector:
"horizontal_direction": DXFAttr(11),
# Flag for table value (unsigned integer):
"table_value": DXFAttr(90),
# Number of rows:
"n_rows": DXFAttr(91),
# Number of columns:
"n_cols": DXFAttr(92),
# Flag for an override:
"override_flag": DXFAttr(93),
# Flag for an override of border color:
"border_color_override_flag": DXFAttr(94),
# Flag for an override of border lineweight:
"border_lineweight_override_flag": DXFAttr(95),
# Flag for an override of border visibility:
"border_visibility_override_flag": DXFAttr(96),
# 141: Row height; this value is repeated, 1 value per row
# 142: Column height; this value is repeated, 1 value per column
# for every cell:
# 171: Cell type; this value is repeated, 1 value per cell:
# 1 = text type
# 2 = block type
# 172: Cell flag value; this value is repeated, 1 value per cell
# 173: Cell merged value; this value is repeated, 1 value per cell
# 174: Boolean flag indicating if the autofit option is set for the
# cell; this value is repeated, 1 value per cell
# 175: Cell border width (applicable only for merged cells); this
# value is repeated, 1 value per cell
# 176: Cell border height (applicable for merged cells); this value
# is repeated, 1 value per cell
# 91: Cell override flag; this value is repeated, 1 value per cell
# (from AutoCAD 2007)
# 178: Flag value for a virtual edge
# 145: Rotation value (real; applicable for a block-type cell and
# a text-type cell)
# 344: Hard pointer ID of the FIELD object. This applies only to a
# text-type cell. If the text in the cell contains one or more
# fields, only the ID of the FIELD object is saved.
# The text string (group codes 1 and 3) is ignored
# 1: Text string in a cell. If the string is shorter than 250
# characters, all characters appear in code 1.
# If the string is longer than 250 characters, it is divided
# into chunks of 250 characters.
# The chunks are contained in one or more code 2 codes.
# If code 2 codes are used, the last group is a code 1 and is
# shorter than 250 characters.
# This value applies only to text-type cells and is repeated,
# 1 value per cell
# 2: Text string in a cell, in 250-character chunks; optional.
# This value applies only to text-type cells and is repeated,
# 1 value per cell
# 340: Hard-pointer ID of the block table record.
# This value applies only to block-type cells and is repeated,
# 1 value per cell
# 144: Block scale (real). This value applies only to block-type
# cells and is repeated, 1 value per cell
# 176: Number of attribute definitions in the block table record
# (applicable only to a block-type cell)
# for every ATTDEF:
# 331: Soft pointer ID of the attribute definition in the
# block table record, referenced by group code 179
# (applicable only for a block-type cell). This value is
# repeated once per attribute definition
# 300: Text string value for an attribute definition, repeated
# once per attribute definition and applicable only for
# a block-type cell
# 7: Text style name (string); override applied at the cell level
# 140: Text height value; override applied at the cell level
# 170: Cell alignment value; override applied at the cell level
# 64: Value for the color of cell content; override applied at the
# cell level
# 63: Value for the background (fill) color of cell content;
# override applied at the cell level
# 69: True color value for the top border of the cell;
# override applied at the cell level
# 65: True color value for the right border of the cell;
# override applied at the cell level
# 66: True color value for the bottom border of the cell;
# override applied at the cell level
# 68: True color value for the left border of the cell;
# override applied at the cell level
# 279: Lineweight for the top border of the cell;
# override applied at the cell level
# 275: Lineweight for the right border of the cell;
# override applied at the cell level
# 276: Lineweight for the bottom border of the cell;
# override applied at the cell level
# 278: Lineweight for the left border of the cell;
# override applied at the cell level
# 283: Boolean flag for whether the fill color is on;
# override applied at the cell level
# 289: Boolean flag for the visibility of the top border of the cell;
# override applied at the cell level
# 285: Boolean flag for the visibility of the right border of the cell;
# override applied at the cell level
# 286: Boolean flag for the visibility of the bottom border of the cell;
# override applied at the cell level
# 288: Boolean flag for the visibility of the left border of the cell;
# override applied at the cell level
# 70: Flow direction;
# override applied at the table entity level
# 40: Horizontal cell margin;
# override applied at the table entity level
# 41: Vertical cell margin;
# override applied at the table entity level
# 280: Flag for whether the title is suppressed;
# override applied at the table entity level
# 281: Flag for whether the header row is suppressed;
# override applied at the table entity level
# 7: Text style name (string);
# override applied at the table entity level.
# There may be one entry for each cell type
# 140: Text height (real);
# override applied at the table entity level.
# There may be one entry for each cell type
# 170: Cell alignment (integer);
# override applied at the table entity level.
# There may be one entry for each cell type
# 63: Color value for cell background or for the vertical, left
# border of the table; override applied at the table entity
# level. There may be one entry for each cell type
# 64: Color value for cell content or for the horizontal, top
# border of the table; override applied at the table entity
# level. There may be one entry for each cell type
# 65: Color value for the horizontal, inside border lines;
# override applied at the table entity level
# 66: Color value for the horizontal, bottom border lines;
# override applied at the table entity level
# 68: Color value for the vertical, inside border lines;
# override applied at the table entity level
# 69: Color value for the vertical, right border lines;
# override applied at the table entity level
# 283: Flag for whether background color is enabled (default = 0);
# override applied at the table entity level.
# There may be one entry for each cell type: 0/1 = Disabled/Enabled
# 274-279: Lineweight for each border type of the cell (default = kLnWtByBlock);
# override applied at the table entity level.
# There may be one group for each cell type
# 284-289: Flag for visibility of each border type of the cell (default = 1);
# override applied at the table entity level.
# There may be one group for each cell type: 0/1 = Invisible/Visible
# 97: Standard/title/header row data type
# 98: Standard/title/header row unit type
# 4: Standard/title/header row format string
#
# AutoCAD 2007 and before:
# 177: Cell override flag value (before AutoCAD 2007)
# 92: Extended cell flags (from AutoCAD 2007), COLLISION: group code
# also used by n_cols
# 301: Text string in a cell. If the string is shorter than 250
# characters, all characters appear in code 302.
# If the string is longer than 250 characters, it is divided into
# chunks of 250 characters.
# The chunks are contained in one or more code 303 codes.
# If code 393 codes are used, the last group is a code 1 and is
# shorter than 250 characters.
# --- WRONG: The text is divided into chunks of group code 2 and the last
# chuck has group code 1.
# This value applies only to text-type cells and is repeated,
# 1 value per cell (from AutoCAD 2007)
# 302: Text string in a cell, in 250-character chunks; optional.
# This value applies only to text-type cells and is repeated,
# 302 value per cell (from AutoCAD 2007)
# --- WRONG: 302 contains all the text as a long string, tested with more
# than 66000 characters
# BricsCAD writes long text in cells with both methods: 302 & (2, 2, 2, ..., 1)
#
# REMARK from Autodesk:
# Group code 178 is a flag value for a virtual edge. A virtual edge is
# used when a grid line is shared by two cells.
# For example, if a table contains one row and two columns and it
# contains cell A and cell B, the central grid line
# contains the right edge of cell A and the left edge of cell B.
# One edge is real, and the other edge is virtual.
# The virtual edge points to the real edge; both edges have the same
# set of properties, including color, lineweight, and visibility.
},
)
acdb_table_group_codes = group_code_mapping(acdb_table)
# todo: implement ACAD_TABLE
class AcadTable(DXFGraphic):
"""DXF ACAD_TABLE entity"""
DXFTYPE = "ACAD_TABLE"
DXFATTRIBS = DXFAttributes(
base_class, acdb_entity, acdb_block_reference, acdb_table
)
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2007
def __init__(self):
super().__init__()
self.data = None
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
"""Copy data."""
assert isinstance(entity, AcadTable)
entity.data = copy.deepcopy(self.data)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_block_reference_group_codes, subclass=2
)
tags = processor.fast_load_dxfattribs(
dxf, acdb_table_group_codes, subclass=3, log=False
)
self.load_table(tags)
return dxf
def load_table(self, tags: Tags):
pass
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_block_reference.name)
self.dxf.export_dxf_attribs(tagwriter, ["geometry", "insert"])
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_table.name)
self.export_table(tagwriter)
def export_table(self, tagwriter: AbstractTagWriter):
pass
def __referenced_blocks__(self) -> Iterable[str]:
"""Support for "ReferencedBlocks" protocol."""
if self.doc:
block_record_handle = self.dxf.get("block_record_handle", None)
if block_record_handle:
return (block_record_handle,)
return tuple()
acdb_table_style = DefSubclass(
"AcDbTableStyle",
{
# Table style version: 0 = 2010
"version": DXFAttr(280),
# Table style description (string; 255 characters maximum):
"name": DXFAttr(3),
# FlowDirection (integer):
# 0 = Down
# 1 = Up
"flow_direction": DXFAttr(7),
# Flags (bit-coded)
"flags": DXFAttr(7),
# Horizontal cell margin (real; default = 0.06)
"horizontal_cell_margin": DXFAttr(40),
# Vertical cell margin (real; default = 0.06)
"vertical_cell_margin": DXFAttr(41),
# Flag for whether the title is suppressed:
# 0/1 = not suppressed/suppressed
"suppress_title": DXFAttr(280),
# Flag for whether the column heading is suppressed:
# 0/1 = not suppressed/suppressed
"suppress_column_header": DXFAttr(281),
# The following group codes are repeated for every cell in the table
# 7: Text style name (string; default = STANDARD)
# 140: Text height (real)
# 170: Cell alignment (integer)
# 62: Text color (integer; default = BYBLOCK)
# 63: Cell fill color (integer; default = 7)
# 283: Flag for whether background color is enabled (default = 0):
# 0/1 = disabled/enabled
# 90: Cell data type
# 91: Cell unit type
# 274-279: Lineweight associated with each border type of the cell
# (default = kLnWtByBlock)
# 284-289: Flag for visibility associated with each border type of the cell
# (default = 1): 0/1 = Invisible/Visible
# 64-69: Color value associated with each border type of the cell
# (default = BYBLOCK)
},
)
# todo: implement TABLESTYLE
class TableStyle(DXFObject):
"""DXF TABLESTYLE entity
Every ACAD_TABLE has its own table style.
Requires DXF version AC1021/R2007
"""
DXFTYPE = "TABLESTYLE"
DXFATTRIBS = DXFAttributes(base_class, acdb_table_style)
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2007
class TableStyleManager(ObjectCollection[TableStyle]):
def __init__(self, doc: Drawing):
super().__init__(doc, dict_name="ACAD_TABLESTYLE", object_type="TABLESTYLE")
@factory.register_entity
class AcadTableBlockContent(DXFTagStorage):
DXFTYPE = "ACAD_TABLE"
DXFATTRIBS = DXFAttributes(
base_class, acdb_entity, acdb_block_reference, acdb_table
)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_block_reference_group_codes, subclass=2
)
processor.fast_load_dxfattribs(
dxf, acdb_table_group_codes, subclass=3, log=False
)
return dxf
def proxy_graphic_content(self) -> Iterable[DXFGraphic]:
return super().__virtual_entities__()
def _block_content(self) -> Iterator[DXFGraphic]:
block_name: str = self.get_block_name()
return self.doc.blocks.get(block_name, []) # type: ignore
def get_block_name(self) -> str:
return self.dxf.get("geometry", "")
def get_insert_location(self) -> Vec3:
return self.dxf.get("insert", Vec3())
def __virtual_entities__(self) -> Iterator[DXFGraphic]:
"""Implements the SupportsVirtualEntities protocol."""
insert: Vec3 = Vec3(self.get_insert_location())
m: Optional[Matrix44] = None
if insert:
# TODO: OCS transformation (extrusion) is ignored yet
m = Matrix44.translate(insert.x, insert.y, insert.z)
for entity in self._block_content():
try:
clone = entity.copy()
except const.DXFTypeError:
continue
if m is not None:
try:
clone.transform(m)
except: # skip entity at any transformation issue
continue
yield clone
def acad_table_to_block(table: DXFEntity) -> None:
"""Converts the given ACAD_TABLE entity to a block references (INSERT entity).
The original ACAD_TABLE entity will be destroyed.
.. versionadded:: 1.1
"""
if not isinstance(table, AcadTableBlockContent):
return
doc = table.doc
owner = table.dxf.owner
block_name = table.get_block_name()
if doc is None or block_name == "" or owner is None:
return
try:
layout = doc.layouts.get_layout_by_key(owner)
except const.DXFKeyError:
return
# replace ACAD_TABLE entity by INSERT entity
layout.add_blockref(
block_name,
insert=table.get_insert_location(),
dxfattribs={"layer": table.dxf.get("layer", "0")},
)
layout.delete_entity(table) # type: ignore
def read_acad_table_content(table: DXFTagStorage) -> list[list[str]]:
"""Returns the content of an ACAD_TABLE entity as list of table rows.
If the count of table rows or table columns is missing the complete content is
stored in the first row.
"""
if table.dxftype() != "ACAD_TABLE":
raise const.DXFTypeError(f"Expected ACAD_TABLE entity, got {str(table)}")
acdb_table = table.xtags.get_subclass("AcDbTable")
nrows = acdb_table.get_first_value(91, 0)
ncols = acdb_table.get_first_value(92, 0)
split_code = 171 # DXF R2004
if acdb_table.has_tag(302):
split_code = 301 # DXF R2007 and later
values = _load_table_values(acdb_table, split_code)
if nrows * ncols == 0:
return [values]
content: list[list[str]] = []
for index in range(nrows):
start = index * ncols
content.append(values[start : start + ncols])
return content
def _load_table_values(tags: Tags, split_code: int) -> list[str]:
values: list[str] = []
for group in group_tags(tags, splitcode=split_code):
g_tags = Tags(group)
if g_tags.has_tag(302): # DXF R2007 and later
# contains all text as one long string, with more than 66000 chars tested
values.append(g_tags.get_first_value(302))
else:
# DXF R2004
# Text is divided into chunks (2, 2, 2, ..., 1) or (3, 3, 3, ..., 1)
# DXF reference says group code 2, BricsCAD writes group code 3
s = "".join(tag.value for tag in g_tags if 1 <= tag.code <= 3)
values.append(s)
return values

View File

@@ -0,0 +1,99 @@
# Copyright (c) 2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from ezdxf.entities import XRecord
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.types import DXFTag
__all__ = ["RoundtripXRecord"]
SECTION_MARKER_CODE = 102
NOT_FOUND = -1
class RoundtripXRecord:
"""Helper class for ACAD Roundtrip Data.
The data is stored in an XRECORD, in sections separated by tags
(102, "ACAD_SECTION_NAME").
Example for inverted clipping path of SPATIAL_FILTER objects:
...
100
AcDbXrecord
280
1
102
ACAD_INVERTEDCLIP_ROUNDTRIP
10
399.725563048036
20
233.417786599994
30
0.0
...
102
ACAD_INVERTEDCLIP_ROUNDTRIP_COMPARE
10
399.725563048036
20
233.417786599994
...
"""
def __init__(self, xrecord: XRecord | None = None) -> None:
if xrecord is None:
xrecord = XRecord()
self.xrecord = xrecord
def has_section(self, key: str) -> bool:
"""Returns True if an entry section for key is present."""
for code, value in self.xrecord.tags:
if code == SECTION_MARKER_CODE and value == key:
return True
return False
def set_section(self, key: str, tags: Tags) -> None:
"""Set content of section `key` to `tags`. Replaces the content of an existing section."""
xrecord_tags = self.xrecord.tags
start, end = find_section(xrecord_tags, key)
if start == NOT_FOUND:
xrecord_tags.append(DXFTag(SECTION_MARKER_CODE, key))
xrecord_tags.extend(tags)
else:
xrecord_tags[start + 1 : end] = tags
def get_section(self, key: str) -> Tags:
"""Returns the content of section `key`."""
xrecord_tags = self.xrecord.tags
start, end = find_section(xrecord_tags, key)
if start != NOT_FOUND:
return xrecord_tags[start + 1 : end]
return Tags()
def discard(self, key: str) -> None:
"""Removes section `key`, section `key` doesn't have to exist."""
xrecord_tags = self.xrecord.tags
start, end = find_section(xrecord_tags, key)
if start != NOT_FOUND:
del xrecord_tags[start:end]
def find_section(tags: Tags, key: str) -> tuple[int, int]:
"""Returns the start- and end index of section `key`.
Returns (-1, -1) if the section does not exist.
"""
start = NOT_FOUND
for index, tag in enumerate(tags):
if tag.code != 102:
continue
if tag.value == key:
start = index
elif start != NOT_FOUND:
return start, index
if start != NOT_FOUND:
return start, len(tags)
return NOT_FOUND, NOT_FOUND

View File

@@ -0,0 +1,837 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Union, Optional, Sequence, Any
from typing_extensions import Self, override
import logging
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
group_code_mapping,
)
from ezdxf.lldxf import const
from ezdxf.lldxf.tags import Tags, DXFTag
from ezdxf.math import Matrix44
from ezdxf.tools import crypt, guid
from ezdxf import msgtypes
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import DXFGraphic, acdb_entity
from .factory import register_entity
from .copy import default_copy
from .temporary_transform import TransformByBlockReference
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf import xref
__all__ = [
"Body",
"Solid3d",
"Region",
"Surface",
"ExtrudedSurface",
"LoftedSurface",
"RevolvedSurface",
"SweptSurface",
]
logger = logging.getLogger("ezdxf")
acdb_modeler_geometry = DefSubclass(
"AcDbModelerGeometry",
{
"version": DXFAttr(70, default=1),
"flags": DXFAttr(290, dxfversion=const.DXF2013),
"uid": DXFAttr(2, dxfversion=const.DXF2013),
},
)
acdb_modeler_geometry_group_codes = group_code_mapping(acdb_modeler_geometry)
# with R2013/AC1027 Modeler Geometry of ACIS data is stored in the ACDSDATA
# section as binary encoded information detection:
# group code 70, 1, 3 is missing
# group code 290, 2 present
#
# 0
# ACDSRECORD
# 90
# 1
# 2
# AcDbDs::ID
# 280
# 10
# 320
# 19B <<< handle of associated 3DSOLID entity in model space
# 2
# ASM_Data
# 280
# 15
# 94
# 7197 <<< size in bytes ???
# 310
# 414349532042696E61727946696C6...
@register_entity
class Body(DXFGraphic):
"""DXF BODY entity - container entity for embedded ACIS data."""
DXFTYPE = "BODY"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_modeler_geometry)
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
def __init__(self) -> None:
super().__init__()
# Store SAT data as immutable sequence of strings, so the data can be shared
# across multiple copies of an ACIS entity.
self._sat: Sequence[str] = tuple()
self._sab: bytes = b""
self._update = False
self._temporary_transformation = TransformByBlockReference()
@property
def acis_data(self) -> Union[bytes, Sequence[str]]:
"""Returns :term:`SAT` data for DXF R2000 up to R2010 and :term:`SAB`
data for DXF R2013 and later
"""
if self.has_binary_data:
return self.sab
return self.sat
@property
def sat(self) -> Sequence[str]:
"""Get/Set :term:`SAT` data as sequence of strings."""
return self._sat
@sat.setter
def sat(self, data: Sequence[str]) -> None:
"""Set :term:`SAT` data as sequence of strings."""
self._sat = tuple(data)
@property
def sab(self) -> bytes:
"""Get/Set :term:`SAB` data as bytes."""
if ( # load SAB data on demand
self.doc is not None and self.has_binary_data and len(self._sab) == 0
):
self._sab = self.doc.acdsdata.get_acis_data(self.dxf.handle)
return self._sab
@sab.setter
def sab(self, data: bytes) -> None:
"""Set :term:`SAB` data as bytes."""
self._update = True
self._sab = data
@property
def has_binary_data(self):
"""Returns ``True`` if the entity contains :term:`SAB` data and
``False`` if the entity contains :term:`SAT` data.
"""
if self.doc:
return self.doc.dxfversion >= const.DXF2013
else:
return False
@override
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, Body)
entity.sat = self.sat
entity.sab = self.sab # load SAB on demand
entity.dxf.uid = guid()
entity._temporary_transformation = self._temporary_transformation
@override
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
super().map_resources(clone, mapping)
clone.convert_acis_data()
def convert_acis_data(self) -> None:
if self.doc is None:
return
msg = ""
dxfversion = self.doc.dxfversion
if dxfversion < const.DXF2013:
if self._sab:
self._sab = b""
msg = "DXF version mismatch, can't convert ACIS data from SAB to SAT, SAB data removed."
else:
if self._sat:
self._sat = tuple()
msg = "DXF version mismatch, can't convert ACIS data from SAT to SAB, SAT data removed."
if msg:
logger.info(msg)
@override
def notify(self, message_type: int, data: Any = None) -> None:
if message_type == msgtypes.COMMIT_PENDING_CHANGES:
self._temporary_transformation.apply_transformation(self)
@override
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
msg = ""
if tagwriter.dxfversion < const.DXF2013:
valid = len(self.sat) > 0
if not valid:
msg = f"{str(self)} doesn't have SAT data, skipping DXF export"
else:
valid = len(self.sab) > 0
if not valid:
msg = f"{str(self)} doesn't have SAB data, skipping DXF export"
if not valid:
logger.info(msg)
if valid and self._temporary_transformation.get_matrix() is not None:
logger.warning(f"{str(self)} has unapplied temporary transformations.")
return valid
@override
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_modeler_geometry_group_codes, 2, log=False
)
if not self.has_binary_data:
self.load_sat_data(processor.subclasses[2])
return dxf
def load_sat_data(self, tags: Tags):
"""Loading interface. (internal API)"""
text_lines = tags2textlines(tag for tag in tags if tag.code in (1, 3))
self._sat = tuple(crypt.decode(text_lines))
@override
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags. (internal API)"""
super().export_entity(tagwriter)
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_modeler_geometry.name)
if tagwriter.dxfversion >= const.DXF2013:
# ACIS data is stored in the ACDSDATA section as SAB
if self.doc and self._update:
# write back changed SAB data into AcDsDataSection or create
# a new ACIS record:
self.doc.acdsdata.set_acis_data(self.dxf.handle, self.sab)
if self.dxf.hasattr("version"):
tagwriter.write_tag2(70, self.dxf.version)
self.dxf.export_dxf_attribs(tagwriter, ["flags", "uid"])
else:
# DXF R2000 - R2010 stores the ACIS data as SAT in the entity
self.dxf.export_dxf_attribs(tagwriter, "version")
self.export_sat_data(tagwriter)
def export_sat_data(self, tagwriter: AbstractTagWriter) -> None:
"""Export ACIS data as DXF tags. (internal API)"""
def cleanup(lines):
for line in lines:
yield line.rstrip().replace("\n", "")
tags = Tags(textlines2tags(crypt.encode(cleanup(self.sat))))
tagwriter.write_tags(tags)
def tostring(self) -> str:
"""Returns ACIS :term:`SAT` data as a single string if the entity has
SAT data.
"""
if self.has_binary_data:
return ""
else:
return "\n".join(self.sat)
@override
def destroy(self) -> None:
if self.has_binary_data:
self.doc.acdsdata.del_acis_data(self.dxf.handle) # type: ignore
super().destroy()
@override
def transform(self, m: Matrix44) -> Self:
self._temporary_transformation.add_matrix(m)
return self
def temporary_transformation(self) -> TransformByBlockReference:
return self._temporary_transformation
def tags2textlines(tags: Iterable) -> Iterable[str]:
"""Yields text lines from code 1 and 3 tags, code 1 starts a line following
code 3 tags are appended to the line.
"""
line = None
for code, value in tags:
if code == 1:
if line is not None:
yield line
line = value
elif code == 3:
line += value
if line is not None:
yield line
def textlines2tags(lines: Iterable[str]) -> Iterable[DXFTag]:
"""Yields text lines as DXFTags, splitting long lines (>255) int code 1
and code 3 tags.
"""
for line in lines:
text = line[:255]
tail = line[255:]
yield DXFTag(1, text)
while len(tail):
text = tail[:255]
tail = tail[255:]
yield DXFTag(3, text)
@register_entity
class Region(Body):
"""DXF REGION entity - container entity for embedded ACIS data."""
DXFTYPE = "REGION"
acdb_3dsolid = DefSubclass(
"AcDb3dSolid",
{
"history_handle": DXFAttr(350, default="0"),
},
)
acdb_3dsolid_group_codes = group_code_mapping(acdb_3dsolid)
@register_entity
class Solid3d(Body):
"""DXF 3DSOLID entity - container entity for embedded ACIS data."""
DXFTYPE = "3DSOLID"
DXFATTRIBS = DXFAttributes(
base_class, acdb_entity, acdb_modeler_geometry, acdb_3dsolid
)
@override
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(dxf, acdb_3dsolid_group_codes, 3)
return dxf
@override
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
# base class export is done by parent class
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
# AcDbModelerGeometry export is done by parent class
if tagwriter.dxfversion > const.DXF2004:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_3dsolid.name)
self.dxf.export_dxf_attribs(tagwriter, "history_handle")
def load_matrix(subclass: Tags, code: int) -> Matrix44:
values = [tag.value for tag in subclass.find_all(code)]
if len(values) != 16:
raise const.DXFStructureError("Invalid transformation matrix.")
return Matrix44(values)
def export_matrix(tagwriter: AbstractTagWriter, code: int, matrix: Matrix44) -> None:
for value in list(matrix):
tagwriter.write_tag2(code, value)
acdb_surface = DefSubclass(
"AcDbSurface",
{
"u_count": DXFAttr(71),
"v_count": DXFAttr(72),
},
)
acdb_surface_group_codes = group_code_mapping(acdb_surface)
@register_entity
class Surface(Body):
"""DXF SURFACE entity - container entity for embedded ACIS data."""
DXFTYPE = "SURFACE"
DXFATTRIBS = DXFAttributes(
base_class, acdb_entity, acdb_modeler_geometry, acdb_surface
)
@override
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(dxf, acdb_surface_group_codes, 3)
return dxf
@override
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
# base class export is done by parent class
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
# AcDbModelerGeometry export is done by parent class
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_surface.name)
self.dxf.export_dxf_attribs(tagwriter, ["u_count", "v_count"])
acdb_extruded_surface = DefSubclass(
"AcDbExtrudedSurface",
{
"class_id": DXFAttr(90),
"sweep_vector": DXFAttr(10, xtype=XType.point3d),
# 16x group code 40: Transform matrix of extruded entity (16 floats;
# row major format; default = identity matrix)
"draft_angle": DXFAttr(42, default=0.0), # in radians
"draft_start_distance": DXFAttr(43, default=0.0),
"draft_end_distance": DXFAttr(44, default=0.0),
"twist_angle": DXFAttr(45, default=0.0), # in radians?
"scale_factor": DXFAttr(48, default=0.0),
"align_angle": DXFAttr(49, default=0.0), # in radians
# 16x group code 46: Transform matrix of sweep entity (16 floats;
# row major format; default = identity matrix)
# 16x group code 47: Transform matrix of path entity (16 floats;
# row major format; default = identity matrix)
"solid": DXFAttr(290, default=0), # bool
# 0=No alignment; 1=Align sweep entity to path:
"sweep_alignment_flags": DXFAttr(70, default=0),
"unknown1": DXFAttr(71, default=0),
# 2=Translate sweep entity to path; 3=Translate path to sweep entity:
"align_start": DXFAttr(292, default=0), # bool
"bank": DXFAttr(293, default=0), # bool
"base_point_set": DXFAttr(294, default=0), # bool
"sweep_entity_transform_computed": DXFAttr(295, default=0), # bool
"path_entity_transform_computed": DXFAttr(296, default=0), # bool
"reference_vector_for_controlling_twist": DXFAttr(11, xtype=XType.point3d),
},
)
acdb_extruded_surface_group_codes = group_code_mapping(acdb_extruded_surface)
@register_entity
class ExtrudedSurface(Surface):
"""DXF EXTRUDEDSURFACE entity - container entity for embedded ACIS data."""
DXFTYPE = "EXTRUDEDSURFACE"
DXFATTRIBS = DXFAttributes(
base_class,
acdb_entity,
acdb_modeler_geometry,
acdb_surface,
acdb_extruded_surface,
)
def __init__(self):
super().__init__()
self.transformation_matrix_extruded_entity = Matrix44()
self.sweep_entity_transformation_matrix = Matrix44()
self.path_entity_transformation_matrix = Matrix44()
@override
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, ExtrudedSurface)
super().copy_data(entity, copy_strategy)
entity.transformation_matrix_extruded_entity = (
self.transformation_matrix_extruded_entity.copy()
)
entity.sweep_entity_transformation_matrix = (
self.sweep_entity_transformation_matrix.copy()
)
entity.path_entity_transformation_matrix = (
self.path_entity_transformation_matrix.copy()
)
@override
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_extruded_surface_group_codes, 4, log=False
)
self.load_matrices(processor.subclasses[4])
return dxf
def load_matrices(self, tags: Tags):
self.transformation_matrix_extruded_entity = load_matrix(tags, code=40)
self.sweep_entity_transformation_matrix = load_matrix(tags, code=46)
self.path_entity_transformation_matrix = load_matrix(tags, code=47)
@override
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
# base class export is done by parent class
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
# AcDbModelerGeometry export is done by parent class
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_extruded_surface.name)
self.dxf.export_dxf_attribs(tagwriter, ["class_id", "sweep_vector"])
export_matrix(
tagwriter,
code=40,
matrix=self.transformation_matrix_extruded_entity,
)
self.dxf.export_dxf_attribs(
tagwriter,
[
"draft_angle",
"draft_start_distance",
"draft_end_distance",
"twist_angle",
"scale_factor",
"align_angle",
],
)
export_matrix(
tagwriter, code=46, matrix=self.sweep_entity_transformation_matrix
)
export_matrix(tagwriter, code=47, matrix=self.path_entity_transformation_matrix)
self.dxf.export_dxf_attribs(
tagwriter,
[
"solid",
"sweep_alignment_flags",
"unknown1",
"align_start",
"bank",
"base_point_set",
"sweep_entity_transform_computed",
"path_entity_transform_computed",
"reference_vector_for_controlling_twist",
],
)
acdb_lofted_surface = DefSubclass(
"AcDbLoftedSurface",
{
# 16x group code 40: Transform matrix of loft entity (16 floats;
# row major format; default = identity matrix)
"plane_normal_lofting_type": DXFAttr(70),
"start_draft_angle": DXFAttr(41, default=0.0), # in radians
"end_draft_angle": DXFAttr(42, default=0.0), # in radians
"start_draft_magnitude": DXFAttr(43, default=0.0),
"end_draft_magnitude": DXFAttr(44, default=0.0),
"arc_length_parameterization": DXFAttr(290, default=0), # bool
"no_twist": DXFAttr(291, default=1), # true/false
"align_direction": DXFAttr(292, default=1), # bool
"simple_surfaces": DXFAttr(293, default=1), # bool
"closed_surfaces": DXFAttr(294, default=0), # bool
"solid": DXFAttr(295, default=0), # true/false
"ruled_surface": DXFAttr(296, default=0), # bool
"virtual_guide": DXFAttr(297, default=0), # bool
},
)
acdb_lofted_surface_group_codes = group_code_mapping(acdb_lofted_surface)
@register_entity
class LoftedSurface(Surface):
"""DXF LOFTEDSURFACE entity - container entity for embedded ACIS data."""
DXFTYPE = "LOFTEDSURFACE"
DXFATTRIBS = DXFAttributes(
base_class,
acdb_entity,
acdb_modeler_geometry,
acdb_surface,
acdb_lofted_surface,
)
def __init__(self):
super().__init__()
self.transformation_matrix_lofted_entity = Matrix44()
@override
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, LoftedSurface)
super().copy_data(entity, copy_strategy)
entity.transformation_matrix_lofted_entity = (
self.transformation_matrix_lofted_entity.copy()
)
@override
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_lofted_surface_group_codes, 4, log=False
)
self.load_matrices(processor.subclasses[4])
return dxf
def load_matrices(self, tags: Tags):
self.transformation_matrix_lofted_entity = load_matrix(tags, code=40)
@override
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
# base class export is done by parent class
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
# AcDbModelerGeometry export is done by parent class
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_lofted_surface.name)
export_matrix(
tagwriter, code=40, matrix=self.transformation_matrix_lofted_entity
)
self.dxf.export_dxf_attribs(tagwriter, acdb_lofted_surface.attribs.keys())
acdb_revolved_surface = DefSubclass(
"AcDbRevolvedSurface",
{
"class_id": DXFAttr(90, default=0.0),
"axis_point": DXFAttr(10, xtype=XType.point3d),
"axis_vector": DXFAttr(11, xtype=XType.point3d),
"revolve_angle": DXFAttr(40), # in radians
"start_angle": DXFAttr(41), # in radians
# 16x group code 42: Transform matrix of revolved entity (16 floats;
# row major format; default = identity matrix)
"draft_angle": DXFAttr(43), # in radians
"start_draft_distance": DXFAttr(44, default=0),
"end_draft_distance": DXFAttr(45, default=0),
"twist_angle": DXFAttr(46, default=0), # in radians
"solid": DXFAttr(290, default=0), # bool
"close_to_axis": DXFAttr(291, default=0), # bool
},
)
acdb_revolved_surface_group_codes = group_code_mapping(acdb_revolved_surface)
@register_entity
class RevolvedSurface(Surface):
"""DXF REVOLVEDSURFACE entity - container entity for embedded ACIS data."""
DXFTYPE = "REVOLVEDSURFACE"
DXFATTRIBS = DXFAttributes(
base_class,
acdb_entity,
acdb_modeler_geometry,
acdb_surface,
acdb_revolved_surface,
)
def __init__(self):
super().__init__()
self.transformation_matrix_revolved_entity = Matrix44()
@override
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, RevolvedSurface)
super().copy_data(entity, copy_strategy)
entity.transformation_matrix_revolved_entity = (
self.transformation_matrix_revolved_entity.copy()
)
@override
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_revolved_surface_group_codes, 4, log=False
)
self.load_matrices(processor.subclasses[4])
return dxf
def load_matrices(self, tags: Tags):
self.transformation_matrix_revolved_entity = load_matrix(tags, code=42)
@override
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
# base class export is done by parent class
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
# AcDbModelerGeometry export is done by parent class
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_revolved_surface.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
"class_id",
"axis_point",
"axis_vector",
"revolve_angle",
"start_angle",
],
)
export_matrix(
tagwriter,
code=42,
matrix=self.transformation_matrix_revolved_entity,
)
self.dxf.export_dxf_attribs(
tagwriter,
[
"draft_angle",
"start_draft_distance",
"end_draft_distance",
"twist_angle",
"solid",
"close_to_axis",
],
)
acdb_swept_surface = DefSubclass(
"AcDbSweptSurface",
{
"swept_entity_id": DXFAttr(90),
# 90: size of binary data (lost on saving)
# 310: binary data (lost on saving)
"path_entity_id": DXFAttr(91),
# 90: size of binary data (lost on saving)
# 310: binary data (lost on saving)
# 16x group code 40: Transform matrix of sweep entity (16 floats;
# row major format; default = identity matrix)
# 16x group code 41: Transform matrix of path entity (16 floats;
# row major format; default = identity matrix)
"draft_angle": DXFAttr(42), # in radians
"draft_start_distance": DXFAttr(43, default=0),
"draft_end_distance": DXFAttr(44, default=0),
"twist_angle": DXFAttr(45, default=0), # in radians
"scale_factor": DXFAttr(48, default=1),
"align_angle": DXFAttr(49, default=0), # in radians
# don't know the meaning of this matrices
# 16x group code 46: Transform matrix of sweep entity (16 floats;
# row major format; default = identity matrix)
# 16x group code 47: Transform matrix of path entity (16 floats;
# row major format; default = identity matrix)
"solid": DXFAttr(290, default=0), # in radians
# 0=No alignment; 1= align sweep entity to path:
"sweep_alignment": DXFAttr(70, default=0),
"unknown1": DXFAttr(71, default=0),
# 2=Translate sweep entity to path; 3=Translate path to sweep entity:
"align_start": DXFAttr(292, default=0), # bool
"bank": DXFAttr(293, default=0), # bool
"base_point_set": DXFAttr(294, default=0), # bool
"sweep_entity_transform_computed": DXFAttr(295, default=0), # bool
"path_entity_transform_computed": DXFAttr(296, default=0), # bool
"reference_vector_for_controlling_twist": DXFAttr(11, xtype=XType.point3d),
},
)
acdb_swept_surface_group_codes = group_code_mapping(acdb_swept_surface)
@register_entity
class SweptSurface(Surface):
"""DXF SWEPTSURFACE entity - container entity for embedded ACIS data."""
DXFTYPE = "SWEPTSURFACE"
DXFATTRIBS = DXFAttributes(
base_class,
acdb_entity,
acdb_modeler_geometry,
acdb_surface,
acdb_swept_surface,
)
def __init__(self):
super().__init__()
self.transformation_matrix_sweep_entity = Matrix44()
self.transformation_matrix_path_entity = Matrix44()
self.sweep_entity_transformation_matrix = Matrix44()
self.path_entity_transformation_matrix = Matrix44()
@override
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, SweptSurface)
super().copy_data(entity, copy_strategy)
entity.transformation_matrix_sweep_entity = (
self.transformation_matrix_sweep_entity.copy()
)
entity.transformation_matrix_path_entity = (
self.transformation_matrix_path_entity.copy()
)
entity.sweep_entity_transformation_matrix = (
self.sweep_entity_transformation_matrix.copy()
)
entity.path_entity_transformation_matrix = (
self.path_entity_transformation_matrix.copy()
)
@override
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_swept_surface_group_codes, 4, log=False
)
self.load_matrices(processor.subclasses[4])
return dxf
def load_matrices(self, tags: Tags):
self.transformation_matrix_sweep_entity = load_matrix(tags, code=40)
self.transformation_matrix_path_entity = load_matrix(tags, code=41)
self.sweep_entity_transformation_matrix = load_matrix(tags, code=46)
self.path_entity_transformation_matrix = load_matrix(tags, code=47)
@override
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
# base class export is done by parent class
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
# AcDbModelerGeometry export is done by parent class
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_swept_surface.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
"swept_entity_id",
"path_entity_id",
],
)
export_matrix(
tagwriter, code=40, matrix=self.transformation_matrix_sweep_entity
)
export_matrix(tagwriter, code=41, matrix=self.transformation_matrix_path_entity)
self.dxf.export_dxf_attribs(
tagwriter,
[
"draft_angle",
"draft_start_distance",
"draft_end_distance",
"twist_angle",
"scale_factor",
"align_angle",
],
)
export_matrix(
tagwriter, code=46, matrix=self.sweep_entity_transformation_matrix
)
export_matrix(tagwriter, code=47, matrix=self.path_entity_transformation_matrix)
self.dxf.export_dxf_attribs(
tagwriter,
[
"solid",
"sweep_alignment",
"unknown1",
"align_start",
"bank",
"base_point_set",
"sweep_entity_transform_computed",
"path_entity_transform_computed",
"reference_vector_for_controlling_twist",
],
)

View File

@@ -0,0 +1,149 @@
# Copyright (c) 2019-2023 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Sequence, Optional, Iterator
from ezdxf.lldxf.types import dxftag, uniform_appid
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.const import DXFKeyError, DXFStructureError
from ezdxf.lldxf.const import (
ACAD_REACTORS,
REACTOR_HANDLE_CODE,
APP_DATA_MARKER,
)
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = ["AppData", "Reactors"]
ERR_INVALID_DXF_ATTRIB = "Invalid DXF attribute for entity {}"
ERR_DXF_ATTRIB_NOT_EXITS = "DXF attribute {} does not exist"
class AppData:
def __init__(self) -> None:
self.data: dict[str, Tags] = dict()
def __contains__(self, appid: str) -> bool:
"""Returns ``True`` if application-defined data exist for `appid`."""
return uniform_appid(appid) in self.data
def __len__(self) -> int:
"""Returns the count of AppData."""
return len(self.data)
def tags(self) -> Iterable[Tags]:
return self.data.values()
def get(self, appid: str) -> Tags:
"""Get application-defined data for `appid` as
:class:`~ezdxf.lldxf.tags.Tags` container.
The first tag is always (102, "{APPID").
The last tag is always (102, "}").
"""
try:
return self.data[uniform_appid(appid)]
except KeyError:
raise DXFKeyError(appid)
def set(self, tags: Tags) -> None:
"""Store raw application-defined data tags.
The first tag has to be (102, "{APPID").
The last tag has to be (102, "}").
"""
if len(tags):
appid = tags[0].value
self.data[appid] = tags
def add(self, appid: str, data: Iterable[Sequence]) -> None:
"""Add application-defined tags for `appid`.
Adds first tag (102, "{APPID") if not exist.
Adds last tag (102, "}" if not exist.
"""
data = Tags(dxftag(code, value) for code, value in data)
appid = uniform_appid(appid)
if data[0] != (APP_DATA_MARKER, appid):
data.insert(0, dxftag(APP_DATA_MARKER, appid))
if data[-1] != (APP_DATA_MARKER, "}"):
data.append(dxftag(APP_DATA_MARKER, "}"))
self.set(data)
def discard(self, appid: str):
"""Delete application-defined data for `appid` without raising and error
if `appid` doesn't exist.
"""
_appid = uniform_appid(appid)
if _appid in self.data:
del self.data[_appid]
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
for data in self.data.values():
tagwriter.write_tags(data)
class Reactors:
"""Handle storage for related reactors.
Reactors are other objects related to the object that contains this
Reactor() instance.
"""
def __init__(self, handles: Optional[Iterable[str]] = None):
self.reactors: set[str] = set(handles or [])
def __len__(self) -> int:
"""Returns count of registered handles."""
return len(self.reactors)
def __contains__(self, handle: str) -> bool:
"""Returns ``True`` if `handle` is registered."""
return handle in self.reactors
def __iter__(self) -> Iterator[str]:
"""Returns an iterator for all registered handles."""
return iter(self.get())
def copy(self) -> Reactors:
"""Returns a copy."""
return Reactors(self.reactors)
@classmethod
def from_tags(cls, tags: Optional[Tags] = None) -> Reactors:
"""Create Reactors() instance from tags.
Expected DXF structure:
[(102, '{ACAD_REACTORS'), (330, handle), ..., (102, '}')]
Args:
tags: list of DXFTags()
"""
if tags is None:
return cls(None)
if len(tags) < 2: # no reactors are valid
raise DXFStructureError("ACAD_REACTORS error")
return cls((handle.value for handle in tags[1:-1]))
def get(self) -> list[str]:
"""Returns all registered handles as sorted list."""
return sorted(self.reactors, key=lambda x: int(x, base=16))
def set(self, handles: Optional[Iterable[str]]) -> None:
"""Reset all handles."""
self.reactors = set(handles or [])
def add(self, handle: str) -> None:
"""Add a single `handle`."""
self.reactors.add(handle)
def discard(self, handle: str):
"""Discard a single `handle`."""
self.reactors.discard(handle)
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
tagwriter.write_tag2(APP_DATA_MARKER, ACAD_REACTORS)
for handle in self.get():
tagwriter.write_tag2(REACTOR_HANDLE_CODE, handle)
tagwriter.write_tag2(APP_DATA_MARKER, "}")

View File

@@ -0,0 +1,62 @@
# Copyright (c) 2019-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import logging
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
group_code_mapping,
)
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER
from ezdxf.entities.dxfentity import base_class, SubclassProcessor, DXFEntity
from ezdxf.entities.layer import acdb_symbol_table_record
from ezdxf.lldxf.validator import is_valid_table_name
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = ["AppID"]
logger = logging.getLogger("ezdxf")
acdb_appid = DefSubclass(
"AcDbRegAppTableRecord",
{
"name": DXFAttr(2, validator=is_valid_table_name),
"flags": DXFAttr(70, default=0),
},
)
acdb_appid_group_codes = group_code_mapping(acdb_appid)
@register_entity
class AppID(DXFEntity):
"""DXF APPID entity"""
DXFTYPE = "APPID"
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_appid)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_appid_group_codes, subclass=2
)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_appid.name)
# for all DXF versions
self.dxf.export_dxf_attribs(tagwriter, ["name", "flags"])

View File

@@ -0,0 +1,148 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterator
import math
import numpy as np
from ezdxf.math import (
Vec3,
Matrix44,
ConstructionArc,
arc_angle_span_deg,
)
from ezdxf.math.transformtools import OCSTransform
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
group_code_mapping,
merge_group_code_mappings,
)
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER
from .dxfentity import base_class
from .dxfgfx import acdb_entity
from .circle import acdb_circle, Circle, merged_circle_group_codes
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = ["Arc"]
acdb_arc = DefSubclass(
"AcDbArc",
{
"start_angle": DXFAttr(50, default=0),
"end_angle": DXFAttr(51, default=360),
},
)
acdb_arc_group_codes = group_code_mapping(acdb_arc)
merged_arc_group_codes = merge_group_code_mappings(
merged_circle_group_codes, acdb_arc_group_codes
)
@register_entity
class Arc(Circle):
"""DXF ARC entity"""
DXFTYPE = "ARC"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_circle, acdb_arc)
MERGED_GROUP_CODES = merged_arc_group_codes
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
# AcDbEntity export is done by parent class
# AcDbCircle export is done by parent class
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_arc.name)
self.dxf.export_dxf_attribs(tagwriter, ["start_angle", "end_angle"])
@property
def start_point(self) -> Vec3:
"""Returns the start point of the arc in :ref:`WCS`, takes the :ref:`OCS` into
account.
"""
v = list(self.vertices([self.dxf.start_angle]))
return v[0]
@property
def end_point(self) -> Vec3:
"""Returns the end point of the arc in :ref:`WCS`, takes the :ref:`OCS` into
account.
"""
v = list(self.vertices([self.dxf.end_angle]))
return v[0]
def angles(self, num: int) -> Iterator[float]:
"""Yields `num` angles from start- to end angle in degrees in counter-clockwise
orientation. All angles are normalized in the range from [0, 360).
"""
if num < 2:
raise ValueError("num >= 2")
start = self.dxf.start_angle % 360
stop = self.dxf.end_angle % 360
if stop <= start:
stop += 360
for angle in np.linspace(start, stop, num=num, endpoint=True):
yield angle % 360
def flattening(self, sagitta: float) -> Iterator[Vec3]:
"""Approximate the arc by vertices in :ref:`WCS`, the argument `sagitta`_
defines the maximum distance from the center of an arc segment to the center of
its chord.
.. _sagitta: https://en.wikipedia.org/wiki/Sagitta_(geometry)
"""
arc = self.construction_tool()
ocs = self.ocs()
elevation = Vec3(self.dxf.center).z
if ocs.transform:
to_wcs = ocs.points_to_wcs
else:
to_wcs = Vec3.generate
yield from to_wcs(Vec3(p.x, p.y, elevation) for p in arc.flattening(sagitta))
def transform(self, m: Matrix44) -> Arc:
"""Transform ARC entity by transformation matrix `m` inplace.
Raises ``NonUniformScalingError()`` for non-uniform scaling.
"""
ocs = OCSTransform(self.dxf.extrusion, m)
super()._transform(ocs)
s: float = self.dxf.start_angle
e: float = self.dxf.end_angle
if not math.isclose(arc_angle_span_deg(s, e), 360.0):
(
self.dxf.start_angle,
self.dxf.end_angle,
) = ocs.transform_ccw_arc_angles_deg(s, e)
self.post_transform(m)
return self
def construction_tool(self) -> ConstructionArc:
"""Returns the 2D construction tool :class:`ezdxf.math.ConstructionArc` but the
extrusion vector is ignored.
"""
dxf = self.dxf
return ConstructionArc(
dxf.center,
dxf.radius,
dxf.start_angle,
dxf.end_angle,
)
def apply_construction_tool(self, arc: ConstructionArc) -> Arc:
"""Set ARC data from the construction tool :class:`ezdxf.math.ConstructionArc`
but the extrusion vector is ignored.
"""
dxf = self.dxf
dxf.center = Vec3(arc.center)
dxf.radius = arc.radius
dxf.start_angle = arc.start_angle
dxf.end_angle = arc.end_angle
return self # floating interface

View File

@@ -0,0 +1,710 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing_extensions import Self
import copy
from ezdxf.lldxf import validator
from ezdxf.math import NULLVEC, Vec3, Z_AXIS, OCS, Matrix44
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf import const
from ezdxf.lldxf.types import EMBEDDED_OBJ_MARKER, EMBEDDED_OBJ_STR
from ezdxf.enums import MAP_MTEXT_ALIGN_TO_FLAGS, TextHAlign, TextVAlign
from ezdxf.tools import set_flag_state
from ezdxf.tools.text import (
load_mtext_content,
fast_plain_mtext,
plain_mtext,
)
from .dxfns import SubclassProcessor, DXFNamespace
from .dxfentity import base_class
from .dxfgfx import acdb_entity, elevation_to_z_axis
from .text import Text, acdb_text, acdb_text_group_codes
from .mtext import (
acdb_mtext_group_codes,
MText,
export_mtext_content,
acdb_mtext,
)
from .factory import register_entity
from .copy import default_copy
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.lldxf.tags import Tags
from ezdxf.entities import DXFEntity
from ezdxf import xref
__all__ = ["AttDef", "Attrib", "copy_attrib_as_text", "BaseAttrib"]
# Where is it valid to place an ATTRIB entity:
# - YES: attached to an INSERT entity
# - NO: stand-alone entity in model space - ignored by BricsCAD and TrueView
# - NO: stand-alone entity in paper space - ignored by BricsCAD and TrueView
# - NO: stand-alone entity in block layout - ignored by BricsCAD and TrueView
#
# The RECOVER command of BricsCAD removes the stand-alone ATTRIB entities:
# "Invalid subentity type AcDbAttribute(<handle>)"
#
# IMPORTANT: placing ATTRIB at an invalid layout does NOT create an invalid DXF file!
#
# Where is it valid to place an ATTDEF entity:
# - NO: attached to an INSERT entity
# - YES: stand-alone entity in a BLOCK layout - BricsCAD and TrueView render the
# TAG in the block editor and does not render the ATTDEF as block content
# for the INSERT entity.
# - YES: stand-alone entity in model space - BricsCAD and TrueView render the
# TAG not the default text - the model space is also a block content
# (XREF, see also INSERT entity)
# - YES: stand-alone entity in paper space - same as model space, although a
# paper space can not be used as XREF.
# DXF Reference for ATTRIB is a total mess and incorrect, the AcDbText subclass
# for the ATTRIB entity is the same as for the TEXT entity, but the valign field
# from the 2nd AcDbText subclass of the TEXT entity is stored in the
# AcDbAttribute subclass:
attrib_fields = {
# "version": DXFAttr(280, default=0, dxfversion=const.DXF2010),
# The "version" tag has the same group code as the lock_position tag!!!!!
# Version number: 0 = 2010
# This tag is not really used (at least by BricsCAD) but there exists DXF files
# which do use this tag: "dxftest\attrib\attrib_with_mtext_R2018.dxf"
# ezdxf stores the last group code 280 as "lock_position" attribute and does
# not export a version tag for any DXF version.
# Tag string (cannot contain spaces):
"tag": DXFAttr(
2,
default="",
validator=validator.is_valid_attrib_tag,
fixer=validator.fix_attrib_tag,
),
# 1 = Attribute is invisible (does not appear)
# 2 = This is a constant attribute
# 4 = Verification is required on input of this attribute
# 8 = Attribute is preset (no prompt during insertion)
"flags": DXFAttr(70, default=0),
# Field length (optional) (not currently used)
"field_length": DXFAttr(73, default=0, optional=True),
# Vertical text justification type (optional); see group code 73 in TEXT
"valign": DXFAttr(
74,
default=0,
optional=True,
validator=validator.is_in_integer_range(0, 4),
fixer=RETURN_DEFAULT,
),
# Lock position flag. Locks the position of the attribute within the block
# reference, example of double use of group codes in one sub class
"lock_position": DXFAttr(
280,
default=0,
dxfversion=const.DXF2007, # tested with BricsCAD 2023/TrueView 2023
optional=True,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# Attribute type:
# 1 = single line
# 2 = multiline ATTRIB
# 4 = multiline ATTDEF
"attribute_type": DXFAttr(
71,
default=1,
dxfversion=const.DXF2018,
optional=True,
validator=validator.is_one_of({1, 2, 4}),
fixer=RETURN_DEFAULT,
),
}
# ATTDEF has an additional field: 'prompt'
# DXF attribute definitions are immutable, a shallow copy is sufficient:
attdef_fields = dict(attrib_fields)
attdef_fields["prompt"] = DXFAttr(
3,
default="",
validator=validator.is_valid_one_line_text,
fixer=validator.fix_one_line_text,
)
acdb_attdef = DefSubclass("AcDbAttributeDefinition", attdef_fields)
acdb_attdef_group_codes = group_code_mapping(acdb_attdef)
acdb_attrib = DefSubclass("AcDbAttribute", attrib_fields)
acdb_attrib_group_codes = group_code_mapping(acdb_attrib)
# --------------------------------------------------------------------------------------
# Does subclass AcDbXrecord really exist? Only the documentation in the DXF reference
# exists, no real world examples seen so far - it wouldn't be the first error or misleading
# information in the DXF reference.
# --------------------------------------------------------------------------------------
# For XRECORD the tag order is important and group codes appear multiple times,
# therefore this attribute definition needs a special treatment!
acdb_attdef_xrecord = DefSubclass(
"AcDbXrecord",
[ # type: ignore
# Duplicate record cloning flag (determines how to merge duplicate entries):
# 1 = Keep existing
("cloning", DXFAttr(280, default=1)),
# MText flag:
# 2 = multiline attribute
# 4 = constant multiline attribute definition
("mtext_flag", DXFAttr(70, default=0)),
# isReallyLocked flag:
# 0 = unlocked
# 1 = locked
(
"really_locked",
DXFAttr(
70,
default=0,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
),
# Number of secondary attributes or attribute definitions:
("secondary_attribs_count", DXFAttr(70, default=0)),
# Hard-pointer id of secondary attribute(s) or attribute definition(s):
("secondary_attribs_handle", DXFAttr(340, default="0")),
# Alignment point of attribute or attribute definition:
("align_point", DXFAttr(10, xtype=XType.point3d, default=NULLVEC)),
("current_annotation_scale", DXFAttr(40, default=0)),
# attribute or attribute definition tag string
(
"tag",
DXFAttr(
2,
default="",
validator=validator.is_valid_attrib_tag,
fixer=validator.fix_attrib_tag,
),
),
],
)
# Just for documentation:
# The "attached" MTEXT feature most likely does not exist!
#
# A special MTEXT entity can follow the ATTDEF and ATTRIB entity, which starts
# as a usual DXF entity with (0, 'MTEXT'), so processing can't be done here,
# because for ezdxf is this a separated Entity.
#
# The attached MTEXT entity: owner is None and handle is None
# Linked as attribute `attached_mtext`.
# I don't have seen this combination of entities in real world examples and is
# ignored by ezdxf for now.
#
# No DXF files available which uses this feature - misleading DXF Reference!?
# Attrib and Attdef can have embedded MTEXT entities located in the
# <Embedded Object> subclass, see issue #258
class BaseAttrib(Text):
XRECORD_DEF = acdb_attdef_xrecord
def __init__(self) -> None:
super().__init__()
# Does subclass AcDbXrecord really exist?
self._xrecord: Optional[Tags] = None
self._embedded_mtext: Optional[EmbeddedMText] = None
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
"""Copy entity data, xrecord data and embedded MTEXT are not stored
in the entity database.
"""
assert isinstance(entity, BaseAttrib)
entity._xrecord = copy.deepcopy(self._xrecord)
entity._embedded_mtext = copy.deepcopy(self._embedded_mtext)
def load_embedded_mtext(self, processor: SubclassProcessor) -> None:
if not processor.embedded_objects:
return
embedded_object = processor.embedded_objects[0]
if embedded_object:
mtext = EmbeddedMText()
mtext.load_dxf_tags(processor)
self._embedded_mtext = mtext
def export_dxf_r2018_features(self, tagwriter: AbstractTagWriter) -> None:
tagwriter.write_tag2(71, self.dxf.attribute_type)
tagwriter.write_tag2(72, 0) # unknown tag
if self.dxf.hasattr("align_point"):
# duplicate align point - why?
tagwriter.write_vertex(11, self.dxf.align_point)
if self._xrecord:
tagwriter.write_tags(self._xrecord)
if self._embedded_mtext:
self._embedded_mtext.export_dxf_tags(tagwriter)
@property
def is_const(self) -> bool:
"""This is a constant attribute if ``True``."""
return bool(self.dxf.flags & const.ATTRIB_CONST)
@is_const.setter
def is_const(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_CONST, state)
@property
def is_invisible(self) -> bool:
"""Attribute is invisible if ``True``."""
return bool(self.dxf.flags & const.ATTRIB_INVISIBLE)
@is_invisible.setter
def is_invisible(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_INVISIBLE, state)
@property
def is_verify(self) -> bool:
"""Verification is required on input of this attribute. (interactive CAD
application feature)
"""
return bool(self.dxf.flags & const.ATTRIB_VERIFY)
@is_verify.setter
def is_verify(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_VERIFY, state)
@property
def is_preset(self) -> bool:
"""No prompt during insertion. (interactive CAD application feature)"""
return bool(self.dxf.flags & const.ATTRIB_IS_PRESET)
@is_preset.setter
def is_preset(self, state: bool) -> None:
self.dxf.flags = set_flag_state(self.dxf.flags, const.ATTRIB_IS_PRESET, state)
@property
def has_embedded_mtext_entity(self) -> bool:
"""Returns ``True`` if the entity has an embedded MTEXT entity for multi-line
support.
"""
return bool(self._embedded_mtext)
def virtual_mtext_entity(self) -> MText:
"""Returns the embedded MTEXT entity as a regular but virtual
:class:`MText` entity with the same graphical properties as the
host entity.
"""
if not self._embedded_mtext:
raise TypeError("no embedded MTEXT entity exist")
mtext = self._embedded_mtext.virtual_mtext_entity()
mtext.update_dxf_attribs(self.graphic_properties())
return mtext
def plain_mtext(self, fast=True) -> str:
"""Returns the embedded MTEXT content without formatting codes.
Returns an empty string if no embedded MTEXT entity exist.
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.
The `accurate` mode is **much** slower than the `fast` mode.
Args:
fast: uses the `fast` mode to extract the plain MTEXT content if
``True`` or the `accurate` mode if set to ``False``
"""
if self._embedded_mtext:
text = self._embedded_mtext.text
if fast:
return fast_plain_mtext(text, split=False) # type: ignore
else:
return plain_mtext(text, split=False) # type: ignore
return ""
def set_mtext(self, mtext: MText, graphic_properties=True) -> None:
"""Set multi-line properties from a :class:`MText` entity.
The multi-line ATTRIB/ATTDEF entity requires DXF R2018, otherwise an
ordinary single line ATTRIB/ATTDEF entity will be exported.
Args:
mtext: source :class:`MText` entity
graphic_properties: copy graphic properties (color, layer, ...) from
source MTEXT if ``True``
"""
if self._embedded_mtext is None:
self._embedded_mtext = EmbeddedMText()
self._embedded_mtext.set_mtext(mtext)
_update_content_from_mtext(self, mtext)
_update_location_from_mtext(self, mtext)
# misc properties
self.dxf.style = mtext.dxf.style
self.dxf.height = mtext.dxf.char_height
self.dxf.discard("width") # controlled in MTEXT by inline codes!
self.dxf.discard("oblique") # controlled in MTEXT by inline codes!
self.dxf.discard("text_generation_flag")
if graphic_properties:
self.update_dxf_attribs(mtext.graphic_properties())
def embed_mtext(self, mtext: MText, graphic_properties=True) -> None:
"""Set multi-line properties from a :class:`MText` entity and destroy the
source entity afterwards.
The multi-line ATTRIB/ATTDEF entity requires DXF R2018, otherwise an
ordinary single line ATTRIB/ATTDEF entity will be exported.
Args:
mtext: source :class:`MText` entity
graphic_properties: copy graphic properties (color, layer, ...) from
source MTEXT if ``True``
"""
self.set_mtext(mtext, graphic_properties)
mtext.destroy()
def register_resources(self, registry: xref.Registry) -> None:
"""Register required resources to the resource registry."""
super().register_resources(registry)
if self._embedded_mtext:
self._embedded_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, BaseAttrib)
super().map_resources(clone, mapping)
if self._embedded_mtext and clone._embedded_mtext:
self._embedded_mtext.map_resources(clone._embedded_mtext, mapping)
# todo: map handles in embedded XRECORD if a real world example shows up
def transform(self, m: Matrix44) -> Self:
if self._embedded_mtext is None:
super().transform(m)
else:
mtext = self._embedded_mtext.virtual_mtext_entity()
mtext.transform(m)
self.set_mtext(mtext, graphic_properties=False)
self.post_transform(m)
return self
def _update_content_from_mtext(text: Text, mtext: MText) -> None:
content = mtext.plain_text(split=True, fast=True)
if content:
# In contrast to AutoCAD, just set the first line as single line
# ATTRIB content. AutoCAD concatenates all lines into a single
# "Line1\PLine2\P...", which (imho) is not very useful.
text.dxf.text = content[0]
def _update_location_from_mtext(text: Text, mtext: MText) -> None:
# TEXT is an OCS entity, MTEXT is a WCS entity
dxf = text.dxf
insert = Vec3(mtext.dxf.insert)
extrusion = Vec3(mtext.dxf.extrusion)
text_direction = mtext.get_text_direction()
if extrusion.isclose(Z_AXIS): # most common case
dxf.rotation = text_direction.angle_deg
else:
ocs = OCS(extrusion)
insert = ocs.from_wcs(insert)
dxf.extrusion = extrusion.normalize()
dxf.rotation = ocs.from_wcs(text_direction).angle_deg
dxf.insert = insert
dxf.align_point = insert # the same point for all MTEXT alignments!
dxf.halign, dxf.valign = MAP_MTEXT_ALIGN_TO_FLAGS.get(
mtext.dxf.attachment_point, (TextHAlign.LEFT, TextVAlign.TOP)
)
@register_entity
class AttDef(BaseAttrib):
"""DXF ATTDEF entity"""
DXFTYPE = "ATTDEF"
# Don't add acdb_attdef_xrecord here:
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_text, acdb_attdef)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super(Text, self).load_dxf_attribs(processor)
# Do not call Text loader.
if processor:
processor.fast_load_dxfattribs(dxf, acdb_text_group_codes, 2, recover=True)
processor.fast_load_dxfattribs(
dxf, acdb_attdef_group_codes, 3, recover=True
)
self._xrecord = processor.find_subclass(self.XRECORD_DEF.name) # type: ignore
self.load_embedded_mtext(processor)
if processor.r12:
# Transform elevation attribute from R11 to z-axis values:
elevation_to_z_axis(dxf, ("insert", "align_point"))
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
# Text() writes 2x AcDbText which is not suitable for AttDef()
self.export_acdb_entity(tagwriter)
self.export_acdb_text(tagwriter)
self.export_acdb_attdef(tagwriter)
if tagwriter.dxfversion >= const.DXF2018:
self.dxf.attribute_type = 4 if self.has_embedded_mtext_entity else 1
self.export_dxf_r2018_features(tagwriter)
def export_acdb_attdef(self, tagwriter: AbstractTagWriter) -> None:
if tagwriter.dxfversion > const.DXF12:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_attdef.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
# write version tag (280, 0) here, if required in the future
"prompt",
"tag",
"flags",
"field_length",
"valign",
"lock_position",
],
)
@register_entity
class Attrib(BaseAttrib):
"""DXF ATTRIB entity"""
DXFTYPE = "ATTRIB"
# Don't add acdb_attdef_xrecord here:
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_text, acdb_attrib)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super(Text, self).load_dxf_attribs(processor)
# Do not call Text loader.
if processor:
processor.fast_load_dxfattribs(dxf, acdb_text_group_codes, 2, recover=True)
processor.fast_load_dxfattribs(
dxf, acdb_attrib_group_codes, 3, recover=True
)
self._xrecord = processor.find_subclass(self.XRECORD_DEF.name) # type: ignore
self.load_embedded_mtext(processor)
if processor.r12:
# Transform elevation attribute from R11 to z-axis values:
elevation_to_z_axis(dxf, ("insert", "align_point"))
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
# Text() writes 2x AcDbText which is not suitable for AttDef()
self.export_acdb_entity(tagwriter)
self.export_acdb_attrib_text(tagwriter)
self.export_acdb_attrib(tagwriter)
if tagwriter.dxfversion >= const.DXF2018:
self.dxf.attribute_type = 2 if self.has_embedded_mtext_entity else 1
self.export_dxf_r2018_features(tagwriter)
def export_acdb_attrib_text(self, tagwriter: AbstractTagWriter) -> None:
# Despite the similarities to TEXT, it is different to
# Text.export_acdb_text():
if tagwriter.dxfversion > const.DXF12:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_text.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
"insert",
"height",
"text",
"thickness",
"rotation",
"oblique",
"style",
"width",
"halign",
"align_point",
"text_generation_flag",
"extrusion",
],
)
def export_acdb_attrib(self, tagwriter: AbstractTagWriter) -> None:
if tagwriter.dxfversion > const.DXF12:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_attrib.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
# write version tag (280, 0) here, if required in the future
"tag",
"flags",
"field_length",
"valign",
"lock_position",
],
)
IGNORE_FROM_ATTRIB = {
"handle",
"owner",
"version",
"prompt",
"tag",
"flags",
"field_length",
"lock_position",
}
def copy_attrib_as_text(attrib: BaseAttrib):
"""Returns the content of the ATTRIB/ATTDEF entity as a new virtual TEXT or
MTEXT entity.
"""
if attrib.has_embedded_mtext_entity:
return attrib.virtual_mtext_entity()
dxfattribs = attrib.dxfattribs(drop=IGNORE_FROM_ATTRIB)
return Text.new(dxfattribs=dxfattribs, doc=attrib.doc)
class EmbeddedMTextNS(DXFNamespace):
_DXFATTRIBS = DXFAttributes(acdb_mtext)
@property
def dxfattribs(self) -> DXFAttributes:
return self._DXFATTRIBS
@property
def dxftype(self) -> str:
return "Embedded MText"
class EmbeddedMText:
"""Representation of the embedded MTEXT object in ATTRIB and ATTDEF.
Introduced in DXF R2018? The DXF reference of the `MTEXT`_ entity
documents only the attached MTEXT entity. The ODA DWG specs includes all
MTEXT attributes of MTEXT starting at group code 10
Stores the required parameters to be shown as as MTEXT.
The AcDbText subclass contains the first line of the embedded MTEXT as
plain text content as group code 1, but this tag seems not to be maintained
if the ATTRIB entity is copied.
Some DXF attributes are duplicated and maintained by the CAD application:
- textstyle: same group code 7 (AcDbText, EmbeddedObject)
- text (char) height: same group code 40 (AcDbText, EmbeddedObject)
.. _MTEXT: https://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-7DD8B495-C3F8-48CD-A766-14F9D7D0DD9B
"""
def __init__(self) -> None:
# Attribute "dxf" contains the DXF attributes defined in subclass
# "AcDbMText"
self.dxf = EmbeddedMTextNS()
self.text: str = ""
def copy(self) -> EmbeddedMText:
copy_ = EmbeddedMText()
copy_.dxf = copy.deepcopy(self.dxf)
return copy_
__copy__ = copy
def load_dxf_tags(self, processor: SubclassProcessor) -> None:
tags = processor.fast_load_dxfattribs(
self.dxf,
group_code_mapping=acdb_mtext_group_codes,
subclass=processor.embedded_objects[0],
recover=False,
)
self.text = load_mtext_content(tags)
def virtual_mtext_entity(self) -> MText:
"""Returns the embedded MTEXT entity as regular but virtual MTEXT
entity. This entity does not have the graphical attributes of the host
entity (ATTRIB/ATTDEF).
"""
mtext = MText.new(dxfattribs=self.dxf.all_existing_dxf_attribs())
mtext.text = self.text
return mtext
def set_mtext(self, mtext: MText) -> None:
"""Set embedded MTEXT attributes from given `mtext` entity."""
self.text = mtext.text
dxf = self.dxf
for k, v in mtext.dxf.all_existing_dxf_attribs().items():
if dxf.is_supported(k):
dxf.set(k, v)
def set_required_dxf_attributes(self):
# These attributes are always present in DXF files created by Autocad:
dxf = self.dxf
for key, default in (
("insert", NULLVEC),
("char_height", 2.5),
("width", 0.0),
("defined_height", 0.0),
("attachment_point", 1),
("flow_direction", 5),
("style", "Standard"),
("line_spacing_style", 1),
("line_spacing_factor", 1.0),
):
if not dxf.hasattr(key):
dxf.set(key, default)
def export_dxf_tags(self, tagwriter: AbstractTagWriter) -> None:
"""Export embedded MTEXT as "Embedded Object"."""
tagwriter.write_tag2(EMBEDDED_OBJ_MARKER, EMBEDDED_OBJ_STR)
self.set_required_dxf_attributes()
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",
],
)
def register_resources(self, registry: xref.Registry) -> None:
"""Register required resources to the resource registry."""
if self.dxf.hasattr("style"):
registry.add_text_style(self.dxf.style)
def map_resources(self, clone: EmbeddedMText, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
if clone.dxf.hasattr("style"):
clone.dxf.style = mapping.get_text_style(clone.dxf.style)

View File

@@ -0,0 +1,242 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing_extensions import Self
from ezdxf.lldxf import validator
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
merge_group_code_mappings,
)
from ezdxf.lldxf.const import (
SUBCLASS_MARKER,
DXF12,
DXF2000,
MODEL_SPACE_R12,
PAPER_SPACE_R12,
MODEL_SPACE_R2000,
PAPER_SPACE_R2000,
)
from ezdxf.math import NULLVEC
from .dxfentity import base_class, SubclassProcessor, DXFEntity
from .factory import register_entity
from ezdxf.audit import Auditor, AuditError
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf import xref
__all__ = ["Block", "EndBlk"]
acdb_entity = DefSubclass(
"AcDbEntity",
{
# No auto fix for invalid layer names!
"layer": DXFAttr(8, default="0", validator=validator.is_valid_layer_name),
"paperspace": DXFAttr(
67,
default=0,
optional=True,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
},
)
acdb_entity_group_codes = group_code_mapping(acdb_entity)
acdb_block_begin = DefSubclass(
"AcDbBlockBegin",
{
"name": DXFAttr(2, validator=validator.is_valid_block_name),
# The 2nd name with group code 3 is handled internally, and is not an
# explicit DXF attribute.
"description": DXFAttr(4, default="", dxfversion=DXF2000, optional=True),
# Flags:
# 0 = Indicates none of the following flags apply
# 1 = This is an anonymous block generated by hatching, associative
# dimensioning, other internal operations, or an application
# 2 = This block has non-constant attribute definitions (this bit is not set
# if the block has any attribute definitions that are constant, or has
# no attribute definitions at all)
# 4 = This block is an external reference (xref)
# 8 = This block is an xref overlay
# 16 = This block is externally dependent
# 32 = This is a resolved external reference, or dependent of an external
# reference (ignored on input)
# 64 = This definition is a referenced external reference (ignored on input)
"flags": DXFAttr(70, default=0),
"base_point": DXFAttr(10, xtype=XType.any_point, default=NULLVEC),
"xref_path": DXFAttr(1, default=""),
},
)
acdb_block_begin_group_codes = group_code_mapping(acdb_block_begin)
merged_block_begin_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_block_begin_group_codes
)
MODEL_SPACE_R2000_LOWER = MODEL_SPACE_R2000.lower()
MODEL_SPACE_R12_LOWER = MODEL_SPACE_R12.lower()
PAPER_SPACE_R2000_LOWER = PAPER_SPACE_R2000.lower()
PAPER_SPACE_R12_LOWER = PAPER_SPACE_R12.lower()
@register_entity
class Block(DXFEntity):
"""DXF BLOCK entity"""
DXFTYPE = "BLOCK"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_block_begin)
# Block entity flags:
# This is an anonymous block generated by hatching, associative
# dimensioning, other internal operations, or an application:
ANONYMOUS = 1
# This block has non-constant attribute definitions (this bit is not set
# if the block has any attribute definitions that are constant, or has no
# attribute definitions at all):
NON_CONSTANT_ATTRIBUTES = 2
# This block is an external reference:
XREF = 4
# This block is an xref overlay:
XREF_OVERLAY = 8
# This block is externally dependent:
EXTERNAL = 16
# This is a resolved external reference, or dependent of an external reference:
RESOLVED = 32
# This definition is a referenced external reference:
REFERENCED = 64
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
dxf = super().load_dxf_attribs(processor)
if processor is None:
return dxf
processor.simple_dxfattribs_loader(dxf, merged_block_begin_group_codes)
if processor.r12:
if dxf.name is None:
dxf.name = ""
name = dxf.name.lower()
if name == MODEL_SPACE_R12_LOWER:
dxf.name = MODEL_SPACE_R2000
elif name == PAPER_SPACE_R12_LOWER:
dxf.name = PAPER_SPACE_R2000
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name)
if self.dxf.hasattr("paperspace"):
tagwriter.write_tag2(67, 1)
self.dxf.export_dxf_attribs(tagwriter, "layer")
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_block_begin.name)
name = self.dxf.name
if tagwriter.dxfversion == DXF12:
# export modelspace and paperspace with leading '$' instead of '*'
if name.lower() == MODEL_SPACE_R2000_LOWER:
name = MODEL_SPACE_R12
elif name.lower() == PAPER_SPACE_R2000_LOWER:
name = PAPER_SPACE_R12
tagwriter.write_tag2(2, name)
self.dxf.export_dxf_attribs(tagwriter, ["flags", "base_point"])
tagwriter.write_tag2(3, name)
self.dxf.export_dxf_attribs(tagwriter, ["xref_path", "description"])
@property
def is_layout_block(self) -> bool:
"""Returns ``True`` if this is a :class:`~ezdxf.layouts.Modelspace` or
:class:`~ezdxf.layouts.Paperspace` block definition.
"""
name = self.dxf.name.lower()
return name.startswith("*model_space") or name.startswith("*paper_space")
@property
def is_anonymous(self) -> bool:
"""Returns ``True`` if this is an anonymous block generated by
hatching, associative dimensioning, other internal operations, or an
application.
"""
return self.get_flag_state(Block.ANONYMOUS)
@property
def is_xref(self) -> bool:
"""Returns ``True`` if bock is an external referenced file."""
return self.get_flag_state(Block.XREF)
@property
def is_xref_overlay(self) -> bool:
"""Returns ``True`` if bock is an external referenced overlay file."""
return self.get_flag_state(Block.XREF_OVERLAY)
def audit(self, auditor: Auditor):
owner_handle = self.dxf.get("owner")
if owner_handle is None: # invalid owner handle - IGNORE
return
owner = auditor.entitydb.get(owner_handle)
if owner is None: # invalid owner entity - IGNORE
return
owner_name = owner.dxf.get("name", "").upper()
block_name = self.dxf.get("name", "").upper()
if owner_name != block_name:
auditor.add_error(
AuditError.BLOCK_NAME_MISMATCH,
f"{str(self)} name '{block_name}' and {str(owner)} name '{owner_name}' mismatch",
)
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
assert isinstance(clone, Block)
super().map_resources(clone, mapping)
clone.dxf.name = mapping.get_block_name(self.dxf.name)
acdb_block_end = DefSubclass("AcDbBlockEnd", {})
@register_entity
class EndBlk(DXFEntity):
"""DXF ENDBLK entity"""
DXFTYPE = "ENDBLK"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_block_end)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
dxf = super().load_dxf_attribs(processor)
if processor:
processor.simple_dxfattribs_loader(dxf, acdb_entity_group_codes) # type: ignore
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name)
if self.dxf.hasattr("paperspace"):
tagwriter.write_tag2(67, 1)
self.dxf.export_dxf_attribs(tagwriter, "layer")
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_block_end.name)

View File

@@ -0,0 +1,315 @@
# Copyright (c) 2019-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from typing_extensions import Self
import logging
from ezdxf.lldxf import validator
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf.const import (
DXF12,
SUBCLASS_MARKER,
DXF2007,
DXFInternalEzdxfError,
)
from ezdxf.entities.dxfentity import base_class, SubclassProcessor, DXFEntity
from ezdxf.entities.layer import acdb_symbol_table_record
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.entities import DXFGraphic, Block, EndBlk
from ezdxf.entities import DXFNamespace
from ezdxf.entitydb import EntitySpace
from ezdxf.layouts import BlockLayout
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf import xref
__all__ = ["BlockRecord"]
logger = logging.getLogger("ezdxf")
acdb_blockrec = DefSubclass(
"AcDbBlockTableRecord",
{
"name": DXFAttr(2, validator=validator.is_valid_block_name),
# handle to associated DXF LAYOUT object
"layout": DXFAttr(340, default="0"),
# 0 = can not explode; 1 = can explode
"explode": DXFAttr(
280,
default=1,
dxfversion=DXF2007,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# 0 = scale non uniformly; 1 = scale uniformly
"scale": DXFAttr(
281,
default=0,
dxfversion=DXF2007,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# see ezdxf/units.py
"units": DXFAttr(
70,
default=0,
dxfversion=DXF2007,
validator=validator.is_in_integer_range(0, 25),
fixer=RETURN_DEFAULT,
),
# 310: Binary data for bitmap preview (optional) - removed (ignored) by ezdxf
},
)
acdb_blockrec_group_codes = group_code_mapping(acdb_blockrec)
# optional handles to existing block references in DXF2000+
# 2: name
# 340: explode
# 102: "{BLKREFS"
# 331: handle to INSERT
# ...
# 102: "}"
# optional XDATA for all DXF versions
# 1000: "ACAD"
# 1001: "DesignCenter Data" (optional)
# 1002: "{"
# 1070: Autodesk Design Center version number
# 1070: Insert units: like 'units'
# 1002: "}"
@register_entity
class BlockRecord(DXFEntity):
"""DXF BLOCK_RECORD table entity
BLOCK_RECORD is the hard owner of all entities in BLOCK definitions, this
means owner tag of entities is handle of BLOCK_RECORD.
"""
DXFTYPE = "BLOCK_RECORD"
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_blockrec)
def __init__(self) -> None:
from ezdxf.entitydb import EntitySpace
super().__init__()
# Store entities in the block_record instead of BlockLayout and Layout,
# because BLOCK_RECORD is also the hard owner of all the entities.
self.entity_space = EntitySpace()
self.block: Optional[Block] = None
self.endblk: Optional[EndBlk] = None
# stores also the block layout structure
self.block_layout: Optional[BlockLayout] = None
def set_block(self, block: Block, endblk: EndBlk):
self.block = block
self.endblk = endblk
self.block.dxf.owner = self.dxf.handle
self.endblk.dxf.owner = self.dxf.handle
def set_entity_space(self, entity_space: EntitySpace) -> None:
self.entity_space = entity_space
def rename(self, name: str) -> None:
assert self.block is not None
self.dxf.name = name
self.block.dxf.name = name
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.simple_dxfattribs_loader(dxf, acdb_blockrec_group_codes) # type: ignore
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
if tagwriter.dxfversion == DXF12:
raise DXFInternalEzdxfError("Exporting BLOCK_RECORDS for DXF R12.")
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_blockrec.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
"name",
"layout",
"units",
"explode",
"scale",
],
)
def export_block_definition(self, tagwriter: AbstractTagWriter) -> None:
"""Exports the BLOCK entity, followed by all content entities and finally the
ENDBLK entity, except for the *Model_Space and *Paper_Space blocks, their
entities are stored in the ENTITIES section.
"""
assert self.block is not None
assert self.endblk is not None
if self.block_layout is not None:
self.block_layout.update_block_flags()
self.block.export_dxf(tagwriter)
if not (self.is_modelspace or self.is_active_paperspace):
self.entity_space.export_dxf(tagwriter)
self.endblk.export_dxf(tagwriter)
def register_resources(self, registry: xref.Registry) -> None:
"""Register required resources to the resource registry."""
assert self.doc is not None, "BLOCK_RECORD entity must be assigned to document"
assert self.doc.entitydb is not None, "entity database required"
super().register_resources(registry)
key = self.dxf.handle
assert key in self.doc.entitydb, "invalid BLOCK_RECORD handle"
if self.block is not None:
registry.add_entity(self.block, block_key=key)
else:
raise DXFInternalEzdxfError(
f"BLOCK entity in BLOCK_RECORD #{key} is invalid"
)
if self.endblk is not None:
registry.add_entity(self.endblk, block_key=key)
else:
raise DXFInternalEzdxfError(
f"ENDBLK entity in BLOCK_RECORD #{key} is invalid"
)
for e in self.entity_space:
registry.add_entity(e, block_key=key)
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
assert isinstance(clone, BlockRecord)
super().map_resources(clone, mapping)
assert self.block is not None
mapping.map_resources_of_copy(self.block)
assert self.endblk is not None
mapping.map_resources_of_copy(self.endblk)
for entity in self.entity_space:
mapping.map_resources_of_copy(entity)
def destroy(self):
"""Destroy associated data:
- BLOCK
- ENDBLK
- all entities stored in this block definition
Does not destroy the linked LAYOUT entity, this is the domain of the
:class:`Layouts` object, which also should initiate the destruction of
'this' BLOCK_RECORD.
"""
if not self.is_alive:
return
self.block.destroy()
self.endblk.destroy()
for entity in self.entity_space:
entity.destroy()
# remove attributes to find invalid access after death
del self.block
del self.endblk
del self.block_layout
super().destroy()
@property
def is_active_paperspace(self) -> bool:
"""``True`` if is "active" paperspace layout."""
return self.dxf.name.lower() == "*paper_space"
@property
def is_any_paperspace(self) -> bool:
"""``True`` if is any kind of paperspace layout."""
return self.dxf.name.lower().startswith("*paper_space")
@property
def is_modelspace(self) -> bool:
"""``True`` if is the modelspace layout."""
return self.dxf.name.lower() == "*model_space"
@property
def is_any_layout(self) -> bool:
"""``True`` if is any kind of modelspace or paperspace layout."""
return self.is_modelspace or self.is_any_paperspace
@property
def is_block_layout(self) -> bool:
"""``True`` if not any kind of modelspace or paperspace layout, just a
regular block definition.
"""
return not self.is_any_layout
@property
def is_xref(self) -> bool:
"""``True`` if represents an XREF (external reference) or XREF_OVERLAY."""
if self.block is not None:
return bool(self.block.dxf.flags & 12)
return False
def add_entity(self, entity: DXFGraphic) -> None:
"""Add an existing DXF entity to BLOCK_RECORD.
Args:
entity: :class:`DXFGraphic`
"""
# assign layout
try:
entity.set_owner(self.dxf.handle, paperspace=int(self.is_any_paperspace))
except AttributeError:
logger.debug(f"Unexpected DXF entity {str(entity)} in {str(self.block)}")
# Add unexpected entities also to the entity space - auditor should fix
# errors!
self.entity_space.add(entity)
def unlink_entity(self, entity: DXFGraphic) -> None:
"""Unlink `entity` from BLOCK_RECORD.
Removes `entity` just from entity space but not from the drawing
database.
Args:
entity: :class:`DXFGraphic`
"""
if entity.is_alive:
self.entity_space.remove(entity)
try:
entity.set_owner(None)
except AttributeError:
pass # unsupported entities as DXFTagStorage
def delete_entity(self, entity: DXFGraphic) -> None:
"""Delete `entity` from BLOCK_RECORD entity space and drawing database.
Args:
entity: :class:`DXFGraphic`
"""
self.unlink_entity(entity) # 1. unlink from entity space
entity.destroy()
def audit(self, auditor: Auditor) -> None:
"""Validity check. (internal API)"""
if not self.is_alive:
return
super().audit(auditor)
self.entity_space.audit(auditor)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional, Iterator
import math
import numpy as np
from ezdxf.lldxf import validator
from ezdxf.math import (
Vec3,
Matrix44,
NULLVEC,
Z_AXIS,
arc_segment_count,
)
from ezdxf.math.transformtools import OCSTransform, NonUniformScalingError
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
merge_group_code_mappings,
)
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER, DXFValueError
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import (
DXFGraphic,
acdb_entity,
add_entity,
replace_entity,
elevation_to_z_axis,
acdb_entity_group_codes,
)
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.entities import Ellipse, Spline
__all__ = ["Circle"]
acdb_circle = DefSubclass(
"AcDbCircle",
{
"center": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# AutCAD/BricsCAD: Radius is <= 0 is valid
"radius": DXFAttr(40, default=1),
# Elevation is a legacy feature from R11 and prior, do not use this
# attribute, store the entity elevation in the z-axis of the vertices.
# ezdxf does not export the elevation attribute!
"elevation": DXFAttr(38, default=0, optional=True),
"thickness": DXFAttr(39, default=0, optional=True),
"extrusion": DXFAttr(
210,
xtype=XType.point3d,
default=Z_AXIS,
optional=True,
validator=validator.is_not_null_vector,
fixer=RETURN_DEFAULT,
),
},
)
acdb_circle_group_codes = group_code_mapping(acdb_circle)
merged_circle_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_circle_group_codes
)
@register_entity
class Circle(DXFGraphic):
"""DXF CIRCLE entity"""
DXFTYPE = "CIRCLE"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_circle)
MERGED_GROUP_CODES = merged_circle_group_codes
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
# bypass DXFGraphic, loading proxy graphic is skipped!
dxf = super(DXFGraphic, self).load_dxf_attribs(processor)
if processor:
processor.simple_dxfattribs_loader(dxf, self.MERGED_GROUP_CODES)
if processor.r12:
# Transform elevation attribute from R11 to z-axis values:
elevation_to_z_axis(dxf, ("center",))
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_circle.name)
self.dxf.export_dxf_attribs(
tagwriter, ["center", "radius", "thickness", "extrusion"]
)
def vertices(self, angles: Iterable[float]) -> Iterator[Vec3]:
"""Yields the vertices of the circle of all given `angles` as
:class:`~ezdxf.math.Vec3` instances in :ref:`WCS`.
Args:
angles: iterable of angles in :ref:`OCS` as degrees, angle goes
counter-clockwise around the extrusion vector, and the OCS x-axis
defines 0-degree.
"""
ocs = self.ocs()
radius: float = abs(self.dxf.radius) # AutoCAD ignores the sign too
center = Vec3(self.dxf.center)
for angle in angles:
yield ocs.to_wcs(Vec3.from_deg_angle(angle, radius) + center)
def flattening(self, sagitta: float) -> Iterator[Vec3]:
"""Approximate the circle by vertices in :ref:`WCS` as :class:`~ezdxf.math.Vec3`
instances. The argument `sagitta`_ is the maximum distance from the center of an
arc segment to the center of its chord. Yields a closed polygon where the start
vertex is equal to the end vertex!
.. _sagitta: https://en.wikipedia.org/wiki/Sagitta_(geometry)
"""
radius = abs(self.dxf.radius)
if radius > 0.0:
count = arc_segment_count(radius, math.tau, sagitta)
yield from self.vertices(np.linspace(0.0, 360.0, count + 1))
def transform(self, m: Matrix44) -> Circle:
"""Transform the CIRCLE entity by transformation matrix `m` inplace.
Raises ``NonUniformScalingError()`` for non-uniform scaling.
"""
circle = self._transform(OCSTransform(self.dxf.extrusion, m))
self.post_transform(m)
return circle
def _transform(self, ocs: OCSTransform) -> Circle:
dxf = self.dxf
if ocs.scale_uniform:
dxf.extrusion = ocs.new_extrusion
dxf.center = ocs.transform_vertex(dxf.center)
# old_ocs has a uniform scaled xy-plane, direction of radius-vector
# in the xy-plane is not important, choose x-axis for no reason:
dxf.radius = ocs.transform_length((dxf.radius, 0, 0))
if dxf.hasattr("thickness"):
# thickness vector points in the z-direction of the old_ocs,
# thickness can be negative
dxf.thickness = ocs.transform_thickness(dxf.thickness)
else:
# Caller has to catch this Exception and convert this
# CIRCLE/ARC into an ELLIPSE.
raise NonUniformScalingError(
"CIRCLE/ARC does not support non uniform scaling"
)
return self
def translate(self, dx: float, dy: float, dz: float) -> Circle:
"""Optimized CIRCLE/ARC translation about `dx` in x-axis, `dy` in
y-axis and `dz` in z-axis, returns `self` (floating interface).
"""
ocs = self.ocs()
self.dxf.center = ocs.from_wcs(Vec3(dx, dy, dz) + ocs.to_wcs(self.dxf.center))
# Avoid Matrix44 instantiation if not required:
if self.is_post_transform_required:
self.post_transform(Matrix44.translate(dx, dy, dz))
return self
def to_ellipse(self, replace=True) -> Ellipse:
"""Convert the CIRCLE/ARC entity to an :class:`~ezdxf.entities.Ellipse` entity.
Adds the new ELLIPSE entity to the entity database and to the same layout as
the source entity.
Args:
replace: replace (delete) source entity by ELLIPSE entity if ``True``
"""
from ezdxf.entities import Ellipse
layout = self.get_layout()
if layout is None:
raise DXFValueError("valid layout required")
ellipse = Ellipse.from_arc(self)
if replace:
replace_entity(self, ellipse, layout)
else:
add_entity(ellipse, layout)
return ellipse
def to_spline(self, replace=True) -> Spline:
"""Convert the CIRCLE/ARC entity to a :class:`~ezdxf.entities.Spline` entity.
Adds the new SPLINE entity to the entity database and to the same layout as the
source entity.
Args:
replace: replace (delete) source entity by SPLINE entity if ``True``
"""
from ezdxf.entities import Spline
layout = self.get_layout()
if layout is None:
raise DXFValueError("valid layout required")
spline = Spline.from_arc(self)
if replace:
replace_entity(self, spline, layout)
else:
add_entity(spline, layout)
return spline

View File

@@ -0,0 +1,102 @@
# Copyright (c) 2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, NamedTuple, Optional
from copy import deepcopy
import logging
from ezdxf.lldxf.const import DXFError
if TYPE_CHECKING:
from ezdxf.entities import DXFEntity
__all__ = ["CopyStrategy", "CopySettings", "CopyNotSupported", "default_copy"]
T = TypeVar("T", bound="DXFEntity")
class CopyNotSupported(DXFError):
pass
class CopySettings(NamedTuple):
reset_handles: bool = True
copy_extension_dict: bool = True
copy_xdata: bool = True
copy_appdata: bool = True
copy_reactors: bool = False
copy_proxy_graphic: bool = True
set_source_of_copy: bool = True
# The processing of copy errors of linked entities has to be done in the
# copy_data() method by the entity itself!
ignore_copy_errors_in_linked_entities: bool = True
class LogMessage(NamedTuple):
message: str
level: int = logging.WARNING
entity: Optional[DXFEntity] = None
class CopyStrategy:
log: list[LogMessage] = []
def __init__(self, settings: CopySettings) -> None:
self.settings = settings
def copy(self, entity: T) -> T:
"""Entity copy for usage in the same document or as virtual entity.
This copy is NOT stored in the entity database and does NOT reside in any
layout, block, table or objects section!
"""
settings = self.settings
clone = entity.__class__()
doc = entity.doc
clone.doc = doc
clone.dxf = entity.dxf.copy(clone)
if settings.reset_handles:
clone.dxf.reset_handles()
if settings.copy_extension_dict:
xdict = entity.extension_dict
if xdict is not None and doc is not None and xdict.is_alive:
# Error handling of unsupported entities in the extension dictionary is
# done by the underlying DICTIONARY entity.
clone.extension_dict = xdict.copy(self)
if settings.copy_reactors and entity.reactors is not None:
clone.reactors = entity.reactors.copy()
if settings.copy_proxy_graphic:
clone.proxy_graphic = entity.proxy_graphic # immutable bytes
# if appdata contains handles, they are treated as shared resources
if settings.copy_appdata:
clone.appdata = deepcopy(entity.appdata)
# if xdata contains handles, they are treated as shared resources
if settings.copy_xdata:
clone.xdata = deepcopy(entity.xdata)
if settings.set_source_of_copy:
clone.set_source_of_copy(entity)
entity.copy_data(clone, copy_strategy=self)
return clone
@classmethod
def add_log_message(
cls, msg: str, level: int = logging.WARNING, entity: Optional[DXFEntity] = None
) -> None:
cls.log.append(LogMessage(msg, level, entity))
@classmethod
def clear_log_message(cls) -> None:
cls.log.clear()
# same strategy as DXFEntity.copy() of v1.1.3
default_copy = CopyStrategy(CopySettings())

View File

@@ -0,0 +1,688 @@
# Copyright (c) 2019-2024, Manfred Moitzi
# License: MIT-License
from __future__ import annotations
from typing import TYPE_CHECKING, Union, Optional
from typing_extensions import Self
import logging
from ezdxf.lldxf import validator
from ezdxf.lldxf.const import (
SUBCLASS_MARKER,
DXFKeyError,
DXFValueError,
DXFTypeError,
DXFStructureError,
)
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf.types import is_valid_handle
from ezdxf.audit import AuditError
from ezdxf.entities import factory, DXFGraphic
from .dxfentity import base_class, SubclassProcessor, DXFEntity
from .dxfobj import DXFObject
from .copy import default_copy, CopyNotSupported
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace, XRecord
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.document import Drawing
from ezdxf.audit import Auditor
from ezdxf import xref
__all__ = ["Dictionary", "DictionaryWithDefault", "DictionaryVar"]
logger = logging.getLogger("ezdxf")
acdb_dictionary = DefSubclass(
"AcDbDictionary",
{
# If hard_owned is set to 1 the entries are owned by the DICTIONARY.
# The 1 state seems to be the default value, but is not documented by
# the DXF reference.
# BricsCAD creates the root DICTIONARY and the top level DICTIONARY entries
# without group code 280 tags, and they are all definitely hard owner of their
# entries, because their entries have the DICTIONARY handle as owner handle.
"hard_owned": DXFAttr(
280,
default=1,
optional=True,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# Duplicate record cloning flag (determines how to merge duplicate entries):
# 0 = not applicable
# 1 = keep existing
# 2 = use clone
# 3 = <xref>$0$<name>
# 4 = $0$<name>
# 5 = Unmangle name
"cloning": DXFAttr(
281,
default=1,
validator=validator.is_in_integer_range(0, 6),
fixer=RETURN_DEFAULT,
),
# 3: entry name
# 350: entry handle, some DICTIONARY objects have 360 as handle group code,
# this is accepted by AutoCAD but not documented by the DXF reference!
# ezdxf replaces group code 360 by 350.
# - group code 350 is a soft-owner handle
# - group code 360 is a hard-owner handle
},
)
acdb_dictionary_group_codes = group_code_mapping(acdb_dictionary)
KEY_CODE = 3
VALUE_CODE = 350
# Some DICTIONARY use group code 360:
SEARCH_CODES = (VALUE_CODE, 360)
@factory.register_entity
class Dictionary(DXFObject):
"""AutoCAD maintains items such as mline styles and group definitions as
objects in dictionaries. Other applications are free to create and use
their own dictionaries as they see fit. The prefix "ACAD_" is reserved
for use by AutoCAD applications.
Dictionary entries are (key, DXFEntity) pairs. DXFEntity could be a string,
because at loading time not all objects are already stored in the EntityDB,
and have to be acquired later.
"""
DXFTYPE = "DICTIONARY"
DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary)
def __init__(self) -> None:
super().__init__()
self._data: dict[str, Union[str, DXFObject]] = dict()
self._value_code = VALUE_CODE
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
"""Copy hard owned entities but do not store the copies in the entity
database, this is a second step (factory.bind), this is just real copying.
"""
assert isinstance(entity, Dictionary)
entity._value_code = self._value_code
if self.dxf.hard_owned:
# Reactors are removed from the cloned DXF objects.
data: dict[str, DXFEntity] = dict()
for key, ent in self.items():
# ignore strings and None - these entities do not exist
# in the entity database
if isinstance(ent, DXFEntity):
try: # todo: follow CopyStrategy.ignore_copy_errors_in_linked entities
data[key] = ent.copy(copy_strategy=copy_strategy)
except CopyNotSupported:
if copy_strategy.settings.ignore_copy_errors_in_linked_entities:
logger.warning(
f"copy process ignored {str(ent)} - this may cause problems in AutoCAD"
)
else:
raise
entity._data = data # type: ignore
else:
entity._data = dict(self._data)
def get_handle_mapping(self, clone: Dictionary) -> dict[str, str]:
"""Returns handle mapping for in-object copies."""
handle_mapping: dict[str, str] = dict()
if not self.is_hard_owner:
return handle_mapping
for key, entity in self.items():
if not isinstance(entity, DXFEntity):
continue
copied_entry = clone.get(key)
if copied_entry:
handle_mapping[entity.dxf.handle] = copied_entry.dxf.handle
return handle_mapping
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
assert isinstance(clone, Dictionary)
super().map_resources(clone, mapping)
if self.is_hard_owner:
return
data = dict()
for key, entity in self.items():
if not isinstance(entity, DXFEntity):
continue
entity_copy = mapping.get_reference_of_copy(entity.dxf.handle)
if entity_copy:
data[key] = entity
clone._data = data # type: ignore
def del_source_of_copy(self) -> None:
super().del_source_of_copy()
for _, entity in self.items():
if isinstance(entity, DXFEntity) and entity.is_alive:
entity.del_source_of_copy()
def post_bind_hook(self) -> None:
"""Called by binding a new or copied dictionary to the document,
bind hard owned sub-entities to the same document and add them to the
objects section.
"""
if not self.dxf.hard_owned:
return
# copied or new dictionary:
doc = self.doc
assert doc is not None
object_section = doc.objects
owner_handle = self.dxf.handle
for _, entity in self.items():
entity.dxf.owner = owner_handle
factory.bind(entity, doc)
# For a correct DXF export add entities to the objects section:
object_section.add_object(entity)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
tags = processor.fast_load_dxfattribs(
dxf, acdb_dictionary_group_codes, 1, log=False
)
self.load_dict(tags)
return dxf
def load_dict(self, tags):
entry_handle = None
dict_key = None
value_code = VALUE_CODE
for code, value in tags:
if code in SEARCH_CODES:
# First store handles, because at this point, NOT all objects
# are stored in the EntityDB, at first access convert the handle
# to a DXFEntity object.
value_code = code
entry_handle = value
elif code == KEY_CODE:
dict_key = value
if dict_key and entry_handle:
# Store entity as handle string:
self._data[dict_key] = entry_handle
entry_handle = None
dict_key = None
# Use same value code as loaded:
self._value_code = value_code
def post_load_hook(self, doc: Drawing) -> None:
super().post_load_hook(doc)
db = doc.entitydb
def items():
for key, handle in self.items():
entity = db.get(handle)
if entity is not None and entity.is_alive:
yield key, entity
if len(self):
for k, v in list(items()):
self.__setitem__(k, v)
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dictionary.name)
self.dxf.export_dxf_attribs(tagwriter, ["hard_owned", "cloning"])
self.export_dict(tagwriter)
def export_dict(self, tagwriter: AbstractTagWriter):
# key: dict key string
# value: DXFEntity or handle as string
# Ignore invalid handles at export, because removing can create an empty
# dictionary, which is more a problem for AutoCAD than invalid handles,
# and removing the whole dictionary is maybe also a problem.
for key, value in self._data.items():
tagwriter.write_tag2(KEY_CODE, key)
# Value can be a handle string or a DXFEntity object:
if isinstance(value, DXFEntity):
if value.is_alive:
value = value.dxf.handle
else:
logger.debug(
f'Key "{key}" points to a destroyed entity '
f'in {str(self)}, target replaced by "0" handle.'
)
value = "0"
# Use same value code as loaded:
tagwriter.write_tag2(self._value_code, value)
@property
def is_hard_owner(self) -> bool:
"""Returns ``True`` if the dictionary is hard owner of entities.
Hard owned entities will be destroyed by deleting the dictionary.
"""
return bool(self.dxf.hard_owned)
def keys(self):
"""Returns a :class:`KeysView` of all dictionary keys."""
return self._data.keys()
def items(self):
"""Returns an :class:`ItemsView` for all dictionary entries as
(key, entity) pairs. An entity can be a handle string if the entity
does not exist.
"""
for key in self.keys():
yield key, self.get(key) # maybe handle -> DXFEntity
def __getitem__(self, key: str) -> DXFEntity:
"""Return self[`key`].
The returned value can be a handle string if the entity does not exist.
Raises:
DXFKeyError: `key` does not exist
"""
if key in self._data:
return self._data[key] # type: ignore
else:
raise DXFKeyError(key)
def __setitem__(self, key: str, entity: DXFObject) -> None:
"""Set self[`key`] = `entity`.
Only DXF objects stored in the OBJECTS section are allowed as content
of :class:`Dictionary` objects. DXF entities stored in layouts are not
allowed.
Raises:
DXFTypeError: invalid DXF type
"""
return self.add(key, entity)
def __delitem__(self, key: str) -> None:
"""Delete self[`key`].
Raises:
DXFKeyError: `key` does not exist
"""
return self.remove(key)
def __contains__(self, key: str) -> bool:
"""Returns `key` ``in`` self."""
return key in self._data
def __len__(self) -> int:
"""Returns count of dictionary entries."""
return len(self._data)
count = __len__
def get(self, key: str, default: Optional[DXFObject] = None) -> Optional[DXFObject]:
"""Returns the :class:`DXFEntity` for `key`, if `key` exist else
`default`. An entity can be a handle string if the entity
does not exist.
"""
return self._data.get(key, default) # type: ignore
def find_key(self, entity: DXFEntity) -> str:
"""Returns the DICTIONARY key string for `entity` or an empty string if not
found.
"""
for key, entry in self._data.items():
if entry is entity:
return key
return ""
def add(self, key: str, entity: DXFObject) -> None:
"""Add entry (key, value).
If the DICTIONARY is hard owner of its entries, the :meth:`add` does NOT take
ownership of the entity automatically.
Raises:
DXFValueError: invalid entity handle
DXFTypeError: invalid DXF type
"""
if isinstance(entity, str):
if not is_valid_handle(entity):
raise DXFValueError(f"Invalid entity handle #{entity} for key {key}")
elif isinstance(entity, DXFGraphic):
if self.doc is not None and self.doc.is_loading: # type: ignore
# AutoCAD add-ons can store graphical entities in DICTIONARIES
# in the OBJECTS section and AutoCAD does not complain - so just
# preserve them!
# Example "ZJMC-288.dxf" in issue #585, add-on: "acdgnlsdraw.crx"?
logger.warning(f"Invalid entity {str(entity)} in {str(self)}")
else:
# Do not allow ezdxf users to add graphical entities to a
# DICTIONARY object!
raise DXFTypeError(f"Graphic entities not allowed: {entity.dxftype()}")
self._data[key] = entity
def take_ownership(self, key: str, entity: DXFObject):
"""Add entry (key, value) and take ownership."""
self.add(key, entity)
entity.dxf.owner = self.dxf.handle
def remove(self, key: str) -> None:
"""Delete entry `key`. Raises :class:`DXFKeyError`, if `key` does not
exist. Destroys hard owned DXF entities.
"""
data = self._data
if key not in data:
raise DXFKeyError(key)
if self.is_hard_owner:
assert self.doc is not None
entity = self.__getitem__(key)
# Presumption: hard owned DXF objects always reside in the OBJECTS
# section.
self.doc.objects.delete_entity(entity) # type: ignore
del data[key]
def discard(self, key: str) -> None:
"""Delete entry `key` if exists. Does not raise an exception if `key`
doesn't exist and does not destroy hard owned DXF entities.
"""
try:
del self._data[key]
except KeyError:
pass
def clear(self) -> None:
"""Delete all entries from the dictionary and destroys hard owned
DXF entities.
"""
if self.is_hard_owner:
self._delete_hard_owned_entries()
self._data.clear()
def _delete_hard_owned_entries(self) -> None:
# Presumption: hard owned DXF objects always reside in the OBJECTS section
objects = self.doc.objects # type: ignore
for key, entity in self.items():
if isinstance(entity, DXFEntity):
objects.delete_entity(entity) # type: ignore
def add_new_dict(self, key: str, hard_owned: bool = False) -> Dictionary:
"""Create a new sub-dictionary of type :class:`Dictionary`.
Args:
key: name of the sub-dictionary
hard_owned: entries of the new dictionary are hard owned
"""
dxf_dict = self.doc.objects.add_dictionary( # type: ignore
owner=self.dxf.handle, hard_owned=hard_owned
)
self.add(key, dxf_dict)
return dxf_dict
def add_dict_var(self, key: str, value: str) -> DictionaryVar:
"""Add a new :class:`DictionaryVar`.
Args:
key: entry name as string
value: entry value as string
"""
new_var = self.doc.objects.add_dictionary_var( # type: ignore
owner=self.dxf.handle, value=value
)
self.add(key, new_var)
return new_var
def add_xrecord(self, key: str) -> XRecord:
"""Add a new :class:`XRecord`.
Args:
key: entry name as string
"""
new_xrecord = self.doc.objects.add_xrecord( # type: ignore
owner=self.dxf.handle,
)
self.add(key, new_xrecord)
return new_xrecord
def set_or_add_dict_var(self, key: str, value: str) -> DictionaryVar:
"""Set or add new :class:`DictionaryVar`.
Args:
key: entry name as string
value: entry value as string
"""
if key not in self:
dict_var = self.doc.objects.add_dictionary_var( # type: ignore
owner=self.dxf.handle, value=value
)
self.add(key, dict_var)
else:
dict_var = self.get(key)
dict_var.dxf.value = str(value) # type: ignore
return dict_var
def link_dxf_object(self, name: str, obj: DXFObject) -> None:
"""Add `obj` and set owner of `obj` to this dictionary.
Graphical DXF entities have to reside in a layout and therefore can not
be owned by a :class:`Dictionary`.
Raises:
DXFTypeError: `obj` has invalid DXF type
"""
if not isinstance(obj, DXFObject):
raise DXFTypeError(f"invalid DXF type: {obj.dxftype()}")
self.add(name, obj)
obj.dxf.owner = self.dxf.handle
def get_required_dict(self, key: str, hard_owned=False) -> Dictionary:
"""Get entry `key` or create a new :class:`Dictionary`,
if `Key` not exist.
"""
dxf_dict = self.get(key)
if dxf_dict is None:
dxf_dict = self.add_new_dict(key, hard_owned=hard_owned)
elif not isinstance(dxf_dict, Dictionary):
raise DXFStructureError(
f"expected a DICTIONARY entity, got {str(dxf_dict)} for key: {key}"
)
return dxf_dict
def audit(self, auditor: Auditor) -> None:
if not self.is_alive:
return
super().audit(auditor)
self._remove_keys_to_missing_entities(auditor)
def _remove_keys_to_missing_entities(self, auditor: Auditor):
trash: list[str] = []
append = trash.append
db = auditor.entitydb
for key, entry in self._data.items():
if isinstance(entry, str):
if entry not in db:
append(key)
elif entry.is_alive:
if entry.dxf.handle not in db:
append(key)
continue
else: # entity is destroyed, remove key
append(key)
for key in trash:
del self._data[key]
auditor.fixed_error(
code=AuditError.INVALID_DICTIONARY_ENTRY,
message=f'Removed entry "{key}" with invalid handle in {str(self)}',
dxf_entity=self,
data=key,
)
def destroy(self) -> None:
if not self.is_alive:
return
if self.is_hard_owner:
self._delete_hard_owned_entries()
super().destroy()
acdb_dict_with_default = DefSubclass(
"AcDbDictionaryWithDefault",
{
"default": DXFAttr(340),
},
)
acdb_dict_with_default_group_codes = group_code_mapping(acdb_dict_with_default)
@factory.register_entity
class DictionaryWithDefault(Dictionary):
DXFTYPE = "ACDBDICTIONARYWDFLT"
DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary, acdb_dict_with_default)
def __init__(self) -> None:
super().__init__()
self._default: Optional[DXFObject] = None
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
super().copy_data(entity, copy_strategy=copy_strategy)
assert isinstance(entity, DictionaryWithDefault)
entity._default = self._default
def post_load_hook(self, doc: Drawing) -> None:
# Set _default to None if default object not exist - audit() replaces
# a not existing default object by a placeholder object.
# AutoCAD ignores not existing default objects!
self._default = doc.entitydb.get(self.dxf.default) # type: ignore
super().post_load_hook(doc)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(dxf, acdb_dict_with_default_group_codes, 2)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_with_default.name)
self.dxf.export_dxf_attribs(tagwriter, "default")
def __getitem__(self, key: str):
return self.get(key)
def get(self, key: str, default: Optional[DXFObject] = None) -> Optional[DXFObject]:
# `default` argument is ignored, exist only for API compatibility,
"""Returns :class:`DXFEntity` for `key` or the predefined dictionary
wide :attr:`dxf.default` entity if `key` does not exist or ``None``
if default value also not exist.
"""
return super().get(key, default=self._default)
def set_default(self, default: DXFObject) -> None:
"""Set dictionary wide default entry.
Args:
default: default entry as :class:`DXFEntity`
"""
self._default = default
self.dxf.default = self._default.dxf.handle
def audit(self, auditor: Auditor) -> None:
def create_missing_default_object():
placeholder = self.doc.objects.add_placeholder(owner=self.dxf.handle)
self.set_default(placeholder)
auditor.fixed_error(
code=AuditError.CREATED_MISSING_OBJECT,
message=f"Created missing default object in {str(self)}.",
)
if self._default is None or not self._default.is_alive:
if auditor.entitydb.locked:
auditor.add_post_audit_job(create_missing_default_object)
else:
create_missing_default_object()
super().audit(auditor)
acdb_dict_var = DefSubclass(
"DictionaryVariables",
{
"schema": DXFAttr(280, default=0),
# Object schema number (currently set to 0)
"value": DXFAttr(1, default=""),
},
)
acdb_dict_var_group_codes = group_code_mapping(acdb_dict_var)
@factory.register_entity
class DictionaryVar(DXFObject):
"""
DICTIONARYVAR objects are used by AutoCAD as a means to store named values
in the database for setvar / getvar purposes without the need to add entries
to the DXF HEADER section. System variables that are stored as
DICTIONARYVAR objects are the following:
- DEFAULTVIEWCATEGORY
- DIMADEC
- DIMASSOC
- DIMDSEP
- DRAWORDERCTL
- FIELDEVAL
- HALOGAP
- HIDETEXT
- INDEXCTL
- INDEXCTL
- INTERSECTIONCOLOR
- INTERSECTIONDISPLAY
- MSOLESCALE
- OBSCOLOR
- OBSLTYPE
- OLEFRAME
- PROJECTNAME
- SORTENTS
- UPDATETHUMBNAIL
- XCLIPFRAME
- XCLIPFRAME
"""
DXFTYPE = "DICTIONARYVAR"
DXFATTRIBS = DXFAttributes(base_class, acdb_dict_var)
@property
def value(self) -> str:
"""Get/set the value of the :class:`DictionaryVar` as string."""
return self.dxf.get("value", "")
@value.setter
def value(self, data: str) -> None:
self.dxf.set("value", str(data))
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(dxf, acdb_dict_var_group_codes, 1)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_var.name)
self.dxf.export_dxf_attribs(tagwriter, ["schema", "value"])

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,959 @@
# Copyright (c) 2019-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional
from typing_extensions import Self
import logging
from ezdxf.enums import MTextLineAlignment
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
VIRTUAL_TAG,
group_code_mapping,
RETURN_DEFAULT,
)
from ezdxf.lldxf import const
from ezdxf.lldxf.const import DXF12, DXF2007, DXF2000
from ezdxf.lldxf import validator
from ezdxf.render.arrows import ARROWS
from .dxfentity import SubclassProcessor, DXFEntity, base_class
from .layer import acdb_symbol_table_record
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf import xref
__all__ = ["DimStyle"]
logger = logging.getLogger("ezdxf")
acdb_dimstyle = DefSubclass(
"AcDbDimStyleTableRecord",
{
"name": DXFAttr(2, default="Standard", validator=validator.is_valid_table_name),
"flags": DXFAttr(70, default=0),
"dimpost": DXFAttr(3, default=""),
"dimapost": DXFAttr(4, default=""),
# Arrow names are the base data -> handle (DXF2000) is set at export
"dimblk": DXFAttr(5, default=""),
"dimblk1": DXFAttr(6, default=""),
"dimblk2": DXFAttr(7, default=""),
"dimscale": DXFAttr(40, default=1),
# 0 has a special but unknown meaning, handle as 1.0
"dimasz": DXFAttr(41, default=2.5),
"dimexo": DXFAttr(42, default=0.625),
"dimdli": DXFAttr(43, default=3.75),
"dimexe": DXFAttr(44, default=1.25),
"dimrnd": DXFAttr(45, default=0),
"dimdle": DXFAttr(46, default=0), # dimension line extension
"dimtp": DXFAttr(47, default=0),
"dimtm": DXFAttr(48, default=0),
# undocumented: length of extension line if fixed (dimfxlon = 1)
"dimfxl": DXFAttr(49, dxfversion=DXF2007, default=2.5),
# jog angle, Angle of oblique dimension line segment in jogged radius dimension
"dimjogang": DXFAttr(50, dxfversion=DXF2007, default=90, optional=True),
# measurement text height
"dimtxt": DXFAttr(140, default=2.5),
# center marks and center lines; 0 = off, <0 = center line, >0 = center mark
"dimcen": DXFAttr(141, default=2.5),
"dimtsz": DXFAttr(142, default=0),
"dimaltf": DXFAttr(143, default=0.03937007874),
# measurement length factor
"dimlfac": DXFAttr(144, default=1),
# text vertical position if dimtad=0
"dimtvp": DXFAttr(145, default=0),
"dimtfac": DXFAttr(146, default=1),
# default gap around the measurement text
"dimgap": DXFAttr(147, default=0.625),
"dimaltrnd": DXFAttr(148, dxfversion=DXF2000, default=0),
# 0=None, 1=canvas color, 2=dimtfillclr
"dimtfill": DXFAttr(69, dxfversion=DXF2007, default=0),
# color index for dimtfill==2
"dimtfillclr": DXFAttr(70, dxfversion=DXF2007, default=0),
"dimtol": DXFAttr(71, default=0),
"dimlim": DXFAttr(72, default=0),
# text inside horizontal
"dimtih": DXFAttr(73, default=0),
# text outside horizontal
"dimtoh": DXFAttr(74, default=0),
# suppress extension line 1
"dimse1": DXFAttr(75, default=0),
# suppress extension line 2
"dimse2": DXFAttr(76, default=0),
# text vertical location: 0=center; 1+2+3=above; 4=below
"dimtad": DXFAttr(77, default=1),
"dimzin": DXFAttr(78, default=8),
# dimazin:
# 0 = Displays all leading and trailing zeros
# 1 = Suppresses leading zeros in decimal dimensions (for example, 0.5000 becomes .5000)
# 2 = Suppresses trailing zeros in decimal dimensions (for example, 12.5000 becomes 12.5)
# 3 = Suppresses leading and trailing zeros (for example, 0.5000 becomes .5)
"dimazin": DXFAttr(79, default=3, dxfversion=DXF2000),
# dimarcsym: show arc symbol
# 0 = preceding text
# 1 = above text
# 2 = disable
"dimarcsym": DXFAttr(90, dxfversion=DXF2000, optional=True),
"dimalt": DXFAttr(170, default=0),
"dimaltd": DXFAttr(171, default=3),
"dimtofl": DXFAttr(172, default=1),
"dimsah": DXFAttr(173, default=0),
# force dimension text inside
"dimtix": DXFAttr(174, default=0),
"dimsoxd": DXFAttr(175, default=0),
# dimension line color
"dimclrd": DXFAttr(176, default=0),
# extension line color
"dimclre": DXFAttr(177, default=0),
# text color
"dimclrt": DXFAttr(178, default=0),
"dimadec": DXFAttr(179, dxfversion=DXF2000, default=2),
"dimunit": DXFAttr(270), # obsolete
"dimdec": DXFAttr(271, dxfversion=DXF2000, default=2),
# can appear multiple times ???
"dimtdec": DXFAttr(272, dxfversion=DXF2000, default=2),
"dimaltu": DXFAttr(273, dxfversion=DXF2000, default=2),
"dimalttd": DXFAttr(274, dxfversion=DXF2000, default=3),
# 0 = Decimal degrees
# 1 = Degrees/minutes/seconds
# 2 = Grad
# 3 = Radians
"dimaunit": DXFAttr(
275,
dxfversion=DXF2000,
default=0,
validator=validator.is_in_integer_range(0, 4),
fixer=RETURN_DEFAULT,
),
"dimfrac": DXFAttr(276, dxfversion=DXF2000, default=0),
"dimlunit": DXFAttr(277, dxfversion=DXF2000, default=2),
"dimdsep": DXFAttr(278, dxfversion=DXF2000, default=44),
# 0 = Moves the dimension line with dimension text
# 1 = Adds a leader when dimension text is moved
# 2 = Allows text to be moved freely without a leader
"dimtmove": DXFAttr(279, dxfversion=DXF2000, default=0),
# 0=center; 1=left; 2=right; 3=above ext1; 4=above ext2
"dimjust": DXFAttr(280, dxfversion=DXF2000, default=0),
# suppress first part of the dimension line
"dimsd1": DXFAttr(281, dxfversion=DXF2000, default=0),
# suppress second part of the dimension line
"dimsd2": DXFAttr(282, dxfversion=DXF2000, default=0),
"dimtolj": DXFAttr(283, dxfversion=DXF2000, default=0),
"dimtzin": DXFAttr(284, dxfversion=DXF2000, default=8),
"dimaltz": DXFAttr(285, dxfversion=DXF2000, default=0),
"dimalttz": DXFAttr(286, dxfversion=DXF2000, default=0),
"dimfit": DXFAttr(287), # obsolete, now use DIMATFIT and DIMTMOVE
"dimupt": DXFAttr(288, dxfversion=DXF2000, default=0),
# Determines how dimension text and arrows are arranged when space is
# not sufficient to place both within the extension lines.
# 0 = Places both text and arrows outside extension lines
# 1 = Moves arrows first, then text
# 2 = Moves text first, then arrows
# 3 = Moves either text or arrows, whichever fits best
"dimatfit": DXFAttr(289, dxfversion=DXF2000, default=3),
# undocumented: 1 = fixed extension line length
"dimfxlon": DXFAttr(290, dxfversion=DXF2007, default=0),
# Virtual tags are transformed at DXF export - for DIMSTYLE the
# resource names are exported as <name>_handle tags:
# virtual: set/get STYLE by name
"dimtxsty": DXFAttr(VIRTUAL_TAG, dxfversion=DXF2000),
# virtual: set/get leader arrow by block name
"dimldrblk": DXFAttr(VIRTUAL_TAG, dxfversion=DXF2000),
# virtual: set/get LINETYPE by name
"dimltype": DXFAttr(VIRTUAL_TAG, dxfversion=DXF2007),
# virtual: set/get referenced LINETYPE by name
"dimltex2": DXFAttr(VIRTUAL_TAG, dxfversion=DXF2007),
# virtual: set/get referenced LINETYPE by name
"dimltex1": DXFAttr(VIRTUAL_TAG, dxfversion=DXF2007),
# Entity handles are not used internally (see virtual tags above),
# these handles are set at DXF export:
# handle of referenced STYLE entry
"dimtxsty_handle": DXFAttr(340, dxfversion=DXF2000),
# handle of referenced BLOCK_RECORD
"dimblk_handle": DXFAttr(342, dxfversion=DXF2000),
# handle of referenced BLOCK_RECORD
"dimblk1_handle": DXFAttr(343, dxfversion=DXF2000),
# handle of referenced BLOCK_RECORD
"dimblk2_handle": DXFAttr(344, dxfversion=DXF2000),
# handle of referenced BLOCK_RECORD
"dimldrblk_handle": DXFAttr(341, dxfversion=DXF2000),
# handle of linetype for dimension line
"dimltype_handle": DXFAttr(345, dxfversion=DXF2007),
# handle of linetype for extension line 1
"dimltex1_handle": DXFAttr(346, dxfversion=DXF2007),
# handle of linetype for extension line 2
"dimltex2_handle": DXFAttr(347, dxfversion=DXF2007),
# dimension line lineweight enum value, default BYBLOCK
"dimlwd": DXFAttr(371, default=const.LINEWEIGHT_BYBLOCK, dxfversion=DXF2000),
# extension line lineweight enum value, default BYBLOCK
"dimlwe": DXFAttr(372, default=const.LINEWEIGHT_BYBLOCK, dxfversion=DXF2000),
},
)
acdb_dimstyle_group_codes = group_code_mapping(acdb_dimstyle)
EXPORT_MAP_R2007 = [
"name",
"flags",
"dimscale",
"dimasz",
"dimexo",
"dimdli",
"dimexe",
"dimrnd",
"dimdle",
"dimtp",
"dimtm",
"dimfxl",
"dimjogang",
"dimtxt",
"dimcen",
"dimtsz",
"dimaltf",
"dimlfac",
"dimtvp",
"dimtfac",
"dimgap",
"dimaltrnd",
"dimtfill",
"dimtfillclr",
"dimtol",
"dimlim",
"dimtih",
"dimtoh",
"dimse1",
"dimse2",
"dimtad",
"dimzin",
"dimazin",
"dimarcsym",
"dimalt",
"dimaltd",
"dimtofl",
"dimsah",
"dimtix",
"dimsoxd",
"dimclrd",
"dimclre",
"dimclrt",
"dimadec",
"dimdec",
"dimtdec",
"dimaltu",
"dimalttd",
"dimaunit",
"dimfrac",
"dimlunit",
"dimdsep",
"dimtmove",
"dimjust",
"dimsd1",
"dimsd2",
"dimtolj",
"dimtzin",
"dimaltz",
"dimalttz",
"dimupt",
"dimatfit",
"dimfxlon",
"dimtxsty_handle",
"dimldrblk_handle",
"dimblk_handle",
"dimblk1_handle",
"dimblk2_handle",
"dimltype_handle",
"dimltex1_handle",
"dimltex2_handle",
"dimlwd",
"dimlwe",
]
EXPORT_MAP_R2000 = [
"name",
"flags",
"dimpost",
"dimapost",
"dimscale",
"dimasz",
"dimexo",
"dimdli",
"dimexe",
"dimrnd",
"dimdle",
"dimtp",
"dimtm",
"dimtxt",
"dimcen",
"dimtsz",
"dimaltf",
"dimlfac",
"dimtvp",
"dimtfac",
"dimgap",
"dimaltrnd",
"dimtol",
"dimlim",
"dimtih",
"dimtoh",
"dimse1",
"dimse2",
"dimtad",
"dimzin",
"dimazin",
"dimarcsym",
"dimalt",
"dimaltd",
"dimtofl",
"dimsah",
"dimtix",
"dimsoxd",
"dimclrd",
"dimclre",
"dimclrt",
"dimadec",
"dimdec",
"dimtdec",
"dimaltu",
"dimalttd",
"dimaunit",
"dimfrac",
"dimlunit",
"dimdsep",
"dimtmove",
"dimjust",
"dimsd1",
"dimsd2",
"dimtolj",
"dimtzin",
"dimaltz",
"dimalttz",
"dimupt",
"dimatfit",
"dimtxsty_handle",
"dimldrblk_handle",
"dimblk_handle",
"dimblk1_handle",
"dimblk2_handle",
"dimlwd",
"dimlwe",
]
EXPORT_MAP_R12 = [
"name",
"flags",
"dimpost",
"dimapost",
"dimblk",
"dimblk1",
"dimblk2",
"dimscale",
"dimasz",
"dimexo",
"dimdli",
"dimexe",
"dimrnd",
"dimdle",
"dimtp",
"dimtm",
"dimtxt",
"dimcen",
"dimtsz",
"dimaltf",
"dimlfac",
"dimtvp",
"dimtfac",
"dimgap",
"dimtol",
"dimlim",
"dimtih",
"dimtoh",
"dimse1",
"dimse2",
"dimtad",
"dimzin",
"dimalt",
"dimaltd",
"dimtofl",
"dimsah",
"dimtix",
"dimsoxd",
"dimclrd",
"dimclre",
"dimclrt",
]
DIM_TEXT_STYLE_ATTR = "dimtxsty"
DIM_ARROW_HEAD_ATTRIBS = ("dimblk", "dimblk1", "dimblk2", "dimldrblk")
DIM_LINETYPE_ATTRIBS = ("dimltype", "dimltex1", "dimltex2")
def dim_filter(name: str) -> bool:
return name.startswith("dim")
@register_entity
class DimStyle(DXFEntity):
"""DXF BLOCK_RECORD table entity"""
DXFTYPE = "DIMSTYLE"
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_dimstyle)
CODE_TO_DXF_ATTRIB = dict(DXFATTRIBS.build_group_code_items(dim_filter))
@property
def dxfversion(self):
return self.doc.dxfversion
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
# group code 70 is used 2x, simple_dxfattribs_loader() can't be used!
processor.fast_load_dxfattribs(dxf, acdb_dimstyle_group_codes, 2)
return dxf
def post_load_hook(self, doc: Drawing) -> None:
# 2nd Loading stage: resolve handles to names.
# ezdxf uses names for blocks, linetypes and text style as internal
# data, handles are set at export.
super().post_load_hook(doc)
db = doc.entitydb
for attrib_name in DIM_ARROW_HEAD_ATTRIBS:
if self.dxf.hasattr(attrib_name):
continue
block_record_handle = self.dxf.get(attrib_name + "_handle")
if block_record_handle and block_record_handle != "0":
try:
name = db[block_record_handle].dxf.name
except KeyError:
logger.info(
f"Replace undefined block reference "
f"#{block_record_handle} by default arrow."
)
name = "" # default arrow name
else:
name = "" # default arrow name
self.dxf.set(attrib_name, name)
style_handle = self.dxf.get("dimtxsty_handle", None)
if style_handle and style_handle != "0":
try:
self.dxf.dimtxsty = db[style_handle].dxf.name
except (KeyError, AttributeError):
logger.info(f"Ignore undefined text style #{style_handle}.")
for attrib_name in DIM_LINETYPE_ATTRIBS:
lt_handle = self.dxf.get(attrib_name + "_handle", None)
if lt_handle and lt_handle != "0":
try:
name = db[lt_handle].dxf.name
except (KeyError, AttributeError):
logger.info(f"Ignore undefined line type #{lt_handle}.")
else:
self.dxf.set(attrib_name, name)
# Remove all handles, to be sure setting handles for resource names
# at export.
self.discard_handles()
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_symbol_table_record.name)
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_dimstyle.name)
if tagwriter.dxfversion > DXF12:
# Set handles from dimblk names:
self.set_handles()
if tagwriter.dxfversion == DXF12:
attribs = EXPORT_MAP_R12
elif tagwriter.dxfversion < DXF2007:
attribs = EXPORT_MAP_R2000
else:
attribs = EXPORT_MAP_R2007
self.dxf.export_dxf_attribs(tagwriter, attribs)
def register_resources(self, registry: xref.Registry) -> None:
"""Register required resources to the resource registry."""
assert self.doc is not None, "DIMSTYLE entity must be assigned to a document"
super().register_resources(registry)
# ezdxf uses names for blocks, linetypes and text style as internal data
# register text style
text_style_name = self.dxf.get(DIM_TEXT_STYLE_ATTR)
if text_style_name:
try:
style = self.doc.styles.get(text_style_name)
registry.add_entity(style)
except const.DXFTableEntryError:
pass
# register linetypes
for attr_name in DIM_LINETYPE_ATTRIBS:
ltype_name = self.dxf.get(attr_name)
if ltype_name is None:
continue
try:
ltype = self.doc.linetypes.get(ltype_name)
registry.add_entity(ltype)
except const.DXFTableEntryError:
pass
# Note: ACAD arrow head blocks are created automatically at export in set_blk_handle()
for attr_name in DIM_ARROW_HEAD_ATTRIBS:
arrow_name = self.dxf.get(attr_name)
if arrow_name is None:
continue
if not ARROWS.is_acad_arrow(arrow_name):
# user defined arrow head block
registry.add_block_name(arrow_name)
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
assert isinstance(clone, DimStyle)
super().map_resources(clone, mapping)
# ezdxf uses names for blocks, linetypes and text style as internal data
# map text style
text_style = self.dxf.get(DIM_TEXT_STYLE_ATTR)
if text_style:
clone.dxf.dimtxsty = mapping.get_text_style(text_style)
# map linetypes
for attr_name in DIM_LINETYPE_ATTRIBS:
ltype_name = self.dxf.get(attr_name)
if ltype_name:
clone.dxf.set(attr_name, mapping.get_linetype(ltype_name))
# Note: ACAD arrow head blocks are created automatically at export in set_blk_handle()
for attr_name in DIM_ARROW_HEAD_ATTRIBS:
arrow_name = self.dxf.get(attr_name)
if arrow_name is None:
continue
if not ARROWS.is_acad_arrow(arrow_name):
# user defined arrow head block
arrow_name = mapping.get_block_name(arrow_name)
clone.dxf.set(attr_name, arrow_name)
def set_handles(self):
style = self.dxf.get(DIM_TEXT_STYLE_ATTR)
if style:
self.dxf.dimtxsty_handle = self.doc.styles.get(style).dxf.handle
for attr_name in DIM_ARROW_HEAD_ATTRIBS:
block_name = self.dxf.get(attr_name)
if block_name:
self.set_blk_handle(attr_name + "_handle", block_name)
for attr_name in DIM_LINETYPE_ATTRIBS:
get_linetype = self.doc.linetypes.get
ltype_name = self.dxf.get(attr_name)
if ltype_name:
handle = get_linetype(ltype_name).dxf.handle
self.dxf.set(attr_name + "_handle", handle)
def discard_handles(self):
for attr in (
"dimblk",
"dimblk1",
"dimblk2",
"dimldrblk",
"dimltype",
"dimltex1",
"dimltex2",
"dimtxsty",
):
self.dxf.discard(attr + "_handle")
def set_blk_handle(self, attr: str, arrow_name: str) -> None:
if arrow_name == ARROWS.closed_filled:
# special arrow, no handle needed (is '0' if set)
# do not create block by default, this will be done if arrow is used
# and block record handle is not needed here
self.dxf.discard(attr)
return
assert self.doc is not None, "valid DXF document required"
blocks = self.doc.blocks
if ARROWS.is_acad_arrow(arrow_name):
# create block, because the block record handle is needed here
block_name = ARROWS.create_block(blocks, arrow_name)
else:
block_name = arrow_name
blk = blocks.get(block_name)
if blk is not None:
self.set_dxf_attrib(attr, blk.block_record_handle)
else:
raise const.DXFValueError(f'Block "{arrow_name}" does not exist.')
def get_arrow_block_name(self, name: str) -> str:
assert self.doc is not None, "valid DXF document required"
handle = self.get_dxf_attrib(name, None)
if handle in (None, "0"):
# unset handle or handle '0' is default closed filled arrow
return ARROWS.closed_filled
else:
block_name = get_block_name_by_handle(handle, self.doc)
# Returns standard arrow name or the user defined block name:
return ARROWS.arrow_name(block_name)
def set_linetypes(self, dimline=None, ext1=None, ext2=None) -> None:
if self.dxfversion < DXF2007:
logger.debug("Linetype support requires DXF R2007+.")
if dimline is not None:
self.dxf.dimltype = dimline
if ext1 is not None:
self.dxf.dimltex1 = ext1
if ext2 is not None:
self.dxf.dimltex2 = ext2
def print_dim_attribs(self) -> None:
attdef = self.DXFATTRIBS.get
for name, value in self.dxfattribs().items():
if name.startswith("dim"):
print(f"{name} ({attdef(name).code}) = {value}") # type: ignore
def copy_to_header(self, doc: Drawing):
"""Copy all dimension style variables to HEADER section of `doc`."""
attribs = self.dxfattribs()
header = doc.header
header["$DIMSTYLE"] = self.dxf.name
for name, value in attribs.items():
if name.startswith("dim"):
header_var = "$" + name.upper()
try:
header[header_var] = value
except const.DXFKeyError:
logger.debug(f"Unsupported header variable: {header_var}.")
def set_arrows(
self, blk: str = "", blk1: str = "", blk2: str = "", ldrblk: str = ""
) -> None:
"""Set arrows by block names or AutoCAD standard arrow names, set
DIMTSZ to ``0`` which disables tick.
Args:
blk: block/arrow name for both arrows, if DIMSAH is 0
blk1: block/arrow name for first arrow, if DIMSAH is 1
blk2: block/arrow name for second arrow, if DIMSAH is 1
ldrblk: block/arrow name for leader
"""
self.set_dxf_attrib("dimblk", blk)
self.set_dxf_attrib("dimblk1", blk1)
self.set_dxf_attrib("dimblk2", blk2)
self.set_dxf_attrib("dimldrblk", ldrblk)
self.set_dxf_attrib("dimtsz", 0) # use blocks
# only existing BLOCK definitions allowed
if self.doc:
blocks = self.doc.blocks
for b in (blk, blk1, blk2, ldrblk):
if ARROWS.is_acad_arrow(b): # not real blocks
ARROWS.create_block(blocks, b)
continue
if b and b not in blocks:
raise const.DXFValueError(f'BLOCK "{blk}" does not exist.')
def set_tick(self, size: float = 1) -> None:
"""Set tick `size`, which also disables arrows, a tick is just an
oblique stroke as marker.
Args:
size: arrow size in drawing units
"""
self.set_dxf_attrib("dimtsz", size)
def set_text_align(
self,
halign: Optional[str] = None,
valign: Optional[str] = None,
vshift: Optional[float] = None,
) -> None:
"""Set measurement text alignment, `halign` defines the horizontal
alignment (requires DXF R2000+), `valign` defines the vertical
alignment, `above1` and `above2` means above extension line 1 or 2 and
aligned with extension line.
Args:
halign: "left", "right", "center", "above1", "above2",
requires DXF R2000+
valign: "above", "center", "below"
vshift: vertical text shift, if `valign` is "center";
>0 shift upward,
<0 shift downwards
"""
if valign:
valign = valign.lower()
self.set_dxf_attrib("dimtad", const.DIMTAD[valign])
if valign == "center" and vshift is not None:
self.set_dxf_attrib("dimtvp", vshift)
if halign:
self.set_dxf_attrib("dimjust", const.DIMJUST[halign.lower()])
def set_text_format(
self,
prefix: str = "",
postfix: str = "",
rnd: Optional[float] = None,
dec: Optional[int] = None,
sep: Optional[str] = None,
leading_zeros: bool = True,
trailing_zeros: bool = True,
):
"""Set dimension text format, like prefix and postfix string, rounding
rule and number of decimal places.
Args:
prefix: Dimension text prefix text as string
postfix: Dimension text postfix text as string
rnd: Rounds all dimensioning distances to the specified value, for
instance, if DIMRND is set to 0.25, all distances round to the
nearest 0.25 unit. If you set DIMRND to 1.0, all distances round
to the nearest integer.
dec: Sets the number of decimal places displayed for the primary
units of a dimension, requires DXF R2000+
sep: "." or "," as decimal separator, requires DXF R2000+
leading_zeros: Suppress leading zeros for decimal dimensions
if ``False``
trailing_zeros: Suppress trailing zeros for decimal dimensions
if ``False``
"""
if prefix or postfix:
self.dxf.dimpost = prefix + "<>" + postfix
if rnd is not None:
self.dxf.dimrnd = rnd
# works only with decimal dimensions not inch and feet, US user set dimzin directly
if leading_zeros is not None or trailing_zeros is not None:
dimzin = 0
if leading_zeros is False:
dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
if trailing_zeros is False:
dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
self.dxf.dimzin = dimzin
if dec is not None:
self.dxf.dimdec = dec
if sep is not None:
self.dxf.dimdsep = ord(sep)
def set_dimline_format(
self,
color: Optional[int] = None,
linetype: Optional[str] = None,
lineweight: Optional[int] = None,
extension: Optional[float] = None,
disable1: Optional[bool] = None,
disable2: Optional[bool] = None,
):
"""Set dimension line properties
Args:
color: color index
linetype: linetype as string, requires DXF R2007+
lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm,
requires DXF R2000+
extension: extension length
disable1: ``True`` to suppress first part of dimension line,
requires DXF R2000+
disable2: ``True`` to suppress second part of dimension line,
requires DXF R2000+
"""
if color is not None:
self.dxf.dimclrd = color
if extension is not None:
self.dxf.dimdle = extension
if lineweight is not None:
self.dxf.dimlwd = lineweight
if disable1 is not None:
self.dxf.dimsd1 = disable1
if disable2 is not None:
self.dxf.dimsd2 = disable2
if linetype is not None:
self.dxf.dimltype = linetype
def set_extline_format(
self,
color: Optional[int] = None,
lineweight: Optional[int] = None,
extension: Optional[float] = None,
offset: Optional[float] = None,
fixed_length: Optional[float] = None,
):
"""Set common extension line attributes.
Args:
color: color index
lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
extension: extension length above dimension line
offset: offset from measurement point
fixed_length: set fixed length extension line, length below the
dimension line
"""
if color is not None:
self.dxf.dimclre = color
if extension is not None:
self.dxf.dimexe = extension
if offset is not None:
self.dxf.dimexo = offset
if lineweight is not None:
self.dxf.dimlwe = lineweight
if fixed_length is not None:
self.dxf.dimfxlon = 1
self.dxf.dimfxl = fixed_length
def set_extline1(self, linetype: Optional[str] = None, disable=False):
"""Set extension line 1 attributes.
Args:
linetype: linetype for extension line 1, requires DXF R2007+
disable: disable extension line 1 if ``True``
"""
if disable:
self.dxf.dimse1 = 1
if linetype is not None:
self.dxf.dimltex1 = linetype
def set_extline2(self, linetype: Optional[str] = None, disable=False):
"""Set extension line 2 attributes.
Args:
linetype: linetype for extension line 2, requires DXF R2007+
disable: disable extension line 2 if ``True``
"""
if disable:
self.dxf.dimse2 = 1
if linetype is not None:
self.dxf.dimltex2 = linetype
def set_tolerance(
self,
upper: float,
lower: Optional[float] = None,
hfactor: float = 1.0,
align: Optional[MTextLineAlignment] = None,
dec: Optional[int] = None,
leading_zeros: Optional[bool] = None,
trailing_zeros: Optional[bool] = None,
) -> None:
"""Set tolerance text format, upper and lower value, text height
factor, number of decimal places or leading and trailing zero
suppression.
Args:
upper: upper tolerance value
lower: lower tolerance value, if ``None`` same as upper
hfactor: tolerance text height factor in relation to the dimension
text height
align: tolerance text alignment enum :class:`ezdxf.enums.MTextLineAlignment`
requires DXF R2000+
dec: Sets the number of decimal places displayed,
requires DXF R2000+
leading_zeros: suppress leading zeros for decimal dimensions
if ``False``, requires DXF R2000+
trailing_zeros: suppress trailing zeros for decimal dimensions
if ``False``, requires DXF R2000+
"""
# Exclusive tolerances mode, disable limits
self.dxf.dimtol = 1
self.dxf.dimlim = 0
self.dxf.dimtp = float(upper)
if lower is not None:
self.dxf.dimtm = float(lower)
else:
self.dxf.dimtm = float(upper)
if hfactor is not None:
self.dxf.dimtfac = float(hfactor)
# Works only with decimal dimensions not inch and feet, US user set
# dimzin directly.
if leading_zeros is not None or trailing_zeros is not None:
dimtzin = 0
if leading_zeros is False:
dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
if trailing_zeros is False:
dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
self.dxf.dimtzin = dimtzin
if align is not None:
self.dxf.dimtolj = int()
if dec is not None:
self.dxf.dimtdec = int(dec)
def set_limits(
self,
upper: float,
lower: float,
hfactor: float = 1.0,
dec: Optional[int] = None,
leading_zeros: Optional[bool] = None,
trailing_zeros: Optional[bool] = None,
) -> None:
"""Set limits text format, upper and lower limit values, text height
factor, number of decimal places or leading and trailing zero
suppression.
Args:
upper: upper limit value added to measurement value
lower: lower limit value subtracted from measurement value
hfactor: limit text height factor in relation to the dimension
text height
dec: Sets the number of decimal places displayed,
requires DXF R2000+
leading_zeros: suppress leading zeros for decimal dimensions
if ``False``, requires DXF R2000+
trailing_zeros: suppress trailing zeros for decimal dimensions
if ``False``, requires DXF R2000+
"""
# Exclusive limits mode, disable tolerances
self.dxf.dimlim = 1
self.dxf.dimtol = 0
self.dxf.dimtp = float(upper)
self.dxf.dimtm = float(lower)
self.dxf.dimtfac = float(hfactor)
# Works only with decimal dimensions not inch and feet, US user set
# dimzin directly.
if leading_zeros is not None or trailing_zeros is not None:
dimtzin = 0
if leading_zeros is False:
dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
if trailing_zeros is False:
dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
self.dxf.dimtzin = dimtzin
self.dxf.dimtolj = 0 # set bottom as default
if dec is not None:
self.dxf.dimtdec = int(dec)
def __referenced_blocks__(self) -> Iterable[str]:
"""Support for "ReferencedBlocks" protocol."""
if self.doc:
blocks = self.doc.blocks
for attrib_name in ("dimblk", "dimblk1", "dimblk2", "dimldrblk"):
name = self.dxf.get(attrib_name, None)
if name:
block = blocks.get(name, None)
if block is not None:
yield block.block_record.dxf.handle
def get_block_name_by_handle(handle, doc: Drawing, default="") -> str:
try:
entry = doc.entitydb[handle]
except const.DXFKeyError:
block_name = default
else:
block_name = entry.dxf.name
return block_name

View File

@@ -0,0 +1,583 @@
# Copyright (c) 2019-2023 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Any, TYPE_CHECKING, Optional
from typing_extensions import Protocol
import logging
from ezdxf.enums import MTextLineAlignment
from ezdxf.lldxf import const
from ezdxf.lldxf.const import DXFAttributeError, DIMJUST, DIMTAD
from ezdxf.math import Vec3, UVec, UCS
from ezdxf.render.arrows import ARROWS
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.entities import DimStyle, Dimension
from ezdxf.render.dim_base import BaseDimensionRenderer
from ezdxf import xref
logger = logging.getLogger("ezdxf")
class SupportsOverride(Protocol):
def override(self) -> DimStyleOverride:
...
class DimStyleOverride:
def __init__(self, dimension: Dimension, override: Optional[dict] = None):
self.dimension = dimension
dim_style_name: str = dimension.get_dxf_attrib("dimstyle", "STANDARD")
self.dimstyle: DimStyle = self.doc.dimstyles.get(dim_style_name)
self.dimstyle_attribs: dict = self.get_dstyle_dict()
# Special ezdxf attributes beyond the DXF reference, therefore not
# stored in the DSTYLE data.
# These are only rendering effects or data transfer objects
# user_location: Vec3 - user location override if not None
# relative_user_location: bool - user location override relative to
# dimline center if True
# text_shift_h: float - shift text in text direction, relative to
# standard text location
# text_shift_v: float - shift text perpendicular to text direction,
# relative to standard text location
self.update(override or {})
@property
def doc(self) -> Drawing:
"""Drawing object (internal API)"""
return self.dimension.doc # type: ignore
@property
def dxfversion(self) -> str:
"""DXF version (internal API)"""
return self.doc.dxfversion
def get_dstyle_dict(self) -> dict:
"""Get XDATA section ACAD:DSTYLE, to override DIMSTYLE attributes for
this DIMENSION entity.
Returns a ``dict`` with DIMSTYLE attribute names as keys.
(internal API)
"""
return self.dimension.get_acad_dstyle(self.dimstyle)
def get(self, attribute: str, default: Any = None) -> Any:
"""Returns DIMSTYLE `attribute` from override dict
:attr:`dimstyle_attribs` or base :class:`DimStyle`.
Returns `default` value for attributes not supported by DXF R12. This
is a hack to use the same algorithm to render DXF R2000 and DXF R12
DIMENSION entities. But the DXF R2000 attributes are not stored in the
DXF R12 file! This method does not catch invalid attribute names!
Check debug log for ignored DIMSTYLE attributes.
"""
if attribute in self.dimstyle_attribs:
result = self.dimstyle_attribs[attribute]
else:
try:
result = self.dimstyle.get_dxf_attrib(attribute, default)
except DXFAttributeError:
result = default
return result
def pop(self, attribute: str, default: Any = None) -> Any:
"""Returns DIMSTYLE `attribute` from override dict :attr:`dimstyle_attribs` and
removes this `attribute` from override dict.
"""
value = self.get(attribute, default)
# delete just from override dict
del self[attribute]
return value
def update(self, attribs: dict) -> None:
"""Update override dict :attr:`dimstyle_attribs`.
Args:
attribs: ``dict`` of DIMSTYLE attributes
"""
self.dimstyle_attribs.update(attribs)
def __getitem__(self, key: str) -> Any:
"""Returns DIMSTYLE attribute `key`, see also :meth:`get`."""
return self.get(key)
def __setitem__(self, key: str, value: Any) -> None:
"""Set DIMSTYLE attribute `key` in :attr:`dimstyle_attribs`."""
self.dimstyle_attribs[key] = value
def __delitem__(self, key: str) -> None:
"""Deletes DIMSTYLE attribute `key` from :attr:`dimstyle_attribs`,
ignores :class:`KeyErrors` silently.
"""
try:
del self.dimstyle_attribs[key]
except KeyError: # silent discard
pass
def register_resources_r12(self, registry: xref.Registry) -> None:
# DXF R2000+ references overridden resources by group code 1005 handles in the
# XDATA section, which are automatically mapped by the parent class DXFEntity!
assert self.doc.dxfversion == const.DXF12
# register arrow heads
for attrib_name in ("dimblk", "dimblk1", "dimblk2", "dimldrblk"):
arrow_name = self.get(attrib_name, "")
if arrow_name:
# arrow head names will be renamed like user blocks
# e.g. "_DOT" -> "xref$0$_DOT"
registry.add_block_name(ARROWS.block_name(arrow_name))
# linetype and text style attributes are not supported by DXF R12!
def map_resources_r12(
self, copy: SupportsOverride, mapping: xref.ResourceMapper
) -> None:
# DXF R2000+ references overridden resources by group code 1005 handles in the
# XDATA section, which are automatically mapped by the parent class DXFEntity!
assert self.doc.dxfversion == const.DXF12
copy_override = copy.override()
# map arrow heads
for attrib_name in ("dimblk", "dimblk1", "dimblk2", "dimldrblk"):
arrow_name = self.get(attrib_name, "")
if arrow_name:
block_name = mapping.get_block_name(ARROWS.block_name(arrow_name))
copy_override[attrib_name] = ARROWS.arrow_name(block_name)
copy_override.commit()
# The linetype attributes dimltype, dimltex1 and dimltex2 and the text style
# attribute dimtxsty are not supported by DXF R12!
#
# Weired behavior for DXF R12 detected
# ------------------------------------
# BricsCAD writes the handles of overridden linetype- and text style attributes
# into the ACAD-DSTYLE dictionary like for DXF R2000+, but exports the table
# entries without handles, so remapping of these handles is only possible if the
# application (which loads this DXF file) assigns internally the same handles
# as BricsCAD does and this also works with Autodesk TrueView (oO = wtf!).
# Ezdxf cannot remap these handles!
def commit(self) -> None:
"""Writes overridden DIMSTYLE attributes into ACAD:DSTYLE section of
XDATA of the DIMENSION entity.
"""
self.dimension.set_acad_dstyle(self.dimstyle_attribs)
def set_arrows(
self,
blk: Optional[str] = None,
blk1: Optional[str] = None,
blk2: Optional[str] = None,
ldrblk: Optional[str] = None,
size: Optional[float] = None,
) -> None:
"""Set arrows or user defined blocks and disable oblique stroke as tick.
Args:
blk: defines both arrows at once as name str or user defined block
blk1: defines left arrow as name str or as user defined block
blk2: defines right arrow as name str or as user defined block
ldrblk: defines leader arrow as name str or as user defined block
size: arrow size in drawing units
"""
def set_arrow(dimvar: str, name: str) -> None:
self.dimstyle_attribs[dimvar] = name
if size is not None:
self.dimstyle_attribs["dimasz"] = float(size)
if blk is not None:
set_arrow("dimblk", blk)
self.dimstyle_attribs["dimsah"] = 0
self.dimstyle_attribs["dimtsz"] = 0.0 # use arrows
if blk1 is not None:
set_arrow("dimblk1", blk1)
self.dimstyle_attribs["dimsah"] = 1
self.dimstyle_attribs["dimtsz"] = 0.0 # use arrows
if blk2 is not None:
set_arrow("dimblk2", blk2)
self.dimstyle_attribs["dimsah"] = 1
self.dimstyle_attribs["dimtsz"] = 0.0 # use arrows
if ldrblk is not None:
set_arrow("dimldrblk", ldrblk)
def get_arrow_names(self) -> tuple[str, str]:
"""Get arrow names as strings like 'ARCHTICK' as tuple (dimblk1, dimblk2)."""
dimtsz = self.get("dimtsz", 0)
blk1, blk2 = "", ""
if dimtsz == 0.0:
if bool(self.get("dimsah")):
blk1 = self.get("dimblk1", "")
blk2 = self.get("dimblk2", "")
else:
blk = self.get("dimblk", "")
blk1 = blk
blk2 = blk
return blk1, blk2
def get_decimal_separator(self) -> str:
dimdsep: int = self.get("dimdsep", 0)
return "," if dimdsep == 0 else chr(dimdsep)
def set_tick(self, size: float = 1) -> None:
"""Use oblique stroke as tick, disables arrows.
Args:
size: arrow size in daring units
"""
self.dimstyle_attribs["dimtsz"] = float(size)
def set_text_align(
self,
halign: Optional[str] = None,
valign: Optional[str] = None,
vshift: Optional[float] = None,
) -> None:
"""Set measurement text alignment, `halign` defines the horizontal
alignment, `valign` defines the vertical alignment, `above1` and
`above2` means above extension line 1 or 2 and aligned with extension
line.
Args:
halign: `left`, `right`, `center`, `above1`, `above2`,
requires DXF R2000+
valign: `above`, `center`, `below`
vshift: vertical text shift, if `valign` is `center`;
>0 shift upward, <0 shift downwards
"""
if halign:
self.dimstyle_attribs["dimjust"] = DIMJUST[halign.lower()]
if valign:
valign = valign.lower()
self.dimstyle_attribs["dimtad"] = DIMTAD[valign]
if valign == "center" and vshift is not None:
self.dimstyle_attribs["dimtvp"] = float(vshift)
def set_tolerance(
self,
upper: float,
lower: Optional[float] = None,
hfactor: Optional[float] = None,
align: Optional[MTextLineAlignment] = None,
dec: Optional[int] = None,
leading_zeros: Optional[bool] = None,
trailing_zeros: Optional[bool] = None,
) -> None:
"""Set tolerance text format, upper and lower value, text height
factor, number of decimal places or leading and trailing zero
suppression.
Args:
upper: upper tolerance value
lower: lower tolerance value, if None same as upper
hfactor: tolerance text height factor in relation to the dimension
text height
align: tolerance text alignment enum :class:`ezdxf.enums.MTextLineAlignment`
dec: Sets the number of decimal places displayed
leading_zeros: suppress leading zeros for decimal dimensions if ``False``
trailing_zeros: suppress trailing zeros for decimal dimensions if ``False``
"""
self.dimstyle_attribs["dimtol"] = 1
self.dimstyle_attribs["dimlim"] = 0
self.dimstyle_attribs["dimtp"] = float(upper)
if lower is not None:
self.dimstyle_attribs["dimtm"] = float(lower)
else:
self.dimstyle_attribs["dimtm"] = float(upper)
if hfactor is not None:
self.dimstyle_attribs["dimtfac"] = float(hfactor)
if align is not None:
self.dimstyle_attribs["dimtolj"] = int(align)
if dec is not None:
self.dimstyle_attribs["dimtdec"] = dec
# Works only with decimal dimensions not inch and feet, US user set
# dimzin directly
if leading_zeros is not None or trailing_zeros is not None:
dimtzin = 0
if leading_zeros is False:
dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
if trailing_zeros is False:
dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
self.dimstyle_attribs["dimtzin"] = dimtzin
def set_limits(
self,
upper: float,
lower: float,
hfactor: Optional[float] = None,
dec: Optional[int] = None,
leading_zeros: Optional[bool] = None,
trailing_zeros: Optional[bool] = None,
) -> None:
"""Set limits text format, upper and lower limit values, text
height factor, number of decimal places or leading and trailing zero
suppression.
Args:
upper: upper limit value added to measurement value
lower: lower limit value subtracted from measurement value
hfactor: limit text height factor in relation to the dimension
text height
dec: Sets the number of decimal places displayed,
requires DXF R2000+
leading_zeros: suppress leading zeros for decimal dimensions if
``False``, requires DXF R2000+
trailing_zeros: suppress trailing zeros for decimal dimensions if
``False``, requires DXF R2000+
"""
# exclusive limits
self.dimstyle_attribs["dimlim"] = 1
self.dimstyle_attribs["dimtol"] = 0
self.dimstyle_attribs["dimtp"] = float(upper)
self.dimstyle_attribs["dimtm"] = float(lower)
if hfactor is not None:
self.dimstyle_attribs["dimtfac"] = float(hfactor)
# Works only with decimal dimensions not inch and feet, US user set
# dimzin directly.
if leading_zeros is not None or trailing_zeros is not None:
dimtzin = 0
if leading_zeros is False:
dimtzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
if trailing_zeros is False:
dimtzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
self.dimstyle_attribs["dimtzin"] = dimtzin
if dec is not None:
self.dimstyle_attribs["dimtdec"] = int(dec)
def set_text_format(
self,
prefix: str = "",
postfix: str = "",
rnd: Optional[float] = None,
dec: Optional[int] = None,
sep: Optional[str] = None,
leading_zeros: Optional[bool] = None,
trailing_zeros: Optional[bool] = None,
) -> None:
"""Set dimension text format, like prefix and postfix string, rounding
rule and number of decimal places.
Args:
prefix: dimension text prefix text as string
postfix: dimension text postfix text as string
rnd: Rounds all dimensioning distances to the specified value, for
instance, if DIMRND is set to 0.25, all distances round to the
nearest 0.25 unit. If you set DIMRND to 1.0, all distances round
to the nearest integer.
dec: Sets the number of decimal places displayed for the primary
units of a dimension. requires DXF R2000+
sep: "." or "," as decimal separator
leading_zeros: suppress leading zeros for decimal dimensions if ``False``
trailing_zeros: suppress trailing zeros for decimal dimensions if ``False``
"""
if prefix or postfix:
self.dimstyle_attribs["dimpost"] = prefix + "<>" + postfix
if rnd is not None:
self.dimstyle_attribs["dimrnd"] = rnd
if dec is not None:
self.dimstyle_attribs["dimdec"] = dec
if sep is not None:
self.dimstyle_attribs["dimdsep"] = ord(sep)
# Works only with decimal dimensions not inch and feet, US user set
# dimzin directly.
if leading_zeros is not None or trailing_zeros is not None:
dimzin = 0
if leading_zeros is False:
dimzin = const.DIMZIN_SUPPRESSES_LEADING_ZEROS
if trailing_zeros is False:
dimzin += const.DIMZIN_SUPPRESSES_TRAILING_ZEROS
self.dimstyle_attribs["dimzin"] = dimzin
def set_dimline_format(
self,
color: Optional[int] = None,
linetype: Optional[str] = None,
lineweight: Optional[int] = None,
extension: Optional[float] = None,
disable1: Optional[bool] = None,
disable2: Optional[bool] = None,
):
"""Set dimension line properties.
Args:
color: color index
linetype: linetype as string
lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
extension: extension length
disable1: True to suppress first part of dimension line
disable2: True to suppress second part of dimension line
"""
if color is not None:
self.dimstyle_attribs["dimclrd"] = color
if linetype is not None:
self.dimstyle_attribs["dimltype"] = linetype
if lineweight is not None:
self.dimstyle_attribs["dimlwd"] = lineweight
if extension is not None:
self.dimstyle_attribs["dimdle"] = extension
if disable1 is not None:
self.dimstyle_attribs["dimsd1"] = disable1
if disable2 is not None:
self.dimstyle_attribs["dimsd2"] = disable2
def set_extline_format(
self,
color: Optional[int] = None,
lineweight: Optional[int] = None,
extension: Optional[float] = None,
offset: Optional[float] = None,
fixed_length: Optional[float] = None,
):
"""Set common extension line attributes.
Args:
color: color index
lineweight: line weight as int, 13 = 0.13mm, 200 = 2.00mm
extension: extension length above dimension line
offset: offset from measurement point
fixed_length: set fixed length extension line, length below the
dimension line
"""
if color is not None:
self.dimstyle_attribs["dimclre"] = color
if lineweight is not None:
self.dimstyle_attribs["dimlwe"] = lineweight
if extension is not None:
self.dimstyle_attribs["dimexe"] = extension
if offset is not None:
self.dimstyle_attribs["dimexo"] = offset
if fixed_length is not None:
self.dimstyle_attribs["dimfxlon"] = 1
self.dimstyle_attribs["dimfxl"] = fixed_length
def set_extline1(self, linetype: Optional[str] = None, disable=False):
"""Set attributes of the first extension line.
Args:
linetype: linetype for the first extension line
disable: disable first extension line if ``True``
"""
if linetype is not None:
self.dimstyle_attribs["dimltex1"] = linetype
if disable:
self.dimstyle_attribs["dimse1"] = 1
def set_extline2(self, linetype: Optional[str] = None, disable=False):
"""Set attributes of the second extension line.
Args:
linetype: linetype for the second extension line
disable: disable the second extension line if ``True``
"""
if linetype is not None:
self.dimstyle_attribs["dimltex2"] = linetype
if disable:
self.dimstyle_attribs["dimse2"] = 1
def set_text(self, text: str = "<>") -> None:
"""Set dimension text.
- `text` = " " to suppress dimension text
- `text` = "" or "<>" to use measured distance as dimension text
- otherwise display `text` literally
"""
self.dimension.dxf.text = text
def shift_text(self, dh: float, dv: float) -> None:
"""Set relative text movement, implemented as user location override
without leader.
Args:
dh: shift text in text direction
dv: shift text perpendicular to text direction
"""
self.dimstyle_attribs["text_shift_h"] = dh
self.dimstyle_attribs["text_shift_v"] = dv
def set_location(self, location: UVec, leader=False, relative=False) -> None:
"""Set text location by user, special version for linear dimensions,
behaves for other dimension types like :meth:`user_location_override`.
Args:
location: user defined text location
leader: create leader from text to dimension line
relative: `location` is relative to default location.
"""
self.user_location_override(location)
linear = self.dimension.dimtype < 2
curved = self.dimension.dimtype in (2, 5, 8)
if linear or curved:
self.dimstyle_attribs["dimtmove"] = 1 if leader else 2
self.dimstyle_attribs["relative_user_location"] = relative
def user_location_override(self, location: UVec) -> None:
"""Set text location by user, `location` is relative to the origin of
the UCS defined in the :meth:`render` method or WCS if the `ucs`
argument is ``None``.
"""
self.dimension.set_flag_state(
self.dimension.USER_LOCATION_OVERRIDE, state=True, name="dimtype"
)
self.dimstyle_attribs["user_location"] = Vec3(location)
def get_renderer(self, ucs: Optional[UCS] = None):
"""Get designated DIMENSION renderer. (internal API)"""
return self.doc.dimension_renderer.dispatch(self, ucs)
def render(self, ucs: Optional[UCS] = None, discard=False) -> BaseDimensionRenderer:
"""Starts the dimension line rendering process and also writes overridden
dimension style attributes into the DSTYLE XDATA section. The rendering process
"draws" the graphical representation of the DIMENSION entity as DXF primitives
(TEXT, LINE, ARC, ...) into an anonymous content BLOCK.
You can discard the content BLOCK for a friendly CAD applications like BricsCAD,
because the rendering of the dimension entity is done automatically by BricsCAD
if the content BLOCK is missing, and the result is in most cases better than the
rendering done by `ezdxf`.
AutoCAD does not render DIMENSION entities automatically, therefore I see
AutoCAD as an unfriendly CAD application.
Args:
ucs: user coordinate system
discard: discard the content BLOCK created by `ezdxf`, this works for
BricsCAD, AutoCAD refuses to open DXF files containing DIMENSION
entities without a content BLOCK
Returns:
The rendering object of the DIMENSION entity for analytics
"""
renderer = self.get_renderer(ucs)
if discard:
self.doc.add_acad_incompatibility_message(
"DIMENSION entity without geometry BLOCK (discard=True)"
)
else:
block = self.doc.blocks.new_anonymous_block(type_char="D")
self.dimension.dxf.geometry = block.name
renderer.render(block)
renderer.finalize()
if len(self.dimstyle_attribs):
self.commit()
return renderer

View File

@@ -0,0 +1,123 @@
# Copyright (c) 2019-2022 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from .dxfns import SubclassProcessor, DXFNamespace
from .dxfentity import DXFEntity
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
group_code_mapping,
)
from ezdxf.lldxf.const import DXF2004, DXF2000
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.lldxf.extendedtags import ExtendedTags
__all__ = ["DXFClass"]
class_def = DefSubclass(
None,
{
# Class DXF record name; always unique
"name": DXFAttr(1),
# C++ class name. Used to bind with software that defines object class
# behavior; always unique
"cpp_class_name": DXFAttr(2),
# Application name. Posted in Alert box when a class definition listed in
# this section is not currently loaded
"app_name": DXFAttr(3),
# Proxy capabilities flag. Bit-coded value that indicates the capabilities
# of this object as a proxy:
# 0 = No operations allowed (0)
# 1 = Erase allowed (0x1)
# 2 = Transform allowed (0x2)
# 4 = Color change allowed (0x4)
# 8 = Layer change allowed (0x8)
# 16 = Linetype change allowed (0x10)
# 32 = Linetype scale change allowed (0x20)
# 64 = Visibility change allowed (0x40)
# 128 = Cloning allowed (0x80)
# 256 = Lineweight change allowed (0x100)
# 512 = Plot Style Name change allowed (0x200)
# 895 = All operations except cloning allowed (0x37F)
# 1023 = All operations allowed (0x3FF)
# 1024 = Disables proxy warning dialog (0x400)
# 32768 = R13 format proxy (0x8000)
"flags": DXFAttr(90, default=0),
# Instance count for a custom class
"instance_count": DXFAttr(91, dxfversion=DXF2004, default=0),
# Was-a-proxy flag. Set to 1 if class was not loaded when this DXF file was
# created, and 0 otherwise
"was_a_proxy": DXFAttr(280, default=0),
# Is-an-entity flag. Set to 1 if class was derived from the AcDbEntity class
# and can reside in the BLOCKS or ENTITIES section. If 0, instances may
# appear only in the OBJECTS section
"is_an_entity": DXFAttr(281, default=0),
},
)
class_def_group_codes = group_code_mapping(class_def)
@register_entity
class DXFClass(DXFEntity):
DXFTYPE = "CLASS"
DXFATTRIBS = DXFAttributes(class_def)
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
@classmethod
def new(
cls,
handle: Optional[str] = None,
owner: Optional[str] = None,
dxfattribs=None,
doc: Optional[Drawing] = None,
) -> DXFClass:
"""New CLASS constructor - has no handle, no owner and do not need
document reference .
"""
dxf_class = cls()
dxf_class.doc = doc
dxfattribs = dxfattribs or {}
dxf_class.update_dxf_attribs(dxfattribs)
return dxf_class
def load_tags(
self, tags: ExtendedTags, dxfversion: Optional[str] = None
) -> None:
"""Called by load constructor. CLASS is special."""
if tags:
# do not process base class!!!
self.dxf = DXFNamespace(entity=self)
processor = SubclassProcessor(tags)
processor.fast_load_dxfattribs(
self.dxf, class_def_group_codes, 0, log=False
)
def export_dxf(self, tagwriter: AbstractTagWriter):
"""Do complete export here, because CLASS is special."""
dxfversion = tagwriter.dxfversion
if dxfversion < DXF2000:
return
attribs = self.dxf
tagwriter.write_tag2(0, self.DXFTYPE)
attribs.export_dxf_attribs(
tagwriter,
[
"name",
"cpp_class_name",
"app_name",
"flags",
"instance_count",
"was_a_proxy",
"is_an_entity",
],
)
@property
def key(self) -> tuple[str, str]:
return self.dxf.name, self.dxf.cpp_class_name

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,728 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Iterable, Any
from typing_extensions import Self, TypeGuard
from ezdxf.entities import factory
from ezdxf import options
from ezdxf.lldxf import validator
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf import colors as clr
from ezdxf.lldxf import const
from ezdxf.lldxf.const import (
DXF12,
DXF2000,
DXF2004,
DXF2007,
DXF2013,
SUBCLASS_MARKER,
TRANSPARENCY_BYBLOCK,
)
from ezdxf.math import OCS, Matrix44, UVec
from ezdxf.proxygraphic import load_proxy_graphic, export_proxy_graphic
from .dxfentity import DXFEntity, base_class, SubclassProcessor, DXFTagStorage
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.document import Drawing
from ezdxf.entities import DXFNamespace
from ezdxf.layouts import BaseLayout
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf import xref
__all__ = [
"DXFGraphic",
"acdb_entity",
"acdb_entity_group_codes",
"SeqEnd",
"add_entity",
"replace_entity",
"elevation_to_z_axis",
"is_graphic_entity",
"get_font_name",
]
GRAPHIC_PROPERTIES = {
"layer",
"linetype",
"color",
"lineweight",
"ltscale",
"true_color",
"color_name",
"transparency",
}
acdb_entity: DefSubclass = DefSubclass(
"AcDbEntity",
{
# Layer name as string, no auto fix for invalid names!
"layer": DXFAttr(8, default="0", validator=validator.is_valid_layer_name),
# Linetype name as string, no auto fix for invalid names!
"linetype": DXFAttr(
6,
default="BYLAYER",
optional=True,
validator=validator.is_valid_table_name,
),
# ACI color index, BYBLOCK=0, BYLAYER=256, BYOBJECT=257:
"color": DXFAttr(
62,
default=256,
optional=True,
validator=validator.is_valid_aci_color,
fixer=RETURN_DEFAULT,
),
# modelspace=0, paperspace=1
"paperspace": DXFAttr(
67,
default=0,
optional=True,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# Lineweight in mm times 100 (e.g. 0.13mm = 13). Smallest line weight is 13
# and biggest line weight is 200, values outside this range prevents AutoCAD
# from loading the file.
# Special values: BYLAYER=-1, BYBLOCK=-2, DEFAULT=-3
"lineweight": DXFAttr(
370,
default=-1,
dxfversion=DXF2000,
optional=True,
validator=validator.is_valid_lineweight,
fixer=validator.fix_lineweight,
),
"ltscale": DXFAttr(
48,
default=1.0,
dxfversion=DXF2000,
optional=True,
validator=validator.is_positive,
fixer=RETURN_DEFAULT,
),
# visible=0, invisible=1
"invisible": DXFAttr(60, default=0, dxfversion=DXF2000, optional=True),
# True color as 0x00RRGGBB 24-bit value
# True color always overrides ACI "color"!
"true_color": DXFAttr(420, dxfversion=DXF2004, optional=True),
# Color name as string. Color books are stored in .stb config files?
"color_name": DXFAttr(430, dxfversion=DXF2004, optional=True),
# Transparency value 0x020000TT 0 = fully transparent / 255 = opaque
# Special value 0x01000000 == ByBlock
# unset value means ByLayer
"transparency": DXFAttr(
440,
dxfversion=DXF2004,
optional=True,
validator=validator.is_transparency,
),
# Shadow mode:
# 0 = Casts and receives shadows
# 1 = Casts shadows
# 2 = Receives shadows
# 3 = Ignores shadows
"shadow_mode": DXFAttr(284, dxfversion=DXF2007, optional=True),
"material_handle": DXFAttr(347, dxfversion=DXF2007, optional=True),
"visualstyle_handle": DXFAttr(348, dxfversion=DXF2007, optional=True),
# PlotStyleName type enum (AcDb::PlotStyleNameType). Stored and moved around
# as a 16-bit integer. Custom non-entity
"plotstyle_enum": DXFAttr(380, dxfversion=DXF2007, default=1, optional=True),
# Handle value of the PlotStyleName object, basically a hard pointer, but
# has a different range to make backward compatibility easier to deal with.
"plotstyle_handle": DXFAttr(390, dxfversion=DXF2007, optional=True),
# 92 or 160?: Number of bytes in the proxy entity graphics represented in
# the subsequent 310 groups, which are binary chunk records (optional)
# 310: Proxy entity graphics data (multiple lines; 256 characters max. per
# line) (optional), compiled by TagCompiler() to a DXFBinaryTag() objects
},
)
acdb_entity_group_codes = group_code_mapping(acdb_entity)
def elevation_to_z_axis(dxf: DXFNamespace, names: Iterable[str]):
# The elevation group code (38) is only used for DXF R11 and prior and
# ignored for DXF R2000 and later.
# DXF R12 and later store the entity elevation in the z-axis of the
# vertices, but AutoCAD supports elevation for R12 if no z-axis is present.
# DXF types with legacy elevation support:
# SOLID, TRACE, TEXT, CIRCLE, ARC, TEXT, ATTRIB, ATTDEF, INSERT, SHAPE
# The elevation is only used for DXF R12 if no z-axis is stored in the DXF
# file. This is a problem because ezdxf loads the vertices always as 3D
# vertex including a z-axis even if no z-axis is present in DXF file.
if dxf.hasattr("elevation"):
elevation = dxf.elevation
# ezdxf does not export the elevation attribute for any DXF version
dxf.discard("elevation")
if elevation == 0:
return
for name in names:
v = dxf.get(name)
# Only use elevation value if z-axis is 0, this will not work for
# situations where an elevation and a z-axis=0 is present, but let's
# assume if the elevation group code is used the z-axis is not
# present if z-axis is 0.
if v is not None and v.z == 0:
dxf.set(name, v.replace(z=elevation))
class DXFGraphic(DXFEntity):
"""Common base class for all graphic entities, a subclass of
:class:`~ezdxf.entities.dxfentity.DXFEntity`. These entities resides in
entity spaces like modelspace, paperspace or block.
"""
DXFTYPE = "DXFGFX"
DEFAULT_ATTRIBS: dict[str, Any] = {"layer": "0"}
DXFATTRIBS = DXFAttributes(base_class, acdb_entity)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Adds subclass processing for 'AcDbEntity', requires previous base
class processing by parent class.
(internal API)
"""
# subclasses using simple_dxfattribs_loader() bypass this method!!!
dxf = super().load_dxf_attribs(processor)
if processor is None:
return dxf
r12 = processor.r12
# It is valid to mix up the base class with AcDbEntity class.
processor.append_base_class_to_acdb_entity()
# Load proxy graphic data if requested
if options.load_proxy_graphics:
# length tag has group code 92 until DXF R2010
if processor.dxfversion and processor.dxfversion < DXF2013:
code = 92
else:
code = 160
self.proxy_graphic = load_proxy_graphic(
processor.subclasses[0 if r12 else 1],
length_code=code,
)
processor.fast_load_dxfattribs(dxf, acdb_entity_group_codes, 1)
return dxf
def post_new_hook(self) -> None:
"""Post-processing and integrity validation after entity creation.
(internal API)
"""
if self.doc:
if self.dxf.linetype not in self.doc.linetypes:
raise const.DXFInvalidLineType(
f'Linetype "{self.dxf.linetype}" not defined.'
)
@property
def rgb(self) -> tuple[int, int, int] | None:
"""Returns RGB true color as (r, g, b) tuple or None if true_color is not set."""
if self.dxf.hasattr("true_color"):
return clr.int2rgb(self.dxf.get("true_color"))
return None
@rgb.setter
def rgb(self, rgb: clr.RGB | tuple[int, int, int]) -> None:
"""Set RGB true color as (r, g , b) tuple e.g. (12, 34, 56).
Raises:
TypeError: input value `rgb` has invalid type
"""
self.dxf.set("true_color", clr.rgb2int(rgb))
@rgb.deleter
def rgb(self) -> None:
"""Delete RGB true color value."""
self.dxf.discard("true_color")
@property
def transparency(self) -> float:
"""Get transparency as float value between 0 and 1, 0 is opaque and 1
is 100% transparent (invisible). Transparency by block returns always 0.
"""
if self.dxf.hasattr("transparency"):
value = self.dxf.get("transparency")
if validator.is_transparency(value):
if value & TRANSPARENCY_BYBLOCK: # just check flag state
return 0.0
return clr.transparency2float(value)
return 0.0
@transparency.setter
def transparency(self, transparency: float) -> None:
"""Set transparency as float value between 0 and 1, 0 is opaque and 1
is 100% transparent (invisible).
"""
self.dxf.set("transparency", clr.float2transparency(transparency))
@property
def is_transparency_by_layer(self) -> bool:
"""Returns ``True`` if entity inherits transparency from layer."""
return not self.dxf.hasattr("transparency")
@property
def is_transparency_by_block(self) -> bool:
"""Returns ``True`` if entity inherits transparency from block."""
return self.dxf.get("transparency", 0) == TRANSPARENCY_BYBLOCK
def graphic_properties(self) -> dict:
"""Returns the important common properties layer, color, linetype,
lineweight, ltscale, true_color and color_name as `dxfattribs` dict.
"""
attribs = dict()
for key in GRAPHIC_PROPERTIES:
if self.dxf.hasattr(key):
attribs[key] = self.dxf.get(key)
return attribs
def ocs(self) -> OCS:
"""Returns object coordinate system (:ref:`ocs`) for 2D entities like
:class:`Text` or :class:`Circle`, returns a pass-through OCS for
entities without OCS support.
"""
# extrusion is only defined for 2D entities like Text, Circle, ...
if self.dxf.is_supported("extrusion"):
extrusion = self.dxf.get("extrusion", default=(0, 0, 1))
return OCS(extrusion)
else:
return OCS()
def set_owner(self, owner: Optional[str], paperspace: int = 0) -> None:
"""Set owner attribute and paperspace flag. (internal API)"""
self.dxf.owner = owner
if paperspace:
self.dxf.paperspace = paperspace
else:
self.dxf.discard("paperspace")
def link_entity(self, entity: DXFEntity) -> None:
"""Store linked or attached entities. Same API for both types of
appended data, because entities with linked entities (POLYLINE, INSERT)
have no attached entities and vice versa.
(internal API)
"""
pass
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags. (internal API)"""
# Base class export is done by parent class.
self.export_acdb_entity(tagwriter)
# XDATA and embedded objects export is also done by the parent class.
def export_acdb_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export subclass 'AcDbEntity' as DXF tags. (internal API)"""
# Full control over tag order and YES, sometimes order matters
not_r12 = tagwriter.dxfversion > DXF12
if not_r12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name)
self.dxf.export_dxf_attribs(
tagwriter,
[
"paperspace",
"layer",
"linetype",
"material_handle",
"color",
"lineweight",
"ltscale",
"invisible",
"true_color",
"color_name",
"transparency",
"plotstyle_enum",
"plotstyle_handle",
"shadow_mode",
"visualstyle_handle",
],
)
if self.proxy_graphic and not_r12 and options.store_proxy_graphics:
# length tag has group code 92 until DXF R2010
export_proxy_graphic(
self.proxy_graphic,
tagwriter=tagwriter,
length_code=(92 if tagwriter.dxfversion < DXF2013 else 160),
)
def get_layout(self) -> Optional[BaseLayout]:
"""Returns the owner layout or returns ``None`` if entity is not
assigned to any layout.
"""
if self.dxf.owner is None or self.doc is None: # unlinked entity
return None
try:
return self.doc.layouts.get_layout_by_key(self.dxf.owner)
except const.DXFKeyError:
pass
try:
return self.doc.blocks.get_block_layout_by_handle(self.dxf.owner)
except const.DXFTableEntryError:
return None
def unlink_from_layout(self) -> None:
"""
Unlink entity from associated layout. Does nothing if entity is already
unlinked.
It is more efficient to call the
:meth:`~ezdxf.layouts.BaseLayout.unlink_entity` method of the associated
layout, especially if you have to unlink more than one entity.
"""
if not self.is_alive:
raise TypeError("Can not unlink destroyed entity.")
if self.doc is None:
# no doc -> no layout
self.dxf.owner = None
return
layout = self.get_layout()
if layout:
layout.unlink_entity(self)
def move_to_layout(
self, layout: BaseLayout, source: Optional[BaseLayout] = None
) -> None:
"""
Move entity from model space or a paper space layout to another layout.
For block layout as source, the block layout has to be specified. Moving
between different DXF drawings is not supported.
Args:
layout: any layout (model space, paper space, block)
source: provide source layout, faster for DXF R12, if entity is
in a block layout
Raises:
DXFStructureError: for moving between different DXF drawings
"""
if source is None:
source = self.get_layout()
if source is None:
raise const.DXFValueError("Source layout for entity not found.")
source.move_to_layout(self, layout)
def copy_to_layout(self, layout: BaseLayout) -> Self:
"""
Copy entity to another `layout`, returns new created entity as
:class:`DXFEntity` object. Copying between different DXF drawings is
not supported.
Args:
layout: any layout (model space, paper space, block)
Raises:
DXFStructureError: for copying between different DXF drawings
"""
if self.doc != layout.doc:
raise const.DXFStructureError(
"Copying between different DXF drawings is not supported."
)
new_entity = self.copy()
layout.add_entity(new_entity)
return new_entity
def audit(self, auditor: Auditor) -> None:
"""Audit and repair graphical DXF entities.
.. important::
Do not delete entities while auditing process, because this
would alter the entity database while iterating, instead use::
auditor.trash(entity)
to delete invalid entities after auditing automatically.
"""
assert self.doc is auditor.doc, "Auditor for different DXF document."
if not self.is_alive:
return
super().audit(auditor)
auditor.check_owner_exist(self)
dxf = self.dxf
if dxf.hasattr("layer"):
auditor.check_for_valid_layer_name(self)
if dxf.hasattr("linetype"):
auditor.check_entity_linetype(self)
if dxf.hasattr("color"):
auditor.check_entity_color_index(self)
if dxf.hasattr("lineweight"):
auditor.check_entity_lineweight(self)
if dxf.hasattr("extrusion"):
auditor.check_extrusion_vector(self)
if dxf.hasattr("transparency"):
auditor.check_transparency(self)
def transform(self, m: Matrix44) -> Self:
"""Inplace transformation interface, returns `self` (floating interface).
Args:
m: 4x4 transformation matrix (:class:`ezdxf.math.Matrix44`)
"""
raise NotImplementedError()
def post_transform(self, m: Matrix44) -> None:
"""Should be called if the main entity transformation was successful."""
if self.xdata is not None:
self.xdata.transform(m)
@property
def is_post_transform_required(self) -> bool:
"""Check if post transform call is required."""
return self.xdata is not None
def translate(self, dx: float, dy: float, dz: float) -> Self:
"""Translate entity inplace about `dx` in x-axis, `dy` in y-axis and
`dz` in z-axis, returns `self` (floating interface).
Basic implementation uses the :meth:`transform` interface, subclasses
may have faster implementations.
"""
return self.transform(Matrix44.translate(dx, dy, dz))
def scale(self, sx: float, sy: float, sz: float) -> Self:
"""Scale entity inplace about `dx` in x-axis, `dy` in y-axis and `dz`
in z-axis, returns `self` (floating interface).
"""
return self.transform(Matrix44.scale(sx, sy, sz))
def scale_uniform(self, s: float) -> Self:
"""Scale entity inplace uniform about `s` in x-axis, y-axis and z-axis,
returns `self` (floating interface).
"""
return self.transform(Matrix44.scale(s))
def rotate_axis(self, axis: UVec, angle: float) -> Self:
"""Rotate entity inplace about vector `axis`, returns `self`
(floating interface).
Args:
axis: rotation axis as tuple or :class:`Vec3`
angle: rotation angle in radians
"""
return self.transform(Matrix44.axis_rotate(axis, angle))
def rotate_x(self, angle: float) -> Self:
"""Rotate entity inplace about x-axis, returns `self`
(floating interface).
Args:
angle: rotation angle in radians
"""
return self.transform(Matrix44.x_rotate(angle))
def rotate_y(self, angle: float) -> Self:
"""Rotate entity inplace about y-axis, returns `self`
(floating interface).
Args:
angle: rotation angle in radians
"""
return self.transform(Matrix44.y_rotate(angle))
def rotate_z(self, angle: float) -> Self:
"""Rotate entity inplace about z-axis, returns `self`
(floating interface).
Args:
angle: rotation angle in radians
"""
return self.transform(Matrix44.z_rotate(angle))
def has_hyperlink(self) -> bool:
"""Returns ``True`` if entity has an attached hyperlink."""
return bool(self.xdata) and ("PE_URL" in self.xdata) # type: ignore
def set_hyperlink(
self,
link: str,
description: Optional[str] = None,
location: Optional[str] = None,
):
"""Set hyperlink of an entity."""
xdata = [(1001, "PE_URL"), (1000, str(link))]
if description:
xdata.append((1002, "{"))
xdata.append((1000, str(description)))
if location:
xdata.append((1000, str(location)))
xdata.append((1002, "}"))
self.discard_xdata("PE_URL")
self.set_xdata("PE_URL", xdata)
if self.doc and "PE_URL" not in self.doc.appids:
self.doc.appids.new("PE_URL")
return self
def get_hyperlink(self) -> tuple[str, str, str]:
"""Returns hyperlink, description and location."""
link = ""
description = ""
location = ""
if self.xdata and "PE_URL" in self.xdata:
xdata = [tag.value for tag in self.get_xdata("PE_URL") if tag.code == 1000]
if len(xdata):
link = xdata[0]
if len(xdata) > 1:
description = xdata[1]
if len(xdata) > 2:
location = xdata[2]
return link, description, location
def remove_dependencies(self, other: Optional[Drawing] = None) -> None:
"""Remove all dependencies from current document.
(internal API)
"""
if not self.is_alive:
return
super().remove_dependencies(other)
# The layer attribute is preserved because layer doesn't need a layer
# table entry, the layer attributes are reset to default attributes
# like color is 7 and linetype is CONTINUOUS
has_linetype = other is not None and (self.dxf.linetype in other.linetypes)
if not has_linetype:
self.dxf.linetype = "BYLAYER"
self.dxf.discard("material_handle")
self.dxf.discard("visualstyle_handle")
self.dxf.discard("plotstyle_enum")
self.dxf.discard("plotstyle_handle")
def _new_compound_entity(self, type_: str, dxfattribs) -> Self:
"""Create and bind new entity with same layout settings as `self`.
Used by INSERT & POLYLINE to create appended DXF entities, don't use it
to create new standalone entities.
(internal API)
"""
dxfattribs = dxfattribs or {}
# if layer is not deliberately set, set same layer as creator entity,
# at least VERTEX should have the same layer as the POLYGON entity.
# Don't know if that is also important for the ATTRIB & INSERT entity.
if "layer" not in dxfattribs:
dxfattribs["layer"] = self.dxf.layer
if self.doc:
entity = factory.create_db_entry(type_, dxfattribs, self.doc)
else:
entity = factory.new(type_, dxfattribs)
entity.dxf.owner = self.dxf.owner
entity.dxf.paperspace = self.dxf.paperspace
return entity # type: ignore
def register_resources(self, registry: xref.Registry) -> None:
"""Register required resources to the resource registry."""
super().register_resources(registry)
dxf = self.dxf
registry.add_layer(dxf.layer)
registry.add_linetype(dxf.linetype)
registry.add_handle(dxf.get("material_handle"))
# unsupported resource attributes:
# - visualstyle_handle
# - plotstyle_handle
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
"""Translate resources from self to the copied entity."""
super().map_resources(clone, mapping)
clone.dxf.layer = mapping.get_layer(self.dxf.layer)
attrib_exist = self.dxf.hasattr
if attrib_exist("linetype"):
clone.dxf.linetype = mapping.get_linetype(self.dxf.linetype)
if attrib_exist("material_handle"):
clone.dxf.material_handle = mapping.get_handle(self.dxf.material_handle)
# unsupported attributes:
clone.dxf.discard("visualstyle_handle")
clone.dxf.discard("plotstyle_handle")
@factory.register_entity
class SeqEnd(DXFGraphic):
DXFTYPE = "SEQEND"
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
# bypass DXFGraphic, loading proxy graphic is skipped!
dxf = super(DXFGraphic, self).load_dxf_attribs(processor)
if processor:
processor.simple_dxfattribs_loader(dxf, acdb_entity_group_codes) # type: ignore
return dxf
def add_entity(entity: DXFGraphic, layout: BaseLayout) -> None:
"""Add `entity` entity to the entity database and to the given `layout`."""
assert entity.dxf.handle is None
assert layout is not None
if layout.doc:
factory.bind(entity, layout.doc)
layout.add_entity(entity)
def replace_entity(source: DXFGraphic, target: DXFGraphic, layout: BaseLayout) -> None:
"""Add `target` entity to the entity database and to the given `layout`
and replace the `source` entity by the `target` entity.
"""
assert target.dxf.handle is None
assert layout is not None
target.dxf.handle = source.dxf.handle
if source in layout:
layout.delete_entity(source)
if layout.doc:
factory.bind(target, layout.doc)
layout.add_entity(target)
else:
source.destroy()
def is_graphic_entity(entity: DXFEntity) -> TypeGuard[DXFGraphic]:
"""Returns ``True`` if the `entity` has a graphical representations and
can reside in the model space, a paper space or a block layout,
otherwise the entity is a table or class entry or a DXF object from the
OBJECTS section.
"""
if isinstance(entity, DXFGraphic):
return True
if isinstance(entity, DXFTagStorage) and entity.is_graphic_entity:
return True
return False
def get_font_name(entity: DXFEntity) -> str:
"""Returns the font name of any DXF entity.
This function always returns a font name even if the entity doesn't support text
styles. The default font name is "txt".
"""
font_name = const.DEFAULT_TEXT_FONT
doc = entity.doc
if doc is None:
return font_name
try:
style_name = entity.dxf.get("style", const.DEFAULT_TEXT_STYLE)
except const.DXFAttributeError:
return font_name
try:
style = doc.styles.get(style_name)
return style.dxf.font
except const.DXFTableEntryError:
return font_name

View File

@@ -0,0 +1,455 @@
# Copyright (c) 2019-2024, Manfred Moitzi
# License: MIT-License
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Iterable,
Iterator,
cast,
Union,
Optional,
)
from contextlib import contextmanager
import logging
from ezdxf.lldxf import validator, const
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.audit import AuditError
from .dxfentity import base_class, SubclassProcessor, DXFEntity
from .dxfobj import DXFObject
from .factory import register_entity
from .objectcollection import ObjectCollection
from .copy import default_copy, CopyNotSupported
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.document import Drawing
from ezdxf.entities import DXFNamespace, Dictionary
from ezdxf.entitydb import EntityDB
from ezdxf.layouts import Layouts
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = ["DXFGroup", "GroupCollection"]
logger = logging.getLogger("ezdxf")
acdb_group = DefSubclass(
"AcDbGroup",
{
# Group description
"description": DXFAttr(300, default=""),
# 1 = Unnamed
# 0 = Named
"unnamed": DXFAttr(
70,
default=1,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# 1 = Selectable
# 0 = Not selectable
"selectable": DXFAttr(
71,
default=1,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# 340: Hard-pointer handle to entity in group (one entry per object)
},
)
acdb_group_group_codes = group_code_mapping(acdb_group)
GROUP_ITEM_CODE = 340
@register_entity
class DXFGroup(DXFObject):
"""Groups are not allowed in block definitions, and each entity can only
reside in one group, so cloning of groups creates also new entities.
"""
DXFTYPE = "GROUP"
DXFATTRIBS = DXFAttributes(base_class, acdb_group)
def __init__(self) -> None:
super().__init__()
self._handles: set[str] = set() # only needed at the loading stage
self._data: list[DXFEntity] = []
def copy(self, copy_strategy=default_copy):
raise CopyNotSupported("Copying of GROUP not supported.")
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
tags = processor.fast_load_dxfattribs(
dxf, acdb_group_group_codes, 1, log=False
)
self.load_group(tags)
return dxf
def load_group(self, tags):
for code, value in tags:
if code == GROUP_ITEM_CODE:
# First store handles, because at this point, objects
# are not stored in the EntityDB:
self._handles.add(value)
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
# remove invalid entities
assert self.doc is not None
self.purge(self.doc)
# export GROUP only if all entities reside on the same layout
if not all_entities_on_same_layout(self._data):
raise const.DXFStructureError(
"All entities have to be in the same layout and are not allowed"
" to be in a block layout."
)
return True
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_group.name)
self.dxf.export_dxf_attribs(tagwriter, ["description", "unnamed", "selectable"])
self.export_group(tagwriter)
def export_group(self, tagwriter: AbstractTagWriter):
for entity in self._data:
tagwriter.write_tag2(GROUP_ITEM_CODE, entity.dxf.handle)
def __iter__(self) -> Iterator[DXFEntity]:
"""Iterate over all DXF entities in :class:`DXFGroup` as instances of
:class:`DXFGraphic` or inherited (LINE, CIRCLE, ...).
"""
return (e for e in self._data if e.is_alive)
def __len__(self) -> int:
"""Returns the count of DXF entities in :class:`DXFGroup`."""
return len(self._data)
def __getitem__(self, item):
"""Returns entities by standard Python indexing and slicing."""
return self._data[item]
def __contains__(self, item: Union[str, DXFEntity]) -> bool:
"""Returns ``True`` if item is in :class:`DXFGroup`. `item` has to be
a handle string or an object of type :class:`DXFEntity` or inherited.
"""
handle = item if isinstance(item, str) else item.dxf.handle
return handle in set(self.handles())
def handles(self) -> Iterable[str]:
"""Iterable of handles of all DXF entities in :class:`DXFGroup`."""
return (entity.dxf.handle for entity in self)
def post_load_hook(self, doc: "Drawing"):
super().post_load_hook(doc)
db_get = doc.entitydb.get
def set_group_entities(): # post init command
name = str(self)
entities = filter_invalid_entities(self._data, self.doc, name)
if not all_entities_on_same_layout(entities):
self.clear()
logger.debug(f"Cleared {name}, had entities from different layouts.")
else:
self._data = entities
def entities():
for handle in self._handles:
entity = db_get(handle)
if entity and entity.is_alive:
yield entity
# Filtering invalid DXF entities is not possible at this stage, just
# store entities as they are:
self._data = list(entities())
del self._handles # all referenced entities are stored in data
return set_group_entities
@contextmanager # type: ignore
def edit_data(self) -> list[DXFEntity]: # type: ignore
"""Context manager which yields all the group entities as
standard Python list::
with group.edit_data() as data:
# add new entities to a group
data.append(modelspace.add_line((0, 0), (3, 0)))
# remove last entity from a group
data.pop()
"""
data = list(self)
yield data
self.set_data(data)
def _validate_entities(self, entities: Iterable[DXFEntity]) -> list[DXFEntity]:
assert self.doc is not None
entities = list(entities)
valid_entities = filter_invalid_entities(entities, self.doc, str(self))
if len(valid_entities) != len(entities):
raise const.DXFStructureError("invalid entities found")
if not all_entities_on_same_layout(valid_entities):
raise const.DXFStructureError(
"All entities have to be in the same layout and are not allowed"
" to be in a block layout."
)
return valid_entities
def set_data(self, entities: Iterable[DXFEntity]) -> None:
"""Set `entities` as new group content, entities should be an iterable of
:class:`DXFGraphic` (LINE, CIRCLE, ...).
Raises:
DXFValueError: not all entities are located on the same layout (modelspace
or any paperspace layout but not block)
"""
valid_entities = self._validate_entities(entities)
self.clear()
self._add_group_reactor(valid_entities)
self._data = valid_entities
def extend(self, entities: Iterable[DXFEntity]) -> None:
"""Add `entities` to :class:`DXFGroup`, entities should be an iterable of
:class:`DXFGraphic` (LINE, CIRCLE, ...).
Raises:
DXFValueError: not all entities are located on the same layout (modelspace
or any paperspace layout but not block)
"""
valid_entities = self._validate_entities(entities)
handles = set(self.handles())
valid_entities = [e for e in valid_entities if e.dxf.handle not in handles]
self._add_group_reactor(valid_entities)
self._data.extend(valid_entities)
def clear(self) -> None:
"""Remove all entities from :class:`DXFGroup`, does not delete any
drawing entities referenced by this group.
"""
# TODO: remove handle of GROUP entity from reactors of entities #1085
self._remove_group_reactor(self._data)
self._data = []
def _add_group_reactor(self, entities: list[DXFEntity]) -> None:
group_handle = self.dxf.handle
for entity in entities:
entity.append_reactor_handle(group_handle)
def _remove_group_reactor(self, entities: list[DXFEntity]) -> None:
group_handle = self.dxf.handle
for entity in entities:
entity.discard_reactor_handle(group_handle)
def audit(self, auditor: Auditor) -> None:
"""Remove invalid entities from :class:`DXFGroup`.
Invalid entities are:
- deleted entities
- all entities which do not reside in model- or paper space
- all entities if they do not reside in the same layout
"""
entity_count = len(self)
assert auditor.doc is not None
# Remove destroyed or invalid entities:
self.purge(auditor.doc)
removed_entity_count = entity_count - len(self)
if removed_entity_count > 0:
auditor.fixed_error(
code=AuditError.INVALID_GROUP_ENTITIES,
message=f"Removed {removed_entity_count} invalid entities from {str(self)}",
)
if not all_entities_on_same_layout(self._data):
auditor.fixed_error(
code=AuditError.GROUP_ENTITIES_IN_DIFFERENT_LAYOUTS,
message=f"Cleared {str(self)}, not all entities are located in "
f"the same layout.",
)
self.clear()
group_handle = self.dxf.handle
if not group_handle:
return
for entity in self._data:
if entity.reactors is None or group_handle not in entity.reactors:
auditor.fixed_error(
code=AuditError.MISSING_PERSISTENT_REACTOR,
message=f"Entity {entity} in group #{group_handle} does not have "
f"group as persistent reactor",
)
entity.append_reactor_handle(group_handle)
def purge(self, doc: Drawing) -> None:
"""Remove invalid group entities."""
self._data = filter_invalid_entities(
entities=self._data, doc=doc, group_name=str(self)
)
def filter_invalid_entities(
entities: Iterable[DXFEntity],
doc: Drawing,
group_name: str = "",
) -> list[DXFEntity]:
assert doc is not None
db = doc.entitydb
valid_owner_handles = valid_layout_handles(doc.layouts)
valid_entities: list[DXFEntity] = []
for entity in entities:
if entity.is_alive and _has_valid_owner(
entity.dxf.owner, db, valid_owner_handles
):
valid_entities.append(entity)
elif group_name:
if entity.is_alive:
logger.debug(f"{str(entity)} in {group_name} has an invalid owner.")
else:
logger.debug(f"Removed deleted entity in {group_name}")
return valid_entities
def _has_valid_owner(owner: str, db: EntityDB, valid_owner_handles: set[str]) -> bool:
# no owner -> no layout association
if owner is None:
return False
# The check for owner.dxf.layout != "0" is not sufficient #521
if valid_owner_handles and owner not in valid_owner_handles:
return False
layout = db.get(owner)
# owner layout does not exist or is destroyed -> no layout association
if layout is None or not layout.is_alive:
return False
# If "valid_owner_handles" is not empty, entities located on BLOCK
# layouts are already removed.
# DXF attribute block_record.layout is "0" if entity is located in a
# block definition, which is invalid:
return layout.dxf.layout != "0"
def all_entities_on_same_layout(entities: Iterable[DXFEntity]):
"""Check if all entities are on the same layout (model space or any paper
layout but not block).
"""
owners = set(entity.dxf.owner for entity in entities)
# 0 for no entities; 1 for all entities on the same layout
return len(owners) < 2
def valid_layout_handles(layouts: Layouts) -> set[str]:
"""Returns valid layout keys for group entities."""
return set(layout.layout_key for layout in layouts if layout.is_any_layout)
class GroupCollection(ObjectCollection[DXFGroup]):
def __init__(self, doc: Drawing):
super().__init__(doc, dict_name="ACAD_GROUP", object_type="GROUP")
self._next_unnamed_number = 0
def groups(self) -> Iterator[DXFGroup]:
"""Iterable of all existing groups."""
for name, group in self:
yield group
def next_name(self) -> str:
name = self._next_name()
while name in self:
name = self._next_name()
return name
def _next_name(self) -> str:
self._next_unnamed_number += 1
return f"*A{self._next_unnamed_number}"
def new(
self,
name: Optional[str] = None,
description: str = "",
selectable: bool = True,
) -> DXFGroup:
r"""Creates a new group. If `name` is ``None`` an unnamed group is
created, which has an automatically generated name like "\*Annnn".
Group names are case-insensitive.
Args:
name: group name as string
description: group description as string
selectable: group is selectable if ``True``
"""
if name is not None and name in self:
raise const.DXFValueError(f"GROUP '{name}' already exists.")
if name is None:
name = self.next_name()
unnamed = 1
else:
unnamed = 0
# The group name isn't stored in the group entity itself.
dxfattribs = {
"description": description,
"unnamed": unnamed,
"selectable": int(bool(selectable)),
}
return cast(DXFGroup, self._new(name, dxfattribs))
def delete(self, group: Union[DXFGroup, str]) -> None:
"""Delete `group`, `group` can be an object of type :class:`DXFGroup`
or a group name as string.
"""
entitydb = self.doc.entitydb
assert entitydb is not None
# Delete group by name:
if isinstance(group, str):
name = group
elif group.dxftype() == "GROUP":
name = get_group_name(group, entitydb)
else:
raise TypeError(group.dxftype())
if name in self:
super().delete(name)
else:
raise const.DXFValueError("GROUP not in group table registered.")
def audit(self, auditor: Auditor) -> None:
"""Removes empty groups and invalid handles from all groups."""
trash = []
for name, group in self:
group = cast(DXFGroup, group)
group.audit(auditor)
if not len(group): # remove empty group
# do not delete groups while iterating over groups!
trash.append(name)
# now delete empty groups
for name in trash:
auditor.fixed_error(
code=AuditError.REMOVE_EMPTY_GROUP,
message=f'Removed empty group "{name}".',
)
self.delete(name)
def get_group_name(group: DXFGroup, db: EntityDB) -> str:
"""Get name of `group`."""
group_table = cast("Dictionary", db[group.dxf.owner])
for name, entity in group_table.items():
if entity is group:
return name
return ""

View File

@@ -0,0 +1,648 @@
# Copyright (c) 2020-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Any, Optional, Union, Iterable, TYPE_CHECKING, Set
import logging
import itertools
from ezdxf import options
from ezdxf.lldxf import const
from ezdxf.lldxf.attributes import XType, DXFAttributes, DXFAttr
from ezdxf.lldxf.types import cast_value, dxftag
from ezdxf.lldxf.tags import Tags
if TYPE_CHECKING:
from ezdxf.lldxf.extendedtags import ExtendedTags
from ezdxf.entities import DXFEntity
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = ["DXFNamespace", "SubclassProcessor"]
logger = logging.getLogger("ezdxf")
ERR_INVALID_DXF_ATTRIB = 'Invalid DXF attribute "{}" for entity {}'
ERR_DXF_ATTRIB_NOT_EXITS = 'DXF attribute "{}" does not exist'
# supported event handler called by setting DXF attributes
# for usage, implement a method named like the dict-value, that accepts the new
# value as argument e.g.:
# Polyline.on_layer_change(name) -> changes also layers of all vertices
SETTER_EVENTS = {
"layer": "on_layer_change",
"linetype": "on_linetype_change",
"style": "on_style_change",
"dimstyle": "on_dimstyle_change",
}
EXCLUDE_FROM_UPDATE = frozenset(["_entity", "handle", "owner"])
class DXFNamespace:
""":class:`DXFNamespace` manages all named DXF attributes of an entity.
The DXFNamespace.__dict__ is used as DXF attribute storage, therefore only
valid Python names can be used as attrib name.
The namespace can only contain immutable objects: string, int, float, bool,
Vec3. Because of the immutability, copy and deepcopy are the same.
(internal class)
"""
def __init__(
self,
processor: Optional[SubclassProcessor] = None,
entity: Optional[DXFEntity] = None,
):
if processor:
base_class = processor.base_class
handle_code = 105 if base_class[0].value == "DIMSTYLE" else 5
# CLASS entities have no handle.
# TABLE entities have no handle if loaded from a DXF R12 file.
# Owner tag is None if loaded from a DXF R12 file
handle = None
owner = None
for tag in base_class:
group_code = tag.code
if group_code == handle_code:
handle = tag.value
if owner:
break
elif group_code == 330:
owner = tag.value
if handle:
break
self.rewire(entity, handle, owner)
else:
self.reset_handles()
self.rewire(entity)
def copy(self, entity: DXFEntity):
namespace = self.__class__()
for k, v in self.__dict__.items():
namespace.__dict__[k] = v
namespace.rewire(entity)
return namespace
def __deepcopy__(self, memodict: Optional[dict] = None):
return self.copy(self._entity)
def reset_handles(self):
"""Reset handle and owner to None."""
self.__dict__["handle"] = None
self.__dict__["owner"] = None
def rewire(
self,
entity: Optional[DXFEntity],
handle: Optional[str] = None,
owner: Optional[str] = None,
) -> None:
"""Rewire DXF namespace with parent entity
Args:
entity: new associated entity
handle: new handle or None
owner: new entity owner handle or None
"""
# bypass __setattr__()
self.__dict__["_entity"] = entity
if handle is not None:
self.__dict__["handle"] = handle
if owner is not None:
self.__dict__["owner"] = owner
def __getattr__(self, key: str) -> Any:
"""Called if DXF attribute `key` does not exist, returns the DXF
default value or ``None``.
Raises:
DXFAttributeError: attribute `key` is not supported
"""
attrib_def: Optional[DXFAttr] = self.dxfattribs.get(key)
if attrib_def:
if attrib_def.xtype == XType.callback:
return attrib_def.get_callback_value(self._entity)
else:
return attrib_def.default
else:
raise const.DXFAttributeError(
ERR_INVALID_DXF_ATTRIB.format(key, self.dxftype)
)
def __setattr__(self, key: str, value: Any) -> None:
"""Set DXF attribute `key` to `value`.
Raises:
DXFAttributeError: attribute `key` is not supported
"""
def entity() -> str:
# DXFNamespace is maybe not assigned to the entity yet:
handle = self.get("handle")
_entity = self._entity
if _entity:
return _entity.dxftype() + f"(#{handle})"
else:
return f"#{handle}"
def check(value):
value = cast_value(attrib_def.code, value)
if not attrib_def.is_valid_value(value):
if attrib_def.fixer:
value = attrib_def.fixer(value)
logger.debug(
f'Fixed invalid attribute "{key}" in entity'
f' {entity()} to "{str(value)}".'
)
else:
raise const.DXFValueError(
f'Invalid value {str(value)} for attribute "{key}" in '
f"entity {entity()}."
)
return value
attrib_def: Optional[DXFAttr] = self.dxfattribs.get(key)
if attrib_def:
if attrib_def.xtype == XType.callback:
attrib_def.set_callback_value(self._entity, value)
else:
self.__dict__[key] = check(value)
else:
raise const.DXFAttributeError(
ERR_INVALID_DXF_ATTRIB.format(key, self.dxftype)
)
if key in SETTER_EVENTS:
handler = getattr(self._entity, SETTER_EVENTS[key], None)
if handler:
handler(value)
def __delattr__(self, key: str) -> None:
"""Delete DXF attribute `key`.
Raises:
DXFAttributeError: attribute `key` does not exist
"""
if self.hasattr(key):
del self.__dict__[key]
else:
raise const.DXFAttributeError(ERR_DXF_ATTRIB_NOT_EXITS.format(key))
def get(self, key: str, default: Any = None) -> Any:
"""Returns value of DXF attribute `key` or the given `default` value
not DXF default value for unset attributes.
Raises:
DXFAttributeError: attribute `key` is not supported
"""
# callback values should not exist as attribute in __dict__
if self.hasattr(key):
# do not return the DXF default value
return self.__dict__[key]
attrib_def: Optional["DXFAttr"] = self.dxfattribs.get(key)
if attrib_def:
if attrib_def.xtype == XType.callback:
return attrib_def.get_callback_value(self._entity)
else:
return default # return give default
else:
raise const.DXFAttributeError(
ERR_INVALID_DXF_ATTRIB.format(key, self.dxftype)
)
def get_default(self, key: str) -> Any:
"""Returns DXF default value for unset DXF attribute `key`."""
value = self.get(key, None)
return self.dxf_default_value(key) if value is None else value
def set(self, key: str, value: Any) -> None:
"""Set DXF attribute `key` to `value`.
Raises:
DXFAttributeError: attribute `key` is not supported
"""
self.__setattr__(key, value)
def unprotected_set(self, key: str, value: Any) -> None:
"""Set DXF attribute `key` to `value` without any validity checks.
Used for fast attribute setting without validity checks at loading time.
(internal API)
"""
self.__dict__[key] = value
def all_existing_dxf_attribs(self) -> dict:
"""Returns all existing DXF attributes, except DXFEntity back-link."""
attribs = dict(self.__dict__)
del attribs["_entity"]
return attribs
def update(
self,
dxfattribs: dict[str, Any],
*,
exclude: Optional[Set[str]] = None,
ignore_errors=False,
) -> None:
"""Update DXF namespace attributes from a dict."""
if exclude is None:
exclude = EXCLUDE_FROM_UPDATE # type: ignore
else: # always exclude "_entity" back-link
exclude = {"_entity"} | exclude
set_attribute = self.__setattr__
for k, v in dxfattribs.items():
if k not in exclude: # type: ignore
try:
set_attribute(k, v)
except (AttributeError, ValueError):
if not ignore_errors:
raise
def discard(self, key: str) -> None:
"""Delete DXF attribute `key` silently without any exception."""
try:
del self.__dict__[key]
except KeyError:
pass
def is_supported(self, key: str) -> bool:
"""Returns True if DXF attribute `key` is supported else False.
Does not grant that attribute `key` really exists and does not
check if the actual DXF version of the document supports this
attribute, unsupported attributes will be ignored at export.
"""
return key in self.dxfattribs
def hasattr(self, key: str) -> bool:
"""Returns True if attribute `key` really exists else False."""
return key in self.__dict__
@property
def dxftype(self) -> str:
"""Returns the DXF entity type."""
return self._entity.DXFTYPE
@property
def dxfattribs(self) -> DXFAttributes:
"""Returns the DXF attribute definition."""
return self._entity.DXFATTRIBS
def dxf_default_value(self, key: str) -> Any:
"""Returns the default value as defined in the DXF standard."""
attrib: Optional[DXFAttr] = self.dxfattribs.get(key)
if attrib:
return attrib.default
else:
return None
def export_dxf_attribs(
self, tagwriter: AbstractTagWriter, attribs: Union[str, Iterable]
) -> None:
"""Exports DXF attribute `name` by `tagwriter`. Non-optional attributes
are forced and optional tags are only written if different to default
value. DXF version check is always on: does not export DXF attribs
which are not supported by tagwriter.dxfversion.
Args:
tagwriter: tag writer object
attribs: DXF attribute name as string or an iterable of names
"""
if isinstance(attribs, str):
self._export_dxf_attribute_optional(tagwriter, attribs)
else:
for name in attribs:
self._export_dxf_attribute_optional(tagwriter, name)
def _export_dxf_attribute_optional(
self, tagwriter: AbstractTagWriter, name: str
) -> None:
"""Exports DXF attribute `name` by `tagwriter`. Optional tags are only
written if different to default value.
Args:
tagwriter: tag writer object
name: DXF attribute name
"""
export_dxf_version = tagwriter.dxfversion
not_force_optional = not tagwriter.force_optional
attrib: Optional[DXFAttr] = self.dxfattribs.get(name)
if attrib:
optional = attrib.optional
default = attrib.default
value = self.get(name, None)
# Force default value e.g. layer
if value is None and not optional:
# Default value could be None
value = default
# Do not export None values
if (value is not None) and (
export_dxf_version >= attrib.dxfversion
):
# Do not write explicit optional attribs if equal to default
# value
if (
optional
and not_force_optional
and default is not None
and default == value
):
return
# Just export x, y for 2D points, if value is a 3D point
if attrib.xtype == XType.point2d and len(value) > 2:
try: # Vec3
value = (value.x, value.y)
except AttributeError:
value = value[:2]
if isinstance(value, str):
assert "\n" not in value, "line break '\\n' not allowed"
assert "\r" not in value, "line break '\\r' not allowed"
tag = dxftag(attrib.code, value)
tagwriter.write_tag(tag)
else:
raise const.DXFAttributeError(
ERR_INVALID_DXF_ATTRIB.format(name, self.dxftype)
)
BASE_CLASS_CODES = {0, 5, 102, 330}
class SubclassProcessor:
"""Helper class for loading tags into entities. (internal class)"""
def __init__(self, tags: ExtendedTags, dxfversion: Optional[str] = None):
if len(tags.subclasses) == 0:
raise ValueError("Invalid tags.")
self.subclasses: list[Tags] = list(tags.subclasses) # copy subclasses
self.embedded_objects: list[Tags] = tags.embedded_objects or []
self.dxfversion: Optional[str] = dxfversion
# DXF R12 and prior have no subclass marker system, all tags of an
# entity in one flat list.
# Later DXF versions have at least 2 subclasses base_class and
# AcDbEntity.
# Exception: CLASS has also only one subclass and no subclass marker,
# handled as DXF R12 entity
self.r12: bool = (dxfversion == const.DXF12) or (
len(self.subclasses) == 1
)
self.name: str = tags.dxftype()
self.handle: str
try:
self.handle = tags.get_handle()
except const.DXFValueError:
self.handle = "<?>"
@property
def base_class(self):
return self.subclasses[0]
def log_unprocessed_tags(
self,
unprocessed_tags: Iterable,
subclass="<?>",
handle: Optional[str] = None,
) -> None:
if options.log_unprocessed_tags:
for tag in unprocessed_tags:
entity = ""
if handle:
entity = f" in entity #{handle}"
logger.info(
f"ignored {repr(tag)} in subclass {subclass}" + entity
)
def find_subclass(self, name: str) -> Optional[Tags]:
for subclass in self.subclasses:
if len(subclass) and subclass[0].value == name:
return subclass
return None
def subclass_by_index(self, index: int) -> Optional[Tags]:
try:
return self.subclasses[index]
except IndexError:
return None
def detect_implementation_version(
self, subclass_index: int, group_code: int, default: int
) -> int:
subclass = self.subclass_by_index(subclass_index)
if subclass and len(subclass) > 1:
# the version tag has to be the 2nd tag after the subclass marker
tag = subclass[1]
if tag.code == group_code:
return tag.value
return default
# TODO: rename to complex_dxfattribs_loader()
def fast_load_dxfattribs(
self,
dxf: DXFNamespace,
group_code_mapping: dict[int, Union[str, list]],
subclass: Union[int, str, Tags],
*,
recover=False,
log=True,
) -> Tags:
"""Load DXF attributes into the DXF namespace and returns the
unprocessed tags without leading subclass marker(100, AcDb...).
Bypasses the DXF attribute validity checks.
Args:
dxf: entity DXF namespace
group_code_mapping: group code to DXF attribute name mapping,
callback attributes have to be marked with a leading "*"
subclass: subclass by index, by name or as Tags()
recover: recover graphic attributes
log: enable/disable logging of unprocessed tags
"""
if self.r12:
tags = self.subclasses[0]
else:
if isinstance(subclass, int):
tags = self.subclass_by_index(subclass) # type: ignore
elif isinstance(subclass, str):
tags = self.find_subclass(subclass) # type: ignore
else:
tags = subclass
unprocessed_tags = Tags()
if tags is None or len(tags) == 0:
return unprocessed_tags
processed_names: set[str] = set()
# Localize attributes:
get_attrib_name = group_code_mapping.get
append_unprocessed_tag = unprocessed_tags.append
unprotected_set_attrib = dxf.unprotected_set
mark_attrib_as_processed = processed_names.add
# Ignore (100, "AcDb...") or (0, "ENTITY") tag in case of DXF R12
start = 1 if tags[0].code in (0, 100) else 0
for tag in tags[start:]:
name = get_attrib_name(tag.code)
if isinstance(name, list): # process group code duplicates:
names = name
# If all existing attrib names are used, treat this tag
# like an unprocessed tag.
name = None
# The attribute names are processed in the order of their
# definition:
for name_ in names:
if name_ not in processed_names:
name = name_
mark_attrib_as_processed(name_)
break
if name:
# Ignore callback attributes and group codes explicit marked
# as "*IGNORE":
if name[0] != "*":
unprotected_set_attrib(
name, cast_value(tag.code, tag.value) # type: ignore
)
else:
append_unprocessed_tag(tag)
if self.r12:
# R12 has always unprocessed tags, because there are all tags in one
# subclass and one subclass definition never covers all tags e.g.
# handle is processed in DXFEntity, so it is an unprocessed tag in
# AcDbEntity.
return unprocessed_tags
# Only DXF R13+
if recover and len(unprocessed_tags):
# TODO: maybe obsolete if simple_dxfattribs_loader() is used for
# most old DXF R12 entities
unprocessed_tags = recover_graphic_attributes(unprocessed_tags, dxf)
if len(unprocessed_tags) and log:
# First tag is the subclass specifier (100, "AcDb...")
name = tags[0].value
self.log_unprocessed_tags(
tags, subclass=name, handle=dxf.get("handle")
)
return unprocessed_tags
def append_base_class_to_acdb_entity(self) -> None:
"""It is valid to mix up the base class with AcDbEntity class.
This method appends all none base class group codes to the
AcDbEntity class.
"""
# This is only needed for DXFEntity, so applying this method
# automatically to all entities is waste of runtime
# -> DXFGraphic.load_dxf_attribs()
# TODO: maybe obsolete if simple_dxfattribs_loader() is used for
# most old DXF R12 entities
if self.r12:
return
acdb_entity_tags = self.subclasses[1]
if acdb_entity_tags[0] == (100, "AcDbEntity"):
acdb_entity_tags.extend(
tag
for tag in self.subclasses[0]
if tag.code not in BASE_CLASS_CODES
)
def simple_dxfattribs_loader(
self, dxf: DXFNamespace, group_code_mapping: dict[int, str]
) -> None:
# tested in test suite 201 for the POINT entity
"""Load DXF attributes from all subclasses into the DXF namespace.
Can not handle same group codes in different subclasses, does not remove
processed tags or log unprocessed tags and bypasses the DXF attribute
validity checks.
This method ignores the subclass structure and can load data from
very malformed DXF files, like such in issue #604.
This method works only for very simple DXF entities with unique group
codes in all subclasses, the old DXF R12 entities:
- POINT
- LINE
- CIRCLE
- ARC
- INSERT
- SHAPE
- SOLID/TRACE/3DFACE
- TEXT (ATTRIB/ATTDEF bypasses TEXT loader)
- BLOCK/ENDBLK
- POLYLINE/VERTEX/SEQEND
- DIMENSION and subclasses
- all table entries: LAYER, LTYPE, ...
And the newer DXF entities:
- ELLIPSE
- RAY/XLINE
The recover mode for graphical attributes is automatically included.
Logging of unprocessed tags is not possible but also not required for
this simple and well known entities.
Args:
dxf: entity DXF namespace
group_code_mapping: group code name mapping for all DXF attributes
from all subclasses, callback attributes have to be marked with
a leading "*"
"""
tags = itertools.chain.from_iterable(self.subclasses)
get_attrib_name = group_code_mapping.get
unprotected_set_attrib = dxf.unprotected_set
for tag in tags:
name = get_attrib_name(tag.code)
if isinstance(name, str) and not name.startswith("*"):
unprotected_set_attrib(
name, cast_value(tag.code, tag.value)
)
GRAPHIC_ATTRIBUTES_TO_RECOVER = {
8: "layer",
6: "linetype",
62: "color",
67: "paperspace",
370: "lineweight",
48: "ltscale",
60: "invisible",
420: "true_color",
430: "color_name",
440: "transparency",
284: "shadow_mode",
347: "material_handle",
348: "visualstyle_handle",
380: "plotstyle_enum",
390: "plotstyle_handle",
}
# TODO: maybe obsolete if simple_dxfattribs_loader() is used for
# most old DXF R12 entities
def recover_graphic_attributes(tags: Tags, dxf: DXFNamespace) -> Tags:
unprocessed_tags = Tags()
for tag in tags:
attrib_name = GRAPHIC_ATTRIBUTES_TO_RECOVER.get(tag.code)
# Don't know if the unprocessed tag is really a misplaced tag,
# so check if the attribute already exist!
if attrib_name and not dxf.hasattr(attrib_name):
dxf.set(attrib_name, tag.value)
else:
unprocessed_tags.append(tag)
return unprocessed_tags

View File

@@ -0,0 +1,400 @@
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Union, Any, Optional
from typing_extensions import Self, TypeGuard
import logging
import array
from ezdxf.lldxf import validator
from ezdxf.lldxf.const import DXF2000, DXFStructureError, SUBCLASS_MARKER
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.types import dxftag, DXFTag, DXFBinaryTag
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.tools import take2
from .dxfentity import DXFEntity, base_class, SubclassProcessor, DXFTagStorage
from .factory import register_entity
from .copy import default_copy
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = [
"DXFObject",
"Placeholder",
"XRecord",
"VBAProject",
"SortEntsTable",
"Field",
"is_dxf_object",
]
logger = logging.getLogger("ezdxf")
class DXFObject(DXFEntity):
"""Non-graphical entities stored in the OBJECTS section."""
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
@register_entity
class Placeholder(DXFObject):
DXFTYPE = "ACDBPLACEHOLDER"
acdb_xrecord = DefSubclass(
"AcDbXrecord",
{
# 0 = not applicable
# 1 = keep existing
# 2 = use clone
# 3 = <xref>$0$<name>
# 4 = $0$<name>
# 5 = Unmangle name
"cloning": DXFAttr(
280,
default=1,
validator=validator.is_in_integer_range(0, 6),
fixer=RETURN_DEFAULT,
),
},
)
def totags(tags: Iterable) -> Iterable[DXFTag]:
for tag in tags:
if isinstance(tag, DXFTag):
yield tag
else:
yield dxftag(tag[0], tag[1])
@register_entity
class XRecord(DXFObject):
"""DXF XRECORD entity"""
DXFTYPE = "XRECORD"
DXFATTRIBS = DXFAttributes(base_class, acdb_xrecord)
def __init__(self):
super().__init__()
self.tags = Tags()
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, XRecord)
entity.tags = Tags(self.tags)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
try:
tags = processor.subclasses[1]
except IndexError:
raise DXFStructureError(
f"Missing subclass AcDbXrecord in XRecord (#{dxf.handle})"
)
start_index = 1
if len(tags) > 1:
# First tag is group code 280, but not for DXF R13/R14.
# SUT: doc may be None, but then doc also can not
# be R13/R14 - ezdxf does not create R13/R14
if self.doc is None or self.doc.dxfversion >= DXF2000:
code, value = tags[1]
if code == 280:
dxf.cloning = value
start_index = 2
else: # just log recoverable error
logger.info(
f"XRecord (#{dxf.handle}): expected group code 280 "
f"as first tag in AcDbXrecord"
)
self.tags = Tags(tags[start_index:])
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_xrecord.name)
tagwriter.write_tag2(280, self.dxf.cloning)
tagwriter.write_tags(Tags(totags(self.tags)))
def reset(self, tags: Iterable[Union[DXFTag, tuple[int, Any]]]) -> None:
"""Reset DXF tags."""
self.tags.clear()
self.tags.extend(totags(tags))
def extend(self, tags: Iterable[Union[DXFTag, tuple[int, Any]]]) -> None:
"""Extend DXF tags."""
self.tags.extend(totags(tags))
def clear(self) -> None:
"""Remove all DXF tags."""
self.tags.clear()
acdb_vba_project = DefSubclass(
"AcDbVbaProject",
{
# 90: Number of bytes of binary chunk data (contained in the group code
# 310 records that follow)
# 310: DXF: Binary object data (multiple entries containing VBA project
# data)
},
)
@register_entity
class VBAProject(DXFObject):
"""DXF VBA_PROJECT entity"""
DXFTYPE = "VBA_PROJECT"
DXFATTRIBS = DXFAttributes(base_class, acdb_vba_project)
def __init__(self):
super().__init__()
self.data = b""
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, VBAProject)
entity.data = entity.data
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
self.load_byte_data(processor.subclasses[1])
return dxf
def load_byte_data(self, tags: Tags) -> None:
byte_array = array.array("B")
# Translation from String to binary data happens in tag_compiler():
for byte_data in (tag.value for tag in tags if tag.code == 310):
byte_array.extend(byte_data)
self.data = byte_array.tobytes()
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_vba_project.name)
tagwriter.write_tag2(90, len(self.data))
self.export_data(tagwriter)
def export_data(self, tagwriter: AbstractTagWriter):
data = self.data
while data:
tagwriter.write_tag(DXFBinaryTag(310, data[:127]))
data = data[127:]
def clear(self) -> None:
self.data = b""
acdb_sort_ents_table = DefSubclass(
"AcDbSortentsTable",
{
# Soft-pointer ID/handle to owner (currently only the *MODEL_SPACE or
# *PAPER_SPACE blocks) in ezdxf the block_record handle for a layout is
# also called layout_key:
"block_record_handle": DXFAttr(330),
# 331: Soft-pointer ID/handle to an entity (zero or more entries may exist)
# 5: Sort handle (zero or more entries may exist)
},
)
acdb_sort_ents_table_group_codes = group_code_mapping(acdb_sort_ents_table)
@register_entity
class SortEntsTable(DXFObject):
"""DXF SORTENTSTABLE entity - sort entities table"""
# should work with AC1015/R2000 but causes problems with TrueView/AutoCAD
# LT 2019: "expected was-a-zombie-flag"
# No problems with AC1018/R2004 and later
#
# If the header variable $SORTENTS Regen flag (bit-code value 16) is set,
# AutoCAD regenerates entities in ascending handle order.
#
# When the DRAWORDER command is used, a SORTENTSTABLE object is attached to
# the *Model_Space or *Paper_Space block's extension dictionary under the
# name ACAD_SORTENTS. The SORTENTSTABLE object related to this dictionary
# associates a different handle with each entity, which redefines the order
# in which the entities are regenerated.
#
# $SORTENTS (280): Controls the object sorting methods (bitcode):
# 0 = Disables SORTENTS
# 1 = Sorts for object selection
# 2 = Sorts for object snap
# 4 = Sorts for redraws; obsolete
# 8 = Sorts for MSLIDE command slide creation; obsolete
# 16 = Sorts for REGEN commands
# 32 = Sorts for plotting
# 64 = Sorts for PostScript output; obsolete
DXFTYPE = "SORTENTSTABLE"
DXFATTRIBS = DXFAttributes(base_class, acdb_sort_ents_table)
def __init__(self) -> None:
super().__init__()
self.table: dict[str, str] = dict()
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, SortEntsTable)
entity.table = dict(entity.table)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
tags = processor.fast_load_dxfattribs(
dxf, acdb_sort_ents_table_group_codes, 1, log=False
)
self.load_table(tags)
return dxf
def load_table(self, tags: Tags) -> None:
for handle, sort_handle in take2(tags):
if handle.code != 331:
raise DXFStructureError(
f"Invalid handle code {handle.code}, expected 331"
)
if sort_handle.code != 5:
raise DXFStructureError(
f"Invalid sort handle code {handle.code}, expected 5"
)
self.table[handle.value] = sort_handle.value
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_sort_ents_table.name)
tagwriter.write_tag2(330, self.dxf.block_record_handle)
self.export_table(tagwriter)
def export_table(self, tagwriter: AbstractTagWriter):
for handle, sort_handle in self.table.items():
tagwriter.write_tag2(331, handle)
tagwriter.write_tag2(5, sort_handle)
def __len__(self) -> int:
return len(self.table)
def __iter__(self) -> Iterable:
"""Yields all redraw associations as (object_handle, sort_handle)
tuples.
"""
return iter(self.table.items())
def append(self, handle: str, sort_handle: str) -> None:
"""Append redraw association (handle, sort_handle).
Args:
handle: DXF entity handle (uppercase hex value without leading '0x')
sort_handle: sort handle (uppercase hex value without leading '0x')
"""
self.table[handle] = sort_handle
def clear(self):
"""Remove all handles from redraw order table."""
self.table = dict()
def set_handles(self, handles: Iterable[tuple[str, str]]) -> None:
"""Set all redraw associations from iterable `handles`, after removing
all existing associations.
Args:
handles: iterable yielding (object_handle, sort_handle) tuples
"""
# The sort_handle doesn't have to be unique, same or all handles can
# share the same sort_handle and sort_handles can use existing handles
# too.
#
# The '0' handle can be used, but this sort_handle will be drawn as
# latest (on top of all other entities) and not as first as expected.
# Invalid entity handles will be ignored by AutoCAD.
self.table = dict(handles)
def remove_invalid_handles(self) -> None:
"""Remove all handles which do not exist in the drawing database."""
if self.doc is None:
return
entitydb = self.doc.entitydb
self.table = {
handle: sort_handle
for handle, sort_handle in self.table.items()
if handle in entitydb
}
def remove_handle(self, handle: str) -> None:
"""Remove handle of DXF entity from redraw order table.
Args:
handle: DXF entity handle (uppercase hex value without leading '0x')
"""
try:
del self.table[handle]
except KeyError:
pass
acdb_field = DefSubclass(
"AcDbField",
{
"evaluator_id": DXFAttr(1),
"field_code": DXFAttr(2),
# Overflow of field code string
"field_code_overflow": DXFAttr(3),
# Number of child fields
"n_child_fields": DXFAttr(90),
# 360: Child field ID (AcDbHardOwnershipId); repeats for number of children
# 97: Number of object IDs used in the field code
# 331: Object ID used in the field code (AcDbSoftPointerId); repeats for
# the number of object IDs used in the field code
# 93: Number of the data set in the field
# 6: Key string for the field data; a key-field pair is repeated for the
# number of data sets in the field
# 7: Key string for the evaluated cache; this key is hard-coded
# as ACFD_FIELD_VALUE
# 90: Data type of field value
# 91: Long value (if data type of field value is long)
# 140: Double value (if data type of field value is double)
# 330: ID value, AcDbSoftPointerId (if data type of field value is ID)
# 92: Binary data buffer size (if data type of field value is binary)
# 310: Binary data (if data type of field value is binary)
# 301: Format string
# 9: Overflow of Format string
# 98: Length of format string
},
)
# todo: implement FIELD
# register when done
class Field(DXFObject):
"""DXF FIELD entity"""
DXFTYPE = "FIELD"
DXFATTRIBS = DXFAttributes(base_class, acdb_field)
def is_dxf_object(entity: DXFEntity) -> TypeGuard[DXFObject]:
"""Returns ``True`` if the `entity` is a DXF object from the OBJECTS section,
otherwise the entity is a table or class entry or a graphic entity which can
not reside in the OBJECTS section.
"""
if isinstance(entity, DXFObject):
return True
if isinstance(entity, DXFTagStorage) and not entity.is_graphic_entity:
return True
return False

View File

@@ -0,0 +1,320 @@
# Copyright (c) 2019-2022 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional
import math
from ezdxf.audit import AuditError
from ezdxf.lldxf import validator
from ezdxf.math import (
Vec3,
Matrix44,
NULLVEC,
X_AXIS,
Z_AXIS,
ellipse,
ConstructionEllipse,
OCS,
)
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
merge_group_code_mappings,
)
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import (
DXFGraphic,
acdb_entity,
add_entity,
replace_entity,
acdb_entity_group_codes,
)
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.entities import DXFNamespace, Spline
from ezdxf.audit import Auditor
__all__ = ["Ellipse"]
MIN_RATIO = 1e-10 # tested with DWG TrueView 2022
MAX_RATIO = 1.0 # tested with DWG TrueView 2022
TOL = 1e-9 # arbitrary choice
def is_valid_ratio(ratio: float) -> bool:
"""Check if axis-ratio is in valid range, takes an upper bound tolerance into
account for floating point imprecision.
"""
return MIN_RATIO <= abs(ratio) < (MAX_RATIO + TOL)
def clamp_axis_ratio(ratio: float) -> float:
"""Clamp axis-ratio into valid range and remove possible floating point imprecision.
"""
sign = -1 if ratio < 0 else +1
ratio = abs(ratio)
if ratio < MIN_RATIO:
return MIN_RATIO * sign
if ratio > MAX_RATIO:
return MAX_RATIO * sign
return ratio * sign
acdb_ellipse = DefSubclass(
"AcDbEllipse",
{
"center": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# Major axis vector from 'center':
"major_axis": DXFAttr(
11,
xtype=XType.point3d,
default=X_AXIS,
validator=validator.is_not_null_vector,
),
# The extrusion vector does not establish an OCS, it is just the normal
# vector of the ellipse plane:
"extrusion": DXFAttr(
210,
xtype=XType.point3d,
default=Z_AXIS,
optional=True,
validator=validator.is_not_null_vector,
fixer=RETURN_DEFAULT,
),
# Ratio has to be in the range: -1.0 ... -1e-10 and +1e-10 ... +1.0:
"ratio": DXFAttr(
40, default=MAX_RATIO, validator=is_valid_ratio, fixer=clamp_axis_ratio
),
# Start of ellipse, this value is 0.0 for a full ellipse:
"start_param": DXFAttr(41, default=0),
# End of ellipse, this value is 2π for a full ellipse:
"end_param": DXFAttr(42, default=math.tau),
},
)
acdb_ellipse_group_code = group_code_mapping(acdb_ellipse)
merged_ellipse_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_ellipse_group_code # type: ignore
)
HALF_PI = math.pi / 2.0
@register_entity
class Ellipse(DXFGraphic):
"""DXF ELLIPSE entity"""
DXFTYPE = "ELLIPSE"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_ellipse)
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
# bypass DXFGraphic, loading proxy graphic is skipped!
dxf = super(DXFGraphic, self).load_dxf_attribs(processor)
if processor:
processor.simple_dxfattribs_loader(dxf, merged_ellipse_group_codes)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_ellipse.name)
# is_valid_ratio() takes floating point imprecision on the upper bound of
# +/- 1.0 into account:
assert is_valid_ratio(
self.dxf.ratio
), f"axis-ratio out of range [{MIN_RATIO}, {MAX_RATIO}]"
self.dxf.ratio = clamp_axis_ratio(self.dxf.ratio)
self.dxf.export_dxf_attribs(
tagwriter,
[
"center",
"major_axis",
"extrusion",
"ratio",
"start_param",
"end_param",
],
)
@property
def minor_axis(self) -> Vec3:
dxf = self.dxf
return ellipse.minor_axis(Vec3(dxf.major_axis), Vec3(dxf.extrusion), dxf.ratio)
@property
def start_point(self) -> Vec3:
return list(self.vertices([self.dxf.start_param]))[0]
@property
def end_point(self) -> Vec3:
return list(self.vertices([self.dxf.end_param]))[0]
def construction_tool(self) -> ConstructionEllipse:
"""Returns construction tool :class:`ezdxf.math.ConstructionEllipse`."""
dxf = self.dxf
return ConstructionEllipse(
dxf.center,
dxf.major_axis,
dxf.extrusion,
dxf.ratio,
dxf.start_param,
dxf.end_param,
)
def apply_construction_tool(self, e: ConstructionEllipse) -> Ellipse:
"""Set ELLIPSE data from construction tool
:class:`ezdxf.math.ConstructionEllipse`.
"""
self.update_dxf_attribs(e.dxfattribs())
return self # floating interface
def params(self, num: int) -> Iterable[float]:
"""Returns `num` params from start- to end param in counter-clockwise
order.
All params are normalized in the range [0, 2π).
"""
start = self.dxf.start_param % math.tau
end = self.dxf.end_param % math.tau
yield from ellipse.get_params(start, end, num)
def vertices(self, params: Iterable[float]) -> Iterable[Vec3]:
"""Yields vertices on ellipse for iterable `params` in WCS.
Args:
params: param values in the range from 0 to 2π in radians,
param goes counter-clockwise around the extrusion vector,
major_axis = local x-axis = 0 rad.
"""
yield from self.construction_tool().vertices(params)
def flattening(self, distance: float, segments: int = 8) -> Iterable[Vec3]:
"""Adaptive recursive flattening. The argument `segments` is the
minimum count of approximation segments, if the distance from the center
of the approximation segment to the curve is bigger than `distance` the
segment will be subdivided. Returns a closed polygon for a full ellipse
where the start vertex has the same value as the end vertex.
Args:
distance: maximum distance from the projected curve point onto the
segment chord.
segments: minimum segment count
"""
return self.construction_tool().flattening(distance, segments)
def swap_axis(self):
"""Swap axis and adjust start- and end parameter."""
e = self.construction_tool()
e.swap_axis()
self.update_dxf_attribs(e.dxfattribs())
@classmethod
def from_arc(cls, entity: DXFGraphic) -> Ellipse:
"""Create a new virtual ELLIPSE entity from an ARC or a CIRCLE entity.
The new entity has no owner, no handle, is not stored in the entity database nor
assigned to any layout!
"""
assert entity.dxftype() in {"ARC", "CIRCLE"}, "ARC or CIRCLE entity required"
attribs = entity.dxfattribs(drop={"owner", "handle", "thickness"})
e = ellipse.ConstructionEllipse.from_arc(
center=attribs.get("center", NULLVEC),
extrusion=attribs.get("extrusion", Z_AXIS),
# Remove all not ELLIPSE attributes:
radius=attribs.pop("radius", 1.0),
start_angle=attribs.pop("start_angle", 0),
end_angle=attribs.pop("end_angle", 360),
)
attribs.update(e.dxfattribs())
return cls.new(dxfattribs=attribs, doc=entity.doc)
def transform(self, m: Matrix44) -> Ellipse:
"""Transform the ELLIPSE entity by transformation matrix `m` inplace."""
e = self.construction_tool()
e.transform(m)
self.update_dxf_attribs(e.dxfattribs())
self.post_transform(m)
return self
def translate(self, dx: float, dy: float, dz: float) -> Ellipse:
"""Optimized ELLIPSE translation about `dx` in x-axis, `dy` in y-axis
and `dz` in z-axis, returns `self` (floating interface).
"""
self.dxf.center = Vec3(dx, dy, dz) + self.dxf.center
# Avoid Matrix44 instantiation if not required:
if self.is_post_transform_required:
self.post_transform(Matrix44.translate(dx, dy, dz))
return self
def to_spline(self, replace=True) -> Spline:
"""Convert ELLIPSE to a :class:`~ezdxf.entities.Spline` entity.
Adds the new SPLINE entity to the entity database and to the
same layout as the source entity.
Args:
replace: replace (delete) source entity by SPLINE entity if ``True``
"""
from ezdxf.entities import Spline
spline = Spline.from_arc(self)
layout = self.get_layout()
assert layout is not None, "valid layout required"
if replace:
replace_entity(self, spline, layout)
else:
add_entity(spline, layout)
return spline
def ocs(self) -> OCS:
# WCS entity which supports the "extrusion" attribute in a
# different way!
return OCS()
def audit(self, auditor: Auditor) -> None:
if not self.is_alive:
return
super().audit(auditor)
entity = str(self)
major_axis = Vec3(self.dxf.major_axis)
if major_axis.is_null:
auditor.trash(self)
auditor.fixed_error(
code=AuditError.INVALID_MAJOR_AXIS,
message=f"Removed {entity} with invalid major axis: (0, 0, 0).",
)
return
axis_ratio = self.dxf.ratio
if is_valid_ratio(axis_ratio):
# remove possible floating point imprecision:
self.dxf.ratio = clamp_axis_ratio(axis_ratio)
return
if abs(axis_ratio) > MAX_RATIO:
self.swap_axis()
auditor.fixed_error(
code=AuditError.INVALID_ELLIPSE_RATIO,
message=f"Fixed invalid axis-ratio in {entity} by swapping axis.",
)
elif abs(axis_ratio) < MIN_RATIO:
self.dxf.ratio = clamp_axis_ratio(axis_ratio)
auditor.fixed_error(
code=AuditError.INVALID_ELLIPSE_RATIO,
message=f"Fixed invalid axis-ratio in {entity}, set to {MIN_RATIO}.",
)

View File

@@ -0,0 +1,136 @@
# Copyright (c) 2019-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.entities import DXFEntity
from ezdxf.lldxf.extendedtags import ExtendedTags
__all__ = [
"register_entity",
"ENTITY_CLASSES",
"replace_entity",
"new",
"cls",
"is_bound",
"create_db_entry",
"load",
"bind",
]
# Stores all registered classes:
ENTITY_CLASSES = {}
# use @set_default_class to register the default entity class:
DEFAULT_CLASS = None
def set_default_class(cls):
global DEFAULT_CLASS
DEFAULT_CLASS = cls
return cls
def replace_entity(cls):
name = cls.DXFTYPE
ENTITY_CLASSES[name] = cls
return cls
def register_entity(cls):
name = cls.DXFTYPE
if name in ENTITY_CLASSES:
raise TypeError(f"Double registration for DXF type {name}.")
ENTITY_CLASSES[name] = cls
return cls
def new(
dxftype: str, dxfattribs=None, doc: Optional[Drawing] = None
) -> DXFEntity:
"""Create a new entity, does not require an instantiated DXF document."""
entity = cls(dxftype).new(
handle=None,
owner=None,
dxfattribs=dxfattribs,
doc=doc,
)
return entity.cast() if hasattr(entity, "cast") else entity
def create_db_entry(dxftype, dxfattribs, doc: Drawing) -> DXFEntity:
entity = new(dxftype=dxftype, dxfattribs=dxfattribs)
bind(entity, doc)
return entity
def load(tags: ExtendedTags, doc: Optional[Drawing] = None) -> DXFEntity:
entity = cls(tags.dxftype()).load(tags, doc)
return entity.cast() if hasattr(entity, "cast") else entity
def cls(dxftype: str) -> DXFEntity:
"""Returns registered class for `dxftype`."""
return ENTITY_CLASSES.get(dxftype, DEFAULT_CLASS)
def bind(entity: DXFEntity, doc: Drawing) -> None:
"""Bind `entity` to the DXF document `doc`.
The bind process stores the DXF `entity` in the entity database of the DXF
document.
"""
assert entity.is_alive, "Can not bind destroyed entity."
assert doc.entitydb is not None, "Missing entity database."
entity.doc = doc
doc.entitydb.add(entity)
# Do not call the post_bind_hook() while loading from external sources,
# not all entities and resources are loaded at this point of time!
if not doc.is_loading: # type: ignore
# bind extension dictionary
if entity.extension_dict is not None:
xdict = entity.extension_dict
if xdict.has_valid_dictionary:
xdict.update_owner(entity.dxf.handle)
dictionary = xdict.dictionary
if not is_bound(dictionary, doc):
bind(dictionary, doc)
doc.objects.add_object(dictionary)
entity.post_bind_hook()
def unbind(entity: DXFEntity):
"""Unbind `entity` from document and layout, but does not destroy the
entity.
Turns `entity` into a virtual entity: no handle, no owner, no document.
"""
if entity.is_alive and not entity.is_virtual:
doc = entity.doc
if entity.dxf.owner is not None:
try:
layout = doc.layouts.get_layout_for_entity(entity) # type: ignore
except KeyError:
pass
else:
layout.unlink_entity(entity) # type: ignore
process_sub_entities = getattr(entity, "process_sub_entities", None)
if process_sub_entities:
process_sub_entities(lambda e: unbind(e))
doc.entitydb.discard(entity) # type: ignore
entity.doc = None
def is_bound(entity: DXFEntity, doc: Drawing) -> bool:
"""Returns ``True`` if `entity`is bound to DXF document `doc`."""
if not entity.is_alive:
return False
if entity.is_virtual or entity.doc is not doc:
return False
assert doc.entitydb, "Missing entity database."
return entity.dxf.handle in doc.entitydb

View File

@@ -0,0 +1,623 @@
# Copyright (c) 2019-2023, Manfred Moitzi
# License: MIT-License
from __future__ import annotations
from typing import TYPE_CHECKING, Sequence, Iterable, Optional
from xml.etree import ElementTree
import math
import re
import logging
from ezdxf.lldxf import validator
from ezdxf.lldxf.attributes import (
DXFAttributes,
DefSubclass,
DXFAttr,
XType,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf.const import (
SUBCLASS_MARKER,
DXFStructureError,
DXF2010,
DXFTypeError,
DXFValueError,
InvalidGeoDataException,
)
from ezdxf.lldxf.packedtags import VertexArray
from ezdxf.lldxf.tags import Tags, DXFTag
from ezdxf.math import NULLVEC, Z_AXIS, Y_AXIS, UVec, Vec3, Vec2, Matrix44
from ezdxf.tools.text import split_mtext_string
from .dxfentity import base_class, SubclassProcessor
from .dxfobj import DXFObject
from .factory import register_entity
from .copy import default_copy, CopyNotSupported
from .. import units
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace
from ezdxf.lldxf.tagwriter import AbstractTagWriter
__all__ = ["GeoData", "MeshVertices"]
logger = logging.getLogger("ezdxf")
acdb_geo_data = DefSubclass(
"AcDbGeoData",
{
# 1 = R2009, but this release has no DXF version,
# 2 = R2010
"version": DXFAttr(90, default=2),
# Handle to host block table record
"block_record_handle": DXFAttr(330, default="0"),
# 0 = unknown
# 1 = local grid
# 2 = projected grid
# 3 = geographic (latitude/longitude)
"coordinate_type": DXFAttr(
70,
default=3,
validator=validator.is_in_integer_range(0, 4),
fixer=RETURN_DEFAULT,
),
# Design point, reference point in WCS coordinates:
"design_point": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# Reference point in coordinate system coordinates, valid only when
# coordinate type is Local Grid:
"reference_point": DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
# Horizontal unit scale, factor which converts horizontal design coordinates
# to meters by multiplication:
"horizontal_unit_scale": DXFAttr(
40,
default=1,
validator=validator.is_not_zero,
fixer=RETURN_DEFAULT,
),
# Horizontal units per UnitsValue enumeration. Will be kUnitsUndefined if
# units specified by horizontal unit scale is not supported by AutoCAD
# enumeration:
"horizontal_units": DXFAttr(91, default=1),
# Vertical unit scale, factor which converts vertical design coordinates
# to meters by multiplication:
"vertical_unit_scale": DXFAttr(41, default=1),
# Vertical units per UnitsValue enumeration. Will be kUnitsUndefined if
# units specified by vertical unit scale is not supported by AutoCAD
# enumeration:
"vertical_units": DXFAttr(92, default=1),
"up_direction": DXFAttr(
210,
xtype=XType.point3d,
default=Z_AXIS,
validator=validator.is_not_null_vector,
fixer=RETURN_DEFAULT,
),
# North direction vector (2D)
# In DXF R2009 the group code 12 is used for source mesh points!
"north_direction": DXFAttr(
12,
xtype=XType.point2d,
default=Y_AXIS,
validator=validator.is_not_null_vector,
fixer=RETURN_DEFAULT,
),
# Scale estimation methods:
# 1 = None
# 2 = User specified scale factor;
# 3 = Grid scale at reference point;
# 4 = Prismoidal
"scale_estimation_method": DXFAttr(
95,
default=1,
validator=validator.is_in_integer_range(0, 5),
fixer=RETURN_DEFAULT,
),
"user_scale_factor": DXFAttr(
141,
default=1,
validator=validator.is_not_zero,
fixer=RETURN_DEFAULT,
),
# Bool flag specifying whether to do sea level correction:
"sea_level_correction": DXFAttr(
294,
default=0,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
"sea_level_elevation": DXFAttr(142, default=0),
"coordinate_projection_radius": DXFAttr(143, default=0),
# 303, 303, ..., 301: Coordinate system definition string
"geo_rss_tag": DXFAttr(302, default="", optional=True),
"observation_from_tag": DXFAttr(305, default="", optional=True),
"observation_to_tag": DXFAttr(306, default="", optional=True),
"observation_coverage_tag": DXFAttr(307, default="", optional=True),
# 93: Number of Geo-Mesh points
# mesh definition:
# source mesh point (12, 22) repeat, mesh_point_count? for version == 1
# target mesh point (13, 14) repeat, mesh_point_count? for version == 1
# source mesh point (13, 23) repeat, mesh_point_count? for version > 1
# target mesh point (14, 24) repeat, mesh_point_count? for version > 1
# 96: # Number of faces
# face index 97 repeat, faces_count
# face index 98 repeat, faces_count
# face index 99 repeat, faces_count
},
)
acdb_geo_data_group_codes = group_code_mapping(acdb_geo_data)
EPSG_3395 = """<?xml version="1.0" encoding="UTF-16" standalone="no" ?>
<Dictionary version="1.0" xmlns="http://www.osgeo.org/mapguide/coordinatesystem">
<ProjectedCoordinateSystem id="WORLD-MERCATOR">
<Name>WORLD-MERCATOR</Name>
<AdditionalInformation>
<ParameterItem type="CsMap">
<Key>CSQuadrantSimplified</Key>
<IntegerValue>1</IntegerValue>
</ParameterItem>
</AdditionalInformation>
<DomainOfValidity>
<Extent>
<GeographicElement>
<GeographicBoundingBox>
<WestBoundLongitude>-180.75</WestBoundLongitude>
<EastBoundLongitude>180.75</EastBoundLongitude>
<SouthBoundLatitude>-80.75</SouthBoundLatitude>
<NorthBoundLatitude>84.75</NorthBoundLatitude>
</GeographicBoundingBox>
</GeographicElement>
</Extent>
</DomainOfValidity>
<DatumId>WGS84</DatumId>
<Axis uom="Meter">
<CoordinateSystemAxis>
<AxisOrder>1</AxisOrder>
<AxisName>Easting</AxisName>
<AxisAbbreviation>E</AxisAbbreviation>
<AxisDirection>East</AxisDirection>
</CoordinateSystemAxis>
<CoordinateSystemAxis>
<AxisOrder>2</AxisOrder>
<AxisName>Northing</AxisName>
<AxisAbbreviation>N</AxisAbbreviation>
<AxisDirection>North</AxisDirection>
</CoordinateSystemAxis>
</Axis>
<Conversion>
<Projection>
<OperationMethodId>Mercator (variant B)</OperationMethodId>
<ParameterValue><OperationParameterId>Longitude of natural origin</OperationParameterId><Value uom="degree">0</Value></ParameterValue>
<ParameterValue><OperationParameterId>Standard Parallel</OperationParameterId><Value uom="degree">0</Value></ParameterValue>
<ParameterValue><OperationParameterId>Scaling factor for coord differences</OperationParameterId><Value uom="unity">1</Value></ParameterValue>
<ParameterValue><OperationParameterId>False easting</OperationParameterId><Value uom="Meter">0</Value></ParameterValue>
<ParameterValue><OperationParameterId>False northing</OperationParameterId><Value uom="Meter">0</Value></ParameterValue>
</Projection>
</Conversion>
</ProjectedCoordinateSystem>
<Alias id="3395" type="CoordinateSystem">
<ObjectId>WORLD-MERCATOR</ObjectId>
<Namespace>EPSG Code</Namespace>
</Alias>
<GeodeticDatum id="WGS84">
<Name>WGS84</Name>
<PrimeMeridianId>Greenwich</PrimeMeridianId>
<EllipsoidId>WGS84</EllipsoidId>
</GeodeticDatum>
<Alias id="6326" type="Datum">
<ObjectId>WGS84</ObjectId>
<Namespace>EPSG Code</Namespace>
</Alias>
<Ellipsoid id="WGS84">
<Name>WGS84</Name>
<SemiMajorAxis uom="meter">6.37814e+06</SemiMajorAxis>
<SecondDefiningParameter>
<SemiMinorAxis uom="meter">6.35675e+06</SemiMinorAxis>
</SecondDefiningParameter>
</Ellipsoid>
<Alias id="7030" type="Ellipsoid">
<ObjectId>WGS84</ObjectId>
<Namespace>EPSG Code</Namespace>
</Alias>
</Dictionary>
"""
class MeshVertices(VertexArray):
VERTEX_SIZE = 2
def mesh_group_codes(version: int) -> tuple[int, int]:
return (12, 13) if version < 2 else (13, 14)
@register_entity
class GeoData(DXFObject):
"""DXF GEODATA entity"""
DXFTYPE = "GEODATA"
DXFATTRIBS = DXFAttributes(base_class, acdb_geo_data)
MIN_DXF_VERSION_FOR_EXPORT = DXF2010
# coordinate_type const
UNKNOWN = 0
LOCAL_GRID = 1
PROJECTED_GRID = 2
GEOGRAPHIC = 3
# scale_estimation_method const
NONE = 1
USER_SCALE = 2
GRID_SCALE = 3
PRISMOIDEAL = 4
def __init__(self) -> None:
super().__init__()
self.source_vertices = MeshVertices()
self.target_vertices = MeshVertices()
self.faces: list[Sequence[int]] = []
self.coordinate_system_definition = ""
def copy(self, copy_strategy=default_copy):
raise CopyNotSupported(f"Copying of {self.DXFTYPE} not supported.")
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
version = processor.detect_implementation_version(
subclass_index=1,
group_code=90,
default=2,
)
tags = processor.fast_load_dxfattribs(
dxf, acdb_geo_data_group_codes, 1, log=False
)
tags = self.load_coordinate_system_definition(tags) # type: ignore
if version > 1:
self.load_mesh_data(tags, version)
else:
# version 1 is not really supported, because the group codes are
# totally messed up!
logger.warning(
"GEODATA version 1 found, perhaps loaded data is incorrect"
)
dxf.discard("north_direction") # group code issue!!!
return dxf
def load_coordinate_system_definition(self, tags: Tags) -> Iterable[DXFTag]:
# 303, 303, 301, Coordinate system definition string, always XML?
lines = []
for tag in tags:
if tag.code in (301, 303):
lines.append(tag.value.replace("^J", "\n"))
else:
yield tag
if len(lines):
self.coordinate_system_definition = "".join(lines)
def load_mesh_data(self, tags: Iterable[DXFTag], version: int = 2):
# group codes of version 1 and 2 differ, see DXF reference R2009
src, target = mesh_group_codes(version)
face_indices = {97, 98, 99}
face: list[int] = []
for code, value in tags:
if code == src:
self.source_vertices.append(value)
elif code == target:
self.target_vertices.append(value)
elif code in face_indices:
if code == 97 and len(face):
if len(face) != 3:
raise DXFStructureError(
f"GEODATA face definition error: invalid index "
f"count {len(face)}."
)
self.faces.append(tuple(face))
face.clear()
face.append(value)
if face: # collect last face
self.faces.append(tuple(face))
if len(self.source_vertices) != len(self.target_vertices):
raise DXFStructureError(
"GEODATA mesh definition error: source and target point count "
"does not match."
)
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_geo_data.name)
if self.dxf.version < 2:
logger.warning(
"exporting unsupported GEODATA version 1, this may corrupt "
"the DXF file!"
)
self.dxf.export_dxf_attribs(
tagwriter,
[
"version",
"block_record_handle",
"coordinate_type",
"design_point",
"reference_point",
"horizontal_unit_scale",
"horizontal_units",
"vertical_unit_scale",
"vertical_units",
"up_direction",
"north_direction",
"scale_estimation_method",
"user_scale_factor",
"sea_level_correction",
"sea_level_elevation",
"coordinate_projection_radius",
],
)
self.export_coordinate_system_definition(tagwriter)
self.dxf.export_dxf_attribs(
tagwriter,
[
"geo_rss_tag",
"observation_from_tag",
"observation_to_tag",
"observation_coverage_tag",
],
)
self.export_mesh_data(tagwriter)
def export_mesh_data(self, tagwriter: AbstractTagWriter):
if len(self.source_vertices) != len(self.target_vertices):
raise DXFStructureError(
"GEODATA mesh definition error: source and target point count "
"does not match."
)
src, target = mesh_group_codes(self.dxf.version)
tagwriter.write_tag2(93, len(self.source_vertices))
for s, t in zip(self.source_vertices, self.target_vertices):
tagwriter.write_vertex(src, s)
tagwriter.write_vertex(target, t)
tagwriter.write_tag2(96, len(self.faces))
for face in self.faces:
if len(face) != 3:
raise DXFStructureError(
f"GEODATA face definition error: invalid index "
f"count {len(face)}."
)
f1, f2, f3 = face
tagwriter.write_tag2(97, f1)
tagwriter.write_tag2(98, f2)
tagwriter.write_tag2(99, f3)
def export_coordinate_system_definition(self, tagwriter: AbstractTagWriter):
text = self.coordinate_system_definition.replace("\n", "^J")
chunks = split_mtext_string(text, size=255)
if len(chunks) == 0:
chunks.append("")
while len(chunks) > 1:
tagwriter.write_tag2(303, chunks.pop(0))
tagwriter.write_tag2(301, chunks[0])
def decoded_units(self) -> tuple[Optional[str], Optional[str]]:
return units.decode(self.dxf.horizontal_units), units.decode(
self.dxf.vertical_units
)
def get_crs(self) -> tuple[int, bool]:
"""Returns the EPSG index and axis-ordering, axis-ordering is ``True``
if fist axis is labeled "E" or "W" and ``False`` if first axis is
labeled "N" or "S".
If axis-ordering is ``False`` the CRS is not compatible with the
``__geo_interface__`` or GeoJSON (see chapter 3.1.1).
Raises:
InvalidGeoDataException: for invalid or unknown XML data
The EPSG number is stored in a tag like:
.. code::
<Alias id="27700" type="CoordinateSystem">
<ObjectId>OSGB1936.NationalGrid</ObjectId>
<Namespace>EPSG Code</Namespace>
</Alias>
The axis-ordering is stored in a tag like:
.. code::
<Axis uom="METER">
<CoordinateSystemAxis>
<AxisOrder>1</AxisOrder>
<AxisName>Easting</AxisName>
<AxisAbbreviation>E</AxisAbbreviation>
<AxisDirection>east</AxisDirection>
</CoordinateSystemAxis>
<CoordinateSystemAxis>
<AxisOrder>2</AxisOrder>
<AxisName>Northing</AxisName>
<AxisAbbreviation>N</AxisAbbreviation>
<AxisDirection>north</AxisDirection>
</CoordinateSystemAxis>
</Axis>
"""
definition = self.coordinate_system_definition
try:
# Remove namespaces so that tags can be searched without prefixing
# their namespace:
definition = _remove_xml_namespaces(definition)
root = ElementTree.fromstring(definition)
except ElementTree.ParseError:
raise InvalidGeoDataException(
"failed to parse coordinate_system_definition as xml"
)
crs = None
for alias in root.findall("Alias"):
try:
namespace = alias.find("Namespace").text # type: ignore
except AttributeError:
namespace = ""
if alias.get("type") == "CoordinateSystem" and namespace == "EPSG Code":
try:
crs = int(alias.get("id")) # type: ignore
except ValueError:
raise InvalidGeoDataException(
f'invalid epsg number: {alias.get("id")}'
)
break
xy_ordering = None
for axis in root.findall(".//CoordinateSystemAxis"):
try:
axis_order = axis.find("AxisOrder").text # type: ignore
except AttributeError:
axis_order = ""
if axis_order == "1":
try:
first_axis = axis.find("AxisAbbreviation").text # type: ignore
except AttributeError:
raise InvalidGeoDataException("first axis not defined")
if first_axis in ("E", "W"):
xy_ordering = True
elif first_axis in ("N", "S"):
xy_ordering = False
else:
raise InvalidGeoDataException(f"unknown first axis: {first_axis}")
break
if crs is None:
raise InvalidGeoDataException("no EPSG code associated with CRS")
elif xy_ordering is None:
raise InvalidGeoDataException("could not determine axis ordering")
else:
return crs, xy_ordering
def get_crs_transformation(
self, *, no_checks: bool = False
) -> tuple[Matrix44, int]:
"""Returns the transformation matrix and the EPSG index to transform
WCS coordinates into CRS coordinates. Because of the lack of proper
documentation this method works only for tested configurations, set
argument `no_checks` to ``True`` to use the method for untested geodata
configurations, but the results may be incorrect.
Supports only "Local Grid" transformation!
Raises:
InvalidGeoDataException: for untested geodata configurations
"""
epsg, xy_ordering = self.get_crs()
if not no_checks:
if (
self.dxf.coordinate_type != GeoData.LOCAL_GRID
or self.dxf.scale_estimation_method != GeoData.NONE
or not math.isclose(self.dxf.user_scale_factor, 1.0)
or self.dxf.sea_level_correction != 0
or not math.isclose(self.dxf.sea_level_elevation, 0)
or self.faces
or not self.dxf.up_direction.isclose((0, 0, 1))
or self.dxf.observation_coverage_tag != ""
or self.dxf.observation_from_tag != ""
or self.dxf.observation_to_tag != ""
or not xy_ordering
):
raise InvalidGeoDataException(
f"Untested geodata configuration: "
f"{self.dxf.all_existing_dxf_attribs()}.\n"
f"You can try with no_checks=True but the "
f"results may be incorrect."
)
source = self.dxf.design_point # in CAD WCS coordinates
target = self.dxf.reference_point # in the CRS of the geodata
north = self.dxf.north_direction
# -pi/2 because north is at pi/2 so if the given north is at pi/2, no
# rotation is necessary:
theta = -(math.atan2(north.y, north.x) - math.pi / 2)
transformation = (
Matrix44.translate(-source.x, -source.y, 0)
@ Matrix44.scale(
self.dxf.horizontal_unit_scale, self.dxf.vertical_unit_scale, 1
)
@ Matrix44.z_rotate(theta)
@ Matrix44.translate(target.x, target.y, 0)
)
return transformation, epsg
def setup_local_grid(
self,
*,
design_point: UVec,
reference_point: UVec,
north_direction: UVec = (0, 1),
crs: str = EPSG_3395,
) -> None:
"""Setup local grid coordinate system. This method is designed to setup
CRS similar to `EPSG:3395 World Mercator`, the basic features of the
CRS should fulfill these assumptions:
- base unit of reference coordinates is 1 meter
- right-handed coordinate system: +Y=north/+X=east/+Z=up
The CRS string is not validated nor interpreted!
.. hint::
The reference point must be a 2D cartesian map coordinate and not
a globe (lon/lat) coordinate like stored in GeoJSON or GPS data.
Args:
design_point: WCS coordinates of the CRS reference point
reference_point: CRS reference point in 2D cartesian coordinates
north_direction: north direction a 2D vertex, default is (0, 1)
crs: Coordinate Reference System definition XML string, default is
the definition string for `EPSG:3395 World Mercator`
"""
doc = self.doc
if doc is None:
raise DXFValueError("Valid DXF document required.")
wcs_units = doc.units
if units == 0:
raise DXFValueError(
"DXF document requires units to be set, " 'current state is "unitless".'
)
meter_factor = units.METER_FACTOR[wcs_units]
if meter_factor is None:
raise DXFValueError(f"Unsupported document units: {wcs_units}")
unit_factor = 1.0 / meter_factor
# Default settings:
self.dxf.up_direction = Z_AXIS
self.dxf.observation_coverage_tag = ""
self.dxf.observation_from_tag = ""
self.dxf.observation_to_tag = ""
self.dxf.scale_estimation_method = GeoData.NONE
self.dxf.coordinate_type = GeoData.LOCAL_GRID
self.dxf.sea_level_correction = 0
self.dxf.horizontal_units = wcs_units
# Factor from WCS -> CRS (m) e.g. 0.01 for horizontal_units==5 (cm),
# 1cm = 0.01m
self.dxf.horizontal_unit_scale = unit_factor
self.dxf.vertical_units = wcs_units
self.dxf.vertical_unit_scale = unit_factor
# User settings:
self.dxf.design_point = Vec3(design_point)
self.dxf.reference_point = Vec3(reference_point)
self.dxf.north_direction = Vec2(north_direction)
self.coordinate_system_definition = str(crs)
def _remove_xml_namespaces(xml_string: str) -> str:
return re.sub('xmlns="[^"]*"', "", xml_string)

Some files were not shown because too many files have changed in this diff Show More