initial
This commit is contained in:
111
.venv/lib/python3.12/site-packages/ezdxf/entities/__init__.py
Normal file
111
.venv/lib/python3.12/site-packages/ezdxf/entities/__init__.py
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
477
.venv/lib/python3.12/site-packages/ezdxf/entities/acad_table.py
Normal file
477
.venv/lib/python3.12/site-packages/ezdxf/entities/acad_table.py
Normal 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
|
||||
@@ -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
|
||||
837
.venv/lib/python3.12/site-packages/ezdxf/entities/acis.py
Normal file
837
.venv/lib/python3.12/site-packages/ezdxf/entities/acis.py
Normal 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",
|
||||
],
|
||||
)
|
||||
149
.venv/lib/python3.12/site-packages/ezdxf/entities/appdata.py
Normal file
149
.venv/lib/python3.12/site-packages/ezdxf/entities/appdata.py
Normal 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, "}")
|
||||
62
.venv/lib/python3.12/site-packages/ezdxf/entities/appid.py
Normal file
62
.venv/lib/python3.12/site-packages/ezdxf/entities/appid.py
Normal 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"])
|
||||
148
.venv/lib/python3.12/site-packages/ezdxf/entities/arc.py
Normal file
148
.venv/lib/python3.12/site-packages/ezdxf/entities/arc.py
Normal 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
|
||||
710
.venv/lib/python3.12/site-packages/ezdxf/entities/attrib.py
Normal file
710
.venv/lib/python3.12/site-packages/ezdxf/entities/attrib.py
Normal 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)
|
||||
242
.venv/lib/python3.12/site-packages/ezdxf/entities/block.py
Normal file
242
.venv/lib/python3.12/site-packages/ezdxf/entities/block.py
Normal 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)
|
||||
315
.venv/lib/python3.12/site-packages/ezdxf/entities/blockrecord.py
Normal file
315
.venv/lib/python3.12/site-packages/ezdxf/entities/blockrecord.py
Normal 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)
|
||||
1388
.venv/lib/python3.12/site-packages/ezdxf/entities/boundary_paths.py
Normal file
1388
.venv/lib/python3.12/site-packages/ezdxf/entities/boundary_paths.py
Normal file
File diff suppressed because it is too large
Load Diff
214
.venv/lib/python3.12/site-packages/ezdxf/entities/circle.py
Normal file
214
.venv/lib/python3.12/site-packages/ezdxf/entities/circle.py
Normal 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
|
||||
102
.venv/lib/python3.12/site-packages/ezdxf/entities/copy.py
Normal file
102
.venv/lib/python3.12/site-packages/ezdxf/entities/copy.py
Normal 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())
|
||||
688
.venv/lib/python3.12/site-packages/ezdxf/entities/dictionary.py
Normal file
688
.venv/lib/python3.12/site-packages/ezdxf/entities/dictionary.py
Normal 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"])
|
||||
1230
.venv/lib/python3.12/site-packages/ezdxf/entities/dimension.py
Normal file
1230
.venv/lib/python3.12/site-packages/ezdxf/entities/dimension.py
Normal file
File diff suppressed because it is too large
Load Diff
959
.venv/lib/python3.12/site-packages/ezdxf/entities/dimstyle.py
Normal file
959
.venv/lib/python3.12/site-packages/ezdxf/entities/dimstyle.py
Normal 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
|
||||
@@ -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
|
||||
123
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfclass.py
Normal file
123
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfclass.py
Normal 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
|
||||
1088
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfentity.py
Normal file
1088
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfentity.py
Normal file
File diff suppressed because it is too large
Load Diff
728
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfgfx.py
Normal file
728
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfgfx.py
Normal 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
|
||||
455
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfgroups.py
Normal file
455
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfgroups.py
Normal 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 ""
|
||||
648
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfns.py
Normal file
648
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfns.py
Normal 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
|
||||
400
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfobj.py
Normal file
400
.venv/lib/python3.12/site-packages/ezdxf/entities/dxfobj.py
Normal 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
|
||||
320
.venv/lib/python3.12/site-packages/ezdxf/entities/ellipse.py
Normal file
320
.venv/lib/python3.12/site-packages/ezdxf/entities/ellipse.py
Normal 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}.",
|
||||
)
|
||||
136
.venv/lib/python3.12/site-packages/ezdxf/entities/factory.py
Normal file
136
.venv/lib/python3.12/site-packages/ezdxf/entities/factory.py
Normal 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
|
||||
623
.venv/lib/python3.12/site-packages/ezdxf/entities/geodata.py
Normal file
623
.venv/lib/python3.12/site-packages/ezdxf/entities/geodata.py
Normal 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
Reference in New Issue
Block a user