refactor: excel parse
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 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,
|
||||
read_acad_table_content,
|
||||
)
|
||||
|
||||
# register graphical entities R2010
|
||||
|
||||
from .geodata import GeoData
|
||||
|
||||
# register graphical entities R2013
|
||||
|
||||
# register graphical entities R2018
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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, "}")
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
@@ -0,0 +1,737 @@
|
||||
# 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.audit import Auditor, AuditError
|
||||
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 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):
|
||||
# Mandatory by AutoCAD!
|
||||
"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=const.ATTRIB_TYPE_SINGLE_LINE,
|
||||
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)
|
||||
|
||||
# set attribute type:
|
||||
if isinstance(self, Attrib):
|
||||
attribute_type = const.ATTRIB_TYPE_MULTI_LINE
|
||||
else:
|
||||
attribute_type = const.ATTDEF_TYPE_MULTI_LINE
|
||||
self.dxf.attribute_type = attribute_type
|
||||
|
||||
# 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 afterward.
|
||||
|
||||
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 discard_mtext(self) -> None:
|
||||
"""Discard multi-line feature.
|
||||
|
||||
The embedded MTEXT will be removed and the ATTRIB/ATTDEF will be converted to a
|
||||
single-line attribute.
|
||||
"""
|
||||
self._embedded_mtext = None
|
||||
self.dxf.attribute_type = const.ATTRIB_TYPE_SINGLE_LINE
|
||||
|
||||
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 audit(self, auditor: Auditor) -> None:
|
||||
"""Validity check."""
|
||||
super().audit(auditor)
|
||||
if not self.dxf.hasattr("tag"):
|
||||
auditor.fixed_error(
|
||||
code=AuditError.TAG_ATTRIBUTE_MISSING,
|
||||
message=f'Missing mandatory "tag" attribute, entity {str(self)} deleted.',
|
||||
)
|
||||
auditor.trash(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.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.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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,315 @@
|
||||
# Copyright (c) 2019-2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing_extensions import Self
|
||||
import logging
|
||||
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import (
|
||||
DXF12,
|
||||
SUBCLASS_MARKER,
|
||||
DXF2007,
|
||||
DXFInternalEzdxfError,
|
||||
)
|
||||
from ezdxf.entities.dxfentity import base_class, SubclassProcessor, DXFEntity
|
||||
from ezdxf.entities.layer import acdb_symbol_table_record
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.audit import Auditor
|
||||
from ezdxf.entities import DXFGraphic, Block, EndBlk
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.entitydb import EntitySpace
|
||||
from ezdxf.layouts import BlockLayout
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["BlockRecord"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
acdb_blockrec = DefSubclass(
|
||||
"AcDbBlockTableRecord",
|
||||
{
|
||||
"name": DXFAttr(2, validator=validator.is_valid_block_name),
|
||||
# handle to associated DXF LAYOUT object
|
||||
"layout": DXFAttr(340, default="0"),
|
||||
# 0 = can not explode; 1 = can explode
|
||||
"explode": DXFAttr(
|
||||
280,
|
||||
default=1,
|
||||
dxfversion=DXF2007,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 0 = scale non uniformly; 1 = scale uniformly
|
||||
"scale": DXFAttr(
|
||||
281,
|
||||
default=0,
|
||||
dxfversion=DXF2007,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# see ezdxf/units.py
|
||||
"units": DXFAttr(
|
||||
70,
|
||||
default=0,
|
||||
dxfversion=DXF2007,
|
||||
validator=validator.is_in_integer_range(0, 25),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 310: Binary data for bitmap preview (optional) - removed (ignored) by ezdxf
|
||||
},
|
||||
)
|
||||
acdb_blockrec_group_codes = group_code_mapping(acdb_blockrec)
|
||||
|
||||
# optional handles to existing block references in DXF2000+
|
||||
# 2: name
|
||||
# 340: explode
|
||||
# 102: "{BLKREFS"
|
||||
# 331: handle to INSERT
|
||||
# ...
|
||||
# 102: "}"
|
||||
|
||||
|
||||
# optional XDATA for all DXF versions
|
||||
# 1000: "ACAD"
|
||||
# 1001: "DesignCenter Data" (optional)
|
||||
# 1002: "{"
|
||||
# 1070: Autodesk Design Center version number
|
||||
# 1070: Insert units: like 'units'
|
||||
# 1002: "}"
|
||||
|
||||
|
||||
@register_entity
|
||||
class BlockRecord(DXFEntity):
|
||||
"""DXF BLOCK_RECORD table entity
|
||||
|
||||
BLOCK_RECORD is the hard owner of all entities in BLOCK definitions, this
|
||||
means owner tag of entities is handle of BLOCK_RECORD.
|
||||
|
||||
"""
|
||||
|
||||
DXFTYPE = "BLOCK_RECORD"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_blockrec)
|
||||
|
||||
def __init__(self) -> None:
|
||||
from ezdxf.entitydb import EntitySpace
|
||||
|
||||
super().__init__()
|
||||
# Store entities in the block_record instead of BlockLayout and Layout,
|
||||
# because BLOCK_RECORD is also the hard owner of all the entities.
|
||||
self.entity_space = EntitySpace()
|
||||
self.block: Optional[Block] = None
|
||||
self.endblk: Optional[EndBlk] = None
|
||||
# stores also the block layout structure
|
||||
self.block_layout: Optional[BlockLayout] = None
|
||||
|
||||
def set_block(self, block: Block, endblk: EndBlk):
|
||||
self.block = block
|
||||
self.endblk = endblk
|
||||
self.block.dxf.owner = self.dxf.handle
|
||||
self.endblk.dxf.owner = self.dxf.handle
|
||||
|
||||
def set_entity_space(self, entity_space: EntitySpace) -> None:
|
||||
self.entity_space = entity_space
|
||||
|
||||
def rename(self, name: str) -> None:
|
||||
assert self.block is not None
|
||||
self.dxf.name = name
|
||||
self.block.dxf.name = name
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.simple_dxfattribs_loader(dxf, acdb_blockrec_group_codes) # type: ignore
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion == DXF12:
|
||||
raise DXFInternalEzdxfError("Exporting BLOCK_RECORDS for DXF R12.")
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_blockrec.name)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"layout",
|
||||
"units",
|
||||
"explode",
|
||||
"scale",
|
||||
],
|
||||
)
|
||||
|
||||
def export_block_definition(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Exports the BLOCK entity, followed by all content entities and finally the
|
||||
ENDBLK entity, except for the *Model_Space and *Paper_Space blocks, their
|
||||
entities are stored in the ENTITIES section.
|
||||
|
||||
"""
|
||||
assert self.block is not None
|
||||
assert self.endblk is not None
|
||||
if self.block_layout is not None:
|
||||
self.block_layout.update_block_flags()
|
||||
self.block.export_dxf(tagwriter)
|
||||
if not (self.is_modelspace or self.is_active_paperspace):
|
||||
self.entity_space.export_dxf(tagwriter)
|
||||
self.endblk.export_dxf(tagwriter)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
assert self.doc is not None, "BLOCK_RECORD entity must be assigned to document"
|
||||
assert self.doc.entitydb is not None, "entity database required"
|
||||
super().register_resources(registry)
|
||||
key = self.dxf.handle
|
||||
assert key in self.doc.entitydb, "invalid BLOCK_RECORD handle"
|
||||
|
||||
if self.block is not None:
|
||||
registry.add_entity(self.block, block_key=key)
|
||||
else:
|
||||
raise DXFInternalEzdxfError(
|
||||
f"BLOCK entity in BLOCK_RECORD #{key} is invalid"
|
||||
)
|
||||
if self.endblk is not None:
|
||||
registry.add_entity(self.endblk, block_key=key)
|
||||
else:
|
||||
raise DXFInternalEzdxfError(
|
||||
f"ENDBLK entity in BLOCK_RECORD #{key} is invalid"
|
||||
)
|
||||
for e in self.entity_space:
|
||||
registry.add_entity(e, block_key=key)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
assert isinstance(clone, BlockRecord)
|
||||
super().map_resources(clone, mapping)
|
||||
|
||||
assert self.block is not None
|
||||
mapping.map_resources_of_copy(self.block)
|
||||
|
||||
assert self.endblk is not None
|
||||
mapping.map_resources_of_copy(self.endblk)
|
||||
|
||||
for entity in self.entity_space:
|
||||
mapping.map_resources_of_copy(entity)
|
||||
|
||||
def destroy(self):
|
||||
"""Destroy associated data:
|
||||
|
||||
- BLOCK
|
||||
- ENDBLK
|
||||
- all entities stored in this block definition
|
||||
|
||||
Does not destroy the linked LAYOUT entity, this is the domain of the
|
||||
:class:`Layouts` object, which also should initiate the destruction of
|
||||
'this' BLOCK_RECORD.
|
||||
|
||||
"""
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
self.block.destroy()
|
||||
self.endblk.destroy()
|
||||
for entity in self.entity_space:
|
||||
entity.destroy()
|
||||
|
||||
# remove attributes to find invalid access after death
|
||||
del self.block
|
||||
del self.endblk
|
||||
del self.block_layout
|
||||
super().destroy()
|
||||
|
||||
@property
|
||||
def is_active_paperspace(self) -> bool:
|
||||
"""``True`` if is "active" paperspace layout."""
|
||||
return self.dxf.name.lower() == "*paper_space"
|
||||
|
||||
@property
|
||||
def is_any_paperspace(self) -> bool:
|
||||
"""``True`` if is any kind of paperspace layout."""
|
||||
return self.dxf.name.lower().startswith("*paper_space")
|
||||
|
||||
@property
|
||||
def is_modelspace(self) -> bool:
|
||||
"""``True`` if is the modelspace layout."""
|
||||
return self.dxf.name.lower() == "*model_space"
|
||||
|
||||
@property
|
||||
def is_any_layout(self) -> bool:
|
||||
"""``True`` if is any kind of modelspace or paperspace layout."""
|
||||
return self.is_modelspace or self.is_any_paperspace
|
||||
|
||||
@property
|
||||
def is_block_layout(self) -> bool:
|
||||
"""``True`` if not any kind of modelspace or paperspace layout, just a
|
||||
regular block definition.
|
||||
"""
|
||||
return not self.is_any_layout
|
||||
|
||||
@property
|
||||
def is_xref(self) -> bool:
|
||||
"""``True`` if represents an XREF (external reference) or XREF_OVERLAY."""
|
||||
if self.block is not None:
|
||||
return bool(self.block.dxf.flags & 12)
|
||||
return False
|
||||
|
||||
def add_entity(self, entity: DXFGraphic) -> None:
|
||||
"""Add an existing DXF entity to BLOCK_RECORD.
|
||||
|
||||
Args:
|
||||
entity: :class:`DXFGraphic`
|
||||
|
||||
"""
|
||||
# assign layout
|
||||
try:
|
||||
entity.set_owner(self.dxf.handle, paperspace=int(self.is_any_paperspace))
|
||||
except AttributeError:
|
||||
logger.debug(f"Unexpected DXF entity {str(entity)} in {str(self.block)}")
|
||||
# Add unexpected entities also to the entity space - auditor should fix
|
||||
# errors!
|
||||
self.entity_space.add(entity)
|
||||
|
||||
def unlink_entity(self, entity: DXFGraphic) -> None:
|
||||
"""Unlink `entity` from BLOCK_RECORD.
|
||||
|
||||
Removes `entity` just from entity space but not from the drawing
|
||||
database.
|
||||
|
||||
Args:
|
||||
entity: :class:`DXFGraphic`
|
||||
|
||||
"""
|
||||
if entity.is_alive:
|
||||
self.entity_space.remove(entity)
|
||||
try:
|
||||
entity.set_owner(None)
|
||||
except AttributeError:
|
||||
pass # unsupported entities as DXFTagStorage
|
||||
|
||||
def delete_entity(self, entity: DXFGraphic) -> None:
|
||||
"""Delete `entity` from BLOCK_RECORD entity space and drawing database.
|
||||
|
||||
Args:
|
||||
entity: :class:`DXFGraphic`
|
||||
|
||||
"""
|
||||
self.unlink_entity(entity) # 1. unlink from entity space
|
||||
entity.destroy()
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
"""Validity check. (internal API)"""
|
||||
if not self.is_alive:
|
||||
return
|
||||
super().audit(auditor)
|
||||
self.entity_space.audit(auditor)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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())
|
||||
@@ -0,0 +1,696 @@
|
||||
# Copyright (c) 2019-2025, 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",
|
||||
{
|
||||
# DXF Reference: 280 - Hard-owner flag.
|
||||
# If set to 1, indicates that elements of the dictionary are to be treated as
|
||||
# hard-owned
|
||||
# No definition of the default state in the DXF reference!
|
||||
#
|
||||
# 2025-04-27:
|
||||
# AutoCAD creates the root DICTIONARY and the top level DICTIONARY entries
|
||||
# without group code 280 tags.
|
||||
# Extension dicts are created with the hard_owned flag set to 1.
|
||||
# See exploration/dict-analyzer.py
|
||||
# Conclusion: default state is 0
|
||||
"hard_owned": DXFAttr(
|
||||
280,
|
||||
# 2025-04-27: changed to 0
|
||||
default=0,
|
||||
# 2024-11-18: changed to False because of issue #1203
|
||||
# 2025-04-09: changed back to True because of issue #1279
|
||||
optional=True,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Duplicate record cloning flag (determines how to merge duplicate entries):
|
||||
# 0 = not applicable
|
||||
# 1 = keep existing
|
||||
# 2 = use clone
|
||||
# 3 = <xref>$0$<name>
|
||||
# 4 = $0$<name>
|
||||
# 5 = Unmangle name
|
||||
"cloning": DXFAttr(
|
||||
281,
|
||||
default=1,
|
||||
validator=validator.is_in_integer_range(0, 6),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 3: entry name
|
||||
# 350: entry handle, some DICTIONARY objects have 360 as handle group code,
|
||||
# this is accepted by AutoCAD but not documented by the DXF reference!
|
||||
# ezdxf replaces group code 360 by 350.
|
||||
# - group code 350 is a soft-owner handle
|
||||
# - group code 360 is a hard-owner handle
|
||||
},
|
||||
)
|
||||
acdb_dictionary_group_codes = group_code_mapping(acdb_dictionary)
|
||||
KEY_CODE = 3
|
||||
VALUE_CODE = 350
|
||||
# Some DICTIONARY use group code 360:
|
||||
SEARCH_CODES = (VALUE_CODE, 360)
|
||||
|
||||
|
||||
@factory.register_entity
|
||||
class Dictionary(DXFObject):
|
||||
"""AutoCAD maintains items such as mline styles and group definitions as
|
||||
objects in dictionaries. Other applications are free to create and use
|
||||
their own dictionaries as they see fit. The prefix "ACAD_" is reserved
|
||||
for use by AutoCAD applications.
|
||||
|
||||
Dictionary entries are (key, DXFEntity) pairs. DXFEntity could be a string,
|
||||
because at loading time not all objects are already stored in the EntityDB,
|
||||
and have to be acquired later.
|
||||
|
||||
"""
|
||||
|
||||
DXFTYPE = "DICTIONARY"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._data: dict[str, Union[str, DXFObject]] = dict()
|
||||
self._value_code = VALUE_CODE
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy hard owned entities but do not store the copies in the entity
|
||||
database, this is a second step (factory.bind), this is just real copying.
|
||||
"""
|
||||
assert isinstance(entity, Dictionary)
|
||||
entity._value_code = self._value_code
|
||||
if self.dxf.hard_owned:
|
||||
# Reactors are removed from the cloned DXF objects.
|
||||
data: dict[str, DXFEntity] = dict()
|
||||
for key, ent in self.items():
|
||||
# ignore strings and None - these entities do not exist
|
||||
# in the entity database
|
||||
if isinstance(ent, DXFEntity):
|
||||
try: # todo: follow CopyStrategy.ignore_copy_errors_in_linked entities
|
||||
data[key] = ent.copy(copy_strategy=copy_strategy)
|
||||
except CopyNotSupported:
|
||||
if copy_strategy.settings.ignore_copy_errors_in_linked_entities:
|
||||
logger.warning(
|
||||
f"copy process ignored {str(ent)} - this may cause problems in AutoCAD"
|
||||
)
|
||||
else:
|
||||
raise
|
||||
entity._data = data # type: ignore
|
||||
else:
|
||||
entity._data = dict(self._data)
|
||||
|
||||
def get_handle_mapping(self, clone: Dictionary) -> dict[str, str]:
|
||||
"""Returns handle mapping for in-object copies."""
|
||||
handle_mapping: dict[str, str] = dict()
|
||||
if not self.is_hard_owner:
|
||||
return handle_mapping
|
||||
|
||||
for key, entity in self.items():
|
||||
if not isinstance(entity, DXFEntity):
|
||||
continue
|
||||
copied_entry = clone.get(key)
|
||||
if copied_entry:
|
||||
handle_mapping[entity.dxf.handle] = copied_entry.dxf.handle
|
||||
return handle_mapping
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
assert isinstance(clone, Dictionary)
|
||||
super().map_resources(clone, mapping)
|
||||
if self.is_hard_owner:
|
||||
return
|
||||
data = dict()
|
||||
for key, entity in self.items():
|
||||
if not isinstance(entity, DXFEntity):
|
||||
continue
|
||||
entity_copy = mapping.get_reference_of_copy(entity.dxf.handle)
|
||||
if entity_copy:
|
||||
data[key] = entity
|
||||
clone._data = data # type: ignore
|
||||
|
||||
def del_source_of_copy(self) -> None:
|
||||
super().del_source_of_copy()
|
||||
for _, entity in self.items():
|
||||
if isinstance(entity, DXFEntity) and entity.is_alive:
|
||||
entity.del_source_of_copy()
|
||||
|
||||
def post_bind_hook(self) -> None:
|
||||
"""Called by binding a new or copied dictionary to the document,
|
||||
bind hard owned sub-entities to the same document and add them to the
|
||||
objects section.
|
||||
"""
|
||||
if not self.dxf.hard_owned:
|
||||
return
|
||||
|
||||
# copied or new dictionary:
|
||||
doc = self.doc
|
||||
assert doc is not None
|
||||
object_section = doc.objects
|
||||
owner_handle = self.dxf.handle
|
||||
for _, entity in self.items():
|
||||
entity.dxf.owner = owner_handle
|
||||
factory.bind(entity, doc)
|
||||
# For a correct DXF export add entities to the objects section:
|
||||
object_section.add_object(entity)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.fast_load_dxfattribs(
|
||||
dxf, acdb_dictionary_group_codes, 1, log=False
|
||||
)
|
||||
self.load_dict(tags)
|
||||
return dxf
|
||||
|
||||
def load_dict(self, tags):
|
||||
entry_handle = None
|
||||
dict_key = None
|
||||
value_code = VALUE_CODE
|
||||
for code, value in tags:
|
||||
if code in SEARCH_CODES:
|
||||
# First store handles, because at this point, NOT all objects
|
||||
# are stored in the EntityDB, at first access convert the handle
|
||||
# to a DXFEntity object.
|
||||
value_code = code
|
||||
entry_handle = value
|
||||
elif code == KEY_CODE:
|
||||
dict_key = value
|
||||
if dict_key and entry_handle:
|
||||
# Store entity as handle string:
|
||||
self._data[dict_key] = entry_handle
|
||||
entry_handle = None
|
||||
dict_key = None
|
||||
# Use same value code as loaded:
|
||||
self._value_code = value_code
|
||||
|
||||
def post_load_hook(self, doc: Drawing) -> None:
|
||||
super().post_load_hook(doc)
|
||||
db = doc.entitydb
|
||||
|
||||
def items():
|
||||
for key, handle in self.items():
|
||||
entity = db.get(handle)
|
||||
if entity is not None and entity.is_alive:
|
||||
yield key, entity
|
||||
|
||||
if len(self):
|
||||
for k, v in list(items()):
|
||||
self.__setitem__(k, v)
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dictionary.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["hard_owned", "cloning"])
|
||||
self.export_dict(tagwriter)
|
||||
|
||||
def export_dict(self, tagwriter: AbstractTagWriter):
|
||||
# key: dict key string
|
||||
# value: DXFEntity or handle as string
|
||||
# Ignore invalid handles at export, because removing can create an empty
|
||||
# dictionary, which is more a problem for AutoCAD than invalid handles,
|
||||
# and removing the whole dictionary is maybe also a problem.
|
||||
for key, value in self._data.items():
|
||||
tagwriter.write_tag2(KEY_CODE, key)
|
||||
# Value can be a handle string or a DXFEntity object:
|
||||
if isinstance(value, DXFEntity):
|
||||
if value.is_alive:
|
||||
value = value.dxf.handle
|
||||
else:
|
||||
logger.debug(
|
||||
f'Key "{key}" points to a destroyed entity '
|
||||
f'in {str(self)}, target replaced by "0" handle.'
|
||||
)
|
||||
value = "0"
|
||||
# Use same value code as loaded:
|
||||
tagwriter.write_tag2(self._value_code, value)
|
||||
|
||||
@property
|
||||
def is_hard_owner(self) -> bool:
|
||||
"""Returns ``True`` if the dictionary is hard owner of entities.
|
||||
Hard owned entities will be destroyed by deleting the dictionary.
|
||||
"""
|
||||
return bool(self.dxf.hard_owned)
|
||||
|
||||
def keys(self):
|
||||
"""Returns a :class:`KeysView` of all dictionary keys."""
|
||||
return self._data.keys()
|
||||
|
||||
def items(self):
|
||||
"""Returns an :class:`ItemsView` for all dictionary entries as
|
||||
(key, entity) pairs. An entity can be a handle string if the entity
|
||||
does not exist.
|
||||
"""
|
||||
for key in self.keys():
|
||||
yield key, self.get(key) # maybe handle -> DXFEntity
|
||||
|
||||
def __getitem__(self, key: str) -> DXFEntity:
|
||||
"""Return self[`key`].
|
||||
|
||||
The returned value can be a handle string if the entity does not exist.
|
||||
|
||||
Raises:
|
||||
DXFKeyError: `key` does not exist
|
||||
|
||||
"""
|
||||
if key in self._data:
|
||||
return self._data[key] # type: ignore
|
||||
else:
|
||||
raise DXFKeyError(key)
|
||||
|
||||
def __setitem__(self, key: str, entity: DXFObject) -> None:
|
||||
"""Set self[`key`] = `entity`.
|
||||
|
||||
Only DXF objects stored in the OBJECTS section are allowed as content
|
||||
of :class:`Dictionary` objects. DXF entities stored in layouts are not
|
||||
allowed.
|
||||
|
||||
Raises:
|
||||
DXFTypeError: invalid DXF type
|
||||
|
||||
"""
|
||||
return self.add(key, entity)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
"""Delete self[`key`].
|
||||
|
||||
Raises:
|
||||
DXFKeyError: `key` does not exist
|
||||
|
||||
"""
|
||||
return self.remove(key)
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
"""Returns `key` ``in`` self."""
|
||||
return key in self._data
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Returns count of dictionary entries."""
|
||||
return len(self._data)
|
||||
|
||||
count = __len__
|
||||
|
||||
def get(self, key: str, default: Optional[DXFObject] = None) -> Optional[DXFObject]:
|
||||
"""Returns the :class:`DXFEntity` for `key`, if `key` exist else
|
||||
`default`. An entity can be a handle string if the entity
|
||||
does not exist.
|
||||
|
||||
"""
|
||||
return self._data.get(key, default) # type: ignore
|
||||
|
||||
def find_key(self, entity: DXFEntity) -> str:
|
||||
"""Returns the DICTIONARY key string for `entity` or an empty string if not
|
||||
found.
|
||||
"""
|
||||
for key, entry in self._data.items():
|
||||
if entry is entity:
|
||||
return key
|
||||
return ""
|
||||
|
||||
def add(self, key: str, entity: DXFObject) -> None:
|
||||
"""Add entry (key, value).
|
||||
|
||||
If the DICTIONARY is hard owner of its entries, the :meth:`add` does NOT take
|
||||
ownership of the entity automatically.
|
||||
|
||||
Raises:
|
||||
DXFValueError: invalid entity handle
|
||||
DXFTypeError: invalid DXF type
|
||||
|
||||
"""
|
||||
if isinstance(entity, str):
|
||||
if not is_valid_handle(entity):
|
||||
raise DXFValueError(f"Invalid entity handle #{entity} for key {key}")
|
||||
elif isinstance(entity, DXFGraphic):
|
||||
if self.doc is not None and self.doc.is_loading: # type: ignore
|
||||
# AutoCAD add-ons can store graphical entities in DICTIONARIES
|
||||
# in the OBJECTS section and AutoCAD does not complain - so just
|
||||
# preserve them!
|
||||
# Example "ZJMC-288.dxf" in issue #585, add-on: "acdgnlsdraw.crx"?
|
||||
logger.warning(f"Invalid entity {str(entity)} in {str(self)}")
|
||||
else:
|
||||
# Do not allow ezdxf users to add graphical entities to a
|
||||
# DICTIONARY object!
|
||||
raise DXFTypeError(f"Graphic entities not allowed: {entity.dxftype()}")
|
||||
self._data[key] = entity
|
||||
|
||||
def take_ownership(self, key: str, entity: DXFObject):
|
||||
"""Add entry (key, value) and take ownership."""
|
||||
self.add(key, entity)
|
||||
entity.dxf.owner = self.dxf.handle
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
"""Delete entry `key`. Raises :class:`DXFKeyError`, if `key` does not
|
||||
exist. Destroys hard owned DXF entities.
|
||||
|
||||
"""
|
||||
data = self._data
|
||||
if key not in data:
|
||||
raise DXFKeyError(key)
|
||||
|
||||
if self.is_hard_owner:
|
||||
assert self.doc is not None
|
||||
entity = self.__getitem__(key)
|
||||
# Presumption: hard owned DXF objects always reside in the OBJECTS
|
||||
# section.
|
||||
self.doc.objects.delete_entity(entity) # type: ignore
|
||||
del data[key]
|
||||
|
||||
def discard(self, key: str) -> None:
|
||||
"""Delete entry `key` if exists. Does not raise an exception if `key`
|
||||
doesn't exist and does not destroy hard owned DXF entities.
|
||||
|
||||
"""
|
||||
try:
|
||||
del self._data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Delete all entries from the dictionary and destroys hard owned
|
||||
DXF entities.
|
||||
"""
|
||||
if self.is_hard_owner:
|
||||
self._delete_hard_owned_entries()
|
||||
self._data.clear()
|
||||
|
||||
def _delete_hard_owned_entries(self) -> None:
|
||||
# Presumption: hard owned DXF objects always reside in the OBJECTS section
|
||||
objects = self.doc.objects # type: ignore
|
||||
for key, entity in self.items():
|
||||
if isinstance(entity, DXFEntity):
|
||||
objects.delete_entity(entity) # type: ignore
|
||||
|
||||
def add_new_dict(self, key: str, hard_owned: bool = False) -> Dictionary:
|
||||
"""Create a new sub-dictionary of type :class:`Dictionary`.
|
||||
|
||||
Args:
|
||||
key: name of the sub-dictionary
|
||||
hard_owned: entries of the new dictionary are hard owned
|
||||
|
||||
"""
|
||||
dxf_dict = self.doc.objects.add_dictionary( # type: ignore
|
||||
owner=self.dxf.handle, hard_owned=hard_owned
|
||||
)
|
||||
self.add(key, dxf_dict)
|
||||
return dxf_dict
|
||||
|
||||
def add_dict_var(self, key: str, value: str) -> DictionaryVar:
|
||||
"""Add a new :class:`DictionaryVar`.
|
||||
|
||||
Args:
|
||||
key: entry name as string
|
||||
value: entry value as string
|
||||
|
||||
"""
|
||||
new_var = self.doc.objects.add_dictionary_var( # type: ignore
|
||||
owner=self.dxf.handle, value=value
|
||||
)
|
||||
self.add(key, new_var)
|
||||
return new_var
|
||||
|
||||
def add_xrecord(self, key: str) -> XRecord:
|
||||
"""Add a new :class:`XRecord`.
|
||||
|
||||
Args:
|
||||
key: entry name as string
|
||||
|
||||
"""
|
||||
new_xrecord = self.doc.objects.add_xrecord( # type: ignore
|
||||
owner=self.dxf.handle,
|
||||
)
|
||||
self.add(key, new_xrecord)
|
||||
return new_xrecord
|
||||
|
||||
def set_or_add_dict_var(self, key: str, value: str) -> DictionaryVar:
|
||||
"""Set or add new :class:`DictionaryVar`.
|
||||
|
||||
Args:
|
||||
key: entry name as string
|
||||
value: entry value as string
|
||||
|
||||
"""
|
||||
if key not in self:
|
||||
dict_var = self.doc.objects.add_dictionary_var( # type: ignore
|
||||
owner=self.dxf.handle, value=value
|
||||
)
|
||||
self.add(key, dict_var)
|
||||
else:
|
||||
dict_var = self.get(key)
|
||||
dict_var.dxf.value = str(value) # type: ignore
|
||||
return dict_var
|
||||
|
||||
def link_dxf_object(self, name: str, obj: DXFObject) -> None:
|
||||
"""Add `obj` and set owner of `obj` to this dictionary.
|
||||
|
||||
Graphical DXF entities have to reside in a layout and therefore can not
|
||||
be owned by a :class:`Dictionary`.
|
||||
|
||||
Raises:
|
||||
DXFTypeError: `obj` has invalid DXF type
|
||||
|
||||
"""
|
||||
if not isinstance(obj, DXFObject):
|
||||
raise DXFTypeError(f"invalid DXF type: {obj.dxftype()}")
|
||||
self.add(name, obj)
|
||||
obj.dxf.owner = self.dxf.handle
|
||||
|
||||
def get_required_dict(self, key: str, hard_owned=False) -> Dictionary:
|
||||
"""Get entry `key` or create a new :class:`Dictionary`,
|
||||
if `Key` not exist.
|
||||
"""
|
||||
dxf_dict = self.get(key)
|
||||
if dxf_dict is None:
|
||||
dxf_dict = self.add_new_dict(key, hard_owned=hard_owned)
|
||||
elif not isinstance(dxf_dict, Dictionary):
|
||||
raise DXFStructureError(
|
||||
f"expected a DICTIONARY entity, got {str(dxf_dict)} for key: {key}"
|
||||
)
|
||||
return dxf_dict
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
if not self.is_alive:
|
||||
return
|
||||
super().audit(auditor)
|
||||
self._remove_keys_to_missing_entities(auditor)
|
||||
|
||||
def _remove_keys_to_missing_entities(self, auditor: Auditor):
|
||||
trash: list[str] = []
|
||||
append = trash.append
|
||||
db = auditor.entitydb
|
||||
for key, entry in self._data.items():
|
||||
if isinstance(entry, str):
|
||||
if entry not in db:
|
||||
append(key)
|
||||
elif entry.is_alive:
|
||||
if entry.dxf.handle not in db:
|
||||
append(key)
|
||||
continue
|
||||
else: # entity is destroyed, remove key
|
||||
append(key)
|
||||
for key in trash:
|
||||
del self._data[key]
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_DICTIONARY_ENTRY,
|
||||
message=f'Removed entry "{key}" with invalid handle in {str(self)}',
|
||||
dxf_entity=self,
|
||||
data=key,
|
||||
)
|
||||
|
||||
def destroy(self) -> None:
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
if self.is_hard_owner:
|
||||
self._delete_hard_owned_entries()
|
||||
super().destroy()
|
||||
|
||||
|
||||
acdb_dict_with_default = DefSubclass(
|
||||
"AcDbDictionaryWithDefault",
|
||||
{
|
||||
"default": DXFAttr(340),
|
||||
},
|
||||
)
|
||||
acdb_dict_with_default_group_codes = group_code_mapping(acdb_dict_with_default)
|
||||
|
||||
|
||||
@factory.register_entity
|
||||
class DictionaryWithDefault(Dictionary):
|
||||
DXFTYPE = "ACDBDICTIONARYWDFLT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_dictionary, acdb_dict_with_default)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._default: Optional[DXFObject] = None
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
super().copy_data(entity, copy_strategy=copy_strategy)
|
||||
assert isinstance(entity, DictionaryWithDefault)
|
||||
entity._default = self._default
|
||||
|
||||
def post_load_hook(self, doc: Drawing) -> None:
|
||||
# Set _default to None if default object not exist - audit() replaces
|
||||
# a not existing default object by a placeholder object.
|
||||
# AutoCAD ignores not existing default objects!
|
||||
self._default = doc.entitydb.get(self.dxf.default) # type: ignore
|
||||
super().post_load_hook(doc)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_dict_with_default_group_codes, 2)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_with_default.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, "default")
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
return self.get(key)
|
||||
|
||||
def get(self, key: str, default: Optional[DXFObject] = None) -> Optional[DXFObject]:
|
||||
# `default` argument is ignored, exist only for API compatibility,
|
||||
"""Returns :class:`DXFEntity` for `key` or the predefined dictionary
|
||||
wide :attr:`dxf.default` entity if `key` does not exist or ``None``
|
||||
if default value also not exist.
|
||||
|
||||
"""
|
||||
return super().get(key, default=self._default)
|
||||
|
||||
def set_default(self, default: DXFObject) -> None:
|
||||
"""Set dictionary wide default entry.
|
||||
|
||||
Args:
|
||||
default: default entry as :class:`DXFEntity`
|
||||
|
||||
"""
|
||||
self._default = default
|
||||
self.dxf.default = self._default.dxf.handle
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
def create_missing_default_object():
|
||||
placeholder = self.doc.objects.add_placeholder(owner=self.dxf.handle)
|
||||
self.set_default(placeholder)
|
||||
auditor.fixed_error(
|
||||
code=AuditError.CREATED_MISSING_OBJECT,
|
||||
message=f"Created missing default object in {str(self)}.",
|
||||
)
|
||||
|
||||
if self._default is None or not self._default.is_alive:
|
||||
if auditor.entitydb.locked:
|
||||
auditor.add_post_audit_job(create_missing_default_object)
|
||||
else:
|
||||
create_missing_default_object()
|
||||
super().audit(auditor)
|
||||
|
||||
|
||||
acdb_dict_var = DefSubclass(
|
||||
"DictionaryVariables",
|
||||
{
|
||||
"schema": DXFAttr(280, default=0),
|
||||
# Object schema number (currently set to 0)
|
||||
"value": DXFAttr(1, default=""),
|
||||
},
|
||||
)
|
||||
acdb_dict_var_group_codes = group_code_mapping(acdb_dict_var)
|
||||
|
||||
|
||||
@factory.register_entity
|
||||
class DictionaryVar(DXFObject):
|
||||
"""
|
||||
DICTIONARYVAR objects are used by AutoCAD as a means to store named values
|
||||
in the database for setvar / getvar purposes without the need to add entries
|
||||
to the DXF HEADER section. System variables that are stored as
|
||||
DICTIONARYVAR objects are the following:
|
||||
|
||||
- DEFAULTVIEWCATEGORY
|
||||
- DIMADEC
|
||||
- DIMASSOC
|
||||
- DIMDSEP
|
||||
- DRAWORDERCTL
|
||||
- FIELDEVAL
|
||||
- HALOGAP
|
||||
- HIDETEXT
|
||||
- INDEXCTL
|
||||
- INDEXCTL
|
||||
- INTERSECTIONCOLOR
|
||||
- INTERSECTIONDISPLAY
|
||||
- MSOLESCALE
|
||||
- OBSCOLOR
|
||||
- OBSLTYPE
|
||||
- OLEFRAME
|
||||
- PROJECTNAME
|
||||
- SORTENTS
|
||||
- UPDATETHUMBNAIL
|
||||
- XCLIPFRAME
|
||||
- XCLIPFRAME
|
||||
|
||||
"""
|
||||
|
||||
DXFTYPE = "DICTIONARYVAR"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_dict_var)
|
||||
|
||||
@property
|
||||
def value(self) -> str:
|
||||
"""Get/set the value of the :class:`DictionaryVar` as string."""
|
||||
return self.dxf.get("value", "")
|
||||
|
||||
@value.setter
|
||||
def value(self, data: str) -> None:
|
||||
self.dxf.set("value", str(data))
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_dict_var_group_codes, 1)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dict_var.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["schema", "value"])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from .dxfns import SubclassProcessor, DXFNamespace
|
||||
from .dxfentity import DXFEntity
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import DXF2004, DXF2000
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.lldxf.extendedtags import ExtendedTags
|
||||
|
||||
__all__ = ["DXFClass"]
|
||||
|
||||
class_def = DefSubclass(
|
||||
None,
|
||||
{
|
||||
# Class DXF record name; always unique
|
||||
"name": DXFAttr(1),
|
||||
# C++ class name. Used to bind with software that defines object class
|
||||
# behavior; always unique
|
||||
"cpp_class_name": DXFAttr(2),
|
||||
# Application name. Posted in Alert box when a class definition listed in
|
||||
# this section is not currently loaded
|
||||
"app_name": DXFAttr(3),
|
||||
# Proxy capabilities flag. Bit-coded value that indicates the capabilities
|
||||
# of this object as a proxy:
|
||||
# 0 = No operations allowed (0)
|
||||
# 1 = Erase allowed (0x1)
|
||||
# 2 = Transform allowed (0x2)
|
||||
# 4 = Color change allowed (0x4)
|
||||
# 8 = Layer change allowed (0x8)
|
||||
# 16 = Linetype change allowed (0x10)
|
||||
# 32 = Linetype scale change allowed (0x20)
|
||||
# 64 = Visibility change allowed (0x40)
|
||||
# 128 = Cloning allowed (0x80)
|
||||
# 256 = Lineweight change allowed (0x100)
|
||||
# 512 = Plot Style Name change allowed (0x200)
|
||||
# 895 = All operations except cloning allowed (0x37F)
|
||||
# 1023 = All operations allowed (0x3FF)
|
||||
# 1024 = Disables proxy warning dialog (0x400)
|
||||
# 32768 = R13 format proxy (0x8000)
|
||||
"flags": DXFAttr(90, default=0),
|
||||
# Instance count for a custom class
|
||||
"instance_count": DXFAttr(91, dxfversion=DXF2004, default=0),
|
||||
# Was-a-proxy flag. Set to 1 if class was not loaded when this DXF file was
|
||||
# created, and 0 otherwise
|
||||
"was_a_proxy": DXFAttr(280, default=0),
|
||||
# Is-an-entity flag. Set to 1 if class was derived from the AcDbEntity class
|
||||
# and can reside in the BLOCKS or ENTITIES section. If 0, instances may
|
||||
# appear only in the OBJECTS section
|
||||
"is_an_entity": DXFAttr(281, default=0),
|
||||
},
|
||||
)
|
||||
class_def_group_codes = group_code_mapping(class_def)
|
||||
|
||||
|
||||
@register_entity
|
||||
class DXFClass(DXFEntity):
|
||||
DXFTYPE = "CLASS"
|
||||
DXFATTRIBS = DXFAttributes(class_def)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
handle: Optional[str] = None,
|
||||
owner: Optional[str] = None,
|
||||
dxfattribs=None,
|
||||
doc: Optional[Drawing] = None,
|
||||
) -> DXFClass:
|
||||
"""New CLASS constructor - has no handle, no owner and do not need
|
||||
document reference .
|
||||
"""
|
||||
dxf_class = cls()
|
||||
dxf_class.doc = doc
|
||||
dxfattribs = dxfattribs or {}
|
||||
dxf_class.update_dxf_attribs(dxfattribs)
|
||||
return dxf_class
|
||||
|
||||
def load_tags(
|
||||
self, tags: ExtendedTags, dxfversion: Optional[str] = None
|
||||
) -> None:
|
||||
"""Called by load constructor. CLASS is special."""
|
||||
if tags:
|
||||
# do not process base class!!!
|
||||
self.dxf = DXFNamespace(entity=self)
|
||||
processor = SubclassProcessor(tags)
|
||||
processor.fast_load_dxfattribs(
|
||||
self.dxf, class_def_group_codes, 0, log=False
|
||||
)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter):
|
||||
"""Do complete export here, because CLASS is special."""
|
||||
dxfversion = tagwriter.dxfversion
|
||||
if dxfversion < DXF2000:
|
||||
return
|
||||
attribs = self.dxf
|
||||
tagwriter.write_tag2(0, self.DXFTYPE)
|
||||
attribs.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"cpp_class_name",
|
||||
"app_name",
|
||||
"flags",
|
||||
"instance_count",
|
||||
"was_a_proxy",
|
||||
"is_an_entity",
|
||||
],
|
||||
)
|
||||
|
||||
@property
|
||||
def key(self) -> tuple[str, str]:
|
||||
return self.dxf.name, self.dxf.cpp_class_name
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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 ""
|
||||
@@ -0,0 +1,687 @@
|
||||
# Copyright (c) 2020-2025, 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 __getstate__(self) -> object:
|
||||
return self.__dict__
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if not isinstance(state, dict):
|
||||
raise TypeError(f"invalid state: {type(state).__name__}")
|
||||
# bypass __setattr__
|
||||
object.__setattr__(self, "__dict__", state)
|
||||
|
||||
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 they differ from the default value.
|
||||
|
||||
"""
|
||||
attrib: Optional[DXFAttr] = self.dxfattribs.get(name)
|
||||
if attrib is None:
|
||||
raise const.DXFAttributeError(
|
||||
ERR_INVALID_DXF_ATTRIB.format(name, self.dxftype)
|
||||
)
|
||||
|
||||
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 can be None!
|
||||
value = default
|
||||
|
||||
if value is None:
|
||||
logger.debug(
|
||||
f"DXF attribute '{name}' not written because its a None value."
|
||||
)
|
||||
return
|
||||
|
||||
# Do not write explicit optional attribs if equal to the default value
|
||||
if (
|
||||
optional
|
||||
and (not tagwriter.force_optional)
|
||||
and default is not None
|
||||
and default == value
|
||||
):
|
||||
return
|
||||
_export_group_codes(tagwriter, attrib, value)
|
||||
|
||||
def export_dxf_attribute_if_exists(
|
||||
self, tagwriter: AbstractTagWriter, name: str
|
||||
) -> None:
|
||||
"""Exports DXF attribute `name` by `tagwriter` if exists.
|
||||
|
||||
If the attribute exists, and it's not None it will be written, the optional-flag
|
||||
is ignored.
|
||||
|
||||
No default value will be written if the attribute doesn't exist!
|
||||
This method can not be used for attributes that are required (e.g. layer)!
|
||||
|
||||
"""
|
||||
if not self.hasattr(name):
|
||||
return
|
||||
|
||||
attrib: Optional[DXFAttr] = self.dxfattribs.get(name)
|
||||
assert (
|
||||
attrib is not None
|
||||
), f"existing DXF attribute '{name}' has no definition class - internal error"
|
||||
|
||||
value = self.get(name)
|
||||
if value is None:
|
||||
logger.debug(
|
||||
f"DXF attribute '{name}' not written because its a None value."
|
||||
)
|
||||
return
|
||||
_export_group_codes(tagwriter, attrib, value)
|
||||
|
||||
|
||||
def _export_group_codes(
|
||||
tagwriter: AbstractTagWriter, attrib: DXFAttr, value: Any
|
||||
) -> None:
|
||||
assert attrib is not None
|
||||
assert value is not None
|
||||
|
||||
# Do write the attribute if the export DXF version is lower than the minimal
|
||||
# required DXF version for the attribute.
|
||||
if tagwriter.dxfversion < attrib.dxfversion:
|
||||
return
|
||||
|
||||
# For explicit 2D points export only x- and y-coordinates.
|
||||
if attrib.xtype == XType.point2d and len(value) > 2:
|
||||
try: # Vec3, Vec2
|
||||
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)
|
||||
|
||||
|
||||
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(
|
||||
unprocessed_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
|
||||
@@ -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
|
||||
@@ -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}.",
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1,124 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
import enum
|
||||
import math
|
||||
|
||||
from ezdxf.colors import RGB, int2rgb, rgb2int
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["Gradient", "GradientType"]
|
||||
|
||||
GRADIENT_CODES = {450, 451, 452, 453, 460, 461, 462, 463, 470, 421, 63}
|
||||
|
||||
|
||||
class GradientType(enum.IntEnum):
|
||||
NONE = 0
|
||||
LINEAR = 1
|
||||
CYLINDER = 2
|
||||
INVCYLINDER = 3
|
||||
SPHERICAL = 4
|
||||
INVSPHERICAL = 5
|
||||
HEMISPHERICAL = 6
|
||||
INVHEMISPHERICAL = 7
|
||||
CURVED = 8
|
||||
INVCURVED = 9
|
||||
|
||||
|
||||
gradient_names = [
|
||||
"",
|
||||
"LINEAR",
|
||||
"CYLINDER",
|
||||
"INVCYLINDER",
|
||||
"SPHERICAL",
|
||||
"INVSPHERICAL",
|
||||
"HEMISPHERICAL",
|
||||
"INVHEMISPHERICAL",
|
||||
"CURVED",
|
||||
"INVCURVED",
|
||||
]
|
||||
|
||||
|
||||
class Gradient:
|
||||
def __init__(self, kind: int = 1, num: int = 2, type=GradientType.LINEAR):
|
||||
# 1 for gradient by default, 0 for Solid
|
||||
self.kind: int = kind
|
||||
self.number_of_colors: int = num
|
||||
self.color1: RGB = RGB(0, 0, 0)
|
||||
self.aci1: Optional[int] = None
|
||||
self.color2: RGB = RGB(255, 255, 255)
|
||||
self.aci2: Optional[int] = None
|
||||
|
||||
# 1 = use a smooth transition between color1 and a specified tint
|
||||
self.one_color: int = 0
|
||||
|
||||
# Use degree NOT radians for rotation, because there should be one
|
||||
# system for all angles:
|
||||
self.rotation: float = 0.0
|
||||
self.centered: float = 0.0
|
||||
self.tint: float = 0.0
|
||||
self.name: str = gradient_names[type]
|
||||
|
||||
@classmethod
|
||||
def load_tags(cls, tags: Tags) -> Gradient:
|
||||
gdata = cls()
|
||||
assert tags[0].code == 450
|
||||
gdata.kind = tags[0].value # 0 = solid; 1 = gradient
|
||||
first_color_value = True
|
||||
first_aci_value = True
|
||||
for code, value in tags:
|
||||
if code == 460:
|
||||
gdata.rotation = math.degrees(value)
|
||||
elif code == 461:
|
||||
gdata.centered = value
|
||||
elif code == 452:
|
||||
gdata.one_color = value
|
||||
elif code == 462:
|
||||
gdata.tint = value
|
||||
elif code == 470:
|
||||
gdata.name = value
|
||||
elif code == 453:
|
||||
gdata.number_of_colors = value
|
||||
elif code == 63:
|
||||
if first_aci_value:
|
||||
gdata.aci1 = value
|
||||
first_aci_value = False
|
||||
else:
|
||||
gdata.aci2 = value
|
||||
elif code == 421:
|
||||
if first_color_value:
|
||||
gdata.color1 = int2rgb(value)
|
||||
first_color_value = False
|
||||
else:
|
||||
gdata.color2 = int2rgb(value)
|
||||
return gdata
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
||||
# Tag order matters!
|
||||
write_tag = tagwriter.write_tag2
|
||||
write_tag(450, self.kind) # gradient or solid
|
||||
write_tag(451, 0) # reserved for the future
|
||||
|
||||
# rotation angle in radians:
|
||||
write_tag(460, math.radians(self.rotation))
|
||||
write_tag(461, self.centered)
|
||||
write_tag(452, self.one_color)
|
||||
write_tag(462, self.tint)
|
||||
write_tag(453, self.number_of_colors)
|
||||
if self.number_of_colors > 0:
|
||||
write_tag(463, 0) # first value, see DXF standard
|
||||
if self.aci1 is not None:
|
||||
# code 63 "color as ACI" could be left off
|
||||
write_tag(63, self.aci1)
|
||||
write_tag(421, rgb2int(self.color1)) # first color
|
||||
if self.number_of_colors > 1:
|
||||
write_tag(463, 1) # second value, see DXF standard
|
||||
if self.aci2 is not None:
|
||||
# code 63 "color as ACI" could be left off
|
||||
write_tag(63, self.aci2)
|
||||
write_tag(421, rgb2int(self.color2)) # second color
|
||||
write_tag(470, self.name)
|
||||
@@ -0,0 +1,323 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.math import NULLVEC, Z_AXIS
|
||||
from .boundary_paths import AbstractBoundaryPath
|
||||
from .dxfentity import base_class
|
||||
from .dxfgfx import acdb_entity
|
||||
from .factory import register_entity
|
||||
from .polygon import DXFPolygon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.colors import RGB
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
|
||||
__all__ = ["Hatch"]
|
||||
|
||||
|
||||
acdb_hatch = DefSubclass(
|
||||
"AcDbHatch",
|
||||
{
|
||||
# This subclass can also represent a MPolygon, whatever this is, never seen
|
||||
# such a MPolygon in the wild.
|
||||
# x- and y-axis always equal 0, z-axis represents the elevation:
|
||||
"elevation": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch pattern name:
|
||||
"pattern_name": DXFAttr(2, default="SOLID"),
|
||||
# HATCH: Solid fill flag:
|
||||
# 0 = pattern fill
|
||||
# 1 = solid fill
|
||||
"solid_fill": DXFAttr(
|
||||
70,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# HATCH: associativity flag
|
||||
# 0 = non-associative
|
||||
# 1 = associative
|
||||
"associative": DXFAttr(
|
||||
71,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 91: Number of boundary paths (loops)
|
||||
# following: Boundary path data. Repeats number of times specified by
|
||||
# code 91. See Boundary Path Data
|
||||
# Hatch style:
|
||||
# 0 = Hatch “odd parity” area (Normal style)
|
||||
# 1 = Hatch outermost area only (Outer style)
|
||||
# 2 = Hatch through entire area (Ignore style)
|
||||
"hatch_style": DXFAttr(
|
||||
75,
|
||||
default=const.HATCH_STYLE_NESTED,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch pattern type:
|
||||
# 0 = User-defined
|
||||
# 1 = Predefined
|
||||
# 2 = Custom
|
||||
"pattern_type": DXFAttr(
|
||||
76,
|
||||
default=const.HATCH_TYPE_PREDEFINED,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch pattern angle (pattern fill only) in degrees:
|
||||
"pattern_angle": DXFAttr(52, default=0),
|
||||
# Hatch pattern scale or spacing (pattern fill only):
|
||||
"pattern_scale": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch pattern double flag (pattern fill only):
|
||||
# 0 = not double
|
||||
# 1 = double
|
||||
"pattern_double": DXFAttr(
|
||||
77,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 78: Number of pattern definition lines
|
||||
# following: Pattern line data. Repeats number of times specified by
|
||||
# code 78. See Pattern Data
|
||||
# Pixel size used to determine the density to perform various intersection
|
||||
# and ray casting operations in hatch pattern computation for associative
|
||||
# hatches and hatches created with the Flood method of hatching
|
||||
"pixel_size": DXFAttr(47, optional=True),
|
||||
# Number of seed points
|
||||
"n_seed_points": DXFAttr(
|
||||
98,
|
||||
default=0,
|
||||
validator=validator.is_greater_or_equal_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 10, 20: Seed point (in OCS) 2D point (multiple entries)
|
||||
# 450 Indicates solid hatch or gradient; if solid hatch, the values for the
|
||||
# remaining codes are ignored but must be present. Optional;
|
||||
#
|
||||
# if code 450 is in the file, then the following codes must be in the
|
||||
# file: 451, 452, 453, 460, 461, 462, and 470.
|
||||
# If code 450 is not in the file, then the following codes must not be
|
||||
# in the file: 451, 452, 453, 460, 461, 462, and 470
|
||||
#
|
||||
# 0 = Solid hatch
|
||||
# 1 = Gradient
|
||||
#
|
||||
# 451 Zero is reserved for future use
|
||||
# 452 Records how colors were defined and is used only by dialog code:
|
||||
#
|
||||
# 0 = Two-color gradient
|
||||
# 1 = Single-color gradient
|
||||
#
|
||||
# 453 Number of colors:
|
||||
#
|
||||
# 0 = Solid hatch
|
||||
# 2 = Gradient
|
||||
#
|
||||
# 460 Rotation angle in radians for gradients (default = 0, 0)
|
||||
# 461 Gradient definition; corresponds to the Centered option on the
|
||||
# Gradient Tab of the Boundary Hatch and Fill dialog box. Each gradient
|
||||
# has two definitions, shifted and non-shifted. A Shift value describes
|
||||
# the blend of the two definitions that should be used. A value of 0.0
|
||||
# means only the non-shifted version should be used, and a value of 1.0
|
||||
# means that only the shifted version should be used.
|
||||
#
|
||||
# 462 Color tint value used by dialog code (default = 0, 0; range is 0.0 to
|
||||
# 1.0). The color tint value is a gradient color and controls the degree
|
||||
# of tint in the dialog when the Hatch group code 452 is set to 1.
|
||||
#
|
||||
# 463 Reserved for future use:
|
||||
#
|
||||
# 0 = First value
|
||||
# 1 = Second value
|
||||
#
|
||||
# 470 String (default = LINEAR)
|
||||
},
|
||||
)
|
||||
acdb_hatch_group_code = group_code_mapping(acdb_hatch)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Hatch(DXFPolygon):
|
||||
"""DXF HATCH entity"""
|
||||
|
||||
DXFTYPE = "HATCH"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_hatch)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
||||
LOAD_GROUP_CODES = acdb_hatch_group_code
|
||||
|
||||
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_hatch.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"elevation",
|
||||
"extrusion",
|
||||
"pattern_name",
|
||||
"solid_fill",
|
||||
"associative",
|
||||
],
|
||||
)
|
||||
self.paths.export_dxf(tagwriter, self.dxftype())
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["hatch_style", "pattern_type"])
|
||||
if self.pattern:
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter, ["pattern_angle", "pattern_scale", "pattern_double"]
|
||||
)
|
||||
self.pattern.export_dxf(tagwriter)
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["pixel_size"])
|
||||
self.export_seeds(tagwriter)
|
||||
if self.gradient and tagwriter.dxfversion > const.DXF2000:
|
||||
self.gradient.export_dxf(tagwriter)
|
||||
|
||||
def load_seeds(self, tags: Tags) -> Tags:
|
||||
try:
|
||||
start_index = tags.tag_index(98)
|
||||
except const.DXFValueError:
|
||||
return tags
|
||||
seed_data = tags.collect_consecutive_tags(
|
||||
{98, 10, 20}, start=start_index
|
||||
)
|
||||
|
||||
# Remove seed data from tags:
|
||||
del tags[start_index : start_index + len(seed_data) + 1]
|
||||
|
||||
# Just process vertices with group code 10
|
||||
self.seeds = [value for code, value in seed_data if code == 10]
|
||||
return tags
|
||||
|
||||
def export_seeds(self, tagwriter: AbstractTagWriter):
|
||||
tagwriter.write_tag2(98, len(self.seeds))
|
||||
for seed in self.seeds:
|
||||
tagwriter.write_vertex(10, seed[:2])
|
||||
|
||||
def remove_dependencies(self, other: Optional[Drawing] = None) -> None:
|
||||
"""Remove all dependencies from actual document. (internal API)"""
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
super().remove_dependencies()
|
||||
self.remove_association()
|
||||
|
||||
def remove_association(self):
|
||||
"""Remove associated path elements."""
|
||||
if self.dxf.associative:
|
||||
self.dxf.associative = 0
|
||||
for path in self.paths:
|
||||
path.source_boundary_objects = []
|
||||
|
||||
def set_solid_fill(
|
||||
self, color: int = 7, style: int = 1, rgb: Optional[RGB] = None
|
||||
):
|
||||
"""Set the solid fill mode and removes all gradient and pattern fill related
|
||||
data.
|
||||
|
||||
Args:
|
||||
color: :ref:`ACI`, (0 = BYBLOCK; 256 = BYLAYER)
|
||||
style: hatch style (0 = normal; 1 = outer; 2 = ignore)
|
||||
rgb: true color value as (r, g, b)-tuple - has higher priority
|
||||
than `color`. True color support requires DXF R2000.
|
||||
|
||||
"""
|
||||
# remove existing gradient and pattern fill
|
||||
self.gradient = None
|
||||
self.pattern = None
|
||||
self.dxf.solid_fill = 1
|
||||
|
||||
# if true color is present, the color attribute is ignored
|
||||
self.dxf.color = color
|
||||
self.dxf.hatch_style = style
|
||||
self.dxf.pattern_name = "SOLID"
|
||||
self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
|
||||
if rgb is not None:
|
||||
self.rgb = rgb
|
||||
|
||||
def associate(
|
||||
self, path: AbstractBoundaryPath, entities: Iterable[DXFEntity]
|
||||
):
|
||||
"""Set association from hatch boundary `path` to DXF geometry `entities`.
|
||||
|
||||
A HATCH entity can be associative to a base geometry, this association
|
||||
is **not** maintained nor verified by `ezdxf`, so if you modify the base
|
||||
geometry the geometry of the boundary path is not updated and no
|
||||
verification is done to check if the associated geometry matches
|
||||
the boundary path, this opens many possibilities to create
|
||||
invalid DXF files: USE WITH CARE!
|
||||
|
||||
"""
|
||||
# I don't see this as a time critical operation, do as much checks as
|
||||
# needed to avoid invalid DXF files.
|
||||
if not self.is_alive:
|
||||
raise const.DXFStructureError("HATCH entity is destroyed")
|
||||
|
||||
doc = self.doc
|
||||
owner = self.dxf.owner
|
||||
handle = self.dxf.handle
|
||||
if doc is None or owner is None or handle is None:
|
||||
raise const.DXFStructureError(
|
||||
"virtual entity can not have associated entities"
|
||||
)
|
||||
|
||||
for entity in entities:
|
||||
if not entity.is_alive or entity.is_virtual:
|
||||
raise const.DXFStructureError(
|
||||
"associated entity is destroyed or a virtual entity"
|
||||
)
|
||||
if doc is not entity.doc:
|
||||
raise const.DXFStructureError(
|
||||
"associated entity is from a different document"
|
||||
)
|
||||
if owner != entity.dxf.owner:
|
||||
raise const.DXFStructureError(
|
||||
"associated entity is from a different layout"
|
||||
)
|
||||
|
||||
path.source_boundary_objects.append(entity.dxf.handle)
|
||||
entity.append_reactor_handle(handle)
|
||||
self.dxf.associative = 1 if len(path.source_boundary_objects) else 0
|
||||
|
||||
def set_seed_points(self, points: Iterable[tuple[float, float]]) -> None:
|
||||
"""Set seed points, `points` is an iterable of (x, y)-tuples.
|
||||
I don't know why there can be more than one seed point.
|
||||
All points in :ref:`OCS` (:attr:`Hatch.dxf.elevation` is the Z value)
|
||||
|
||||
"""
|
||||
points = list(points)
|
||||
if len(points) < 1:
|
||||
raise const.DXFValueError(
|
||||
"Argument points should be an iterable of 2D points and requires"
|
||||
" at least one point."
|
||||
)
|
||||
self.seeds = list(points)
|
||||
self.dxf.n_seed_points = len(self.seeds)
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER
|
||||
from ezdxf.math import NULLVEC, X_AXIS, Z_AXIS
|
||||
from .spline import Spline, acdb_spline
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import acdb_entity
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.math import Matrix44
|
||||
|
||||
__all__ = ["Helix"]
|
||||
|
||||
acdb_helix = DefSubclass(
|
||||
"AcDbHelix",
|
||||
{
|
||||
"major_release_number": DXFAttr(90, default=29),
|
||||
"maintenance_release_number": DXFAttr(91, default=63),
|
||||
"axis_base_point": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
"start_point": DXFAttr(
|
||||
11,
|
||||
xtype=XType.point3d,
|
||||
default=X_AXIS,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"axis_vector": DXFAttr(
|
||||
12,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# base radius = (start-point - axis_base_point).magnitude
|
||||
"radius": DXFAttr(40, default=1), # top radius
|
||||
"turns": DXFAttr(41, default=1),
|
||||
"turn_height": DXFAttr(42, default=1),
|
||||
# Handedness:
|
||||
# 0 = left
|
||||
# 1 = right
|
||||
"handedness": DXFAttr(
|
||||
290,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Constrain type:
|
||||
# 0 = Constrain turn height
|
||||
# 1 = Constrain turns
|
||||
# 2 = Constrain height
|
||||
"constrain": DXFAttr(
|
||||
280,
|
||||
default=1,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_helix_group_codes = group_code_mapping(acdb_helix)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Helix(Spline):
|
||||
"""DXF HELIX entity"""
|
||||
|
||||
DXFTYPE = "HELIX"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_spline, acdb_helix)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_helix_group_codes, 3, recover=True
|
||||
)
|
||||
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_helix.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"major_release_number",
|
||||
"maintenance_release_number",
|
||||
"axis_base_point",
|
||||
"start_point",
|
||||
"axis_vector",
|
||||
"radius",
|
||||
"turns",
|
||||
"turn_height",
|
||||
"handedness",
|
||||
"constrain",
|
||||
],
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> Helix:
|
||||
"""Transform the HELIX entity by transformation matrix `m` inplace."""
|
||||
super().transform(m)
|
||||
self.dxf.axis_base_point = m.transform(self.dxf.axis_base_point)
|
||||
self.dxf.axis_vector = m.transform_direction(self.dxf.axis_vector)
|
||||
self.dxf.start_point = m.transform(self.dxf.start_point)
|
||||
self.dxf.radius = m.transform_direction(
|
||||
(self.dxf.radius, 0, 0)
|
||||
).magnitude
|
||||
self.post_transform(m)
|
||||
return self
|
||||
@@ -0,0 +1,139 @@
|
||||
# Copyright (c) 2019-2023, Manfred Moitzi
|
||||
# License: MIT-License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing_extensions import Self
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXFStructureError
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
DXFAttr,
|
||||
group_code_mapping,
|
||||
)
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfobj import DXFObject
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
|
||||
__all__ = ["IDBuffer", "FieldList", "LayerFilter"]
|
||||
|
||||
acdb_id_buffer = DefSubclass("AcDbIdBuffer", {})
|
||||
|
||||
|
||||
@register_entity
|
||||
class IDBuffer(DXFObject):
|
||||
"""DXF IDBUFFER entity"""
|
||||
|
||||
DXFTYPE = "IDBUFFER"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_id_buffer)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.handles: list[str] = []
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy handles"""
|
||||
assert isinstance(entity, IDBuffer)
|
||||
entity.handles = list(self.handles)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
if len(processor.subclasses) < 2:
|
||||
raise DXFStructureError(
|
||||
f"Missing required subclass in IDBUFFER(#{dxf.handle})"
|
||||
)
|
||||
self.load_handles(processor.subclasses[1])
|
||||
return dxf
|
||||
|
||||
def load_handles(self, tags: Tags):
|
||||
self.handles = [value for code, value in tags if code == 330]
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_id_buffer.name)
|
||||
self.export_handles(tagwriter)
|
||||
|
||||
def export_handles(self, tagwriter: AbstractTagWriter):
|
||||
for handle in self.handles:
|
||||
tagwriter.write_tag2(330, handle)
|
||||
|
||||
|
||||
acdb_id_set = DefSubclass(
|
||||
"AcDbIdSet",
|
||||
{
|
||||
"flags": DXFAttr(90, default=0), # not documented by Autodesk
|
||||
},
|
||||
)
|
||||
acdb_id_set_group_codes = group_code_mapping(acdb_id_set)
|
||||
acdb_field_list = DefSubclass("AcDbFieldList", {})
|
||||
|
||||
|
||||
@register_entity
|
||||
class FieldList(IDBuffer):
|
||||
"""DXF FIELDLIST entity"""
|
||||
|
||||
DXFTYPE = "FIELDLIST"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_id_set, acdb_field_list)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super(DXFObject, self).load_dxf_attribs(processor)
|
||||
if processor:
|
||||
if len(processor.subclasses) < 3:
|
||||
raise DXFStructureError(
|
||||
f"Missing required subclass in FIELDLIST(#{dxf.handle})"
|
||||
)
|
||||
processor.fast_load_dxfattribs(dxf, acdb_id_set_group_codes, 1)
|
||||
# Load field list:
|
||||
self.load_handles(processor.subclasses[1])
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super(DXFObject, self).export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_id_set.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, "flags")
|
||||
self.export_handles(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_field_list.name)
|
||||
|
||||
|
||||
acdb_filter = DefSubclass("AcDbFilter", {})
|
||||
acdb_layer_filter = DefSubclass("AcDbLayerFilter", {})
|
||||
|
||||
|
||||
@register_entity
|
||||
class LayerFilter(IDBuffer):
|
||||
"""DXF LAYER_FILTER entity"""
|
||||
|
||||
DXFTYPE = "LAYER_FILTER"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_filter, acdb_layer_filter)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super(DXFObject, self).load_dxf_attribs(processor)
|
||||
if processor:
|
||||
if len(processor.subclasses) < 3:
|
||||
raise DXFStructureError(
|
||||
f"Missing required subclass in LAYER_FILTER(#{dxf.handle})"
|
||||
)
|
||||
# Load layer filter:
|
||||
self.load_handles(processor.subclasses[2])
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super(DXFObject, self).export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_filter.name)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_layer_filter.name)
|
||||
self.export_handles(tagwriter)
|
||||
@@ -0,0 +1,760 @@
|
||||
# Copyright (c) 2019-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
cast,
|
||||
Optional,
|
||||
Callable,
|
||||
Union,
|
||||
Type,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
import logging
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000, DXF2010
|
||||
from ezdxf.math import Vec3, Vec2, BoundingBox2d, UVec, Matrix44
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .dxfobj import DXFObject
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.audit import Auditor
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity, Dictionary
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.lldxf.types import DXFTag
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["Image", "ImageDef", "ImageDefReactor", "RasterVariables", "Wipeout"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
class ImageBase(DXFGraphic):
|
||||
"""DXF IMAGE entity"""
|
||||
|
||||
DXFTYPE = "IMAGEBASE"
|
||||
_CLS_GROUP_CODES: dict[int, Union[str, list[str]]] = dict()
|
||||
_SUBCLASS_NAME = "dummy"
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
SHOW_IMAGE = 1
|
||||
SHOW_IMAGE_WHEN_NOT_ALIGNED = 2
|
||||
USE_CLIPPING_BOUNDARY = 4
|
||||
USE_TRANSPARENCY = 8
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# Boundary/Clipping path coordinates:
|
||||
# 0/0 is in the Left/Top corner of the image!
|
||||
# x-coordinates increases in u_pixel vector direction
|
||||
# y-coordinates increases against the v_pixel vector!
|
||||
# see also WCS coordinate calculation
|
||||
self._boundary_path: list[Vec2] = []
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
assert isinstance(entity, ImageBase)
|
||||
entity._boundary_path = list(self._boundary_path)
|
||||
|
||||
def post_new_hook(self) -> None:
|
||||
super().post_new_hook()
|
||||
self.reset_boundary_path()
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
path_tags = processor.subclasses[2].pop_tags(codes=(14,))
|
||||
self.load_boundary_path(path_tags)
|
||||
processor.fast_load_dxfattribs(dxf, self._CLS_GROUP_CODES, 2, recover=True)
|
||||
if len(self.boundary_path) < 2: # something is wrong
|
||||
self.dxf = dxf
|
||||
self.reset_boundary_path()
|
||||
return dxf
|
||||
|
||||
def load_boundary_path(self, tags: Iterable[DXFTag]):
|
||||
self._boundary_path = [Vec2(value) for code, value in tags if code == 14]
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, self._SUBCLASS_NAME)
|
||||
self.dxf.count_boundary_points = len(self.boundary_path)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"class_version",
|
||||
"insert",
|
||||
"u_pixel",
|
||||
"v_pixel",
|
||||
"image_size",
|
||||
"image_def_handle",
|
||||
"flags",
|
||||
"clipping",
|
||||
"brightness",
|
||||
"contrast",
|
||||
"fade",
|
||||
"image_def_reactor_handle",
|
||||
"clipping_boundary_type",
|
||||
"count_boundary_points",
|
||||
],
|
||||
)
|
||||
self.export_boundary_path(tagwriter)
|
||||
if tagwriter.dxfversion >= DXF2010:
|
||||
self.dxf.export_dxf_attribs(tagwriter, "clip_mode")
|
||||
|
||||
def export_boundary_path(self, tagwriter: AbstractTagWriter):
|
||||
for vertex in self.boundary_path:
|
||||
tagwriter.write_vertex(14, vertex)
|
||||
|
||||
@property
|
||||
def boundary_path(self):
|
||||
"""Returns the boundray path in raw form in pixel coordinates.
|
||||
|
||||
A list of vertices as pixel coordinates, Two vertices describe a
|
||||
rectangle, lower left corner is (-0.5, -0.5) and upper right corner
|
||||
is (ImageSizeX-0.5, ImageSizeY-0.5), more than two vertices is a
|
||||
polygon as clipping path. All vertices as pixel coordinates. (read/write)
|
||||
"""
|
||||
return self._boundary_path
|
||||
|
||||
@boundary_path.setter
|
||||
def boundary_path(self, vertices: Iterable[UVec]) -> None:
|
||||
self.set_boundary_path(vertices)
|
||||
|
||||
def set_boundary_path(self, vertices: Iterable[UVec]) -> None:
|
||||
"""Set boundary path to `vertices`. Two vertices describe a rectangle
|
||||
(lower left and upper right corner), more than two vertices is a polygon
|
||||
as clipping path.
|
||||
"""
|
||||
_vertices = Vec2.list(vertices)
|
||||
if len(_vertices):
|
||||
if len(_vertices) > 2 and not _vertices[-1].isclose(_vertices[0]):
|
||||
# Close path, otherwise AutoCAD crashes
|
||||
_vertices.append(_vertices[0])
|
||||
self._boundary_path = _vertices
|
||||
self.set_flag_state(self.USE_CLIPPING_BOUNDARY, state=True)
|
||||
self.dxf.clipping = 1
|
||||
self.dxf.clipping_boundary_type = 1 if len(_vertices) < 3 else 2
|
||||
self.dxf.count_boundary_points = len(self._boundary_path)
|
||||
else:
|
||||
self.reset_boundary_path()
|
||||
|
||||
def reset_boundary_path(self) -> None:
|
||||
"""Reset boundary path to the default rectangle [(-0.5, -0.5),
|
||||
(ImageSizeX-0.5, ImageSizeY-0.5)].
|
||||
"""
|
||||
lower_left_corner = Vec2(-0.5, -0.5)
|
||||
upper_right_corner = Vec2(self.dxf.image_size) + lower_left_corner
|
||||
self._boundary_path = [lower_left_corner, upper_right_corner]
|
||||
self.set_flag_state(Image.USE_CLIPPING_BOUNDARY, state=False)
|
||||
self.dxf.clipping = 0
|
||||
self.dxf.clipping_boundary_type = 1
|
||||
self.dxf.count_boundary_points = 2
|
||||
|
||||
def transform(self, m: Matrix44) -> Self:
|
||||
"""Transform IMAGE entity by transformation matrix `m` inplace."""
|
||||
self.dxf.insert = m.transform(self.dxf.insert)
|
||||
self.dxf.u_pixel = m.transform_direction(self.dxf.u_pixel)
|
||||
self.dxf.v_pixel = m.transform_direction(self.dxf.v_pixel)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def get_wcs_transform(self) -> Matrix44:
|
||||
m = Matrix44()
|
||||
m.set_row(0, Vec3(self.dxf.u_pixel))
|
||||
m.set_row(1, Vec3(self.dxf.v_pixel))
|
||||
m.set_row(3, Vec3(self.dxf.insert))
|
||||
return m
|
||||
|
||||
def pixel_boundary_path(self) -> list[Vec2]:
|
||||
"""Returns the boundary path as closed loop in pixel coordinates. Resolves the
|
||||
simple form of two vertices as a rectangle. The image coordinate system has an
|
||||
inverted y-axis and the top-left corner is (0, 0).
|
||||
|
||||
.. versionchanged:: 1.2.0
|
||||
|
||||
renamed from :meth:`boundray_path_ocs()`
|
||||
|
||||
"""
|
||||
boundary_path = self.boundary_path
|
||||
if len(boundary_path) == 2: # rectangle
|
||||
p0, p1 = boundary_path
|
||||
boundary_path = [p0, Vec2(p1.x, p0.y), p1, Vec2(p0.x, p1.y)]
|
||||
if not boundary_path[0].isclose(boundary_path[-1]):
|
||||
boundary_path.append(boundary_path[0])
|
||||
return boundary_path
|
||||
|
||||
def boundary_path_wcs(self) -> list[Vec3]:
|
||||
"""Returns the boundary/clipping path in WCS coordinates.
|
||||
|
||||
It's recommended to acquire the clipping path as :class:`~ezdxf.path.Path` object
|
||||
by the :func:`~ezdxf.path.make_path` function::
|
||||
|
||||
from ezdxf.path import make_path
|
||||
|
||||
image = ... # get image entity
|
||||
clipping_path = make_path(image)
|
||||
|
||||
"""
|
||||
|
||||
u = Vec3(self.dxf.u_pixel)
|
||||
v = Vec3(self.dxf.v_pixel)
|
||||
origin = Vec3(self.dxf.insert)
|
||||
origin += u * 0.5 - v * 0.5
|
||||
height = self.dxf.image_size.y
|
||||
# Boundary/Clipping path origin 0/0 is in the Left/Top corner of the image!
|
||||
vertices = [
|
||||
origin + (u * p.x) + (v * (height - p.y))
|
||||
for p in self.pixel_boundary_path()
|
||||
]
|
||||
return vertices
|
||||
|
||||
def destroy(self) -> None:
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
del self._boundary_path
|
||||
super().destroy()
|
||||
|
||||
|
||||
acdb_image = DefSubclass(
|
||||
"AcDbRasterImage",
|
||||
{
|
||||
"class_version": DXFAttr(90, dxfversion=DXF2000, default=0),
|
||||
"insert": DXFAttr(10, xtype=XType.point3d),
|
||||
# U-vector of a single pixel (points along the visual bottom of the image,
|
||||
# starting at the insertion point)
|
||||
"u_pixel": DXFAttr(11, xtype=XType.point3d),
|
||||
# V-vector of a single pixel (points along the visual left side of the
|
||||
# image, starting at the insertion point)
|
||||
"v_pixel": DXFAttr(12, xtype=XType.point3d),
|
||||
# Image size in pixels
|
||||
"image_size": DXFAttr(13, xtype=XType.point2d),
|
||||
# Hard reference to image def object
|
||||
"image_def_handle": DXFAttr(340),
|
||||
# Image display properties:
|
||||
# 1 = Show image
|
||||
# 2 = Show image when not aligned with screen
|
||||
# 4 = Use clipping boundary
|
||||
# 8 = Transparency is on
|
||||
"flags": DXFAttr(70, default=3),
|
||||
# Clipping state:
|
||||
# 0 = Off
|
||||
# 1 = On
|
||||
"clipping": DXFAttr(
|
||||
280,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Brightness value (0-100; default = 50)
|
||||
"brightness": DXFAttr(
|
||||
281,
|
||||
default=50,
|
||||
validator=validator.is_in_integer_range(0, 101),
|
||||
fixer=validator.fit_into_integer_range(0, 101),
|
||||
),
|
||||
# Contrast value (0-100; default = 50)
|
||||
"contrast": DXFAttr(
|
||||
282,
|
||||
default=50,
|
||||
validator=validator.is_in_integer_range(0, 101),
|
||||
fixer=validator.fit_into_integer_range(0, 101),
|
||||
),
|
||||
# Fade value (0-100; default = 0)
|
||||
"fade": DXFAttr(
|
||||
283,
|
||||
default=0,
|
||||
validator=validator.is_in_integer_range(0, 101),
|
||||
fixer=validator.fit_into_integer_range(0, 101),
|
||||
),
|
||||
# Hard reference to image def reactor object, not required by AutoCAD
|
||||
"image_def_reactor_handle": DXFAttr(360),
|
||||
# Clipping boundary type:
|
||||
# 1 = Rectangular
|
||||
# 2 = Polygonal
|
||||
"clipping_boundary_type": DXFAttr(
|
||||
71,
|
||||
default=1,
|
||||
validator=validator.is_one_of({1, 2}),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Number of clip boundary vertices that follow
|
||||
"count_boundary_points": DXFAttr(91),
|
||||
# Clip mode:
|
||||
# 0 = outside
|
||||
# 1 = inside mode
|
||||
"clip_mode": DXFAttr(
|
||||
290,
|
||||
dxfversion=DXF2010,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# boundary path coordinates are pixel coordinates NOT drawing units
|
||||
},
|
||||
)
|
||||
acdb_image_group_codes = group_code_mapping(acdb_image)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Image(ImageBase):
|
||||
"""DXF IMAGE entity"""
|
||||
|
||||
DXFTYPE = "IMAGE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_image)
|
||||
_CLS_GROUP_CODES = acdb_image_group_codes
|
||||
_SUBCLASS_NAME = acdb_image.name # type: ignore
|
||||
DEFAULT_ATTRIBS = {"layer": "0", "flags": 3}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._boundary_path: list[Vec2] = []
|
||||
self._image_def: Optional[ImageDef] = None
|
||||
self._image_def_reactor: Optional[ImageDefReactor] = None
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls: Type[Image],
|
||||
handle: Optional[str] = None,
|
||||
owner: Optional[str] = None,
|
||||
dxfattribs: Optional[dict] = None,
|
||||
doc: Optional[Drawing] = None,
|
||||
) -> Image:
|
||||
dxfattribs = dxfattribs or {}
|
||||
# 'image_def' is not a real DXF attribute (image_def_handle)
|
||||
image_def = dxfattribs.pop("image_def", None)
|
||||
image_size = (1, 1)
|
||||
if image_def and image_def.is_alive:
|
||||
image_size = image_def.dxf.image_size
|
||||
dxfattribs.setdefault("image_size", image_size)
|
||||
|
||||
image = cast("Image", super().new(handle, owner, dxfattribs, doc))
|
||||
image.image_def = image_def
|
||||
return image
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
assert isinstance(entity, Image)
|
||||
super().copy_data(entity, copy_strategy=copy_strategy)
|
||||
# Each IMAGE has its own ImageDefReactor object, which will be created by
|
||||
# binding the copy to the document.
|
||||
entity.dxf.discard("image_def_reactor_handle")
|
||||
entity._image_def_reactor = None
|
||||
# shared IMAGE_DEF
|
||||
entity._image_def = self._image_def
|
||||
|
||||
def post_bind_hook(self) -> None:
|
||||
# Document in LOAD process -> post_load_hook()
|
||||
if self.doc.is_loading: # type: ignore
|
||||
return
|
||||
if self._image_def_reactor: # ImageDefReactor already exist
|
||||
return
|
||||
# The new Image was created by ezdxf and the ImageDefReactor
|
||||
# object does not exist:
|
||||
self._create_image_def_reactor()
|
||||
|
||||
def post_load_hook(self, doc: Drawing) -> Optional[Callable]:
|
||||
super().post_load_hook(doc)
|
||||
db = doc.entitydb
|
||||
self._image_def = db.get(self.dxf.get("image_def_handle", None)) # type: ignore
|
||||
if self._image_def is None:
|
||||
# unrecoverable structure error
|
||||
self.destroy()
|
||||
return None
|
||||
|
||||
self._image_def_reactor = db.get( # type: ignore
|
||||
self.dxf.get("image_def_reactor_handle", None)
|
||||
)
|
||||
if self._image_def_reactor is None:
|
||||
# Image and ImageDef exist - this is recoverable by creating
|
||||
# an ImageDefReactor, but the objects section does not exist yet!
|
||||
# Return a post init command:
|
||||
return self._fix_missing_image_def_reactor
|
||||
return None
|
||||
|
||||
def _fix_missing_image_def_reactor(self):
|
||||
try:
|
||||
self._create_image_def_reactor()
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"An exception occurred while executing fixing command for "
|
||||
f"{str(self)}, destroying entity.",
|
||||
exc_info=e,
|
||||
)
|
||||
self.destroy()
|
||||
return
|
||||
logger.debug(f"Created missing ImageDefReactor for {str(self)}")
|
||||
|
||||
def _create_image_def_reactor(self):
|
||||
# ImageDef -> ImageDefReactor -> Image
|
||||
image_def_reactor = self.doc.objects.add_image_def_reactor(self.dxf.handle)
|
||||
reactor_handle = image_def_reactor.dxf.handle
|
||||
# Link Image to ImageDefReactor:
|
||||
self.dxf.image_def_reactor_handle = reactor_handle
|
||||
self._image_def_reactor = image_def_reactor
|
||||
# Link ImageDef to ImageDefReactor if in same document (XREF mapping!):
|
||||
if self.doc is self._image_def.doc:
|
||||
self._image_def.append_reactor_handle(reactor_handle)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
super().register_resources(registry)
|
||||
if isinstance(self.image_def, ImageDef):
|
||||
registry.add_handle(self.image_def.dxf.handle)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
assert isinstance(clone, Image)
|
||||
super().map_resources(clone, mapping)
|
||||
source_image_def = self.image_def
|
||||
if isinstance(source_image_def, ImageDef):
|
||||
name = self.get_image_def_name()
|
||||
name, clone_image_def = mapping.map_acad_dict_entry(
|
||||
"ACAD_IMAGE_DICT", name, source_image_def
|
||||
)
|
||||
if isinstance(clone_image_def, ImageDef):
|
||||
clone.image_def = clone_image_def
|
||||
if isinstance(clone._image_def_reactor, ImageDefReactor):
|
||||
clone_image_def.append_reactor_handle(
|
||||
clone._image_def_reactor.dxf.handle
|
||||
)
|
||||
# Note:
|
||||
# The IMAGEDEF_REACTOR was created automatically at binding the copy to
|
||||
# a new document, but the handle of the IMAGEDEF_REACTOR was not add to the
|
||||
# IMAGEDEF reactor handles, because at this point the IMAGE had still a reference
|
||||
# to the IMAGEDEF of the source document.
|
||||
|
||||
def get_image_def_name(self) -> str:
|
||||
"""Returns the name of the `image_def` entry in the ACAD_IMAGE_DICT."""
|
||||
if self.doc is None:
|
||||
return ""
|
||||
image_dict = self.doc.rootdict.get_required_dict("ACAD_IMAGE_DICT")
|
||||
for name, entry in image_dict.items():
|
||||
if entry is self._image_def:
|
||||
return name
|
||||
return ""
|
||||
|
||||
@property
|
||||
def image_def(self) -> Optional[ImageDef]:
|
||||
"""Returns the associated IMAGEDEF entity, see :class:`ImageDef`."""
|
||||
return self._image_def
|
||||
|
||||
@image_def.setter
|
||||
def image_def(self, image_def: ImageDef) -> None:
|
||||
if image_def and image_def.is_alive:
|
||||
self.dxf.image_def_handle = image_def.dxf.handle
|
||||
self._image_def = image_def
|
||||
else:
|
||||
self.dxf.discard("image_def_handle")
|
||||
self._image_def = None
|
||||
|
||||
@property
|
||||
def image_def_reactor(self) -> Optional[ImageDefReactor]:
|
||||
"""Returns the associated IMAGEDEF_REACTOR entity."""
|
||||
return self._image_def_reactor
|
||||
|
||||
def destroy(self) -> None:
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
reactor = self._image_def_reactor
|
||||
if reactor and reactor.is_alive:
|
||||
image_def = self.image_def
|
||||
if image_def and image_def.is_alive:
|
||||
image_def.discard_reactor_handle(reactor.dxf.handle)
|
||||
reactor.destroy()
|
||||
super().destroy()
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
super().audit(auditor)
|
||||
|
||||
|
||||
# DXF reference error: Subclass marker (AcDbRasterImage)
|
||||
acdb_wipeout = DefSubclass("AcDbWipeout", dict(acdb_image.attribs))
|
||||
acdb_wipeout_group_codes = group_code_mapping(acdb_wipeout)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Wipeout(ImageBase):
|
||||
"""DXF WIPEOUT entity"""
|
||||
|
||||
DXFTYPE = "WIPEOUT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_wipeout)
|
||||
DEFAULT_ATTRIBS = {
|
||||
"layer": "0",
|
||||
"flags": 7,
|
||||
"clipping": 1,
|
||||
"brightness": 50,
|
||||
"contrast": 50,
|
||||
"fade": 0,
|
||||
"image_size": (1, 1),
|
||||
"image_def_handle": "0", # has no ImageDef()
|
||||
"image_def_reactor_handle": "0", # has no ImageDefReactor()
|
||||
"clip_mode": 0,
|
||||
}
|
||||
_CLS_GROUP_CODES = acdb_wipeout_group_codes
|
||||
_SUBCLASS_NAME = acdb_wipeout.name # type: ignore
|
||||
|
||||
def set_masking_area(self, vertices: Iterable[UVec]) -> None:
|
||||
"""Set a new masking area, the area is placed in the layout xy-plane."""
|
||||
self.update_dxf_attribs(self.DEFAULT_ATTRIBS)
|
||||
vertices = Vec2.list(vertices)
|
||||
bounds = BoundingBox2d(vertices)
|
||||
x_size, y_size = bounds.size
|
||||
|
||||
dxf = self.dxf
|
||||
dxf.insert = Vec3(bounds.extmin)
|
||||
dxf.u_pixel = Vec3(x_size, 0, 0)
|
||||
dxf.v_pixel = Vec3(0, y_size, 0)
|
||||
|
||||
def boundary_path():
|
||||
extmin = bounds.extmin
|
||||
for vertex in vertices:
|
||||
v = vertex - extmin
|
||||
yield Vec2(v.x / x_size - 0.5, 0.5 - v.y / y_size)
|
||||
|
||||
self.set_boundary_path(boundary_path())
|
||||
|
||||
def _reset_handles(self):
|
||||
self.dxf.image_def_reactor_handle = "0"
|
||||
self.dxf.image_def_handle = "0"
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
self._reset_handles()
|
||||
super().audit(auditor)
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
self._reset_handles()
|
||||
super().export_entity(tagwriter)
|
||||
|
||||
|
||||
# About Image File Paths:
|
||||
# See notes in knowledge graph: [[IMAGE File Paths]]
|
||||
# https://ezdxf.mozman.at/notes/#/page/image%20file%20paths
|
||||
|
||||
acdb_image_def = DefSubclass(
|
||||
"AcDbRasterImageDef",
|
||||
{
|
||||
"class_version": DXFAttr(90, default=0),
|
||||
# File name of image:
|
||||
"filename": DXFAttr(1),
|
||||
# Image size in pixels:
|
||||
"image_size": DXFAttr(10, xtype=XType.point2d),
|
||||
# Default size of one pixel in AutoCAD units:
|
||||
"pixel_size": DXFAttr(11, xtype=XType.point2d, default=Vec2(0.01, 0.01)),
|
||||
"loaded": DXFAttr(280, default=1),
|
||||
# Resolution units - this enums differ from the usual drawing units,
|
||||
# units.py, same as for RasterVariables.dxf.units, but only these 3 values
|
||||
# are valid - confirmed by ODA Specs 20.4.81 IMAGEDEF:
|
||||
# 0 = No units
|
||||
# 2 = Centimeters
|
||||
# 5 = Inch
|
||||
"resolution_units": DXFAttr(
|
||||
281,
|
||||
default=0,
|
||||
validator=validator.is_one_of({0, 2, 5}),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_image_def_group_codes = group_code_mapping(acdb_image_def)
|
||||
|
||||
|
||||
@register_entity
|
||||
class ImageDef(DXFObject):
|
||||
"""DXF IMAGEDEF entity"""
|
||||
|
||||
DXFTYPE = "IMAGEDEF"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_image_def)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_image_def_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_image_def.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"class_version",
|
||||
"filename",
|
||||
"image_size",
|
||||
"pixel_size",
|
||||
"loaded",
|
||||
"resolution_units",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
acdb_image_def_reactor = DefSubclass(
|
||||
"AcDbRasterImageDefReactor",
|
||||
{
|
||||
"class_version": DXFAttr(90, default=2),
|
||||
# Handle to image:
|
||||
"image_handle": DXFAttr(330),
|
||||
},
|
||||
)
|
||||
acdb_image_def_reactor_group_codes = group_code_mapping(acdb_image_def_reactor)
|
||||
|
||||
|
||||
@register_entity
|
||||
class ImageDefReactor(DXFObject):
|
||||
"""DXF IMAGEDEF_REACTOR entity"""
|
||||
|
||||
DXFTYPE = "IMAGEDEF_REACTOR"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_image_def_reactor)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_image_def_reactor_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_image_def_reactor.name)
|
||||
tagwriter.write_tag2(90, self.dxf.class_version)
|
||||
tagwriter.write_tag2(330, self.dxf.image_handle)
|
||||
|
||||
|
||||
acdb_raster_variables = DefSubclass(
|
||||
"AcDbRasterVariables",
|
||||
{
|
||||
"class_version": DXFAttr(90, default=0),
|
||||
# Frame:
|
||||
# 0 = no frame
|
||||
# 1 = show frame
|
||||
"frame": DXFAttr(
|
||||
70,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Quality:
|
||||
# 0 = draft
|
||||
# 1 = high
|
||||
"quality": DXFAttr(
|
||||
71,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Units:
|
||||
# 0 = None
|
||||
# 1 = mm
|
||||
# 2 = cm
|
||||
# 3 = m
|
||||
# 4 = km
|
||||
# 5 = in
|
||||
# 6 = ft
|
||||
# 7 = yd
|
||||
# 8 = mi
|
||||
"units": DXFAttr(
|
||||
72,
|
||||
default=3,
|
||||
validator=validator.is_in_integer_range(0, 9),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_raster_variables_group_codes = group_code_mapping(acdb_raster_variables)
|
||||
|
||||
|
||||
@register_entity
|
||||
class RasterVariables(DXFObject):
|
||||
"""DXF RASTERVARIABLES entity"""
|
||||
|
||||
DXFTYPE = "RASTERVARIABLES"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_raster_variables)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_raster_variables_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_raster_variables.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"class_version",
|
||||
"frame",
|
||||
"quality",
|
||||
"units",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
acdb_wipeout_variables = DefSubclass(
|
||||
"AcDbWipeoutVariables",
|
||||
{
|
||||
# Display-image-frame flag:
|
||||
# 0 = No frame
|
||||
# 1 = Display frame
|
||||
"frame": DXFAttr(
|
||||
70,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_wipeout_variables_group_codes = group_code_mapping(acdb_wipeout_variables)
|
||||
|
||||
|
||||
@register_entity
|
||||
class WipeoutVariables(DXFObject):
|
||||
"""DXF WIPEOUTVARIABLES entity"""
|
||||
|
||||
DXFTYPE = "WIPEOUTVARIABLES"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_wipeout_variables)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_wipeout_variables_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_wipeout_variables.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, "frame")
|
||||
@@ -0,0 +1,790 @@
|
||||
# Copyright (c) 2019-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Iterator,
|
||||
cast,
|
||||
Union,
|
||||
Optional,
|
||||
Callable,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
import math
|
||||
import logging
|
||||
|
||||
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 (
|
||||
DXF12,
|
||||
SUBCLASS_MARKER,
|
||||
DXFValueError,
|
||||
DXFKeyError,
|
||||
DXFStructureError,
|
||||
)
|
||||
from ezdxf.math import (
|
||||
Vec3,
|
||||
UVec,
|
||||
X_AXIS,
|
||||
Y_AXIS,
|
||||
Z_AXIS,
|
||||
Matrix44,
|
||||
UCS,
|
||||
NULLVEC,
|
||||
)
|
||||
from ezdxf.math.transformtools import (
|
||||
InsertTransformationError,
|
||||
InsertCoordinateSystem,
|
||||
)
|
||||
from ezdxf.explode import (
|
||||
explode_block_reference,
|
||||
virtual_block_reference_entities,
|
||||
)
|
||||
from ezdxf.entities import factory
|
||||
from ezdxf.query import EntityQuery
|
||||
from ezdxf.audit import AuditError
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import (
|
||||
DXFGraphic,
|
||||
acdb_entity,
|
||||
elevation_to_z_axis,
|
||||
acdb_entity_group_codes,
|
||||
)
|
||||
from .subentity import LinkedEntities
|
||||
from .attrib import Attrib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.audit import Auditor
|
||||
from ezdxf.entities import DXFNamespace, AttDef, DXFEntity
|
||||
from ezdxf.layouts import BaseLayout, BlockLayout
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["Insert"]
|
||||
|
||||
logger = logging.getLogger("ezdxf")
|
||||
ABS_TOL = 1e-9
|
||||
|
||||
# DXF files as XREF:
|
||||
# The INSERT entity is used to attach XREFS.
|
||||
# The model space is the block content, if the whole document is used as
|
||||
# BLOCK (XREF), but this is only supported for the DWG format.
|
||||
# AutoCAD does not support DXF files as XREFS, they are ignored, but the DXF
|
||||
# file is valid! BricsCAD shows DXF files as XREFS, but does not allow to attach
|
||||
# DXF files as XREFS by the application itself.
|
||||
|
||||
# Multi-INSERT has subclass id AcDbMInsertBlock
|
||||
acdb_block_reference = DefSubclass(
|
||||
"AcDbBlockReference",
|
||||
{
|
||||
"attribs_follow": DXFAttr(66, default=0, optional=True),
|
||||
"name": DXFAttr(2, validator=validator.is_valid_block_name),
|
||||
"insert": DXFAttr(10, xtype=XType.any_point),
|
||||
# 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),
|
||||
"xscale": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"yscale": DXFAttr(
|
||||
42,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"zscale": DXFAttr(
|
||||
43,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"rotation": DXFAttr(50, default=0, optional=True),
|
||||
"column_count": DXFAttr(
|
||||
70,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"row_count": DXFAttr(
|
||||
71,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"column_spacing": DXFAttr(44, default=0, optional=True),
|
||||
"row_spacing": DXFAttr(45, 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_block_reference_group_codes = group_code_mapping(acdb_block_reference)
|
||||
merged_insert_group_codes = merge_group_code_mappings(
|
||||
acdb_entity_group_codes, acdb_block_reference_group_codes # type: ignore
|
||||
)
|
||||
|
||||
|
||||
# Notes to SEQEND:
|
||||
#
|
||||
# The INSERT entity requires only a SEQEND if ATTRIB entities are attached.
|
||||
# So a loaded INSERT could have a missing SEQEND.
|
||||
#
|
||||
# A bounded INSERT needs a SEQEND to be valid at export if there are attached
|
||||
# ATTRIB entities, but the LinkedEntities.post_bind_hook() method creates
|
||||
# always a new SEQEND after binding the INSERT entity to a document.
|
||||
#
|
||||
# Nonetheless, the Insert.add_attrib() method also creates the required SEQEND entity if
|
||||
# necessary.
|
||||
|
||||
|
||||
@factory.register_entity
|
||||
class Insert(LinkedEntities):
|
||||
"""DXF INSERT entity
|
||||
|
||||
The INSERT entity is hard owner of its ATTRIB entities and the SEQEND entity:
|
||||
|
||||
ATTRIB.dxf.owner == INSERT.dxf.handle
|
||||
SEQEND.dxf.owner == INSERT.dxf.handle
|
||||
|
||||
Note:
|
||||
|
||||
The ATTDEF entity in block definitions is owned by the BLOCK_RECORD like
|
||||
all graphical entities.
|
||||
|
||||
"""
|
||||
|
||||
DXFTYPE = "INSERT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_block_reference)
|
||||
|
||||
@property
|
||||
def attribs(self) -> list[Attrib]:
|
||||
return self._sub_entities # type: ignore
|
||||
|
||||
@property
|
||||
def attribs_follow(self) -> bool:
|
||||
return bool(len(self.attribs))
|
||||
|
||||
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_insert_group_codes)
|
||||
if processor.r12:
|
||||
# Transform elevation attribute from R11 to z-axis values:
|
||||
elevation_to_z_axis(dxf, ("insert",))
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
if (self.dxf.column_count > 1) or (self.dxf.row_count > 1):
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbMInsertBlock")
|
||||
else:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbBlockReference")
|
||||
if self.attribs_follow:
|
||||
tagwriter.write_tag2(66, 1)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"insert",
|
||||
"xscale",
|
||||
"yscale",
|
||||
"zscale",
|
||||
"rotation",
|
||||
"column_count",
|
||||
"row_count",
|
||||
"column_spacing",
|
||||
"row_spacing",
|
||||
"extrusion",
|
||||
],
|
||||
)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter):
|
||||
super().export_dxf(tagwriter)
|
||||
# Do no export SEQEND if no ATTRIBS attached:
|
||||
if self.attribs_follow:
|
||||
self.process_sub_entities(lambda e: e.export_dxf(tagwriter))
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
# The attached ATTRIB entities are registered by the parent class LinkedEntities
|
||||
super().register_resources(registry)
|
||||
registry.add_block_name(self.dxf.name)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
# The attached ATTRIB entities are mapped by the parent class LinkedEntities
|
||||
super().map_resources(clone, mapping)
|
||||
clone.dxf.name = mapping.get_block_name(self.dxf.name)
|
||||
|
||||
@property
|
||||
def has_scaling(self) -> bool:
|
||||
"""Returns ``True`` if scaling is applied to any axis."""
|
||||
if self.dxf.hasattr("xscale") and self.dxf.xscale != 1:
|
||||
return True
|
||||
if self.dxf.hasattr("yscale") and self.dxf.yscale != 1:
|
||||
return True
|
||||
if self.dxf.hasattr("zscale") and self.dxf.zscale != 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def has_uniform_scaling(self) -> bool:
|
||||
"""Returns ``True`` if the scale factor is uniform for x-, y- and z-axis,
|
||||
ignoring reflections e.g. (1, 1, -1) is uniform scaling.
|
||||
|
||||
"""
|
||||
return abs(self.dxf.xscale) == abs(self.dxf.yscale) == abs(self.dxf.zscale)
|
||||
|
||||
def set_scale(self, factor: float):
|
||||
"""Set a uniform scale factor."""
|
||||
if factor == 0:
|
||||
raise ValueError("Invalid scale factor.")
|
||||
self.dxf.xscale = factor
|
||||
self.dxf.yscale = factor
|
||||
self.dxf.zscale = factor
|
||||
return self
|
||||
|
||||
def block(self) -> Optional[BlockLayout]:
|
||||
"""Returns the associated :class:`~ezdxf.layouts.BlockLayout`."""
|
||||
if self.doc:
|
||||
return self.doc.blocks.get(self.dxf.name)
|
||||
return None
|
||||
|
||||
def is_xref(self) -> bool:
|
||||
"""Return ``True`` if the INSERT entity represents a XREF or XREF_OVERLAY."""
|
||||
block = self.block()
|
||||
if block is not None:
|
||||
return block.block_record.is_xref
|
||||
return False
|
||||
|
||||
def place(
|
||||
self,
|
||||
insert: Optional[UVec] = None,
|
||||
scale: Optional[tuple[float, float, float]] = None,
|
||||
rotation: Optional[float] = None,
|
||||
) -> Insert:
|
||||
"""
|
||||
Set the location, scaling and rotation attributes. Arguments which are ``None``
|
||||
will be ignored.
|
||||
|
||||
Args:
|
||||
insert: insert location as (x, y [,z]) tuple
|
||||
scale: (x-scale, y-scale, z-scale) tuple
|
||||
rotation : rotation angle in degrees
|
||||
|
||||
"""
|
||||
if insert is not None:
|
||||
self.dxf.insert = insert
|
||||
if scale is not None:
|
||||
if len(scale) != 3:
|
||||
raise DXFValueError("Argument scale has to be a (x, y[, z]) tuple.")
|
||||
x, y, z = scale
|
||||
self.dxf.xscale = x
|
||||
self.dxf.yscale = y
|
||||
self.dxf.zscale = z
|
||||
if rotation is not None:
|
||||
self.dxf.rotation = rotation
|
||||
return self
|
||||
|
||||
def grid(
|
||||
self, size: tuple[int, int] = (1, 1), spacing: tuple[float, float] = (1, 1)
|
||||
) -> Insert:
|
||||
"""Place block reference in a grid layout, grid `size` defines the
|
||||
row- and column count, `spacing` defines the distance between two block
|
||||
references.
|
||||
|
||||
Args:
|
||||
size: grid size as (row_count, column_count) tuple
|
||||
spacing: distance between placing as (row_spacing, column_spacing) tuple
|
||||
|
||||
"""
|
||||
try:
|
||||
rows, cols = size
|
||||
except ValueError:
|
||||
raise DXFValueError("Size has to be a (row_count, column_count) tuple.")
|
||||
self.dxf.row_count = rows
|
||||
self.dxf.column_count = cols
|
||||
try:
|
||||
row_spacing, col_spacing = spacing
|
||||
except ValueError:
|
||||
raise DXFValueError(
|
||||
"Spacing has to be a (row_spacing, column_spacing) tuple."
|
||||
)
|
||||
self.dxf.row_spacing = row_spacing
|
||||
self.dxf.column_spacing = col_spacing
|
||||
return self
|
||||
|
||||
def get_attrib(
|
||||
self, tag: str, search_const: bool = False
|
||||
) -> Optional[Union[Attrib, AttDef]]:
|
||||
"""Get an attached :class:`Attrib` entity with the given `tag`,
|
||||
returns ``None`` if not found. Some applications do not attach constant
|
||||
ATTRIB entities, set `search_const` to ``True``, to get at least the
|
||||
associated :class:`AttDef` entity.
|
||||
|
||||
Args:
|
||||
tag: tag name of the ATTRIB entity
|
||||
search_const: search also const ATTDEF entities
|
||||
|
||||
"""
|
||||
for attrib in self.attribs:
|
||||
if tag == attrib.dxf.tag:
|
||||
return attrib
|
||||
if search_const and self.doc is not None:
|
||||
block = self.doc.blocks[self.dxf.name]
|
||||
for attdef in block.get_const_attdefs():
|
||||
if tag == attdef.dxf.tag:
|
||||
return attdef
|
||||
return None
|
||||
|
||||
def get_attrib_text(
|
||||
self, tag: str, default: str = "", search_const: bool = False
|
||||
) -> str:
|
||||
"""Get content text of an attached :class:`Attrib` entity with
|
||||
the given `tag`, returns the `default` value if not found.
|
||||
Some applications do not attach constant ATTRIB entities, set
|
||||
`search_const` to ``True``, to get content text of the
|
||||
associated :class:`AttDef` entity.
|
||||
|
||||
Args:
|
||||
tag: tag name of the ATTRIB entity
|
||||
default: default value if ATTRIB `tag` is absent
|
||||
search_const: search also const ATTDEF entities
|
||||
|
||||
"""
|
||||
attrib = self.get_attrib(tag, search_const)
|
||||
if attrib is None:
|
||||
return default
|
||||
return attrib.dxf.text
|
||||
|
||||
def has_attrib(self, tag: str, search_const: bool = False) -> bool:
|
||||
"""Returns ``True`` if the INSERT entity has an attached ATTRIB entity with the
|
||||
given `tag`. Some applications do not attach constant ATTRIB entities, set
|
||||
`search_const` to ``True``, to check for an associated :class:`AttDef` entity
|
||||
with constant content.
|
||||
|
||||
|
||||
Args:
|
||||
tag: tag name fo the ATTRIB entity
|
||||
search_const: search also const ATTDEF entities
|
||||
|
||||
"""
|
||||
return self.get_attrib(tag, search_const) is not None
|
||||
|
||||
def add_attrib(
|
||||
self, tag: str, text: str, insert: UVec = (0, 0), dxfattribs=None
|
||||
) -> Attrib:
|
||||
"""Attach an :class:`Attrib` entity to the block reference.
|
||||
|
||||
Example for appending an attribute to an INSERT entity::
|
||||
|
||||
e.add_attrib('EXAMPLETAG', 'example text').set_placement(
|
||||
(3, 7), align=TextEntityAlignment.MIDDLE_CENTER
|
||||
)
|
||||
|
||||
Args:
|
||||
tag: tag name of the ATTRIB entity
|
||||
text: content text as string
|
||||
insert: insert location as (x, y[, z]) tuple in :ref:`OCS`
|
||||
dxfattribs: additional DXF attributes for the ATTRIB entity
|
||||
|
||||
"""
|
||||
dxfattribs = dict(dxfattribs or {})
|
||||
dxfattribs["tag"] = tag
|
||||
dxfattribs["text"] = text
|
||||
dxfattribs["insert"] = insert
|
||||
attrib = cast("Attrib", self._new_compound_entity("ATTRIB", dxfattribs))
|
||||
self.attribs.append(attrib)
|
||||
|
||||
# This case is only possible if the INSERT was read from a file without
|
||||
# attached ATTRIB entities:
|
||||
if self.seqend is None:
|
||||
self.new_seqend()
|
||||
return attrib
|
||||
|
||||
def delete_attrib(self, tag: str, ignore=False) -> None:
|
||||
"""Delete an attached :class:`Attrib` entity from INSERT. Raises an
|
||||
:class:`DXFKeyError` exception, if no ATTRIB for the given `tag` exist if
|
||||
`ignore` is ``False``.
|
||||
|
||||
Args:
|
||||
tag: tag name of the ATTRIB entity
|
||||
ignore: ``False`` for raising :class:`DXFKeyError` if ATTRIB `tag`
|
||||
does not exist.
|
||||
|
||||
Raises:
|
||||
DXFKeyError: no ATTRIB for the given `tag` exist
|
||||
|
||||
"""
|
||||
for index, attrib in enumerate(self.attribs):
|
||||
if attrib.dxf.tag == tag:
|
||||
del self.attribs[index]
|
||||
attrib.destroy()
|
||||
return
|
||||
if not ignore:
|
||||
raise DXFKeyError(tag)
|
||||
|
||||
def delete_all_attribs(self) -> None:
|
||||
"""Delete all :class:`Attrib` entities attached to the INSERT entity."""
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
for attrib in self.attribs:
|
||||
attrib.destroy()
|
||||
self._sub_entities = []
|
||||
|
||||
def transform(self, m: Matrix44) -> Insert:
|
||||
"""Transform INSERT entity by transformation matrix `m` inplace.
|
||||
|
||||
Unlike the transformation matrix `m`, the INSERT entity can not
|
||||
represent a non-orthogonal target coordinate system and an
|
||||
:class:`InsertTransformationError` will be raised in that case.
|
||||
|
||||
"""
|
||||
dxf = self.dxf
|
||||
source_system = InsertCoordinateSystem(
|
||||
insert=Vec3(dxf.insert),
|
||||
scale=(dxf.xscale, dxf.yscale, dxf.zscale),
|
||||
rotation=dxf.rotation,
|
||||
extrusion=dxf.extrusion,
|
||||
)
|
||||
try:
|
||||
target_system = source_system.transform(m, ABS_TOL)
|
||||
except InsertTransformationError:
|
||||
raise InsertTransformationError(
|
||||
"INSERT entity can not represent a non-orthogonal target coordinate system."
|
||||
)
|
||||
dxf.insert = target_system.insert
|
||||
dxf.rotation = target_system.rotation
|
||||
dxf.extrusion = target_system.extrusion
|
||||
dxf.xscale = target_system.scale_factor_x
|
||||
dxf.yscale = target_system.scale_factor_y
|
||||
dxf.zscale = target_system.scale_factor_z
|
||||
|
||||
for attrib in self.attribs:
|
||||
attrib.transform(m)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def translate(self, dx: float, dy: float, dz: float) -> Insert:
|
||||
"""Optimized INSERT translation about `dx` in x-axis, `dy` in y-axis
|
||||
and `dz` in z-axis.
|
||||
|
||||
"""
|
||||
ocs = self.ocs()
|
||||
self.dxf.insert = ocs.from_wcs(Vec3(dx, dy, dz) + ocs.to_wcs(self.dxf.insert))
|
||||
for attrib in self.attribs:
|
||||
attrib.translate(dx, dy, dz)
|
||||
return self
|
||||
|
||||
def matrix44(self) -> Matrix44:
|
||||
"""Returns a transformation matrix to transform the block entities from the
|
||||
block reference coordinate system into the :ref:`WCS`.
|
||||
"""
|
||||
dxf = self.dxf
|
||||
sx = dxf.xscale
|
||||
sy = dxf.yscale
|
||||
sz = dxf.zscale
|
||||
|
||||
ocs = self.ocs()
|
||||
extrusion = ocs.uz
|
||||
ux = Vec3(ocs.to_wcs(X_AXIS))
|
||||
uy = Vec3(ocs.to_wcs(Y_AXIS))
|
||||
m = Matrix44.ucs(ux=ux * sx, uy=uy * sy, uz=extrusion * sz)
|
||||
# same as Matrix44.ucs(ux, uy, extrusion) * Matrix44.scale(sx, sy, sz)
|
||||
|
||||
angle = math.radians(dxf.rotation)
|
||||
if angle:
|
||||
m *= Matrix44.axis_rotate(extrusion, angle)
|
||||
|
||||
insert = ocs.to_wcs(dxf.get("insert", NULLVEC))
|
||||
|
||||
block_layout = self.block()
|
||||
if block_layout is not None:
|
||||
# transform block base point into WCS without translation
|
||||
insert -= m.transform_direction(block_layout.block.dxf.base_point) # type: ignore
|
||||
|
||||
# set translation
|
||||
m.set_row(3, insert.xyz)
|
||||
return m
|
||||
|
||||
def ucs(self):
|
||||
"""Returns the block reference coordinate system as :class:`ezdxf.math.UCS`
|
||||
object.
|
||||
"""
|
||||
m = self.matrix44()
|
||||
ucs = UCS()
|
||||
ucs.matrix = m
|
||||
return ucs
|
||||
|
||||
def reset_transformation(self) -> None:
|
||||
"""Reset block reference attributes location, rotation angle and
|
||||
the extrusion vector but preserves the scale factors.
|
||||
|
||||
"""
|
||||
self.dxf.insert = NULLVEC
|
||||
self.dxf.discard("rotation")
|
||||
self.dxf.discard("extrusion")
|
||||
|
||||
def explode(
|
||||
self, target_layout: Optional[BaseLayout] = None, *, redraw_order=False
|
||||
) -> EntityQuery:
|
||||
"""Explodes the block reference entities into the target layout, if target
|
||||
layout is ``None``, the layout of the block reference will be used.
|
||||
This method destroys the source block reference entity.
|
||||
|
||||
Transforms the block entities into the required :ref:`WCS` location by
|
||||
applying the block reference attributes `insert`, `extrusion`,
|
||||
`rotation` and the scale factors `xscale`, `yscale` and `zscale`.
|
||||
|
||||
Attached ATTRIB entities are converted to TEXT entities, this is the
|
||||
behavior of the BURST command of the AutoCAD Express Tools.
|
||||
|
||||
.. warning::
|
||||
|
||||
**Non-uniform scale factors** may lead to incorrect results some entities
|
||||
(TEXT, MTEXT, ATTRIB).
|
||||
|
||||
Args:
|
||||
target_layout: target layout for exploded entities, ``None`` for
|
||||
same layout as source entity.
|
||||
redraw_order: create entities in ascending redraw order if ``True``
|
||||
|
||||
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 DXFStructureError(
|
||||
"INSERT without layout assignment, specify target layout"
|
||||
)
|
||||
return explode_block_reference(
|
||||
self, target_layout=target_layout, redraw_order=redraw_order
|
||||
)
|
||||
|
||||
def __virtual_entities__(self) -> Iterator[DXFGraphic]:
|
||||
"""Implements the SupportsVirtualEntities protocol.
|
||||
|
||||
This protocol is for consistent internal usage and does not replace
|
||||
the method :meth:`virtual_entities`! Ignores the redraw-order!
|
||||
"""
|
||||
return self.virtual_entities()
|
||||
|
||||
def virtual_entities(
|
||||
self,
|
||||
*,
|
||||
skipped_entity_callback: Optional[Callable[[DXFGraphic, str], None]] = None,
|
||||
redraw_order=False,
|
||||
) -> Iterator[DXFGraphic]:
|
||||
"""
|
||||
Yields the transformed referenced block content as virtual entities.
|
||||
|
||||
This method is meant to examine the block reference entities at the target
|
||||
location without exploding the block reference.
|
||||
These entities are not stored in the entity database, have no handle and
|
||||
are not assigned to any layout. It is possible to convert these entities
|
||||
into regular drawing entities by adding the entities to the entities
|
||||
database and a layout of the same DXF document as the block reference::
|
||||
|
||||
doc.entitydb.add(entity)
|
||||
msp = doc.modelspace()
|
||||
msp.add_entity(entity)
|
||||
|
||||
.. warning::
|
||||
|
||||
**Non-uniform scale factors** may return incorrect results for some entities
|
||||
(TEXT, MTEXT, ATTRIB).
|
||||
|
||||
This method does not resolve the MINSERT attributes, only the
|
||||
sub-entities of the first INSERT will be returned. To resolve MINSERT
|
||||
entities check if multi insert processing is required, that's the case
|
||||
if the property :attr:`Insert.mcount` > 1, use the :meth:`Insert.multi_insert`
|
||||
method to resolve the MINSERT entity into multiple INSERT entities.
|
||||
|
||||
This method does not apply the clipping path created by the XCLIP command.
|
||||
The method returns all entities and ignores the clipping path polygon and no
|
||||
entity is clipped.
|
||||
|
||||
The `skipped_entity_callback()` will be called for all entities which are not
|
||||
processed, signature:
|
||||
:code:`skipped_entity_callback(entity: DXFEntity, reason: str)`,
|
||||
`entity` is the original (untransformed) DXF entity of the block definition, the
|
||||
`reason` string is an explanation why the entity was skipped.
|
||||
|
||||
Args:
|
||||
skipped_entity_callback: called whenever the transformation of an
|
||||
entity is not supported and so was skipped
|
||||
redraw_order: yield entities in ascending redraw order if ``True``
|
||||
|
||||
"""
|
||||
for e in virtual_block_reference_entities(
|
||||
self,
|
||||
skipped_entity_callback=skipped_entity_callback,
|
||||
redraw_order=redraw_order,
|
||||
):
|
||||
e.set_source_block_reference(self)
|
||||
yield e
|
||||
|
||||
@property
|
||||
def mcount(self) -> int:
|
||||
"""Returns the multi-insert count, MINSERT (multi-insert) processing
|
||||
is required if :attr:`mcount` > 1.
|
||||
|
||||
"""
|
||||
return (self.dxf.row_count if self.dxf.row_spacing else 1) * (
|
||||
self.dxf.column_count if self.dxf.column_spacing else 1
|
||||
)
|
||||
|
||||
def multi_insert(self) -> Iterator[Insert]:
|
||||
"""Yields a virtual INSERT entity for each grid element of a MINSERT
|
||||
entity (multi-insert).
|
||||
"""
|
||||
|
||||
def transform_attached_attrib_entities(insert, offset):
|
||||
for attrib in insert.attribs:
|
||||
attrib.dxf.insert += offset
|
||||
|
||||
def adjust_dxf_attribs(insert, offset):
|
||||
dxf = insert.dxf
|
||||
dxf.insert += offset
|
||||
dxf.discard("row_count")
|
||||
dxf.discard("column_count")
|
||||
dxf.discard("row_spacing")
|
||||
dxf.discard("column_spacing")
|
||||
|
||||
done = set()
|
||||
row_spacing = self.dxf.row_spacing
|
||||
col_spacing = self.dxf.column_spacing
|
||||
rotation = self.dxf.rotation
|
||||
for row in range(self.dxf.row_count):
|
||||
for col in range(self.dxf.column_count):
|
||||
# All transformations in OCS:
|
||||
offset = Vec3(col * col_spacing, row * row_spacing)
|
||||
# If any spacing is 0, yield only unique locations:
|
||||
if offset not in done:
|
||||
done.add(offset)
|
||||
if rotation: # Apply rotation to the grid.
|
||||
offset = offset.rotate_deg(rotation)
|
||||
# Do not apply scaling to the grid!
|
||||
insert = self.copy()
|
||||
adjust_dxf_attribs(insert, offset)
|
||||
transform_attached_attrib_entities(insert, offset)
|
||||
yield insert
|
||||
|
||||
def add_auto_attribs(self, values: dict[str, str]) -> Insert:
|
||||
"""
|
||||
Attach for each :class:`~ezdxf.entities.Attdef` entity, defined in the
|
||||
block definition, automatically an :class:`Attrib` entity to the block
|
||||
reference and set ``tag/value`` DXF attributes of the ATTRIB entities
|
||||
by the ``key/value`` pairs (both as strings) of the `values` dict.
|
||||
The ATTRIB entities are placed relative to the insert location of the
|
||||
block reference, which is identical to the block base point.
|
||||
|
||||
This method avoids the wrapper block of the
|
||||
:meth:`~ezdxf.layouts.BaseLayout.add_auto_blockref` method, but the
|
||||
visual results may not match the results of CAD applications, especially
|
||||
for non-uniform scaling. If the visual result is very important to you,
|
||||
use the :meth:`add_auto_blockref` method.
|
||||
|
||||
Args:
|
||||
values: :class:`~ezdxf.entities.Attrib` tag values as ``tag/value``
|
||||
pairs
|
||||
|
||||
"""
|
||||
def unpack(dxfattribs) -> tuple[str, str, UVec]:
|
||||
tag = dxfattribs.pop("tag")
|
||||
text = values.get(tag, None)
|
||||
if text is None: # get default value from ATTDEF
|
||||
text = dxfattribs.get("text", "")
|
||||
location = dxfattribs.pop("insert")
|
||||
return tag, text, location
|
||||
|
||||
def autofill() -> None:
|
||||
for attdef in block_layout.attdefs(): # type: ignore
|
||||
dxfattribs = attdef.dxfattribs(drop={"prompt", "handle"})
|
||||
|
||||
# Caution! Some mandatory values may not exist!
|
||||
# These are DXF structure errors, but loaded DXF files may have errors!
|
||||
if "tag" not in dxfattribs:
|
||||
# ATTRIB without "tag" makes no sense!
|
||||
logger.warning(
|
||||
f"Skipping {str(attdef)}: missing mandatory 'tag' attribute"
|
||||
)
|
||||
continue
|
||||
if "insert" not in dxfattribs:
|
||||
# Don't know where to place the ATTRIB entity.
|
||||
logger.warning(
|
||||
f"Skipping {str(attdef)}: missing mandatory 'insert' attribute"
|
||||
)
|
||||
continue
|
||||
|
||||
tag, text, location = unpack(dxfattribs)
|
||||
attrib = self.add_attrib(tag, text, location, dxfattribs)
|
||||
if attdef.has_embedded_mtext_entity:
|
||||
mtext = attdef.virtual_mtext_entity()
|
||||
mtext.text = text
|
||||
attrib.embed_mtext(mtext)
|
||||
attrib.transform(m)
|
||||
|
||||
block_layout = self.block()
|
||||
if block_layout is not None:
|
||||
m = self.matrix44()
|
||||
autofill()
|
||||
return self
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
"""Validity check."""
|
||||
super().audit(auditor)
|
||||
doc = auditor.doc
|
||||
if doc and doc.blocks:
|
||||
name = self.dxf.name
|
||||
if name is None:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.UNDEFINED_BLOCK_NAME,
|
||||
message=f"Deleted entity {str(self)} without a BLOCK name",
|
||||
)
|
||||
auditor.trash(self)
|
||||
elif name not in doc.blocks:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.UNDEFINED_BLOCK,
|
||||
message=f"Deleted entity {str(self)} without required BLOCK"
|
||||
f" definition.",
|
||||
)
|
||||
auditor.trash(self)
|
||||
|
||||
def __referenced_blocks__(self) -> Iterable[str]:
|
||||
"""Support for the "ReferencedBlocks" protocol."""
|
||||
block = self.block()
|
||||
if block is not None:
|
||||
return (block.block_record_handle,)
|
||||
return tuple()
|
||||
@@ -0,0 +1,794 @@
|
||||
# Copyright (c) 2019-2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional, cast, Any
|
||||
from typing_extensions import Self
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
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,
|
||||
SUBCLASS_MARKER,
|
||||
DXF2000,
|
||||
DXF2007,
|
||||
DXF2004,
|
||||
INVALID_NAME_CHARACTERS,
|
||||
DXFValueError,
|
||||
LINEWEIGHT_BYBLOCK,
|
||||
LINEWEIGHT_BYLAYER,
|
||||
LINEWEIGHT_DEFAULT,
|
||||
)
|
||||
from ezdxf.audit import AuditError
|
||||
from ezdxf.entities.dxfentity import base_class, SubclassProcessor, DXFEntity
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace, Viewport, XRecord
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.entitydb import EntityDB
|
||||
from ezdxf import xref
|
||||
from ezdxf.audit import Auditor
|
||||
|
||||
|
||||
__all__ = ["Layer", "acdb_symbol_table_record", "LayerOverrides"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
def is_valid_layer_color_index(aci: int) -> bool:
|
||||
# BYBLOCK or BYLAYER is not valid a layer color!
|
||||
return (-256 < aci < 256) and aci != 0
|
||||
|
||||
|
||||
def fix_layer_color(aci: int) -> int:
|
||||
return aci if is_valid_layer_color_index(aci) else 7
|
||||
|
||||
|
||||
def is_valid_layer_lineweight(lw: int) -> bool:
|
||||
if validator.is_valid_lineweight(lw):
|
||||
if lw not in (LINEWEIGHT_BYLAYER, LINEWEIGHT_BYBLOCK):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fix_layer_lineweight(lw: int) -> int:
|
||||
if lw in (LINEWEIGHT_BYLAYER, LINEWEIGHT_BYBLOCK):
|
||||
return LINEWEIGHT_DEFAULT
|
||||
else:
|
||||
return validator.fix_lineweight(lw)
|
||||
|
||||
|
||||
acdb_symbol_table_record: DefSubclass = DefSubclass("AcDbSymbolTableRecord", {})
|
||||
|
||||
acdb_layer_table_record = DefSubclass(
|
||||
"AcDbLayerTableRecord",
|
||||
{
|
||||
# Layer name as string
|
||||
"name": DXFAttr(2, validator=validator.is_valid_layer_name),
|
||||
"flags": DXFAttr(70, default=0),
|
||||
# ACI color index, color < 0 indicates layer status: off
|
||||
"color": DXFAttr(
|
||||
62,
|
||||
default=7,
|
||||
validator=is_valid_layer_color_index,
|
||||
fixer=fix_layer_color,
|
||||
),
|
||||
# True color as 24 bit int value: 0x00RRGGBB
|
||||
"true_color": DXFAttr(420, dxfversion=DXF2004, optional=True),
|
||||
# Linetype name as string
|
||||
"linetype": DXFAttr(
|
||||
6, default="Continuous", validator=validator.is_valid_table_name
|
||||
),
|
||||
# 0 = don't plot layer; 1 = plot layer
|
||||
"plot": DXFAttr(
|
||||
290,
|
||||
default=1,
|
||||
dxfversion=DXF2000,
|
||||
optional=True,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Default lineweight 1/100 mm, min 0 = 0.0mm, max 211 = 2.11mm
|
||||
"lineweight": DXFAttr(
|
||||
370,
|
||||
default=LINEWEIGHT_DEFAULT,
|
||||
dxfversion=DXF2000,
|
||||
validator=is_valid_layer_lineweight,
|
||||
fixer=fix_layer_lineweight,
|
||||
),
|
||||
# Handle to PlotStyleName, group code 390 is required by AutoCAD
|
||||
"plotstyle_handle": DXFAttr(390, dxfversion=DXF2000),
|
||||
# Handle to Material object
|
||||
"material_handle": DXFAttr(347, dxfversion=DXF2007),
|
||||
# Handle to ???
|
||||
"unknown1": DXFAttr(348, dxfversion=DXF2007, optional=True),
|
||||
},
|
||||
)
|
||||
acdb_layer_table_record_group_codes = group_code_mapping(acdb_layer_table_record)
|
||||
AcAecLayerStandard = "AcAecLayerStandard"
|
||||
AcCmTransparency = "AcCmTransparency"
|
||||
|
||||
|
||||
@register_entity
|
||||
class Layer(DXFEntity):
|
||||
"""DXF LAYER entity"""
|
||||
|
||||
DXFTYPE = "LAYER"
|
||||
DXFATTRIBS = DXFAttributes(
|
||||
base_class, acdb_symbol_table_record, acdb_layer_table_record
|
||||
)
|
||||
DEFAULT_ATTRIBS = {"name": "0"}
|
||||
FROZEN = 0b00000001
|
||||
THAW = 0b11111110
|
||||
LOCK = 0b00000100
|
||||
UNLOCK = 0b11111011
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.simple_dxfattribs_loader(
|
||||
dxf, acdb_layer_table_record_group_codes # type: ignore
|
||||
)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_layer_table_record.name)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"flags",
|
||||
"color",
|
||||
"true_color",
|
||||
"linetype",
|
||||
"plot",
|
||||
"lineweight",
|
||||
"plotstyle_handle",
|
||||
"material_handle",
|
||||
"unknown1",
|
||||
],
|
||||
)
|
||||
|
||||
def set_required_attributes(self):
|
||||
assert self.doc is not None, "valid DXF document required"
|
||||
if not self.dxf.hasattr("material_handle"):
|
||||
global_ = self.doc.materials["Global"]
|
||||
if isinstance(global_, DXFEntity):
|
||||
handle = global_.dxf.handle
|
||||
else:
|
||||
handle = global_
|
||||
self.dxf.material_handle = handle
|
||||
if not self.dxf.hasattr("plotstyle_handle"):
|
||||
normal = self.doc.plotstyles["Normal"]
|
||||
if isinstance(normal, DXFEntity):
|
||||
handle = normal.dxf.handle
|
||||
else:
|
||||
handle = normal
|
||||
self.dxf.plotstyle_handle = handle
|
||||
|
||||
def is_frozen(self) -> bool:
|
||||
"""Returns ``True`` if layer is frozen."""
|
||||
return self.dxf.flags & Layer.FROZEN > 0
|
||||
|
||||
def freeze(self) -> None:
|
||||
"""Freeze layer."""
|
||||
self.dxf.flags = self.dxf.flags | Layer.FROZEN
|
||||
|
||||
def thaw(self) -> None:
|
||||
"""Thaw layer."""
|
||||
self.dxf.flags = self.dxf.flags & Layer.THAW
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""Returns ``True`` if layer is locked."""
|
||||
return self.dxf.flags & Layer.LOCK > 0
|
||||
|
||||
def lock(self) -> None:
|
||||
"""Lock layer, entities on this layer are not editable - just important
|
||||
in CAD applications.
|
||||
"""
|
||||
self.dxf.flags = self.dxf.flags | Layer.LOCK
|
||||
|
||||
def unlock(self) -> None:
|
||||
"""Unlock layer, entities on this layer are editable - just important
|
||||
in CAD applications.
|
||||
"""
|
||||
self.dxf.flags = self.dxf.flags & Layer.UNLOCK
|
||||
|
||||
def is_off(self) -> bool:
|
||||
"""Returns ``True`` if layer is off."""
|
||||
return self.dxf.color < 0
|
||||
|
||||
def is_on(self) -> bool:
|
||||
"""Returns ``True`` if layer is on."""
|
||||
return not self.is_off()
|
||||
|
||||
def on(self) -> None:
|
||||
"""Switch layer `on` (visible)."""
|
||||
self.dxf.color = abs(self.dxf.color)
|
||||
|
||||
def off(self) -> None:
|
||||
"""Switch layer `off` (invisible)."""
|
||||
self.dxf.color = -abs(self.dxf.color)
|
||||
|
||||
def get_color(self) -> int:
|
||||
"""Get layer color, safe method for getting the layer color, because
|
||||
:attr:`dxf.color` is negative for layer status `off`.
|
||||
"""
|
||||
return abs(self.dxf.color)
|
||||
|
||||
def set_color(self, color: int) -> None:
|
||||
"""Set layer color, safe method for setting the layer color, because
|
||||
:attr:`dxf.color` is negative for layer status `off`.
|
||||
"""
|
||||
color = abs(color) if self.is_on() else -abs(color)
|
||||
self.dxf.color = color
|
||||
|
||||
@property
|
||||
def rgb(self) -> Optional[tuple[int, int, int]]:
|
||||
"""Returns RGB true color as (r, g, b)-tuple or None if attribute
|
||||
dxf.true_color is not set.
|
||||
"""
|
||||
if self.dxf.hasattr("true_color"):
|
||||
return clr.int2rgb(self.dxf.get("true_color"))
|
||||
else:
|
||||
return None
|
||||
|
||||
@rgb.setter
|
||||
def rgb(self, rgb: tuple[int, int, int]) -> None:
|
||||
"""Set RGB true color as (r, g, b)-tuple e.g. (12, 34, 56)."""
|
||||
self.dxf.set("true_color", clr.rgb2int(rgb))
|
||||
|
||||
@property
|
||||
def color(self) -> int:
|
||||
"""Get layer color, safe method for getting the layer color, because
|
||||
:attr:`dxf.color` is negative for layer status `off`.
|
||||
"""
|
||||
return self.get_color()
|
||||
|
||||
@color.setter
|
||||
def color(self, value: int) -> None:
|
||||
"""Set layer color, safe method for setting the layer color, because
|
||||
:attr:`dxf.color` is negative for layer status `off`.
|
||||
"""
|
||||
self.set_color(value)
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
try:
|
||||
xdata = self.get_xdata(AcAecLayerStandard)
|
||||
except DXFValueError:
|
||||
return ""
|
||||
else:
|
||||
if len(xdata) > 1:
|
||||
# this is the usual case in BricsCAD
|
||||
return xdata[1].value
|
||||
else:
|
||||
return ""
|
||||
|
||||
@description.setter
|
||||
def description(self, value: str) -> None:
|
||||
# create AppID table entry if not present
|
||||
if self.doc and AcAecLayerStandard not in self.doc.appids:
|
||||
self.doc.appids.new(AcAecLayerStandard)
|
||||
self.discard_xdata(AcAecLayerStandard)
|
||||
self.set_xdata(AcAecLayerStandard, [(1000, ""), (1000, value)])
|
||||
|
||||
@property
|
||||
def transparency(self) -> float:
|
||||
try:
|
||||
xdata = self.get_xdata(AcCmTransparency)
|
||||
except DXFValueError:
|
||||
return 0.0
|
||||
else:
|
||||
t = xdata[0].value
|
||||
if t & 0x2000000: # is this a real transparency value?
|
||||
# Transparency BYBLOCK (0x01000000) make no sense for a layer!?
|
||||
return clr.transparency2float(t)
|
||||
return 0.0
|
||||
|
||||
@transparency.setter
|
||||
def transparency(self, value: float) -> None:
|
||||
# create AppID table entry if not present
|
||||
if self.doc and AcCmTransparency not in self.doc.appids:
|
||||
self.doc.appids.new(AcCmTransparency)
|
||||
if 0 <= value <= 1:
|
||||
self.discard_xdata(AcCmTransparency)
|
||||
self.set_xdata(AcCmTransparency, [(1071, clr.float2transparency(value))])
|
||||
else:
|
||||
raise ValueError("Value out of range [0, 1].")
|
||||
|
||||
def rename(self, name: str) -> None:
|
||||
"""
|
||||
Rename layer and all known (documented) references to this layer.
|
||||
|
||||
.. warning::
|
||||
|
||||
The DXF format is not consistent in storing layer references, the
|
||||
layers are mostly referenced by their case-insensitive name,
|
||||
some later introduced entities do reference layers by handle, which
|
||||
is the safer way in the context of renaming layers.
|
||||
|
||||
There is no complete overview of where layer references are
|
||||
stored, third-party entities are black-boxes with unknown content
|
||||
and layer names could be stored in the extended data section of any
|
||||
DXF entity or in XRECORD entities.
|
||||
Which means that in some rare cases references to the old layer name
|
||||
can persist, at least this does not invalidate the DXF document.
|
||||
|
||||
Args:
|
||||
name: new layer name
|
||||
|
||||
Raises:
|
||||
ValueError: `name` contains invalid characters: <>/\\":;?*|=`
|
||||
ValueError: layer `name` already exist
|
||||
ValueError: renaming of layers ``'0'`` and ``'DEFPOINTS'`` not
|
||||
possible
|
||||
|
||||
"""
|
||||
if not validator.is_valid_layer_name(name):
|
||||
raise ValueError(
|
||||
f"Name contains invalid characters: {INVALID_NAME_CHARACTERS}."
|
||||
)
|
||||
assert self.doc is not None, "valid DXF document is required"
|
||||
layers = self.doc.layers
|
||||
if self.dxf.name.lower() in ("0", "defpoints"):
|
||||
raise ValueError(f'Can not rename layer "{self.dxf.name}".')
|
||||
if layers.has_entry(name):
|
||||
raise ValueError(f'Layer "{name}" already exist.')
|
||||
old = self.dxf.name
|
||||
self.dxf.name = name
|
||||
layers.replace(old, self)
|
||||
self._rename_layer_references(old, name)
|
||||
|
||||
def _rename_layer_references(self, old_name: str, new_name: str) -> None:
|
||||
assert self.doc is not None, "valid DXF document is required"
|
||||
key = self.doc.layers.key
|
||||
old_key = key(old_name)
|
||||
for e in self.doc.entitydb.values():
|
||||
if e.dxf.hasattr("layer") and key(e.dxf.layer) == old_key:
|
||||
e.dxf.layer = new_name
|
||||
entity_type = e.dxftype()
|
||||
if entity_type == "VIEWPORT":
|
||||
e.rename_frozen_layer(old_name, new_name) # type: ignore
|
||||
elif entity_type == "LAYER_FILTER":
|
||||
# todo: if LAYER_FILTER implemented, add support for
|
||||
# renaming layers
|
||||
logger.debug(
|
||||
f'renaming layer "{old_name}" - document contains ' f"LAYER_FILTER"
|
||||
)
|
||||
elif entity_type == "LAYER_INDEX":
|
||||
# todo: if LAYER_INDEX implemented, add support for
|
||||
# renaming layers
|
||||
logger.debug(
|
||||
f'renaming layer "{old_name}" - document contains ' f"LAYER_INDEX"
|
||||
)
|
||||
|
||||
def get_vp_overrides(self) -> LayerOverrides:
|
||||
"""Returns the :class:`LayerOverrides` object for this layer."""
|
||||
return LayerOverrides(self)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
assert self.doc is not None, "LAYER entity must be assigned to a document"
|
||||
super().register_resources(registry)
|
||||
registry.add_linetype(self.dxf.linetype)
|
||||
registry.add_handle(self.dxf.get("material_handle"))
|
||||
# current plot style will be replaced by default plot style "Normal"
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
assert isinstance(clone, Layer)
|
||||
super().map_resources(clone, mapping)
|
||||
self.dxf.linetype = mapping.get_linetype(self.dxf.linetype)
|
||||
|
||||
mapping.map_existing_handle(self, clone, "material_handle", optional=True)
|
||||
# remove handles pointing to the source document:
|
||||
clone.dxf.discard("plotstyle_handle") # replaced by plot style "Normal"
|
||||
clone.dxf.discard("unknown1")
|
||||
|
||||
# create required handles to resources in the target document
|
||||
clone.set_required_attributes()
|
||||
# todo: map layer overrides
|
||||
# remove layer overrides
|
||||
clone.discard_extension_dict()
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
super().audit(auditor)
|
||||
linetype = self.dxf.linetype
|
||||
if auditor.doc.linetypes.has_entry(linetype):
|
||||
return
|
||||
self.dxf.linetype = "Continuous"
|
||||
auditor.fixed_error(
|
||||
code=AuditError.UNDEFINED_LINETYPE,
|
||||
message=f"Replaced undefined linetype {linetype} in layer '{self.dxf.name}' by CONTINUOUS",
|
||||
dxf_entity=self,
|
||||
data=linetype,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OverrideAttributes:
|
||||
aci: int
|
||||
rgb: Optional[clr.RGB]
|
||||
transparency: float
|
||||
linetype: str
|
||||
lineweight: int
|
||||
|
||||
|
||||
class LayerOverrides:
|
||||
"""This object stores the layer attribute overridden in VIEWPORT entities,
|
||||
where each VIEWPORT can have individual layer attribute overrides.
|
||||
|
||||
Layer attributes which can be overridden:
|
||||
|
||||
- ACI color
|
||||
- true color (rgb)
|
||||
- linetype
|
||||
- lineweight
|
||||
- transparency
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, layer: Layer):
|
||||
assert layer.doc is not None, "valid DXF document required"
|
||||
self._layer = layer
|
||||
self._overrides = load_layer_overrides(layer)
|
||||
|
||||
def has_overrides(self, vp_handle: Optional[str] = None) -> bool:
|
||||
"""Returns ``True`` if attribute overrides exist for the given
|
||||
:class:`Viewport` handle.
|
||||
Returns ``True`` if `any` attribute overrides exist if the given
|
||||
handle is ``None``.
|
||||
"""
|
||||
if vp_handle is None:
|
||||
return bool(self._overrides)
|
||||
return vp_handle in self._overrides
|
||||
|
||||
def commit(self) -> None:
|
||||
"""Write :class:`Viewport` overrides back into the :class:`Layer` entity.
|
||||
Without a commit() all changes are lost!
|
||||
"""
|
||||
store_layer_overrides(self._layer, self._overrides)
|
||||
|
||||
def _acquire_overrides(self, vp_handle: str) -> OverrideAttributes:
|
||||
"""Returns the OverrideAttributes() instance for `vp_handle`, creates a new
|
||||
OverrideAttributes() instance if none exist.
|
||||
"""
|
||||
return self._overrides.setdefault(
|
||||
vp_handle,
|
||||
default_ovr_settings(self._layer),
|
||||
)
|
||||
|
||||
def _get_overrides(self, vp_handle: str) -> OverrideAttributes:
|
||||
"""Returns the overrides for `vp_handle`, returns the default layer
|
||||
settings if no Override() instance exist.
|
||||
"""
|
||||
try:
|
||||
return self._overrides[vp_handle]
|
||||
except KeyError:
|
||||
return default_ovr_settings(self._layer)
|
||||
|
||||
def set_color(self, vp_handle: str, value: int) -> None:
|
||||
"""Override the :ref:`ACI`.
|
||||
|
||||
Raises:
|
||||
ValueError: invalid color value
|
||||
"""
|
||||
# BYBLOCK or BYLAYER is not valid a layer color
|
||||
if not is_valid_layer_color_index(value):
|
||||
raise ValueError(f"invalid ACI value: {value}")
|
||||
vp_overrides = self._acquire_overrides(vp_handle)
|
||||
vp_overrides.aci = value
|
||||
|
||||
def get_color(self, vp_handle: str) -> int:
|
||||
"""Returns the :ref:`ACI` override or the original layer value if no
|
||||
override exist.
|
||||
"""
|
||||
vp_overrides = self._get_overrides(vp_handle)
|
||||
return vp_overrides.aci
|
||||
|
||||
def set_rgb(self, vp_handle: str, value: Optional[clr.RGB]):
|
||||
"""Set the RGB override as (red, gree, blue) tuple or ``None`` to remove
|
||||
the true color setting.
|
||||
|
||||
Raises:
|
||||
ValueError: invalid RGB value
|
||||
|
||||
"""
|
||||
if value is not None and not validator.is_valid_rgb(value):
|
||||
raise ValueError(f"invalid RGB value: {value}")
|
||||
vp_overrides = self._acquire_overrides(vp_handle)
|
||||
vp_overrides.rgb = value
|
||||
|
||||
def get_rgb(self, vp_handle: str) -> Optional[clr.RGB]:
|
||||
"""Returns the RGB override or the original layer value if no
|
||||
override exist. Returns ``None`` if no true color value is set.
|
||||
"""
|
||||
vp_overrides = self._get_overrides(vp_handle)
|
||||
return vp_overrides.rgb
|
||||
|
||||
def set_transparency(self, vp_handle: str, value: float) -> None:
|
||||
"""Set the transparency override. A transparency of 0.0 is opaque and
|
||||
1.0 is fully transparent.
|
||||
|
||||
Raises:
|
||||
ValueError: invalid transparency value
|
||||
|
||||
"""
|
||||
if not (0.0 <= value <= 1.0):
|
||||
raise ValueError(
|
||||
f"invalid transparency: {value}, has to be in the range [0, 1]"
|
||||
)
|
||||
vp_overrides = self._acquire_overrides(vp_handle)
|
||||
vp_overrides.transparency = value
|
||||
|
||||
def get_transparency(self, vp_handle: str) -> float:
|
||||
"""Returns the transparency override or the original layer value if no
|
||||
override exist. Returns 0.0 for opaque and 1.0 for fully transparent.
|
||||
"""
|
||||
vp_overrides = self._get_overrides(vp_handle)
|
||||
return vp_overrides.transparency
|
||||
|
||||
def set_linetype(self, vp_handle: str, value: str) -> None:
|
||||
"""Set the linetype override.
|
||||
|
||||
Raises:
|
||||
ValueError: linetype without a LTYPE table entry
|
||||
"""
|
||||
if value not in self._layer.doc.linetypes: # type: ignore
|
||||
raise ValueError(
|
||||
f"invalid linetype: {value}, a linetype table entry is required"
|
||||
)
|
||||
vp_overrides = self._acquire_overrides(vp_handle)
|
||||
vp_overrides.linetype = value
|
||||
|
||||
def get_linetype(self, vp_handle: str) -> str:
|
||||
"""Returns the linetype override or the original layer value if no
|
||||
override exist.
|
||||
"""
|
||||
vp_overrides = self._get_overrides(vp_handle)
|
||||
return vp_overrides.linetype
|
||||
|
||||
def get_lineweight(self, vp_handle: str) -> int:
|
||||
"""Returns the lineweight override or the original layer value if no
|
||||
override exist.
|
||||
"""
|
||||
vp_overrides = self._get_overrides(vp_handle)
|
||||
return vp_overrides.lineweight
|
||||
|
||||
def set_lineweight(self, vp_handle: str, value: int) -> None:
|
||||
"""Set the lineweight override.
|
||||
|
||||
Raises:
|
||||
ValueError: invalid lineweight value
|
||||
"""
|
||||
if not is_valid_layer_lineweight(value):
|
||||
raise ValueError(
|
||||
f"invalid lineweight: {value}, a linetype table entry is required"
|
||||
)
|
||||
vp_overrides = self._acquire_overrides(vp_handle)
|
||||
vp_overrides.lineweight = value
|
||||
|
||||
def discard(self, vp_handle: Optional[str] = None) -> None:
|
||||
"""Discard all attribute overrides for the given :class:`Viewport`
|
||||
handle or for all :class:`Viewport` entities if the handle is ``None``.
|
||||
"""
|
||||
if vp_handle is None:
|
||||
self._overrides.clear()
|
||||
return
|
||||
try:
|
||||
del self._overrides[vp_handle]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def default_ovr_settings(layer) -> OverrideAttributes:
|
||||
"""Returns the default settings of the layer."""
|
||||
return OverrideAttributes(
|
||||
aci=layer.color,
|
||||
rgb=layer.rgb,
|
||||
transparency=layer.transparency,
|
||||
linetype=layer.dxf.linetype,
|
||||
lineweight=layer.dxf.lineweight,
|
||||
)
|
||||
|
||||
|
||||
def is_layer_frozen_in_vp(layer, vp_handle) -> bool:
|
||||
"""Returns ``True`` if layer is frozen in VIEWPORT defined by the vp_handle."""
|
||||
vp = cast("Viewport", layer.doc.entitydb.get(vp_handle))
|
||||
if vp is not None:
|
||||
return layer.dxf.name in vp.frozen_layers
|
||||
return False
|
||||
|
||||
|
||||
def load_layer_overrides(layer: Layer) -> dict[str, OverrideAttributes]:
|
||||
"""Load all VIEWPORT overrides from the layer extension dictionary."""
|
||||
|
||||
def get_ovr(vp_handle: str):
|
||||
ovr = overrides.get(vp_handle)
|
||||
if ovr is None:
|
||||
ovr = default_ovr_settings(layer)
|
||||
overrides[vp_handle] = ovr
|
||||
return ovr
|
||||
|
||||
def set_alpha(vp_handle: str, value: int):
|
||||
ovr = get_ovr(vp_handle)
|
||||
ovr.transparency = clr.transparency2float(value)
|
||||
|
||||
def set_color(vp_handle: str, value: int):
|
||||
ovr = get_ovr(vp_handle)
|
||||
type_, data = clr.decode_raw_color(value)
|
||||
if type_ == clr.COLOR_TYPE_ACI:
|
||||
ovr.aci = data
|
||||
elif type_ == clr.COLOR_TYPE_RGB:
|
||||
ovr.rgb = data
|
||||
|
||||
def set_ltype(vp_handle: str, lt_handle: str):
|
||||
ltype = entitydb.get(lt_handle)
|
||||
if ltype is not None:
|
||||
ovr = get_ovr(vp_handle)
|
||||
ovr.linetype = ltype.dxf.name
|
||||
|
||||
def set_lw(vp_handle: str, value: int):
|
||||
ovr = get_ovr(vp_handle)
|
||||
ovr.lineweight = value
|
||||
|
||||
def set_xdict_state():
|
||||
xdict = layer.get_extension_dict()
|
||||
for key, code, setter in [
|
||||
(const.OVR_ALPHA_KEY, const.OVR_ALPHA_CODE, set_alpha),
|
||||
(const.OVR_COLOR_KEY, const.OVR_COLOR_CODE, set_color),
|
||||
(const.OVR_LTYPE_KEY, const.OVR_LTYPE_CODE, set_ltype),
|
||||
(const.OVR_LW_KEY, const.OVR_LW_CODE, set_lw),
|
||||
]:
|
||||
xrec = cast("XRecord", xdict.get(key))
|
||||
if xrec is not None:
|
||||
for vp_handle, value in _load_ovr_values(xrec, code):
|
||||
setter(vp_handle, value)
|
||||
|
||||
assert layer.doc is not None, "valid DXF document required"
|
||||
entitydb: EntityDB = layer.doc.entitydb
|
||||
assert entitydb is not None, "valid entity database required"
|
||||
|
||||
overrides: dict[str, OverrideAttributes] = dict()
|
||||
if not layer.has_extension_dict:
|
||||
return overrides
|
||||
|
||||
set_xdict_state()
|
||||
return overrides
|
||||
|
||||
|
||||
def _load_ovr_values(xrec: XRecord, group_code):
|
||||
tags = xrec.tags
|
||||
handles = [value for code, value in tags.find_all(const.OVR_VP_HANDLE_CODE)]
|
||||
values = [value for code, value in tags.find_all(group_code)]
|
||||
return zip(handles, values)
|
||||
|
||||
|
||||
def store_layer_overrides(
|
||||
layer: Layer, overrides: dict[str, OverrideAttributes]
|
||||
) -> None:
|
||||
"""Store all VIEWPORT overrides in the layer extension dictionary.
|
||||
Replaces all existing overrides!
|
||||
"""
|
||||
from ezdxf.lldxf.types import DXFTag
|
||||
|
||||
def get_xdict():
|
||||
if layer.has_extension_dict:
|
||||
return layer.get_extension_dict()
|
||||
else:
|
||||
return layer.new_extension_dict()
|
||||
|
||||
def set_xdict_tags(key: str, tags: list[DXFTag]):
|
||||
from ezdxf.entities import XRecord
|
||||
|
||||
xdict = get_xdict()
|
||||
xrec = xdict.get(key)
|
||||
if not isinstance(xrec, XRecord) and xrec is not None:
|
||||
logger.debug(
|
||||
f"Found entity {str(xrec)} as override storage in {str(layer)} "
|
||||
f"but expected XRECORD"
|
||||
)
|
||||
xrec = None
|
||||
if xrec is None:
|
||||
xrec = xdict.add_xrecord(key)
|
||||
xrec.dxf.cloning = 1
|
||||
xrec.reset(tags)
|
||||
|
||||
def del_xdict_tags(key: str):
|
||||
if not layer.has_extension_dict:
|
||||
return
|
||||
xdict = layer.get_extension_dict()
|
||||
xrec = xdict.get(key)
|
||||
if xrec is not None:
|
||||
xrec.destroy()
|
||||
xdict.discard(key)
|
||||
|
||||
def make_tags(data: list[tuple[Any, str]], name: str, code: int) -> list[DXFTag]:
|
||||
tags: list[DXFTag] = []
|
||||
for value, vp_handle in data:
|
||||
tags.extend(
|
||||
[
|
||||
DXFTag(102, name),
|
||||
DXFTag(const.OVR_VP_HANDLE_CODE, vp_handle),
|
||||
DXFTag(code, value),
|
||||
DXFTag(102, "}"),
|
||||
]
|
||||
)
|
||||
return tags
|
||||
|
||||
def collect_alphas():
|
||||
for vp_handle, ovr in vp_exist.items():
|
||||
if ovr.transparency != default.transparency:
|
||||
yield clr.float2transparency(ovr.transparency), vp_handle
|
||||
|
||||
def collect_colors():
|
||||
for vp_handle, ovr in vp_exist.items():
|
||||
if ovr.aci != default.aci or ovr.rgb != default.rgb:
|
||||
if ovr.rgb is None:
|
||||
raw_color = clr.encode_raw_color(ovr.aci)
|
||||
else:
|
||||
raw_color = clr.encode_raw_color(ovr.rgb)
|
||||
yield raw_color, vp_handle
|
||||
|
||||
def collect_linetypes():
|
||||
for vp_handle, ovr in vp_exist.items():
|
||||
if ovr.linetype != default.linetype:
|
||||
ltype = layer.doc.linetypes.get(ovr.linetype)
|
||||
if ltype is not None:
|
||||
yield ltype.dxf.handle, vp_handle
|
||||
|
||||
def collect_lineweights():
|
||||
for vp_handle, ovr in vp_exist.items():
|
||||
if ovr.lineweight != default.lineweight:
|
||||
yield ovr.lineweight, vp_handle
|
||||
|
||||
assert layer.doc is not None, "valid DXF document required"
|
||||
entitydb = layer.doc.entitydb
|
||||
vp_exist = {
|
||||
vp_handle: ovr
|
||||
for vp_handle, ovr in overrides.items()
|
||||
if (vp_handle in entitydb) and entitydb[vp_handle].is_alive
|
||||
}
|
||||
default = default_ovr_settings(layer)
|
||||
alphas = list(collect_alphas())
|
||||
if alphas:
|
||||
tags = make_tags(alphas, const.OVR_APP_ALPHA, const.OVR_ALPHA_CODE)
|
||||
set_xdict_tags(const.OVR_ALPHA_KEY, tags)
|
||||
else:
|
||||
del_xdict_tags(const.OVR_ALPHA_KEY)
|
||||
|
||||
colors = list(collect_colors())
|
||||
if colors:
|
||||
tags = make_tags(colors, const.OVR_APP_COLOR, const.OVR_COLOR_CODE)
|
||||
set_xdict_tags(const.OVR_COLOR_KEY, tags)
|
||||
else:
|
||||
del_xdict_tags(const.OVR_COLOR_KEY)
|
||||
|
||||
linetypes = list(collect_linetypes())
|
||||
if linetypes:
|
||||
tags = make_tags(linetypes, const.OVR_APP_LTYPE, const.OVR_LTYPE_CODE)
|
||||
set_xdict_tags(const.OVR_LTYPE_KEY, tags)
|
||||
else:
|
||||
del_xdict_tags(const.OVR_LTYPE_KEY)
|
||||
|
||||
lineweights = list(collect_lineweights())
|
||||
if lineweights:
|
||||
tags = make_tags(lineweights, const.OVR_APP_LW, const.OVR_LW_CODE)
|
||||
set_xdict_tags(const.OVR_LW_KEY, tags)
|
||||
else:
|
||||
del_xdict_tags(const.OVR_LW_KEY)
|
||||
@@ -0,0 +1,394 @@
|
||||
# Copyright (c) 2020-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.const import SUBCLASS_MARKER
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.math import Vec3, Vec2, NULLVEC, X_AXIS, Y_AXIS
|
||||
from .dxfentity import base_class, SubclassProcessor, DXFEntity
|
||||
from .dxfobj import DXFObject
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.entities.dxfns import DXFNamespace
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["PlotSettings", "DXFLayout"]
|
||||
|
||||
acdb_plot_settings = DefSubclass(
|
||||
"AcDbPlotSettings",
|
||||
{
|
||||
# acdb_plot_settings is also part of LAYOUT and LAYOUT has a 'name' attribute
|
||||
"page_setup_name": DXFAttr(1, default=""),
|
||||
|
||||
# An optional empty string selects the default printer/plotter
|
||||
"plot_configuration_file": DXFAttr(2, default="", optional=True),
|
||||
"paper_size": DXFAttr(4, default="A3"),
|
||||
"plot_view_name": DXFAttr(6, default=""),
|
||||
"left_margin": DXFAttr(40, default=7.5), # in mm
|
||||
"bottom_margin": DXFAttr(41, default=20), # in mm
|
||||
"right_margin": DXFAttr(42, default=7.5), # in mm
|
||||
"top_margin": DXFAttr(43, default=20), # in mm
|
||||
"paper_width": DXFAttr(44, default=420), # in mm
|
||||
"paper_height": DXFAttr(45, default=297), # in mm
|
||||
"plot_origin_x_offset": DXFAttr(46, default=0.0), # in mm
|
||||
"plot_origin_y_offset": DXFAttr(47, default=0.0), # in mm
|
||||
"plot_window_x1": DXFAttr(48, default=0.0),
|
||||
"plot_window_y1": DXFAttr(49, default=0.0),
|
||||
"plot_window_x2": DXFAttr(140, default=0.0),
|
||||
"plot_window_y2": DXFAttr(141, default=0.0),
|
||||
# Numerator of custom print scale: real world (paper) units:
|
||||
"scale_numerator": DXFAttr(142, default=1.0),
|
||||
# Denominator of custom print scale: drawing units:
|
||||
"scale_denominator": DXFAttr(143, default=1.0),
|
||||
# Plot layout flags:
|
||||
# 1 = plot viewport borders
|
||||
# 2 = show plot-styles
|
||||
# 4 = plot centered
|
||||
# 8 = plot hidden == hide paperspace entities?
|
||||
# 16 = use standard scale
|
||||
# 32 = plot with plot-styles
|
||||
# 64 = scale lineweights
|
||||
# 128 = plot entity lineweights
|
||||
# 512 = draw viewports first
|
||||
# 1024 = model type
|
||||
# 2048 = update paper
|
||||
# 4096 = zoom to paper on update
|
||||
# 8192 = initializing
|
||||
# 16384 = prev plot-init
|
||||
# the "Plot transparencies" option is stored in the XDATA section
|
||||
"plot_layout_flags": DXFAttr(70, default=688),
|
||||
# Plot paper units:
|
||||
# 0 = Plot in inches
|
||||
# 1 = Plot in millimeters
|
||||
# 2 = Plot in pixels
|
||||
"plot_paper_units": DXFAttr(
|
||||
72,
|
||||
default=1,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Plot rotation:
|
||||
# 0 = No rotation
|
||||
# 1 = 90 degrees counterclockwise
|
||||
# 2 = Upside-down
|
||||
# 3 = 90 degrees clockwise
|
||||
"plot_rotation": DXFAttr(
|
||||
73,
|
||||
default=0,
|
||||
validator=validator.is_in_integer_range(0, 4),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Plot type:
|
||||
# 0 = Last screen display
|
||||
# 1 = Drawing extents
|
||||
# 2 = Drawing limits
|
||||
# 3 = View specified by code 6
|
||||
# 4 = Window specified by codes 48, 49, 140, and 141
|
||||
# 5 = Layout information
|
||||
"plot_type": DXFAttr(
|
||||
74,
|
||||
default=5,
|
||||
validator=validator.is_in_integer_range(0, 6),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Associated CTB-file
|
||||
"current_style_sheet": DXFAttr(7, default=""),
|
||||
# Standard scale type:
|
||||
# 0 = Scaled to Fit
|
||||
# 1 = 1/128"=1'
|
||||
# 2 = 1/64"=1'
|
||||
# 3 = 1/32"=1'
|
||||
# 4 = 1/16"=1'
|
||||
# 5 = 3/32"=1'
|
||||
# 6 = 1/8"=1'
|
||||
# 7 = 3/16"=1'
|
||||
# 8 = 1/4"=1'
|
||||
# 9 = 3/8"=1'
|
||||
# 10 = 1/2"=1'
|
||||
# 11 = 3/4"=1'
|
||||
# 12 = 1"=1'
|
||||
# 13 = 3"=1'
|
||||
# 14 = 6"=1'
|
||||
# 15 = 1'=1'
|
||||
# 16 = 1:1
|
||||
# 17 = 1:2
|
||||
# 18 = 1:4
|
||||
# 19 = 1:8
|
||||
# 20 = 1:10
|
||||
# 21 = 1:16
|
||||
# 22 = 1:20
|
||||
# 23 = 1:30
|
||||
# 24 = 1:40
|
||||
# 25 = 1:50
|
||||
# 26 = 1:100
|
||||
# 27 = 2:1
|
||||
# 28 = 4:1
|
||||
# 29 = 8:1
|
||||
# 30 = 10:1
|
||||
# 31 = 100:1
|
||||
# 32 = 1000:1
|
||||
"standard_scale_type": DXFAttr(
|
||||
75,
|
||||
default=16,
|
||||
validator=validator.is_in_integer_range(0, 33),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Shade plot mode:
|
||||
# 0 = As Displayed
|
||||
# 1 = Wireframe
|
||||
# 2 = Hidden
|
||||
# 3 = Rendered
|
||||
"shade_plot_mode": DXFAttr(
|
||||
76,
|
||||
default=0,
|
||||
validator=validator.is_in_integer_range(0, 4),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Shade plot resolution level:
|
||||
# 0 = Draft
|
||||
# 1 = Preview
|
||||
# 2 = Normal
|
||||
# 3 = Presentation
|
||||
# 4 = Maximum
|
||||
# 5 = Custom
|
||||
"shade_plot_resolution_level": DXFAttr(
|
||||
77,
|
||||
default=2,
|
||||
validator=validator.is_in_integer_range(0, 6),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Valid range: 100 to 32767, Only applied when the shade_plot_resolution
|
||||
# level is set to 5 (Custom)
|
||||
"shade_plot_custom_dpi": DXFAttr(
|
||||
78,
|
||||
default=300,
|
||||
validator=validator.is_in_integer_range(100, 32768),
|
||||
fixer=validator.fit_into_integer_range(100, 32768),
|
||||
),
|
||||
# Factor for unit conversion (mm -> inches)
|
||||
# 147: DXF Reference error: 'A floating point scale factor that represents
|
||||
# the standard scale value specified in code 75'
|
||||
"unit_factor": DXFAttr(
|
||||
147,
|
||||
default=1.0,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"paper_image_origin_x": DXFAttr(148, default=0),
|
||||
"paper_image_origin_y": DXFAttr(149, default=0),
|
||||
"shade_plot_handle": DXFAttr(333, optional=True),
|
||||
},
|
||||
)
|
||||
acdb_plot_settings_group_codes = group_code_mapping(acdb_plot_settings)
|
||||
|
||||
# The "Plot transparencies" option is stored in the XDATA section of the
|
||||
# LAYOUT entity:
|
||||
# 1001
|
||||
# PLOTTRANSPARENCY
|
||||
# 1071
|
||||
# 1
|
||||
|
||||
|
||||
@register_entity
|
||||
class PlotSettings(DXFObject):
|
||||
DXFTYPE = "PLOTSETTINGS"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_plot_settings)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_plot_settings_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_plot_settings.name)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"page_setup_name",
|
||||
"plot_configuration_file",
|
||||
"paper_size",
|
||||
"plot_view_name",
|
||||
"left_margin",
|
||||
"bottom_margin",
|
||||
"right_margin",
|
||||
"top_margin",
|
||||
"paper_width",
|
||||
"paper_height",
|
||||
"plot_origin_x_offset",
|
||||
"plot_origin_y_offset",
|
||||
"plot_window_x1",
|
||||
"plot_window_y1",
|
||||
"plot_window_x2",
|
||||
"plot_window_y2",
|
||||
"scale_numerator",
|
||||
"scale_denominator",
|
||||
"plot_layout_flags",
|
||||
"plot_paper_units",
|
||||
"plot_rotation",
|
||||
"plot_type",
|
||||
"current_style_sheet",
|
||||
"standard_scale_type",
|
||||
"shade_plot_mode",
|
||||
"shade_plot_resolution_level",
|
||||
"shade_plot_custom_dpi",
|
||||
"unit_factor",
|
||||
"paper_image_origin_x",
|
||||
"paper_image_origin_y",
|
||||
],
|
||||
)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
super().register_resources(registry)
|
||||
registry.add_handle(self.dxf.get("shade_plot_handle"))
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
super().map_resources(clone, mapping)
|
||||
shade_plot_handle = self.dxf.get("shade_plot_handle")
|
||||
if shade_plot_handle and shade_plot_handle != "0":
|
||||
clone.dxf.shade_plot_handle = mapping.get_handle(shade_plot_handle)
|
||||
else:
|
||||
clone.dxf.discard("shade_plot_handle")
|
||||
|
||||
|
||||
acdb_layout = DefSubclass(
|
||||
"AcDbLayout",
|
||||
{
|
||||
# Layout name:
|
||||
"name": DXFAttr(1, default="Layoutname"),
|
||||
# Flag (bit-coded) to control the following:
|
||||
# 1 = Indicates the PSLTSCALE value for this layout when this layout is current
|
||||
# 2 = Indicates the LIMCHECK value for this layout when this layout is current
|
||||
"layout_flags": DXFAttr(70, default=1),
|
||||
# Tab order: This number is an ordinal indicating this layout's ordering in
|
||||
# the tab control that is attached to the AutoCAD drawing frame window.
|
||||
# Note that the "Model" tab always appears as the first tab regardless of
|
||||
# its tab order.
|
||||
"taborder": DXFAttr(71, default=1),
|
||||
# Minimum limits:
|
||||
"limmin": DXFAttr(10, xtype=XType.point2d, default=Vec2(0, 0)),
|
||||
# Maximum limits:
|
||||
"limmax": DXFAttr(11, xtype=XType.point2d, default=Vec2(420, 297)),
|
||||
# Insertion base point for this layout:
|
||||
"insert_base": DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
|
||||
# Minimum extents for this layout:
|
||||
"extmin": DXFAttr(14, xtype=XType.point3d, default=Vec3(1e20, 1e20, 1e20)),
|
||||
# Maximum extents for this layout:
|
||||
"extmax": DXFAttr(15, xtype=XType.point3d, default=Vec3(-1e20, -1e20, -1e20)),
|
||||
"elevation": DXFAttr(146, default=0.0),
|
||||
"ucs_origin": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
|
||||
"ucs_xaxis": DXFAttr(
|
||||
16,
|
||||
xtype=XType.point3d,
|
||||
default=X_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"ucs_yaxis": DXFAttr(
|
||||
17,
|
||||
xtype=XType.point3d,
|
||||
default=Y_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Orthographic type of UCS:
|
||||
# 0 = UCS is not orthographic
|
||||
# 1 = Top
|
||||
# 2 = Bottom
|
||||
# 3 = Front
|
||||
# 4 = Back
|
||||
# 5 = Left
|
||||
# 6 = Right
|
||||
"ucs_type": DXFAttr(
|
||||
76,
|
||||
default=1,
|
||||
validator=validator.is_in_integer_range(0, 7),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Handle of parent BLOCK_RECORD
|
||||
"block_record_handle": DXFAttr(330),
|
||||
# Handle to the viewport that was last active in this
|
||||
# layout when the layout was current:
|
||||
"viewport_handle": DXFAttr(331),
|
||||
# Handle of AcDbUCSTableRecord if UCS is a named
|
||||
# UCS. If not present, then UCS is unnamed
|
||||
"ucs_handle": DXFAttr(345),
|
||||
# Handle of AcDbUCSTableRecord of base UCS if UCS is
|
||||
# orthographic (76 code is non-zero). If not present and
|
||||
# 76 code is non-zero, then base UCS is taken to be WORLD
|
||||
"base_ucs_handle": DXFAttr(346),
|
||||
},
|
||||
)
|
||||
acdb_layout_group_codes = group_code_mapping(acdb_layout)
|
||||
|
||||
|
||||
@register_entity
|
||||
class DXFLayout(PlotSettings):
|
||||
DXFTYPE = "LAYOUT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_plot_settings, acdb_layout)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_layout_group_codes, 2)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
# Set correct model type flag
|
||||
self.set_flag_state(1024, self.dxf.name.upper() == "MODEL", "plot_layout_flags")
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_layout.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"layout_flags",
|
||||
"taborder",
|
||||
"limmin",
|
||||
"limmax",
|
||||
"insert_base",
|
||||
"extmin",
|
||||
"extmax",
|
||||
"elevation",
|
||||
"ucs_origin",
|
||||
"ucs_xaxis",
|
||||
"ucs_yaxis",
|
||||
"ucs_type",
|
||||
"block_record_handle",
|
||||
"viewport_handle",
|
||||
"ucs_handle",
|
||||
"base_ucs_handle",
|
||||
],
|
||||
)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
super().register_resources(registry)
|
||||
registry.add_handle(self.dxf.get("ucs_handle"))
|
||||
registry.add_handle(self.dxf.get("base_ucs_handle"))
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
super().map_resources(clone, mapping)
|
||||
|
||||
# The content of paperspace layouts is not copied automatically and the
|
||||
# associated BLOCK_RECORD is created and assigned in a special method.
|
||||
mapping.map_existing_handle(self, clone, "ucs_handle", optional=True)
|
||||
mapping.map_existing_handle(self, clone, "base_ucs_handle", optional=True)
|
||||
mapping.map_existing_handle(self, clone, "viewport_handle", optional=True)
|
||||
@@ -0,0 +1,360 @@
|
||||
# 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 logging
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.tags import Tags, DXFTag
|
||||
from ezdxf.lldxf import const
|
||||
|
||||
from ezdxf.math import Vec3, UVec, X_AXIS, Z_AXIS, NULLVEC
|
||||
from ezdxf.math.transformtools import transform_extrusion
|
||||
from ezdxf.explode import explode_entity
|
||||
from ezdxf.audit import AuditError
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .factory import register_entity
|
||||
from .dimension import OverrideMixin, register_override_handles
|
||||
from .dimstyleoverride import DimStyleOverride
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.audit import Auditor
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.layouts import BaseLayout
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.math import Matrix44
|
||||
from ezdxf.query import EntityQuery
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["Leader"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
acdb_leader = DefSubclass(
|
||||
"AcDbLeader",
|
||||
{
|
||||
"dimstyle": DXFAttr(
|
||||
3,
|
||||
default="Standard",
|
||||
validator=validator.is_valid_table_name,
|
||||
# no fixer!
|
||||
),
|
||||
# Arrowhead flag: 0/1 = no/yes
|
||||
"has_arrowhead": DXFAttr(
|
||||
71,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Leader path type:
|
||||
# 0 = Straight line segments
|
||||
# 1 = Spline
|
||||
"path_type": DXFAttr(
|
||||
72,
|
||||
default=0,
|
||||
optional=True,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Annotation type or leader creation flag:
|
||||
# 0 = Created with text annotation
|
||||
# 1 = Created with tolerance annotation;
|
||||
# 2 = Created with block reference annotation
|
||||
# 3 = Created without any annotation
|
||||
"annotation_type": DXFAttr(
|
||||
73,
|
||||
default=3,
|
||||
validator=validator.is_in_integer_range(0, 4),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hook line direction flag:
|
||||
# 1 = Hook line (or end of tangent for a spline leader) is the opposite
|
||||
# direction from the horizontal vector
|
||||
# 0 = Hook line (or end of tangent for a spline leader) is the same
|
||||
# direction as horizontal vector (see code 75)
|
||||
# DXF reference error: swapped meaning of 1/0
|
||||
"hookline_direction": DXFAttr(
|
||||
74,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hook line flag: 0/1 = no/yes
|
||||
"has_hookline": DXFAttr(
|
||||
75,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Text annotation height:
|
||||
"text_height": DXFAttr(
|
||||
40,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Text annotation width:
|
||||
"text_width": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 76: Number of vertices in leader (ignored for OPEN)
|
||||
# 10, 20, 30: Vertex coordinates (one entry for each vertex)
|
||||
# Color to use if leader's DIMCLRD = BYBLOCK
|
||||
"block_color": DXFAttr(
|
||||
77,
|
||||
default=7,
|
||||
optional=True,
|
||||
validator=validator.is_valid_aci_color,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hard reference to associated annotation:
|
||||
# (mtext, tolerance, or insert entity)
|
||||
"annotation_handle": DXFAttr(340, default="0", optional=True),
|
||||
"normal_vector": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 'horizontal' direction for leader
|
||||
"horizontal_direction": DXFAttr(
|
||||
211,
|
||||
xtype=XType.point3d,
|
||||
default=X_AXIS,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Offset of last leader vertex from block reference insertion point
|
||||
"leader_offset_block_ref": DXFAttr(
|
||||
212, xtype=XType.point3d, default=NULLVEC, optional=True
|
||||
),
|
||||
# Offset of last leader vertex from annotation placement point
|
||||
"leader_offset_annotation_placement": DXFAttr(
|
||||
213, xtype=XType.point3d, default=NULLVEC, optional=True
|
||||
),
|
||||
# Xdata belonging to the application ID "ACAD" follows a leader entity if
|
||||
# any dimension overrides have been applied to this entity. See Dimension
|
||||
# Style Overrides.
|
||||
},
|
||||
)
|
||||
acdb_leader_group_codes = group_code_mapping(acdb_leader)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Leader(DXFGraphic, OverrideMixin):
|
||||
"""DXF LEADER entity"""
|
||||
|
||||
DXFTYPE = "LEADER"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_leader)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.vertices: list[Vec3] = []
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy vertices."""
|
||||
assert isinstance(entity, Leader)
|
||||
entity.vertices = Vec3.list(self.vertices)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.subclass_by_index(2)
|
||||
if tags:
|
||||
tags = Tags(self.load_vertices(tags))
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_leader_group_codes, tags, recover=True
|
||||
)
|
||||
else:
|
||||
raise const.DXFStructureError(
|
||||
f"missing 'AcDbLeader' subclass in LEADER(#{dxf.handle})"
|
||||
)
|
||||
|
||||
return dxf
|
||||
|
||||
def load_vertices(self, tags: Tags) -> Iterable[DXFTag]:
|
||||
for tag in tags:
|
||||
if tag.code == 10:
|
||||
self.vertices.append(tag.value)
|
||||
elif tag.code == 76:
|
||||
# Number of vertices in leader (ignored for OPEN)
|
||||
pass
|
||||
else:
|
||||
yield tag
|
||||
|
||||
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
|
||||
if len(self.vertices) < 2:
|
||||
logger.debug(f"Invalid {str(self)}: more than 1 vertex required.")
|
||||
return False
|
||||
else:
|
||||
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_leader.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"dimstyle",
|
||||
"has_arrowhead",
|
||||
"path_type",
|
||||
"annotation_type",
|
||||
"hookline_direction",
|
||||
"has_hookline",
|
||||
"text_height",
|
||||
"text_width",
|
||||
],
|
||||
)
|
||||
self.export_vertices(tagwriter)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"block_color",
|
||||
"annotation_handle",
|
||||
"normal_vector",
|
||||
"horizontal_direction",
|
||||
"leader_offset_block_ref",
|
||||
"leader_offset_annotation_placement",
|
||||
],
|
||||
)
|
||||
|
||||
def export_vertices(self, tagwriter: AbstractTagWriter) -> None:
|
||||
tagwriter.write_tag2(76, len(self.vertices))
|
||||
for vertex in self.vertices:
|
||||
tagwriter.write_vertex(10, vertex)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
assert self.doc is not None
|
||||
super().register_resources(registry)
|
||||
registry.add_dim_style(self.dxf.dimstyle)
|
||||
|
||||
# The leader entity cannot register the annotation entity!
|
||||
if not self.has_xdata_list("ACAD", "DSTYLE"):
|
||||
return
|
||||
|
||||
if self.doc.dxfversion > const.DXF12:
|
||||
# overridden resources are referenced by handle
|
||||
register_override_handles(self, registry)
|
||||
else:
|
||||
# overridden resources are referenced by name
|
||||
self.override().register_resources_r12(registry)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
super().map_resources(clone, mapping)
|
||||
if self.dxf.hasattr("annotation_handle"):
|
||||
clone.dxf.annotation_handle = mapping.get_handle(self.dxf.annotation_handle)
|
||||
|
||||
# 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 is not None
|
||||
if self.doc.dxfversion > const.DXF12:
|
||||
return
|
||||
self_override = self.override()
|
||||
if not self_override.dimstyle_attribs:
|
||||
return # has no overrides
|
||||
|
||||
assert isinstance(clone, Leader)
|
||||
self_override.map_resources_r12(clone, mapping)
|
||||
|
||||
def override(self) -> DimStyleOverride:
|
||||
"""Returns the :class:`~ezdxf.entities.DimStyleOverride` object.
|
||||
|
||||
.. warning::
|
||||
|
||||
The LEADER entity shares only the DIMSTYLE override infrastructure with the
|
||||
DIMENSION entity but does not support any other features of the DIMENSION
|
||||
entity!
|
||||
|
||||
HANDLE WITH CARE!
|
||||
|
||||
"""
|
||||
return DimStyleOverride(self) # type: ignore
|
||||
|
||||
def set_vertices(self, vertices: Iterable[UVec]):
|
||||
"""Set vertices of the leader, vertices is an iterable of
|
||||
(x, y [,z]) tuples or :class:`~ezdxf.math.Vec3`.
|
||||
|
||||
"""
|
||||
self.vertices = [Vec3(v) for v in vertices]
|
||||
|
||||
def transform(self, m: Matrix44) -> Leader:
|
||||
"""Transform LEADER entity by transformation matrix `m` inplace."""
|
||||
self.vertices = list(m.transform_vertices(self.vertices))
|
||||
self.dxf.normal_vector, _ = transform_extrusion(
|
||||
self.dxf.normal_vector, m
|
||||
) # ???
|
||||
self.dxf.horizontal_direction = m.transform_direction(
|
||||
self.dxf.horizontal_direction
|
||||
)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def __virtual_entities__(self) -> Iterator[DXFGraphic]:
|
||||
"""Implements the SupportsVirtualEntities protocol."""
|
||||
from ezdxf.render.leader import virtual_entities
|
||||
|
||||
for e in virtual_entities(self):
|
||||
e.set_source_of_copy(self)
|
||||
yield e
|
||||
|
||||
def virtual_entities(self) -> Iterator[DXFGraphic]:
|
||||
"""Yields the DXF primitives the LEADER entity is build up as virtual entities.
|
||||
|
||||
These entities are located at the original location, but are not stored
|
||||
in the entity database, have no handle and are not assigned to any
|
||||
layout.
|
||||
"""
|
||||
return self.__virtual_entities__()
|
||||
|
||||
def explode(self, target_layout: Optional[BaseLayout] = None) -> EntityQuery:
|
||||
"""Explode parts of the LEADER entity as DXF primitives into target layout,
|
||||
if target layout is ``None``, the target layout is the layout of the LEADER
|
||||
entity. This method destroys the source entity.
|
||||
|
||||
Returns an :class:`~ezdxf.query.EntityQuery` container referencing all
|
||||
DXF primitives.
|
||||
|
||||
Args:
|
||||
target_layout: target layout for the created DXF primitives, ``None`` for
|
||||
the same layout as the source entity.
|
||||
|
||||
"""
|
||||
return explode_entity(self, target_layout)
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
"""Validity check."""
|
||||
super().audit(auditor)
|
||||
if len(self.vertices) < 2:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_VERTEX_COUNT,
|
||||
message=f"Deleted entity {str(self)} with invalid vertex count "
|
||||
f"= {len(self.vertices)}.",
|
||||
dxf_entity=self,
|
||||
)
|
||||
self.destroy()
|
||||
@@ -0,0 +1,152 @@
|
||||
# Copyright (c) 2019-2022, Manfred Moitzi
|
||||
# License: MIT-License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2007
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
DXFAttr,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import acdb_entity, DXFGraphic
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.math import Matrix44
|
||||
|
||||
__all__ = ["Light"]
|
||||
|
||||
acdb_light = DefSubclass(
|
||||
"AcDbLight",
|
||||
{
|
||||
"version": DXFAttr(90, default=0),
|
||||
# Light name
|
||||
"name": DXFAttr(1, default=""),
|
||||
# Light type:
|
||||
# 1 = distant
|
||||
# 2 = point
|
||||
# 3 = spot
|
||||
"type": DXFAttr(
|
||||
70,
|
||||
default=1,
|
||||
validator=validator.is_in_integer_range(1, 4),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"status": DXFAttr(
|
||||
290,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"plot_glyph": DXFAttr(
|
||||
291,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"intensity": DXFAttr(40, default=1),
|
||||
# Light position
|
||||
"location": DXFAttr(10, xtype=XType.point3d),
|
||||
# Target location
|
||||
"target": DXFAttr(11, xtype=XType.point3d),
|
||||
# Attenuation type:
|
||||
# 0 = None
|
||||
# 1 = Inverse Linear
|
||||
# 2 = Inverse Square
|
||||
"attenuation_type": DXFAttr(
|
||||
72,
|
||||
default=2,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"use_attenuation_limits": DXFAttr(
|
||||
292,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"attenuation_start_limits": DXFAttr(41),
|
||||
"attenuation_end_limits": DXFAttr(42),
|
||||
"hotspot_angle": DXFAttr(50), # in degrees
|
||||
"falloff_angle": DXFAttr(51), # in degrees
|
||||
"cast_shadows": DXFAttr(
|
||||
293,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Shadow Type:
|
||||
# 0 = Ray traced shadows
|
||||
# 1 = Shadow maps
|
||||
"shadow_type": DXFAttr(
|
||||
73,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"shadow_map_size": DXFAttr(91),
|
||||
"shadow_map_softness": DXFAttr(280),
|
||||
},
|
||||
)
|
||||
acdb_light_group_codes = group_code_mapping(acdb_light)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Light(DXFGraphic):
|
||||
"""DXF LIGHT entity"""
|
||||
|
||||
DXFTYPE = "LIGHT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_light)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2007
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_light_group_codes, 2, recover=True
|
||||
)
|
||||
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_light.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"version",
|
||||
"name",
|
||||
"type",
|
||||
"status",
|
||||
"plot_glyph",
|
||||
"intensity",
|
||||
"location",
|
||||
"target",
|
||||
"attenuation_type",
|
||||
"use_attenuation_limits",
|
||||
"attenuation_start_limits",
|
||||
"attenuation_end_limits",
|
||||
"hotspot_angle",
|
||||
"falloff_angle",
|
||||
"cast_shadows",
|
||||
"shadow_type",
|
||||
"shadow_map_size",
|
||||
"shadow_map_softness",
|
||||
],
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> Light:
|
||||
"""Transform the LIGHT entity by transformation matrix `m` inplace."""
|
||||
self.dxf.location = m.transform(self.dxf.location)
|
||||
self.dxf.target = m.transform(self.dxf.target)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
@@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
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 DXF12, SUBCLASS_MARKER
|
||||
from ezdxf.math import Vec3, Matrix44, NULLVEC, Z_AXIS, OCS
|
||||
from ezdxf.math.transformtools import (
|
||||
transform_thickness_and_extrusion_without_ocs,
|
||||
)
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity, acdb_entity_group_codes
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["Line"]
|
||||
|
||||
acdb_line = DefSubclass(
|
||||
"AcDbLine",
|
||||
{
|
||||
"start": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
"end": DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
|
||||
"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_line_group_codes = group_code_mapping(acdb_line)
|
||||
merged_line_group_codes = merge_group_code_mappings(
|
||||
acdb_entity_group_codes, acdb_line_group_codes # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Line(DXFGraphic):
|
||||
"""The LINE entity represents a 3D line from `start` to `end`"""
|
||||
|
||||
DXFTYPE = "LINE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_line)
|
||||
|
||||
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_line_group_codes)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags. (internal API)"""
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_line.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"start",
|
||||
"end",
|
||||
"thickness",
|
||||
"extrusion",
|
||||
],
|
||||
)
|
||||
|
||||
def ocs(self) -> OCS:
|
||||
# WCS entity which supports the "extrusion" attribute in a
|
||||
# different way!
|
||||
return OCS()
|
||||
|
||||
def transform(self, m: Matrix44) -> Line:
|
||||
"""Transform the LINE entity by transformation matrix `m` inplace."""
|
||||
start, end = m.transform_vertices([self.dxf.start, self.dxf.end])
|
||||
self.dxf.start = start
|
||||
self.dxf.end = end
|
||||
transform_thickness_and_extrusion_without_ocs(self, m)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def translate(self, dx: float, dy: float, dz: float) -> Line:
|
||||
"""Optimized LINE translation about `dx` in x-axis, `dy` in y-axis and
|
||||
`dz` in z-axis.
|
||||
|
||||
"""
|
||||
vec = Vec3(dx, dy, dz)
|
||||
self.dxf.start = vec + self.dxf.start
|
||||
self.dxf.end = vec + self.dxf.end
|
||||
# Avoid Matrix44 instantiation if not required:
|
||||
if self.is_post_transform_required:
|
||||
self.post_transform(Matrix44.translate(dx, dy, dz))
|
||||
return self
|
||||
@@ -0,0 +1,270 @@
|
||||
# Copyright (c) 2019-2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
Iterable,
|
||||
Sequence,
|
||||
Optional,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
from copy import deepcopy
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER
|
||||
from ezdxf.lldxf.types import DXFTag
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
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 ezdxf.tools.complex_ltype import lin_compiler
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["Linetype", "compile_line_pattern", "CONTINUOUS_PATTERN"]
|
||||
|
||||
acdb_linetype = DefSubclass(
|
||||
"AcDbLinetypeTableRecord",
|
||||
{
|
||||
"name": DXFAttr(2, validator=is_valid_table_name),
|
||||
"description": DXFAttr(3, default=""),
|
||||
"flags": DXFAttr(70, default=0),
|
||||
# 'length': DXFAttr(40),
|
||||
# 'items': DXFAttr(73),
|
||||
},
|
||||
)
|
||||
acdb_linetype_group_codes = group_code_mapping(acdb_linetype)
|
||||
CONTINUOUS_PATTERN: Sequence[float] = tuple()
|
||||
|
||||
|
||||
class LinetypePattern:
|
||||
def __init__(self, tags: Tags):
|
||||
"""For now just store tags"""
|
||||
self.tags = tags
|
||||
|
||||
def __len__(self):
|
||||
return len(self.tags)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter):
|
||||
if tagwriter.dxfversion <= DXF12:
|
||||
self.export_r12_dxf(tagwriter)
|
||||
else:
|
||||
tagwriter.write_tags(self.tags)
|
||||
|
||||
def export_r12_dxf(self, tagwriter: AbstractTagWriter):
|
||||
tags49 = Tags(tag for tag in self.tags if tag.code == 49)
|
||||
tagwriter.write_tag2(72, 65)
|
||||
tagwriter.write_tag2(73, len(tags49))
|
||||
tagwriter.write_tag(self.tags.get_first_tag(40))
|
||||
if len(tags49):
|
||||
tagwriter.write_tags(tags49)
|
||||
|
||||
def is_complex_type(self):
|
||||
return self.tags.has_tag(340)
|
||||
|
||||
def get_style_handle(self):
|
||||
return self.tags.get_first_value(340, "0")
|
||||
|
||||
def set_style_handle(self, handle):
|
||||
return self.tags.update(DXFTag(340, handle))
|
||||
|
||||
def compile(self) -> Sequence[float]:
|
||||
"""Returns the simplified dash-gap-dash... line pattern,
|
||||
a dash-length of 0 represents a point.
|
||||
"""
|
||||
# complex line types with text and shapes are not supported
|
||||
if self.is_complex_type():
|
||||
return CONTINUOUS_PATTERN
|
||||
|
||||
pattern_length = 0.0
|
||||
elements = []
|
||||
for tag in self.tags:
|
||||
if tag.code == 40:
|
||||
pattern_length = tag.value
|
||||
elif tag.code == 49:
|
||||
elements.append(tag.value)
|
||||
|
||||
if len(elements) < 2:
|
||||
return CONTINUOUS_PATTERN
|
||||
return compile_line_pattern(pattern_length, elements)
|
||||
|
||||
|
||||
def _merge_dashes(elements: Sequence[float]) -> Iterable[float]:
|
||||
"""Merge multiple consecutive lines, gaps or points into a single element."""
|
||||
|
||||
def sign(v):
|
||||
if v < 0:
|
||||
return -1
|
||||
elif v > 0:
|
||||
return +1
|
||||
return 0
|
||||
|
||||
buffer = elements[0]
|
||||
prev_sign = sign(buffer)
|
||||
for e in elements[1:]:
|
||||
if sign(e) == prev_sign:
|
||||
buffer += e
|
||||
else:
|
||||
yield buffer
|
||||
buffer = e
|
||||
prev_sign = sign(e)
|
||||
yield buffer
|
||||
|
||||
|
||||
def compile_line_pattern(
|
||||
total_length: Optional[float], elements: Sequence[float]
|
||||
) -> Sequence[float]:
|
||||
"""Returns the simplified dash-gap-dash... line pattern,
|
||||
a dash-length of 0 represents a point.
|
||||
"""
|
||||
elements = list(_merge_dashes(elements))
|
||||
if total_length is None:
|
||||
pass
|
||||
elif len(elements) < 2 or total_length <= 0.0:
|
||||
return CONTINUOUS_PATTERN
|
||||
|
||||
sum_elements = sum(abs(e) for e in elements)
|
||||
if total_length and total_length > sum_elements: # append a gap
|
||||
elements.append(sum_elements - total_length)
|
||||
|
||||
if elements[0] < 0: # start with a gap
|
||||
e = elements.pop(0)
|
||||
if elements[-1] < 0: # extend last gap
|
||||
elements[-1] += e
|
||||
else: # add last gap
|
||||
elements.append(e)
|
||||
# returns dash-gap-point
|
||||
# possible: dash-point or point-dash - ignore this yet
|
||||
# never: dash-dash or gap-gap or point-point
|
||||
return tuple(abs(e) for e in elements)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Linetype(DXFEntity):
|
||||
"""DXF LTYPE entity"""
|
||||
|
||||
DXFTYPE = "LTYPE"
|
||||
DXFATTRIBS = DXFAttributes(
|
||||
base_class, acdb_symbol_table_record, acdb_linetype
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
"""Default constructor"""
|
||||
super().__init__()
|
||||
self.pattern_tags = LinetypePattern(Tags())
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy pattern_tags."""
|
||||
assert isinstance(entity, Linetype)
|
||||
entity.pattern_tags = deepcopy(self.pattern_tags)
|
||||
|
||||
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_linetype_group_codes, 2, log=False
|
||||
)
|
||||
self.pattern_tags = LinetypePattern(tags)
|
||||
return dxf
|
||||
|
||||
def preprocess_export(self, tagwriter: AbstractTagWriter):
|
||||
if len(self.pattern_tags) == 0:
|
||||
return False
|
||||
# Do not export complex linetypes for DXF12
|
||||
if tagwriter.dxfversion == DXF12:
|
||||
return not self.pattern_tags.is_complex_type()
|
||||
return True
|
||||
|
||||
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_linetype.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["name", "flags", "description"])
|
||||
if self.pattern_tags:
|
||||
self.pattern_tags.export_dxf(tagwriter)
|
||||
|
||||
def setup_pattern(
|
||||
self, pattern: Union[Sequence[float], str], length: float = 0
|
||||
) -> None:
|
||||
# The new() function gets no doc reference, therefore complex linetype
|
||||
# setup has to be done later. See also: LinetypeTable.new_entry()
|
||||
complex_line_type = True if isinstance(pattern, str) else False
|
||||
if complex_line_type: # a .lin like line type definition string
|
||||
tags = self._setup_complex_pattern(pattern, length) # type: ignore
|
||||
else:
|
||||
# pattern: [2.0, 1.25, -0.25, 0.25, -0.25] - 1. element is total
|
||||
# pattern length pattern elements: >0 line, <0 gap, =0 point
|
||||
tags = Tags(
|
||||
[
|
||||
DXFTag(72, 65), # letter 'A'
|
||||
DXFTag(73, len(pattern) - 1),
|
||||
DXFTag(40, float(pattern[0])),
|
||||
]
|
||||
)
|
||||
for element in pattern[1:]:
|
||||
tags.append(DXFTag(49, float(element)))
|
||||
tags.append(DXFTag(74, 0))
|
||||
self.pattern_tags = LinetypePattern(tags)
|
||||
|
||||
def _setup_complex_pattern(self, pattern: str, length: float) -> Tags:
|
||||
tokens = lin_compiler(pattern)
|
||||
tags = Tags(
|
||||
[
|
||||
DXFTag(72, 65), # letter 'A'
|
||||
]
|
||||
)
|
||||
|
||||
tags2 = [DXFTag(73, 0), DXFTag(40, length)] # temp length of 0
|
||||
count = 0
|
||||
for token in tokens:
|
||||
if isinstance(token, DXFTag):
|
||||
if tags2[-1].code == 49: # useless 74 only after 49 :))
|
||||
tags2.append(DXFTag(74, 0))
|
||||
tags2.append(token)
|
||||
count += 1
|
||||
else: # TEXT or SHAPE
|
||||
tags2.extend(token.complex_ltype_tags(self.doc))
|
||||
tags2.append(DXFTag(74, 0)) # useless 74 at the end :))
|
||||
tags2[0] = DXFTag(73, count)
|
||||
tags.extend(tags2)
|
||||
return tags
|
||||
|
||||
def simplified_line_pattern(self) -> Sequence[float]:
|
||||
"""Returns the simplified dash-gap-dash... line pattern,
|
||||
a dash-length of 0 represents a point. Complex line types including text
|
||||
or shapes are not supported and return a continuous line pattern.
|
||||
"""
|
||||
return self.pattern_tags.compile()
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
assert self.doc is not None, "LTYPE entity must be assigned to a document"
|
||||
super().register_resources(registry)
|
||||
# register text styles and shape files for complex linetypes
|
||||
style_handle = self.pattern_tags.get_style_handle()
|
||||
style = self.doc.styles.get_entry_by_handle(style_handle)
|
||||
if style is not None:
|
||||
registry.add_entity(style)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate registered resources from self to the copied entity."""
|
||||
assert isinstance(clone, Linetype)
|
||||
super().map_resources(clone, mapping)
|
||||
style_handle = self.pattern_tags.get_style_handle()
|
||||
if style_handle != "0":
|
||||
# map text style or shape file handle of complex linetype
|
||||
clone.pattern_tags.set_style_handle(mapping.get_handle(style_handle))
|
||||
@@ -0,0 +1,539 @@
|
||||
# Copyright (c) 2019-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Sequence,
|
||||
Iterable,
|
||||
Union,
|
||||
Iterator,
|
||||
Optional,
|
||||
)
|
||||
from typing_extensions import TypeAlias, Self
|
||||
import array
|
||||
import copy
|
||||
from contextlib import contextmanager
|
||||
from ezdxf.math import Vec3, Matrix44, Z_AXIS
|
||||
from ezdxf.math.transformtools import OCSTransform, NonUniformScalingError
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import (
|
||||
SUBCLASS_MARKER,
|
||||
DXF2000,
|
||||
LWPOLYLINE_CLOSED,
|
||||
DXFStructureError,
|
||||
)
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.lldxf.types import DXFTag, DXFVertex
|
||||
from ezdxf.lldxf.packedtags import VertexArray
|
||||
from ezdxf.render.polyline import virtual_lwpolyline_entities
|
||||
from ezdxf.explode import explode_entity
|
||||
from ezdxf.query import EntityQuery
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace, Line, Arc, DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.layouts import BaseLayout
|
||||
|
||||
__all__ = ["LWPolyline", "FORMAT_CODES"]
|
||||
|
||||
LWPointType: TypeAlias = Tuple[float, float, float, float, float]
|
||||
|
||||
FORMAT_CODES = frozenset("xysebv")
|
||||
DEFAULT_FORMAT = "xyseb"
|
||||
LWPOINTCODES = (10, 20, 40, 41, 42)
|
||||
|
||||
# Order does matter:
|
||||
# If tag 90 is not the first TAG, AutoCAD does not close the polyline, when the
|
||||
# `close` flag is set.
|
||||
acdb_lwpolyline = DefSubclass(
|
||||
"AcDbPolyline",
|
||||
{
|
||||
# Count always returns the actual length:
|
||||
"count": DXFAttr(90, xtype=XType.callback, getter="__len__"),
|
||||
# Elevation: OCS z-axis value for all vertices:
|
||||
"elevation": DXFAttr(38, default=0, optional=True),
|
||||
# Thickness can be negative!
|
||||
"thickness": DXFAttr(39, default=0, optional=True),
|
||||
# Flags:
|
||||
# 1 = Closed
|
||||
# 128 = Plinegen
|
||||
"flags": DXFAttr(70, default=0),
|
||||
# Const width: DXF reference error - AutoCAD uses just const width if not 0,
|
||||
# for all line segments.
|
||||
"const_width": DXFAttr(43, default=0, optional=True),
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 10, 20 : Vertex x, y
|
||||
# 91: vertex identifier ???
|
||||
# 40, 41, 42: start width, end width, bulge
|
||||
},
|
||||
)
|
||||
|
||||
acdb_lwpolyline_group_codes = group_code_mapping(acdb_lwpolyline)
|
||||
|
||||
|
||||
@register_entity
|
||||
class LWPolyline(DXFGraphic):
|
||||
"""DXF LWPOLYLINE entity"""
|
||||
|
||||
DXFTYPE = "LWPOLYLINE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_lwpolyline)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.lwpoints = LWPolylinePoints()
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy lwpoints."""
|
||||
assert isinstance(entity, LWPolyline)
|
||||
entity.lwpoints = copy.deepcopy(self.lwpoints)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
"""
|
||||
Adds subclass processing for AcDbPolyline, requires previous base class
|
||||
and AcDbEntity processing by parent class.
|
||||
"""
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.subclass_by_index(2)
|
||||
if tags:
|
||||
tags = self.load_vertices(tags)
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf,
|
||||
acdb_lwpolyline_group_codes,
|
||||
subclass=tags,
|
||||
recover=True,
|
||||
)
|
||||
else:
|
||||
raise DXFStructureError(
|
||||
f"missing 'AcDbPolyline' subclass in LWPOLYLINE(#{dxf.handle})"
|
||||
)
|
||||
return dxf
|
||||
|
||||
def load_vertices(self, tags: Tags) -> Tags:
|
||||
self.lwpoints, unprocessed_tags = LWPolylinePoints.from_tags(tags)
|
||||
return unprocessed_tags
|
||||
|
||||
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
|
||||
# Returns True if entity should be exported
|
||||
# Do not export polylines without vertices
|
||||
return len(self.lwpoints) > 0
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_lwpolyline.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
["count", "flags", "const_width", "elevation", "thickness"],
|
||||
)
|
||||
tagwriter.write_tags(Tags(self.lwpoints.dxftags()))
|
||||
self.dxf.export_dxf_attribs(tagwriter, "extrusion")
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
"""Get/set closed state of polyline. A closed polyline has a connection
|
||||
segment from the last vertex to the first vertex.
|
||||
"""
|
||||
return self.get_flag_state(LWPOLYLINE_CLOSED)
|
||||
|
||||
@closed.setter
|
||||
def closed(self, status: bool) -> None:
|
||||
self.set_flag_state(LWPOLYLINE_CLOSED, status)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""Get closed state of LWPOLYLINE.
|
||||
Compatibility interface to :class:`Polyline`
|
||||
"""
|
||||
return self.get_flag_state(LWPOLYLINE_CLOSED)
|
||||
|
||||
def close(self, state: bool = True) -> None:
|
||||
"""Set closed state of LWPOLYLINE.
|
||||
Compatibility interface to :class:`Polyline`
|
||||
"""
|
||||
self.closed = state
|
||||
|
||||
@property
|
||||
def has_arc(self) -> bool:
|
||||
"""Returns ``True`` if LWPOLYLINE has an arc segment."""
|
||||
return any(bool(b) for x, y, s, e, b in self.lwpoints)
|
||||
|
||||
@property
|
||||
def has_width(self) -> bool:
|
||||
"""Returns ``True`` if LWPOLYLINE has any segment with width attributes
|
||||
or the DXF attribute const_width is not 0.
|
||||
|
||||
"""
|
||||
if self.dxf.hasattr("const_width"):
|
||||
# 'const_width' overrides all individual start- or end width settings.
|
||||
# The DXF reference claims the opposite, but that is simply not true.
|
||||
return self.dxf.const_width != 0.0
|
||||
return any((s or e) for x, y, s, e, b in self.lwpoints)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Returns count of polyline points."""
|
||||
return len(self.lwpoints)
|
||||
|
||||
def __iter__(self) -> Iterator[LWPointType]:
|
||||
"""Returns iterable of tuples (x, y, start_width, end_width, bulge)."""
|
||||
return iter(self.lwpoints)
|
||||
|
||||
def __getitem__(self, index: int) -> LWPointType:
|
||||
"""Returns point at position `index` as (x, y, start_width, end_width,
|
||||
bulge) tuple. start_width, end_width and bulge is 0 if not present,
|
||||
supports extended slicing. Point format is fixed as "xyseb".
|
||||
|
||||
All coordinates in :ref:`OCS`.
|
||||
|
||||
"""
|
||||
return self.lwpoints[index]
|
||||
|
||||
def __setitem__(self, index: int, value: Sequence[float]) -> None:
|
||||
"""
|
||||
Set point at position `index` as (x, y, [start_width, [end_width,
|
||||
[bulge]]]) tuple. If start_width or end_width is 0 or left off the
|
||||
default width value is used. If the bulge value is left off, bulge is 0
|
||||
by default (straight line).
|
||||
Does NOT support extend slicing. Point format is fixed as "xyseb".
|
||||
|
||||
All coordinates in :ref:`OCS`.
|
||||
|
||||
Args:
|
||||
index: point index
|
||||
value: point value as (x, y, [start_width, [end_width, [bulge]]]) tuple
|
||||
|
||||
"""
|
||||
self.lwpoints[index] = compile_array(value)
|
||||
|
||||
def __delitem__(self, index: int) -> None:
|
||||
"""Delete point at position `index`, supports extended slicing."""
|
||||
del self.lwpoints[index]
|
||||
|
||||
def vertices(self) -> Iterator[tuple[float, float]]:
|
||||
"""
|
||||
Returns iterable of all polyline points as (x, y) tuples in :ref:`OCS`
|
||||
(:attr:`dxf.elevation` is the z-axis value).
|
||||
|
||||
"""
|
||||
for point in self:
|
||||
yield point[0], point[1]
|
||||
|
||||
def vertices_in_wcs(self) -> Iterator[Vec3]:
|
||||
"""Returns iterable of all polyline points as Vec3(x, y, z) in :ref:`WCS`."""
|
||||
ocs = self.ocs()
|
||||
elevation = self.get_dxf_attrib("elevation", default=0.0)
|
||||
for x, y in self.vertices():
|
||||
yield ocs.to_wcs(Vec3(x, y, elevation))
|
||||
|
||||
def vertices_in_ocs(self) -> Iterator[Vec3]:
|
||||
"""Returns iterable of all polyline points as Vec3(x, y, z) in :ref:`OCS`."""
|
||||
elevation = self.get_dxf_attrib("elevation", default=0.0)
|
||||
for x, y in self.vertices():
|
||||
yield Vec3(x, y, elevation)
|
||||
|
||||
def append(self, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
|
||||
"""Append `point` to polyline, `format` specifies a user defined
|
||||
point format.
|
||||
|
||||
All coordinates in :ref:`OCS`.
|
||||
|
||||
Args:
|
||||
point: (x, y, [start_width, [end_width, [bulge]]]) tuple
|
||||
format: format string, default is "xyseb", see: `format codes`_
|
||||
|
||||
"""
|
||||
self.lwpoints.append(point, format=format)
|
||||
|
||||
def insert(
|
||||
self, pos: int, point: Sequence[float], format: str = DEFAULT_FORMAT
|
||||
) -> None:
|
||||
"""Insert new point in front of positions `pos`, `format` specifies a
|
||||
user defined point format.
|
||||
|
||||
All coordinates in :ref:`OCS`.
|
||||
|
||||
Args:
|
||||
pos: insert position
|
||||
point: point data
|
||||
format: format string, default is "xyseb", see: `format codes`_
|
||||
|
||||
"""
|
||||
data = compile_array(point, format=format)
|
||||
self.lwpoints.insert(pos, data)
|
||||
|
||||
def append_points(
|
||||
self, points: Iterable[Sequence[float]], format: str = DEFAULT_FORMAT
|
||||
) -> None:
|
||||
"""
|
||||
Append new `points` to polyline, `format` specifies a user defined
|
||||
point format.
|
||||
|
||||
All coordinates in :ref:`OCS`.
|
||||
|
||||
Args:
|
||||
points: iterable of point, point is (x, y, [start_width, [end_width,
|
||||
[bulge]]]) tuple
|
||||
format: format string, default is "xyseb", see: `format codes`_
|
||||
|
||||
"""
|
||||
for point in points:
|
||||
self.lwpoints.append(point, format=format)
|
||||
|
||||
@contextmanager
|
||||
def points(self, format: str = DEFAULT_FORMAT) -> Iterator[list[Sequence[float]]]:
|
||||
"""Context manager for polyline points. Returns a standard Python list
|
||||
of points, according to the format string.
|
||||
|
||||
All coordinates in :ref:`OCS`.
|
||||
|
||||
Args:
|
||||
format: format string, see `format codes`_
|
||||
|
||||
"""
|
||||
points = self.get_points(format=format)
|
||||
yield points
|
||||
self.set_points(points, format=format)
|
||||
|
||||
def get_points(self, format: str = DEFAULT_FORMAT) -> list[Sequence[float]]:
|
||||
"""Returns all points as list of tuples, format specifies a user
|
||||
defined point format.
|
||||
|
||||
All points in :ref:`OCS` as (x, y) tuples (:attr:`dxf.elevation` is
|
||||
the z-axis value).
|
||||
|
||||
Args:
|
||||
format: format string, default is "xyseb", see `format codes`_
|
||||
|
||||
"""
|
||||
return [format_point(p, format=format) for p in self.lwpoints]
|
||||
|
||||
def set_points(
|
||||
self, points: Iterable[Sequence[float]], format: str = DEFAULT_FORMAT
|
||||
) -> None:
|
||||
"""Remove all points and append new `points`.
|
||||
|
||||
All coordinates in :ref:`OCS`.
|
||||
|
||||
Args:
|
||||
points: iterable of point, point is (x, y, [start_width, [end_width,
|
||||
[bulge]]]) tuple
|
||||
format: format string, default is "xyseb", see `format codes`_
|
||||
|
||||
"""
|
||||
self.lwpoints.clear()
|
||||
self.append_points(points, format=format)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Remove all points."""
|
||||
self.lwpoints.clear()
|
||||
|
||||
def transform(self, m: Matrix44) -> LWPolyline:
|
||||
"""Transform the LWPOLYLINE entity by transformation matrix `m` inplace.
|
||||
|
||||
A non-uniform scaling is not supported if the entity contains circular
|
||||
arc segments (bulges).
|
||||
|
||||
Args:
|
||||
m: transformation :class:`~ezdxf.math.Matrix44`
|
||||
|
||||
Raises:
|
||||
NonUniformScalingError: for non-uniform scaling of entity containing
|
||||
circular arc segments (bulges)
|
||||
|
||||
"""
|
||||
dxf = self.dxf
|
||||
ocs = OCSTransform(self.dxf.extrusion, m)
|
||||
if not ocs.scale_uniform and self.has_arc:
|
||||
raise NonUniformScalingError(
|
||||
"LWPOLYLINE containing arcs (bulges) does not support non uniform scaling"
|
||||
)
|
||||
# The caller function has to catch this exception and explode the
|
||||
# LWPOLYLINE into LINE and ELLIPSE entities.
|
||||
vertices = list(ocs.transform_vertex(v) for v in self.vertices_in_ocs())
|
||||
lwpoints = []
|
||||
for v, p in zip(vertices, self.lwpoints):
|
||||
_, _, start_width, end_width, bulge = p
|
||||
# assume a uniform scaling!
|
||||
start_width = ocs.transform_width(start_width)
|
||||
end_width = ocs.transform_width(end_width)
|
||||
lwpoints.append((v.x, v.y, start_width, end_width, bulge))
|
||||
self.set_points(lwpoints)
|
||||
|
||||
# All new OCS vertices must have the same z-axis, which is the elevation
|
||||
# of the polyline:
|
||||
if vertices:
|
||||
dxf.elevation = vertices[0].z
|
||||
|
||||
if dxf.hasattr("const_width"): # assume a uniform scaling!
|
||||
dxf.const_width = ocs.transform_width(dxf.const_width)
|
||||
|
||||
if dxf.hasattr("thickness"):
|
||||
dxf.thickness = ocs.transform_thickness(dxf.thickness)
|
||||
dxf.extrusion = ocs.new_extrusion
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def virtual_entities(self) -> Iterator[Union[Line, Arc]]:
|
||||
"""Yields the graphical representation of LWPOLYLINE as virtual DXF
|
||||
primitives (LINE or ARC).
|
||||
|
||||
These virtual entities are located at the original location, but are not
|
||||
stored in the entity database, have no handle and are not assigned to
|
||||
any layout.
|
||||
|
||||
"""
|
||||
for e in virtual_lwpolyline_entities(self):
|
||||
e.set_source_of_copy(self)
|
||||
yield e
|
||||
|
||||
def explode(self, target_layout: Optional[BaseLayout] = None) -> EntityQuery:
|
||||
"""Explode the LWPOLYLINE entity as DXF primitives (LINE or ARC) into
|
||||
the target layout, if the target layout is ``None``, the target layout
|
||||
is the layout of the source entity. This method destroys the source entity.
|
||||
|
||||
Returns an :class:`~ezdxf.query.EntityQuery` container referencing all DXF
|
||||
primitives.
|
||||
|
||||
Args:
|
||||
target_layout: target layout for the DXF primitives, ``None`` for
|
||||
same layout as the source entity.
|
||||
|
||||
"""
|
||||
return explode_entity(self, target_layout)
|
||||
|
||||
|
||||
class LWPolylinePoints(VertexArray):
|
||||
__slots__ = ("values",)
|
||||
VERTEX_CODE = 10
|
||||
START_WIDTH_CODE = 40
|
||||
END_WIDTH_CODE = 41
|
||||
BULGE_CODE = 42
|
||||
VERTEX_SIZE = 5
|
||||
|
||||
@classmethod
|
||||
def from_tags(cls, tags: Iterable[DXFTag]) -> tuple[Self, Tags]: # type: ignore
|
||||
"""Setup point array from tags."""
|
||||
|
||||
def build_vertex(point: list[float]) -> list[float]:
|
||||
point.append(attribs.get(cls.START_WIDTH_CODE, 0))
|
||||
point.append(attribs.get(cls.END_WIDTH_CODE, 0))
|
||||
point.append(attribs.get(cls.BULGE_CODE, 0))
|
||||
return point
|
||||
|
||||
unprocessed_tags = Tags()
|
||||
vertices: list[Sequence[float]]= []
|
||||
point: list[float] | None = None
|
||||
attribs: dict[int, float]= {}
|
||||
for tag in tags:
|
||||
if tag.code in LWPOINTCODES:
|
||||
if tag.code == 10:
|
||||
if point is not None:
|
||||
vertices.append(build_vertex(point))
|
||||
# just use x- and y-axis
|
||||
point = list(tag.value[0:2])
|
||||
attribs = {}
|
||||
else:
|
||||
attribs[tag.code] = tag.value
|
||||
else:
|
||||
unprocessed_tags.append(tag)
|
||||
if point is not None:
|
||||
vertices.append(build_vertex(point))
|
||||
return cls(data=vertices), unprocessed_tags
|
||||
|
||||
def append(self, point: Sequence[float], format: str = DEFAULT_FORMAT) -> None:
|
||||
super().append(compile_array(point, format=format))
|
||||
|
||||
def dxftags(self) -> Iterator[DXFTag]:
|
||||
for point in self:
|
||||
x, y, start_width, end_width, bulge = point
|
||||
yield DXFVertex(self.VERTEX_CODE, (x, y))
|
||||
if start_width or end_width:
|
||||
# Export always start- and end width together,
|
||||
# required for BricsCAD but not AutoCAD!
|
||||
yield DXFTag(self.START_WIDTH_CODE, start_width)
|
||||
yield DXFTag(self.END_WIDTH_CODE, end_width)
|
||||
if bulge:
|
||||
yield DXFTag(self.BULGE_CODE, bulge)
|
||||
|
||||
|
||||
def format_point(point: Sequence[float], format: str = "xyseb") -> Sequence[float]:
|
||||
"""Reformat point components.
|
||||
|
||||
Format codes:
|
||||
|
||||
- ``x`` = x-coordinate
|
||||
- ``y`` = y-coordinate
|
||||
- ``s`` = start width
|
||||
- ``e`` = end width
|
||||
- ``b`` = bulge value
|
||||
- ``v`` = (x, y) as tuple
|
||||
|
||||
Args:
|
||||
point: list or tuple of (x, y, start_width, end_width, bulge)
|
||||
format: format string, default is "xyseb"
|
||||
|
||||
Returns:
|
||||
Sequence[float]: tuple of selected components
|
||||
|
||||
"""
|
||||
x, y, s, e, b = point
|
||||
v = (x, y)
|
||||
vars = locals()
|
||||
return tuple(vars[code] for code in format.lower() if code in FORMAT_CODES)
|
||||
|
||||
|
||||
def compile_array(data: Sequence[float], format="xyseb") -> array.array:
|
||||
"""Gather point components from input data.
|
||||
|
||||
Format codes:
|
||||
|
||||
- ``x`` = x-coordinate
|
||||
- ``y`` = y-coordinate
|
||||
- ``s`` = start width
|
||||
- ``e`` = end width
|
||||
- ``b`` = bulge value
|
||||
- ``v`` = (x, y [,z]) tuple (z-axis is ignored)
|
||||
|
||||
Args:
|
||||
data: list or tuple of point components
|
||||
format: format string, default is "xyseb"
|
||||
|
||||
Returns:
|
||||
array.array: array.array('d', (x, y, start_width, end_width, bulge))
|
||||
|
||||
"""
|
||||
a = array.array("d", (0.0, 0.0, 0.0, 0.0, 0.0))
|
||||
format = [code for code in format.lower() if code in FORMAT_CODES]
|
||||
for code, value in zip(format, data):
|
||||
if code not in FORMAT_CODES:
|
||||
continue
|
||||
if code == "v":
|
||||
vertex = Vec3(value)
|
||||
a[0] = vertex.x
|
||||
a[1] = vertex.y
|
||||
else:
|
||||
a["xyseb".index(code)] = value
|
||||
return a
|
||||
@@ -0,0 +1,432 @@
|
||||
# Copyright (c) 2018-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing_extensions import Self
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfobj import DXFObject
|
||||
from .factory import register_entity
|
||||
from .objectcollection import ObjectCollection
|
||||
from ezdxf.math import Matrix44
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.document import Drawing
|
||||
|
||||
__all__ = ["Material", "MaterialCollection"]
|
||||
|
||||
|
||||
def fetch_matrix(tags: Tags, code: int) -> tuple[Tags, Optional[Matrix44]]:
|
||||
values = []
|
||||
remaining = Tags()
|
||||
for tag in tags:
|
||||
if tag.code == code:
|
||||
values.append(tag.value)
|
||||
if len(values) == 16:
|
||||
# enough values collected, code 43 is maybe used for two matrices
|
||||
code = -1
|
||||
else:
|
||||
remaining.append(tag)
|
||||
if len(values) == 16:
|
||||
# only if valid matrix
|
||||
return remaining, Matrix44(values)
|
||||
else:
|
||||
return tags, None
|
||||
|
||||
|
||||
def export_matrix(tagwriter: AbstractTagWriter, code: int, matrix: Matrix44) -> None:
|
||||
if matrix is not None:
|
||||
for value in matrix:
|
||||
tagwriter.write_tag2(code, value)
|
||||
|
||||
|
||||
acdb_material = DefSubclass(
|
||||
"AcDbMaterial",
|
||||
{
|
||||
"name": DXFAttr(1),
|
||||
"description": DXFAttr(2, default=""),
|
||||
"ambient_color_method": DXFAttr(
|
||||
70, default=0
|
||||
), # 0=use current color; 1=override current color
|
||||
"ambient_color_factor": DXFAttr(40, default=1.0), # valid range is 0.0 to 1.0
|
||||
"ambient_color_value": DXFAttr(90), # integer representing an AcCmEntityColor
|
||||
"diffuse_color_method": DXFAttr(
|
||||
71, default=0
|
||||
), # 0=use current color; 1=override current color
|
||||
"diffuse_color_factor": DXFAttr(41, default=1.0), # valid range is 0.0 to 1.0
|
||||
"diffuse_color_value": DXFAttr(
|
||||
91, default=-1023410177
|
||||
), # integer representing an AcCmEntityColor
|
||||
"diffuse_map_blend_factor": DXFAttr(
|
||||
42, default=1.0
|
||||
), # valid range is 0.0 to 1.0
|
||||
"diffuse_map_source": DXFAttr(72, default=1),
|
||||
# 0=use current scene; 1=use image file (specified by file name; null file name specifies no map)
|
||||
"diffuse_map_file_name": DXFAttr(3, default=""),
|
||||
"diffuse_map_projection_method": DXFAttr(
|
||||
73, default=1
|
||||
), # 1=Planar; 2=Box; 3=Cylinder; 4=Sphere
|
||||
"diffuse_map_tiling_method": DXFAttr(74, default=1), # 1=Tile; 2=Crop; 3=Clamp
|
||||
"diffuse_map_auto_transform_method": DXFAttr(75, default=1), # bitset;
|
||||
# 1 = No auto transform
|
||||
# 2 = Scale mapper to current entity extents; translate mapper to entity origin
|
||||
# 4 = Include current block transform in mapper transform
|
||||
# 16x group code 43: Transform matrix of diffuse map mapper (16 reals; row major format; default = identity matrix)
|
||||
"specular_gloss_factor": DXFAttr(44, default=0.5), # valid range is 0.0 to 1.0
|
||||
"specular_color_method": DXFAttr(
|
||||
73, default=0
|
||||
), # 0=use current color; 1=override current color
|
||||
"specular_color_factor": DXFAttr(45, default=1.0), # valid range is 0.0 to 1.0
|
||||
"specular_color_value": DXFAttr(92), # integer representing an AcCmEntityColor
|
||||
"specular_map_blend_factor": DXFAttr(
|
||||
46, default=1.0
|
||||
), # valid range is 0.0 to 1.0
|
||||
"specular_map_source": DXFAttr(77, default=1),
|
||||
# 0=use current scene; 1=use image file (specified by file name; null file name specifies no map)
|
||||
"specular_map_file_name": DXFAttr(4, default=""),
|
||||
"specular_map_projection_method": DXFAttr(
|
||||
78, default=1
|
||||
), # 1=Planar; 2=Box; 3=Cylinder; 4=Sphere
|
||||
"specular_map_tiling_method": DXFAttr(79, default=1), # 1=Tile; 2=Crop; 3=Clamp
|
||||
"specular_map_auto_transform_method": DXFAttr(170, default=1), # bitset;
|
||||
# 1 = No auto transform
|
||||
# 2 = Scale mapper to current entity extents; translate mapper to entity origin
|
||||
# 4 = Include current block transform in mapper transform
|
||||
# 16x group code 47: Transform matrix of specular map mapper (16 reals; row major format; default = identity matrix)
|
||||
"reflection_map_blend_factor": DXFAttr(
|
||||
48, default=1.0
|
||||
), # valid range is 0.0 to 1.0
|
||||
"reflection_map_source": DXFAttr(171, default=1),
|
||||
# 0=use current scene; 1=use image file (specified by file name; null file name specifies no map)
|
||||
"reflection_map_file_name": DXFAttr(6, default=""),
|
||||
"reflection_map_projection_method": DXFAttr(
|
||||
172, default=1
|
||||
), # 1=Planar; 2=Box; 3=Cylinder; 4=Sphere
|
||||
"reflection_map_tiling_method": DXFAttr(
|
||||
173, default=1
|
||||
), # 1=Tile; 2=Crop; 3=Clamp
|
||||
"reflection_map_auto_transform_method": DXFAttr(174, default=1), # bitset;
|
||||
# 1 = No auto transform
|
||||
# 2 = Scale mapper to current entity extents; translate mapper to entity origin
|
||||
# 4 = Include current block transform in mapper transform
|
||||
# 16x group code 49: Transform matrix of reflection map mapper (16 reals; row major format; default = identity matrix)
|
||||
"opacity": DXFAttr(140, default=1.0), # valid range is 0.0 to 1.0
|
||||
"opacity_map_blend_factor": DXFAttr(
|
||||
141, default=1.0
|
||||
), # valid range is 0.0 to 1.0
|
||||
"opacity_map_source": DXFAttr(175, default=1),
|
||||
# 0=use current scene; 1=use image file (specified by file name; null file name specifies no map)
|
||||
"opacity_map_file_name": DXFAttr(7, default=""),
|
||||
"opacity_map_projection_method": DXFAttr(
|
||||
176, default=1
|
||||
), # 1=Planar; 2=Box; 3=Cylinder; 4=Sphere
|
||||
"opacity_map_tiling_method": DXFAttr(177, default=1), # 1=Tile; 2=Crop; 3=Clamp
|
||||
"opacity_map_auto_transform_method": DXFAttr(178, default=1), # bitset;
|
||||
# 1 = No auto transform
|
||||
# 2 = Scale mapper to current entity extents; translate mapper to entity origin
|
||||
# 4 = Include current block transform in mapper transform
|
||||
# 16x group code 142: Transform matrix of reflection map mapper (16 reals; row major format; default = identity matrix)
|
||||
"bump_map_blend_factor": DXFAttr(143, default=1.0), # valid range is 0.0 to 1.0
|
||||
"bump_map_source": DXFAttr(179, default=1),
|
||||
# 0=use current scene; 1=use image file (specified by file name; null file name specifies no map)
|
||||
"bump_map_file_name": DXFAttr(8, default=""),
|
||||
"bump_map_projection_method": DXFAttr(
|
||||
270, default=1
|
||||
), # 1=Planar; 2=Box; 3=Cylinder; 4=Sphere
|
||||
"bump_map_tiling_method": DXFAttr(271, default=1), # 1=Tile; 2=Crop; 3=Clamp
|
||||
"bump_map_auto_transform_method": DXFAttr(272, default=1), # bitset;
|
||||
# 1 = No auto transform
|
||||
# 2 = Scale mapper to current entity extents; translate mapper to entity origin
|
||||
# 4 = Include current block transform in mapper transform
|
||||
# 16x group code 144: Transform matrix of bump map mapper (16 reals; row major format; default = identity matrix)
|
||||
"refraction_index": DXFAttr(145, default=1.0), # valid range is 0.0 to 1.0
|
||||
"refraction_map_blend_factor": DXFAttr(
|
||||
146, default=1.0
|
||||
), # valid range is 0.0 to 1.0
|
||||
"refraction_map_source": DXFAttr(273, default=1),
|
||||
# 0=use current scene; 1=use image file (specified by file name; null file name specifies no map)
|
||||
"refraction_map_file_name": DXFAttr(9, default=""),
|
||||
"refraction_map_projection_method": DXFAttr(
|
||||
274, default=1
|
||||
), # 1=Planar; 2=Box; 3=Cylinder; 4=Sphere
|
||||
"refraction_map_tiling_method": DXFAttr(
|
||||
275, default=1
|
||||
), # 1=Tile; 2=Crop; 3=Clamp
|
||||
"refraction_map_auto_transform_method": DXFAttr(276, default=1), # bitset;
|
||||
# 1 = No auto transform
|
||||
# 2 = Scale mapper to current entity extents; translate mapper to entity origin
|
||||
# 4 = Include current block transform in mapper transform
|
||||
# 16x group code 147: Transform matrix of reflection map mapper (16 reals; row major format; default = identity matrix)
|
||||
# normal map shares group codes with diffuse map
|
||||
"normal_map_method": DXFAttr(271),
|
||||
"normal_map_strength": DXFAttr(465),
|
||||
"normal_map_blend_factor": DXFAttr(
|
||||
42, default=1.0
|
||||
), # valid range is 0.0 to 1.0
|
||||
"normal_map_source": DXFAttr(72, default=1),
|
||||
# 0=use current scene; 1=use image file (specified by file name; null file name specifies no map)
|
||||
"normal_map_file_name": DXFAttr(3, default=""),
|
||||
"normal_map_projection_method": DXFAttr(
|
||||
73, default=1
|
||||
), # 1=Planar; 2=Box; 3=Cylinder; 4=Sphere
|
||||
"normal_map_tiling_method": DXFAttr(74, default=1), # 1=Tile; 2=Crop; 3=Clamp
|
||||
"normal_map_auto_transform_method": DXFAttr(75, default=1), # bitset;
|
||||
# 1 = No auto transform
|
||||
# 2 = Scale mapper to current entity extents; translate mapper to entity origin
|
||||
# 4 = Include current block transform in mapper transform
|
||||
# 16x group code 43: Transform matrix of reflection map mapper (16 reals; row major format; default = identity matrix)
|
||||
"color_bleed_scale": DXFAttr(460),
|
||||
"indirect_dump_scale": DXFAttr(461),
|
||||
"reflectance_scale": DXFAttr(462),
|
||||
"transmittance_scale": DXFAttr(463),
|
||||
"two_sided_material": DXFAttr(290),
|
||||
"luminance": DXFAttr(464),
|
||||
"luminance_mode": DXFAttr(270), # multiple usage of group code 270
|
||||
"materials_anonymous": DXFAttr(293),
|
||||
"global_illumination_mode": DXFAttr(272),
|
||||
"final_gather_mode": DXFAttr(273),
|
||||
"gen_proc_name": DXFAttr(300),
|
||||
"gen_proc_val_bool": DXFAttr(291),
|
||||
"gen_proc_val_int": DXFAttr(271),
|
||||
"gen_proc_val_real": DXFAttr(469),
|
||||
"gen_proc_val_text": DXFAttr(301),
|
||||
"gen_proc_table_end": DXFAttr(292),
|
||||
"gen_proc_val_color_index": DXFAttr(62),
|
||||
"gen_proc_val_color_rgb": DXFAttr(420),
|
||||
"gen_proc_val_color_name": DXFAttr(430),
|
||||
"map_utile": DXFAttr(270), # multiple usage of group code 270
|
||||
"translucence": DXFAttr(148),
|
||||
"self_illumination": DXFAttr(90),
|
||||
"reflectivity": DXFAttr(468),
|
||||
"illumination_model": DXFAttr(93),
|
||||
"channel_flags": DXFAttr(94, default=63),
|
||||
},
|
||||
)
|
||||
acdb_material_group_codes = group_code_mapping(acdb_material)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Material(DXFObject):
|
||||
DXFTYPE = "MATERIAL"
|
||||
DEFAULT_ATTRIBS = {
|
||||
"diffuse_color_method": 1,
|
||||
"diffuse_color_value": -1023410177,
|
||||
}
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_material)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.diffuse_mapper_matrix: Optional[Matrix44] = None # code 43
|
||||
self.specular_mapper_matrix: Optional[Matrix44] = None # code 47
|
||||
self.reflexion_mapper_matrix: Optional[Matrix44] = None # code 49
|
||||
self.opacity_mapper_matrix: Optional[Matrix44] = None # group 142
|
||||
self.bump_mapper_matrix: Optional[Matrix44] = None # group 144
|
||||
self.refraction_mapper_matrix: Optional[Matrix44] = None # code 147
|
||||
self.normal_mapper_matrix: Optional[Matrix44] = None # code 43 ???
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy material mapper matrices"""
|
||||
|
||||
def copy(matrix):
|
||||
return None if matrix is None else matrix.copy()
|
||||
|
||||
assert isinstance(entity, Material)
|
||||
entity.diffuse_mapper_matrix = copy(self.diffuse_mapper_matrix)
|
||||
entity.specular_mapper_matrix = copy(self.specular_mapper_matrix)
|
||||
entity.reflexion_mapper_matrix = copy(self.reflexion_mapper_matrix)
|
||||
entity.opacity_mapper_matrix = copy(self.opacity_mapper_matrix)
|
||||
entity.bump_mapper_matrix = copy(self.bump_mapper_matrix)
|
||||
entity.refraction_mapper_matrix = copy(self.refraction_mapper_matrix)
|
||||
entity.normal_mapper_matrix = copy(self.normal_mapper_matrix)
|
||||
|
||||
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_material_group_codes, 1, log=False
|
||||
)
|
||||
self.load_matrices(tags)
|
||||
return dxf
|
||||
|
||||
def load_matrices(self, tags):
|
||||
tags, matrix = fetch_matrix(tags, 43)
|
||||
if matrix:
|
||||
self.diffuse_mapper_matrix = matrix
|
||||
tags, matrix = fetch_matrix(tags, 47)
|
||||
if matrix:
|
||||
self.specular_mapper_matrix = matrix
|
||||
tags, matrix = fetch_matrix(tags, 49)
|
||||
if matrix:
|
||||
self.reflexion_mapper_matrix = matrix
|
||||
tags, matrix = fetch_matrix(tags, 142)
|
||||
if matrix:
|
||||
self.opacity_mapper_matrix = matrix
|
||||
tags, matrix = fetch_matrix(tags, 144)
|
||||
if matrix:
|
||||
self.bump_mapper_matrix = matrix
|
||||
tags, matrix = fetch_matrix(tags, 147)
|
||||
if matrix:
|
||||
self.refraction_mapper_matrix = matrix
|
||||
tags, matrix = fetch_matrix(tags, 43)
|
||||
if matrix:
|
||||
self.normal_mapper_matrix = matrix
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_material.name)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"description",
|
||||
"ambient_color_method",
|
||||
"ambient_color_factor",
|
||||
"ambient_color_value",
|
||||
"diffuse_color_method",
|
||||
"diffuse_color_factor",
|
||||
"diffuse_color_value",
|
||||
"diffuse_map_blend_factor",
|
||||
"diffuse_map_source",
|
||||
"diffuse_map_file_name",
|
||||
"diffuse_map_projection_method",
|
||||
"diffuse_map_tiling_method",
|
||||
"diffuse_map_auto_transform_method",
|
||||
],
|
||||
)
|
||||
export_matrix(tagwriter, 43, self.diffuse_mapper_matrix)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"specular_gloss_factor",
|
||||
"specular_color_method",
|
||||
"specular_color_factor",
|
||||
"specular_color_value",
|
||||
"specular_map_blend_factor",
|
||||
"specular_map_source",
|
||||
"specular_map_file_name",
|
||||
"specular_map_projection_method",
|
||||
"specular_map_tiling_method",
|
||||
"specular_map_auto_transform_method",
|
||||
],
|
||||
)
|
||||
export_matrix(tagwriter, 47, self.specular_mapper_matrix)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"reflection_map_blend_factor",
|
||||
"reflection_map_source",
|
||||
"reflection_map_file_name",
|
||||
"reflection_map_projection_method",
|
||||
"reflection_map_tiling_method",
|
||||
"reflection_map_auto_transform_method",
|
||||
],
|
||||
)
|
||||
export_matrix(tagwriter, 49, self.reflexion_mapper_matrix)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"opacity",
|
||||
"opacity_map_blend_factor",
|
||||
"opacity_map_source",
|
||||
"opacity_map_file_name",
|
||||
"opacity_map_projection_method",
|
||||
"opacity_map_tiling_method",
|
||||
"opacity_map_auto_transform_method",
|
||||
],
|
||||
)
|
||||
export_matrix(tagwriter, 142, self.opacity_mapper_matrix)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"bump_map_blend_factor",
|
||||
"bump_map_source",
|
||||
"bump_map_file_name",
|
||||
"bump_map_projection_method",
|
||||
"bump_map_tiling_method",
|
||||
"bump_map_auto_transform_method",
|
||||
],
|
||||
)
|
||||
export_matrix(tagwriter, 144, self.bump_mapper_matrix)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"refraction_index",
|
||||
"refraction_map_blend_factor",
|
||||
"refraction_map_source",
|
||||
"refraction_map_file_name",
|
||||
"refraction_map_projection_method",
|
||||
"refraction_map_tiling_method",
|
||||
"refraction_map_auto_transform_method",
|
||||
],
|
||||
)
|
||||
export_matrix(tagwriter, 147, self.refraction_mapper_matrix)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"normal_map_method",
|
||||
"normal_map_strength",
|
||||
"normal_map_blend_factor",
|
||||
"normal_map_source",
|
||||
"normal_map_file_name",
|
||||
"normal_map_projection_method",
|
||||
"normal_map_tiling_method",
|
||||
"normal_map_auto_transform_method",
|
||||
],
|
||||
)
|
||||
export_matrix(tagwriter, 43, self.normal_mapper_matrix)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"color_bleed_scale",
|
||||
"indirect_dump_scale",
|
||||
"reflectance_scale",
|
||||
"transmittance_scale",
|
||||
"two_sided_material",
|
||||
"luminance",
|
||||
"luminance_mode",
|
||||
"materials_anonymous",
|
||||
"global_illumination_mode",
|
||||
"final_gather_mode",
|
||||
"gen_proc_name",
|
||||
"gen_proc_val_bool",
|
||||
"gen_proc_val_int",
|
||||
"gen_proc_val_real",
|
||||
"gen_proc_val_text",
|
||||
"gen_proc_table_end",
|
||||
"gen_proc_val_color_index",
|
||||
"gen_proc_val_color_rgb",
|
||||
"gen_proc_val_color_name",
|
||||
"map_utile",
|
||||
"translucence",
|
||||
"self_illumination",
|
||||
"reflectivity",
|
||||
"illumination_model",
|
||||
"channel_flags",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class MaterialCollection(ObjectCollection[Material]):
|
||||
def __init__(self, doc: Drawing):
|
||||
super().__init__(doc, dict_name="ACAD_MATERIAL", object_type="MATERIAL")
|
||||
self.create_required_entries()
|
||||
|
||||
def create_required_entries(self) -> None:
|
||||
for name in ("ByBlock", "ByLayer", "Global"):
|
||||
if name not in self:
|
||||
self.new(name)
|
||||
@@ -0,0 +1,509 @@
|
||||
# Copyright (c) 2019-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Sequence,
|
||||
Union,
|
||||
Iterator,
|
||||
Optional,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
import array
|
||||
import copy
|
||||
from itertools import chain
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ezdxf.audit import AuditError
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import (
|
||||
SUBCLASS_MARKER,
|
||||
DXF2000,
|
||||
DXFValueError,
|
||||
DXFStructureError,
|
||||
DXFIndexError,
|
||||
)
|
||||
from ezdxf.lldxf.packedtags import VertexArray, TagArray, TagList
|
||||
from ezdxf.math import Matrix44, UVec, Vec3
|
||||
from ezdxf.tools import take2
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.audit import Auditor
|
||||
|
||||
__all__ = ["Mesh", "MeshData"]
|
||||
|
||||
acdb_mesh = DefSubclass(
|
||||
"AcDbSubDMesh",
|
||||
{
|
||||
"version": DXFAttr(71, default=2),
|
||||
"blend_crease": DXFAttr(
|
||||
72,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 0 is no smoothing
|
||||
"subdivision_levels": DXFAttr(
|
||||
91,
|
||||
default=0,
|
||||
validator=validator.is_greater_or_equal_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 92: Vertex count of level 0
|
||||
# 10: Vertex position, multiple entries
|
||||
# 93: Size of face list of level 0
|
||||
# 90: Face list item, >=3 possible
|
||||
# 90: length of face list
|
||||
# 90: 1st vertex index
|
||||
# 90: 2nd vertex index ...
|
||||
# 94: Edge count of level 0
|
||||
# 90: Vertex index of 1st edge
|
||||
# 90: Vertex index of 2nd edge
|
||||
# 95: Edge crease count of level 0
|
||||
# 95 same as 94, or how is the 'edge create value' associated to edge index
|
||||
# 140: Edge crease value
|
||||
#
|
||||
# Overriding properties: how does this work?
|
||||
# 90: Count of sub-entity which property has been overridden
|
||||
# 91: Sub-entity marker
|
||||
# 92: Count of property was overridden
|
||||
# 90: Property type
|
||||
# 0 = Color
|
||||
# 1 = Material
|
||||
# 2 = Transparency
|
||||
# 3 = Material mapper
|
||||
},
|
||||
)
|
||||
acdb_mesh_group_codes = group_code_mapping(acdb_mesh)
|
||||
|
||||
|
||||
class EdgeArray(TagArray):
|
||||
DTYPE = "L"
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.values) // 2
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[int, int]]:
|
||||
for edge in take2(self.values):
|
||||
yield edge
|
||||
|
||||
def set_data(self, edges: Iterable[tuple[int, int]]) -> None:
|
||||
self.values = array.array(self.DTYPE, chain.from_iterable(edges))
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter):
|
||||
# count = count of edges not tags!
|
||||
tagwriter.write_tag2(94, len(self.values) // 2)
|
||||
for index in self.values:
|
||||
tagwriter.write_tag2(90, index)
|
||||
|
||||
|
||||
class FaceList(TagList):
|
||||
def __len__(self) -> int:
|
||||
return len(self.values)
|
||||
|
||||
def __iter__(self) -> Iterable[array.array]:
|
||||
return iter(self.values)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter):
|
||||
# count = count of tags not faces!
|
||||
tagwriter.write_tag2(93, self.tag_count())
|
||||
for face in self.values:
|
||||
tagwriter.write_tag2(90, len(face))
|
||||
for index in face:
|
||||
tagwriter.write_tag2(90, index)
|
||||
|
||||
def tag_count(self) -> int:
|
||||
return len(self.values) + sum(len(f) for f in self.values)
|
||||
|
||||
def set_data(self, faces: Iterable[Sequence[int]]) -> None:
|
||||
_faces = []
|
||||
for face in faces:
|
||||
_faces.append(face_to_array(face))
|
||||
self.values = _faces
|
||||
|
||||
|
||||
def face_to_array(face: Sequence[int]) -> array.array:
|
||||
max_index = max(face)
|
||||
if max_index < 256:
|
||||
dtype = "B"
|
||||
elif max_index < 65536:
|
||||
dtype = "I"
|
||||
else:
|
||||
dtype = "L"
|
||||
return array.array(dtype, face)
|
||||
|
||||
|
||||
def create_vertex_array(tags: Tags, start_index: int) -> VertexArray:
|
||||
vertex_tags = tags.collect_consecutive_tags(codes=(10,), start=start_index)
|
||||
return VertexArray(data=[t.value for t in vertex_tags])
|
||||
|
||||
|
||||
def create_face_list(tags: Tags, start_index: int) -> FaceList:
|
||||
faces = FaceList()
|
||||
faces_list = faces.values
|
||||
face: list[int] = []
|
||||
counter = 0
|
||||
for tag in tags.collect_consecutive_tags(codes=(90,), start=start_index):
|
||||
if not counter:
|
||||
# leading counter tag
|
||||
counter = tag.value
|
||||
if face:
|
||||
# group code 90 = 32 bit integer
|
||||
faces_list.append(face_to_array(face))
|
||||
face = []
|
||||
else:
|
||||
# followed by count face tags
|
||||
counter -= 1
|
||||
face.append(tag.value)
|
||||
|
||||
# add last face
|
||||
if face:
|
||||
# group code 90 = 32 bit integer
|
||||
faces_list.append(face_to_array(face))
|
||||
|
||||
return faces
|
||||
|
||||
|
||||
def create_edge_array(tags: Tags, start_index: int) -> EdgeArray:
|
||||
return EdgeArray(data=collect_values(tags, start_index, code=90)) # int values
|
||||
|
||||
|
||||
def collect_values(
|
||||
tags: Tags, start_index: int, code: int
|
||||
) -> Iterable[Union[float, int]]:
|
||||
values = tags.collect_consecutive_tags(codes=(code,), start=start_index)
|
||||
return (t.value for t in values)
|
||||
|
||||
|
||||
def create_crease_array(tags: Tags, start_index: int) -> array.array:
|
||||
return array.array("f", collect_values(tags, start_index, code=140)) # float values
|
||||
|
||||
|
||||
COUNT_ERROR_MSG = "'MESH (#{}) without {} count.'"
|
||||
|
||||
|
||||
@register_entity
|
||||
class Mesh(DXFGraphic):
|
||||
"""DXF MESH entity"""
|
||||
|
||||
DXFTYPE = "MESH"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mesh)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._vertices = VertexArray() # vertices stored as array.array('d')
|
||||
self._faces = FaceList() # face lists data
|
||||
self._edges = EdgeArray() # edge indices stored as array.array('L')
|
||||
self._creases = array.array("f") # creases stored as array.array('f')
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy data: vertices, faces, edges, creases."""
|
||||
assert isinstance(entity, Mesh)
|
||||
entity._vertices = copy.deepcopy(self._vertices)
|
||||
entity._faces = copy.deepcopy(self._faces)
|
||||
entity._edges = copy.deepcopy(self._edges)
|
||||
entity._creases = copy.deepcopy(self._creases)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.subclass_by_index(2)
|
||||
if tags:
|
||||
# Load mesh data and remove their tags from subclass
|
||||
self.load_mesh_data(tags, dxf.handle)
|
||||
# Load remaining data into name space
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_mesh_group_codes, 2, recover=True
|
||||
)
|
||||
else:
|
||||
raise DXFStructureError(
|
||||
f"missing 'AcDbSubMesh' subclass in MESH(#{dxf.handle})"
|
||||
)
|
||||
return dxf
|
||||
|
||||
def load_mesh_data(self, mesh_tags: Tags, handle: str) -> None:
|
||||
def process_vertices():
|
||||
try:
|
||||
vertex_count_index = mesh_tags.tag_index(92)
|
||||
except DXFValueError:
|
||||
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "vertex"))
|
||||
vertices = create_vertex_array(mesh_tags, vertex_count_index + 1)
|
||||
# Remove vertex count tag and all vertex tags
|
||||
end_index = vertex_count_index + 1 + len(vertices)
|
||||
del mesh_tags[vertex_count_index:end_index]
|
||||
return vertices
|
||||
|
||||
def process_faces():
|
||||
try:
|
||||
face_count_index = mesh_tags.tag_index(93)
|
||||
except DXFValueError:
|
||||
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "face"))
|
||||
else:
|
||||
# Remove face count tag and all face tags
|
||||
faces = create_face_list(mesh_tags, face_count_index + 1)
|
||||
end_index = face_count_index + 1 + faces.tag_count()
|
||||
del mesh_tags[face_count_index:end_index]
|
||||
return faces
|
||||
|
||||
def process_edges():
|
||||
try:
|
||||
edge_count_index = mesh_tags.tag_index(94)
|
||||
except DXFValueError:
|
||||
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "edge"))
|
||||
else:
|
||||
edges = create_edge_array(mesh_tags, edge_count_index + 1)
|
||||
# Remove edge count tag and all edge tags
|
||||
end_index = edge_count_index + 1 + len(edges.values)
|
||||
del mesh_tags[edge_count_index:end_index]
|
||||
return edges
|
||||
|
||||
def process_creases():
|
||||
try:
|
||||
crease_count_index = mesh_tags.tag_index(95)
|
||||
except DXFValueError:
|
||||
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "crease"))
|
||||
else:
|
||||
creases = create_crease_array(mesh_tags, crease_count_index + 1)
|
||||
# Remove crease count tag and all crease tags
|
||||
end_index = crease_count_index + 1 + len(creases)
|
||||
del mesh_tags[crease_count_index:end_index]
|
||||
return creases
|
||||
|
||||
self._vertices = process_vertices()
|
||||
self._faces = process_faces()
|
||||
self._edges = process_edges()
|
||||
self._creases = process_creases()
|
||||
|
||||
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
|
||||
"""Pre requirement check and pre-processing for export.
|
||||
|
||||
Returns ``False`` if entity should not be exported at all.
|
||||
|
||||
- MESH without vertices creates an invalid DXF file for AutoCAD and BricsCAD.
|
||||
- MESH without faces creates an invalid DXF file for AutoCAD and BricsCAD.
|
||||
|
||||
(internal API)
|
||||
"""
|
||||
if len(self.vertices) == 0 or len(self.faces) == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_mesh.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter, ["version", "blend_crease", "subdivision_levels"]
|
||||
)
|
||||
self.export_mesh_data(tagwriter)
|
||||
self.export_override_data(tagwriter)
|
||||
|
||||
def export_mesh_data(self, tagwriter: AbstractTagWriter):
|
||||
tagwriter.write_tag2(92, len(self.vertices))
|
||||
self._vertices.export_dxf(tagwriter, code=10)
|
||||
self._faces.export_dxf(tagwriter)
|
||||
self._edges.export_dxf(tagwriter)
|
||||
|
||||
creases = self._fixed_crease_values()
|
||||
tagwriter.write_tag2(95, len(self.creases))
|
||||
for crease_value in creases:
|
||||
tagwriter.write_tag2(140, crease_value)
|
||||
|
||||
def _fixed_crease_values(self) -> list[float]:
|
||||
# The edge count has to match the crease count, otherwise its an invalid
|
||||
# DXF file to AutoCAD!
|
||||
edge_count = len(self._edges)
|
||||
creases = list(self.creases)
|
||||
crease_count = len(creases)
|
||||
if edge_count < crease_count:
|
||||
creases = creases[:edge_count]
|
||||
while edge_count > len(creases):
|
||||
creases.append(0.0)
|
||||
return creases
|
||||
|
||||
def export_override_data(self, tagwriter: AbstractTagWriter):
|
||||
tagwriter.write_tag2(90, 0)
|
||||
|
||||
@property
|
||||
def creases(self) -> array.array:
|
||||
"""Creases as :class:`array.array`. (read/write)"""
|
||||
return self._creases
|
||||
|
||||
@creases.setter
|
||||
def creases(self, values: Iterable[float]) -> None:
|
||||
self._creases = array.array("f", values)
|
||||
|
||||
@property
|
||||
def vertices(self):
|
||||
"""Vertices as list like :class:`~ezdxf.lldxf.packedtags.VertexArray`.
|
||||
(read/write)
|
||||
"""
|
||||
return self._vertices
|
||||
|
||||
@vertices.setter
|
||||
def vertices(self, points: Iterable[UVec]) -> None:
|
||||
self._vertices = VertexArray(points)
|
||||
|
||||
@property
|
||||
def edges(self):
|
||||
"""Edges as list like :class:`~ezdxf.lldxf.packedtags.TagArray`.
|
||||
(read/write)
|
||||
"""
|
||||
return self._edges
|
||||
|
||||
@edges.setter
|
||||
def edges(self, edges: Iterable[tuple[int, int]]) -> None:
|
||||
self._edges.set_data(edges)
|
||||
|
||||
@property
|
||||
def faces(self):
|
||||
"""Faces as list like :class:`~ezdxf.lldxf.packedtags.TagList`.
|
||||
(read/write)
|
||||
"""
|
||||
return self._faces
|
||||
|
||||
@faces.setter
|
||||
def faces(self, faces: Iterable[Sequence[int]]) -> None:
|
||||
self._faces.set_data(faces)
|
||||
|
||||
def get_data(self) -> MeshData:
|
||||
return MeshData(self)
|
||||
|
||||
def set_data(self, data: MeshData) -> None:
|
||||
self.vertices = data.vertices
|
||||
self._faces.set_data(data.faces)
|
||||
self._edges.set_data(data.edges)
|
||||
self.creases = array.array("f", data.edge_crease_values)
|
||||
if len(self.edges) != len(self.creases):
|
||||
raise DXFValueError("count of edges must match count of creases")
|
||||
|
||||
@contextmanager
|
||||
def edit_data(self) -> Iterator[MeshData]:
|
||||
"""Context manager for various mesh data, returns a :class:`MeshData` instance.
|
||||
|
||||
Despite that vertices, edge and faces are accessible as packed data types, the
|
||||
usage of :class:`MeshData` by context manager :meth:`edit_data` is still
|
||||
recommended.
|
||||
"""
|
||||
data = self.get_data()
|
||||
yield data
|
||||
self.set_data(data)
|
||||
|
||||
def transform(self, m: Matrix44) -> Mesh:
|
||||
"""Transform the MESH entity by transformation matrix `m` inplace."""
|
||||
self._vertices.transform(m)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
if not self.is_alive:
|
||||
return
|
||||
super().audit(auditor)
|
||||
if len(self.edges) != len(self.creases):
|
||||
self.creases = self._fixed_crease_values() # type: ignore
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_CREASE_VALUE_COUNT,
|
||||
message=f"fixed invalid count of crease values in {str(self)}",
|
||||
dxf_entity=self,
|
||||
)
|
||||
if len(self.vertices) == 0 or len(self.faces) == 0:
|
||||
# A MESH without vertices or vertices creates an invalid DXF file for
|
||||
# AutoCAD and BricsCAD
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_VERTEX_COUNT,
|
||||
message=f"removed {str(self)} without vertices or faces",
|
||||
)
|
||||
auditor.trash(self)
|
||||
|
||||
|
||||
class MeshData:
|
||||
def __init__(self, mesh: Mesh) -> None:
|
||||
self.vertices: list[Vec3] = Vec3.list(mesh.vertices)
|
||||
self.faces: list[Sequence[int]] = list(mesh.faces)
|
||||
self.edges: list[tuple[int, int]] = list(mesh.edges)
|
||||
self.edge_crease_values: list[float] = list(mesh.creases)
|
||||
|
||||
def add_face(self, vertices: Iterable[UVec]) -> Sequence[int]:
|
||||
"""Add a face by a list of vertices."""
|
||||
indices = tuple(self.add_vertex(vertex) for vertex in vertices)
|
||||
self.faces.append(indices)
|
||||
return indices
|
||||
|
||||
def add_edge_crease(self, v1: int, v2: int, crease: float):
|
||||
"""Add an edge crease value, the edge is defined by the vertex indices
|
||||
`v1` and `v2`.
|
||||
|
||||
The crease value defines the amount of subdivision that will be applied
|
||||
to this edge. A crease value of the subdivision level prevents the edge from
|
||||
deformation and a value of 0.0 means no protection from subdividing.
|
||||
|
||||
"""
|
||||
if v1 < 0 or v1 > len(self.vertices):
|
||||
raise DXFIndexError("vertex index `v1` out of range")
|
||||
if v2 < 0 or v2 > len(self.vertices):
|
||||
raise DXFIndexError("vertex index `v2` out of range")
|
||||
self.edges.append((v1, v2))
|
||||
self.edge_crease_values.append(crease)
|
||||
|
||||
def add_vertex(self, vertex: UVec) -> int:
|
||||
if len(vertex) != 3:
|
||||
raise DXFValueError("Parameter vertex has to be a 3-tuple (x, y, z).")
|
||||
index = len(self.vertices)
|
||||
self.vertices.append(Vec3(vertex))
|
||||
return index
|
||||
|
||||
def optimize(self):
|
||||
"""Reduce vertex count by merging coincident vertices."""
|
||||
|
||||
def merge_coincident_vertices() -> dict[int, int]:
|
||||
original_vertices = [
|
||||
(v.xyz, index, v) for index, v in enumerate(self.vertices)
|
||||
]
|
||||
original_vertices.sort()
|
||||
self.vertices = []
|
||||
index_map: dict[int, int] = {}
|
||||
prev_vertex = sentinel = Vec3()
|
||||
index = 0
|
||||
for _, original_index, vertex in original_vertices:
|
||||
if prev_vertex is sentinel or not vertex.isclose(prev_vertex):
|
||||
index = len(self.vertices)
|
||||
self.vertices.append(vertex)
|
||||
index_map[original_index] = index
|
||||
prev_vertex = vertex
|
||||
else: # this is a coincident vertex
|
||||
index_map[original_index] = index
|
||||
return index_map
|
||||
|
||||
def remap_faces() -> None:
|
||||
self.faces = remap_indices(self.faces)
|
||||
|
||||
def remap_edges() -> None:
|
||||
self.edges = remap_indices(self.edges) # type: ignore
|
||||
|
||||
def remap_indices(indices: Sequence[Sequence[int]]) -> list[Sequence[int]]:
|
||||
mapped_indices: list[Sequence[int]] = []
|
||||
for entry in indices:
|
||||
mapped_indices.append(tuple(index_map[index] for index in entry))
|
||||
return mapped_indices
|
||||
|
||||
index_map = merge_coincident_vertices()
|
||||
remap_faces()
|
||||
remap_edges()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,967 @@
|
||||
# Copyright (c) 2018-2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Optional,
|
||||
Sequence,
|
||||
Iterator,
|
||||
)
|
||||
from typing_extensions import Self
|
||||
|
||||
from collections import OrderedDict, namedtuple
|
||||
import math
|
||||
|
||||
from ezdxf.audit import AuditError
|
||||
from ezdxf.entities.factory import register_entity
|
||||
from ezdxf.lldxf import const, validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.tags import Tags, group_tags
|
||||
from ezdxf.math import NULLVEC, X_AXIS, Y_AXIS, Z_AXIS, UVec, Vec3, UCS, OCS
|
||||
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfobj import DXFObject
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .objectcollection import ObjectCollection
|
||||
from .copy import default_copy
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.audit import Auditor
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.layouts import BaseLayout
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.math import Matrix44
|
||||
from ezdxf.query import EntityQuery
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["MLine", "MLineVertex", "MLineStyle", "MLineStyleCollection"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
# Usage example: CADKitSamples\Lock-Off.dxf
|
||||
|
||||
|
||||
def filter_close_vertices(
|
||||
vertices: Iterable[Vec3], abs_tol: float = 1e-12
|
||||
) -> Iterable[Vec3]:
|
||||
prev = None
|
||||
for vertex in vertices:
|
||||
if prev is None:
|
||||
yield vertex
|
||||
prev = vertex
|
||||
else:
|
||||
if not vertex.isclose(prev, abs_tol=abs_tol):
|
||||
yield vertex
|
||||
prev = vertex
|
||||
|
||||
|
||||
acdb_mline = DefSubclass(
|
||||
"AcDbMline",
|
||||
OrderedDict(
|
||||
{
|
||||
"style_name": DXFAttr(2, default="Standard"),
|
||||
"style_handle": DXFAttr(340),
|
||||
"scale_factor": DXFAttr(
|
||||
40,
|
||||
default=1,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Justification
|
||||
# 0 = Top (Right)
|
||||
# 1 = Zero (Center)
|
||||
# 2 = Bottom (Left)
|
||||
"justification": DXFAttr(
|
||||
70,
|
||||
default=0,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Flags (bit-coded values):
|
||||
# 1 = Has at least one vertex (code 72 is greater than 0)
|
||||
# 2 = Closed
|
||||
# 4 = Suppress start caps
|
||||
# 8 = Suppress end caps
|
||||
"flags": DXFAttr(71, default=1),
|
||||
# Number of MLINE vertices
|
||||
"count": DXFAttr(72, xtype=XType.callback, getter="__len__"),
|
||||
# Number of elements in MLINESTYLE definition
|
||||
"style_element_count": DXFAttr(73, default=2),
|
||||
# start location in WCS!
|
||||
"start_location": DXFAttr(
|
||||
10, xtype=XType.callback, getter="start_location"
|
||||
),
|
||||
# Normal vector of the entity plane, but all vertices in WCS!
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# MLine data:
|
||||
# 11: vertex coordinates
|
||||
# Multiple entries; one entry for each vertex.
|
||||
# 12: Direction vector of segment starting at this vertex
|
||||
# Multiple entries; one for each vertex.
|
||||
# 13: Direction vector of miter at this vertex
|
||||
# Multiple entries: one for each vertex.
|
||||
# 74: Number of parameters for this element,
|
||||
# repeats for each element in segment
|
||||
# 41: Element parameters,
|
||||
# repeats based on previous code 74
|
||||
# 75: Number of area fill parameters for this element,
|
||||
# repeats for each element in segment
|
||||
# 42: Area fill parameters,
|
||||
# repeats based on previous code 75
|
||||
}
|
||||
),
|
||||
)
|
||||
acdb_mline_group_codes = group_code_mapping(acdb_mline)
|
||||
|
||||
|
||||
# For information about line- and fill parametrization see comments in class
|
||||
# MLineVertex().
|
||||
#
|
||||
# The 2 group codes in mline entities and mlinestyle objects are redundant
|
||||
# fields. These groups should not be modified under any circumstances, although
|
||||
# it is safe to read them and use their values. The correct fields to modify
|
||||
# are as follows:
|
||||
#
|
||||
# Mline
|
||||
# The 340 group in the same object, which indicates the proper MLINESTYLE
|
||||
# object.
|
||||
#
|
||||
# Mlinestyle
|
||||
# The 3 group value in the MLINESTYLE dictionary, which precedes the 350 group
|
||||
# that has the handle or entity name of
|
||||
# the current mlinestyle.
|
||||
|
||||
# Facts and assumptions not clearly defined by the DXF reference:
|
||||
# - the reference line is defined by the group code 11 points (fact)
|
||||
# - all line segments are parallel to the reference line (assumption)
|
||||
# - all line vertices are located in the same plane, the orientation of the plane
|
||||
# is defined by the extrusion vector (assumption)
|
||||
# - the scale factor is applied to to all geometries
|
||||
# - the start- and end angle (MLineStyle) is also applied to the first and last
|
||||
# miter direction vector
|
||||
# - the last two points mean: all geometries and direction vectors can be used
|
||||
# as stored in the DXF file no additional scaling or rotation is necessary
|
||||
# for the MLINE rendering. Disadvantage: minor changes of DXF attributes
|
||||
# require a refresh of the MLineVertices.
|
||||
|
||||
# Ezdxf does not support the creation of line-break (gap) features, but will be
|
||||
# preserving this data if the MLINE stays unchanged.
|
||||
# Editing the MLINE entity by ezdxf removes the line-break features (gaps).
|
||||
|
||||
|
||||
class MLineVertex:
|
||||
def __init__(self) -> None:
|
||||
self.location: Vec3 = NULLVEC
|
||||
self.line_direction: Vec3 = X_AXIS
|
||||
self.miter_direction: Vec3 = Y_AXIS
|
||||
|
||||
# Line parametrization (74/41)
|
||||
# ----------------------------
|
||||
# The line parameterization is a list of float values.
|
||||
# The list may contain zero or more items.
|
||||
#
|
||||
# The first value (miter-offset) is the distance from the vertex
|
||||
# location along the miter direction vector to the point where the
|
||||
# line element's path intersects the miter vector.
|
||||
#
|
||||
# The next value (line-start-offset) is the distance along the line
|
||||
# direction from the miter/line path intersection point to the actual
|
||||
# start of the line element.
|
||||
#
|
||||
# The next value (dash-length) is the distance from the start of the
|
||||
# line element (dash) to the first break (or gap) in the line element.
|
||||
# The successive values continue to list the start and stop points of
|
||||
# the line element in this segment of the mline.
|
||||
# Linetypes do not affect the line parametrization.
|
||||
#
|
||||
#
|
||||
# 1. line element: [miter-offset, line-start-offset, dash, gap, dash, ...]
|
||||
# 2. line element: [...]
|
||||
# ...
|
||||
self.line_params: list[Sequence[float]] = []
|
||||
""" The line parameterization is a list of float values.
|
||||
The list may contain zero or more items.
|
||||
"""
|
||||
|
||||
# Fill parametrization (75/42)
|
||||
# ----------------------------
|
||||
#
|
||||
# The fill parameterization is also a list of float values.
|
||||
# Similar to the line parametrization, it describes the
|
||||
# parametrization of the fill area for this mline segment.
|
||||
# The values are interpreted identically to the line parameters and when
|
||||
# taken as a whole for all line elements in the mline segment, they
|
||||
# define the boundary of the fill area for the mline segment.
|
||||
#
|
||||
# A common example of the use of the fill mechanism is when an
|
||||
# unfilled mline crosses over a filled mline and "mledit" is used to
|
||||
# cause the filled mline to appear unfilled in the crossing area.
|
||||
# This would result in two fill parameters for each line element in the
|
||||
# affected mline segment; one for the fill stop and one for the fill
|
||||
# start.
|
||||
#
|
||||
# [dash-length, gap-length, ...]?
|
||||
self.fill_params: list[Sequence[float]] = []
|
||||
|
||||
def __copy__(self) -> MLineVertex:
|
||||
vtx = self.__class__()
|
||||
vtx.location = self.location
|
||||
vtx.line_direction = self.line_direction
|
||||
vtx.miter_direction = self.miter_direction
|
||||
vtx.line_params = list(self.line_params)
|
||||
vtx.fill_params = list(self.fill_params)
|
||||
return vtx
|
||||
|
||||
copy = __copy__
|
||||
|
||||
@classmethod
|
||||
def load(cls, tags: Tags) -> MLineVertex:
|
||||
vtx = MLineVertex()
|
||||
line_params: list[float] = []
|
||||
line_params_count = 0
|
||||
fill_params: list[float] = []
|
||||
fill_params_count = 0
|
||||
for code, value in tags:
|
||||
if code == 11:
|
||||
vtx.location = Vec3(value)
|
||||
elif code == 12:
|
||||
vtx.line_direction = Vec3(value)
|
||||
elif code == 13:
|
||||
vtx.miter_direction = Vec3(value)
|
||||
elif code == 74:
|
||||
line_params_count = value
|
||||
if line_params_count == 0:
|
||||
vtx.line_params.append(tuple())
|
||||
else:
|
||||
line_params = []
|
||||
elif code == 41:
|
||||
line_params.append(value)
|
||||
line_params_count -= 1
|
||||
if line_params_count == 0:
|
||||
vtx.line_params.append(tuple(line_params))
|
||||
line_params = []
|
||||
elif code == 75:
|
||||
fill_params_count = value
|
||||
if fill_params_count == 0:
|
||||
vtx.fill_params.append(tuple())
|
||||
else:
|
||||
fill_params = []
|
||||
elif code == 42:
|
||||
fill_params.append(value)
|
||||
fill_params_count -= 1
|
||||
if fill_params_count == 0:
|
||||
vtx.fill_params.append(tuple(fill_params))
|
||||
return vtx
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter):
|
||||
tagwriter.write_vertex(11, self.location)
|
||||
tagwriter.write_vertex(12, self.line_direction)
|
||||
tagwriter.write_vertex(13, self.miter_direction)
|
||||
for line_params, fill_params in zip(self.line_params, self.fill_params):
|
||||
tagwriter.write_tag2(74, len(line_params))
|
||||
for param in line_params:
|
||||
tagwriter.write_tag2(41, param)
|
||||
tagwriter.write_tag2(75, len(fill_params))
|
||||
for param in fill_params:
|
||||
tagwriter.write_tag2(42, param)
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
start: UVec,
|
||||
line_direction: UVec,
|
||||
miter_direction: UVec,
|
||||
line_params: Optional[Iterable[Sequence[float]]] = None,
|
||||
fill_params: Optional[Iterable[Sequence[float]]] = None,
|
||||
) -> MLineVertex:
|
||||
vtx = MLineVertex()
|
||||
vtx.location = Vec3(start)
|
||||
vtx.line_direction = Vec3(line_direction)
|
||||
vtx.miter_direction = Vec3(miter_direction)
|
||||
vtx.line_params = list(line_params or [])
|
||||
vtx.fill_params = list(fill_params or [])
|
||||
if len(vtx.line_params) != len(vtx.fill_params):
|
||||
raise const.DXFValueError("Count mismatch of line- and fill parameters")
|
||||
return vtx
|
||||
|
||||
def transform(self, m: Matrix44) -> MLineVertex:
|
||||
"""Transform MLineVertex by transformation matrix `m` inplace."""
|
||||
self.location = m.transform(self.location)
|
||||
self.line_direction = m.transform_direction(self.line_direction)
|
||||
self.miter_direction = m.transform_direction(self.miter_direction)
|
||||
return self
|
||||
|
||||
|
||||
@register_entity
|
||||
class MLine(DXFGraphic):
|
||||
DXFTYPE = "MLINE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mline)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
||||
TOP = const.MLINE_TOP
|
||||
ZERO = const.MLINE_ZERO
|
||||
BOTTOM = const.MLINE_BOTTOM
|
||||
HAS_VERTICES = const.MLINE_HAS_VERTICES
|
||||
CLOSED = const.MLINE_CLOSED
|
||||
SUPPRESS_START_CAPS = const.MLINE_SUPPRESS_START_CAPS
|
||||
SUPPRESS_END_CAPS = const.MLINE_SUPPRESS_END_CAPS
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# The MLINE geometry stored in vertices, is the final geometry,
|
||||
# scaling factor, justification and MLineStyle settings are already
|
||||
# applied. This is why the geometry has to be updated every time a
|
||||
# change is applied.
|
||||
self.vertices: list[MLineVertex] = []
|
||||
|
||||
def __len__(self):
|
||||
"""Count of MLINE vertices."""
|
||||
return len(self.vertices)
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
assert isinstance(entity, MLine)
|
||||
entity.vertices = [v.copy() for v in self.vertices]
|
||||
|
||||
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_mline_group_codes, 2, log=False
|
||||
)
|
||||
self.load_vertices(tags)
|
||||
return dxf
|
||||
|
||||
def load_vertices(self, tags: Tags) -> None:
|
||||
self.vertices.extend(
|
||||
MLineVertex.load(tags) for tags in group_tags(tags, splitcode=11)
|
||||
)
|
||||
|
||||
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
|
||||
# Do not export MLines without vertices
|
||||
return len(self.vertices) > 1
|
||||
# todo: check if line- and fill parametrization is compatible with
|
||||
# MLINE style, requires same count of elements!
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
# ezdxf does not export MLINE entities without vertices,
|
||||
# see method preprocess_export()
|
||||
self.set_flag_state(self.HAS_VERTICES, True)
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_mline.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, acdb_mline.attribs.keys())
|
||||
self.export_vertices(tagwriter)
|
||||
|
||||
def export_vertices(self, tagwriter: AbstractTagWriter) -> None:
|
||||
for vertex in self.vertices:
|
||||
vertex.export_dxf(tagwriter)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
super().register_resources(registry)
|
||||
registry.add_handle(self.dxf.style_handle)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
super().map_resources(clone, mapping)
|
||||
style = mapping.get_reference_of_copy(self.dxf.style_handle)
|
||||
if not isinstance(style, MLineStyle):
|
||||
assert clone.doc is not None
|
||||
style = clone.doc.mline_styles.get("Standard")
|
||||
if isinstance(style, MLineStyle):
|
||||
clone.dxf.style_handle = style.dxf.handle
|
||||
clone.dxf.style_name = style.dxf.name
|
||||
else:
|
||||
clone.dxf.style_handle = "0"
|
||||
clone.dxf.style_name = "Standard"
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""Returns ``True`` if MLINE is closed.
|
||||
Compatibility interface to :class:`Polyline`
|
||||
"""
|
||||
return self.get_flag_state(self.CLOSED)
|
||||
|
||||
def close(self, state: bool = True) -> None:
|
||||
"""Get/set closed state of MLINE and update geometry accordingly.
|
||||
Compatibility interface to :class:`Polyline`
|
||||
"""
|
||||
state = bool(state)
|
||||
if state != self.is_closed:
|
||||
self.set_flag_state(self.CLOSED, state)
|
||||
self.update_geometry()
|
||||
|
||||
@property
|
||||
def start_caps(self) -> bool:
|
||||
"""Get/Set start caps state. ``True`` to enable start caps and
|
||||
``False`` tu suppress start caps."""
|
||||
return not self.get_flag_state(self.SUPPRESS_START_CAPS)
|
||||
|
||||
@start_caps.setter
|
||||
def start_caps(self, value: bool) -> None:
|
||||
"""Set start caps state."""
|
||||
self.set_flag_state(self.SUPPRESS_START_CAPS, not bool(value))
|
||||
|
||||
@property
|
||||
def end_caps(self) -> bool:
|
||||
"""Get/Set end caps state. ``True`` to enable end caps and
|
||||
``False`` tu suppress start caps."""
|
||||
return not self.get_flag_state(self.SUPPRESS_END_CAPS)
|
||||
|
||||
@end_caps.setter
|
||||
def end_caps(self, value: bool) -> None:
|
||||
"""Set start caps state."""
|
||||
self.set_flag_state(self.SUPPRESS_END_CAPS, not bool(value))
|
||||
|
||||
def set_scale_factor(self, value: float) -> None:
|
||||
"""Set the scale factor and update geometry accordingly."""
|
||||
value = float(value)
|
||||
if not math.isclose(self.dxf.scale_factor, value):
|
||||
self.dxf.scale_factor = value
|
||||
self.update_geometry()
|
||||
|
||||
def set_justification(self, value: int) -> None:
|
||||
"""Set MLINE justification and update geometry accordingly.
|
||||
See :attr:`dxf.justification` for valid settings.
|
||||
"""
|
||||
value = int(value)
|
||||
if self.dxf.justification != value:
|
||||
self.dxf.justification = value
|
||||
self.update_geometry()
|
||||
|
||||
@property
|
||||
def style(self) -> Optional[MLineStyle]:
|
||||
"""Get associated MLINESTYLE."""
|
||||
if self.doc is None:
|
||||
return None
|
||||
_style = self.doc.entitydb.get(self.dxf.style_handle)
|
||||
if _style is None:
|
||||
_style = self.doc.mline_styles.get(self.dxf.style_name)
|
||||
return _style # type: ignore
|
||||
|
||||
def set_style(self, name: str) -> None:
|
||||
"""Set MLINESTYLE by name and update geometry accordingly.
|
||||
The MLINESTYLE definition must exist.
|
||||
"""
|
||||
if self.doc is None:
|
||||
logger.debug("Can't change style of unbounded MLINE entity.")
|
||||
return
|
||||
try:
|
||||
style = self.doc.mline_styles[name]
|
||||
except const.DXFKeyError:
|
||||
raise const.DXFValueError(f"Undefined MLINE style: {name}")
|
||||
assert isinstance(style, MLineStyle)
|
||||
# Line- and fill parametrization depends on the count of
|
||||
# elements, a change in the number of elements triggers a
|
||||
# reset of the parametrization:
|
||||
old_style = self.style
|
||||
new_element_count = len(style.elements)
|
||||
reset = False
|
||||
if old_style is not None:
|
||||
# Do not trust the stored "style_element_count" value
|
||||
reset = len(old_style.elements) != new_element_count
|
||||
|
||||
self.dxf.style_name = name
|
||||
self.dxf.style_handle = style.dxf.handle
|
||||
self.dxf.style_element_count = new_element_count
|
||||
if reset:
|
||||
self.update_geometry()
|
||||
|
||||
def start_location(self) -> Vec3:
|
||||
"""Returns the start location of the reference line. Callback function
|
||||
for :attr:`dxf.start_location`.
|
||||
"""
|
||||
if len(self.vertices):
|
||||
return self.vertices[0].location
|
||||
else:
|
||||
return NULLVEC
|
||||
|
||||
def get_locations(self) -> list[Vec3]:
|
||||
"""Returns the vertices of the reference line."""
|
||||
return [v.location for v in self.vertices]
|
||||
|
||||
def extend(self, vertices: Iterable[UVec]) -> None:
|
||||
"""Append multiple vertices to the reference line.
|
||||
|
||||
It is possible to work with 3D vertices, but all vertices have to be in
|
||||
the same plane and the normal vector of this plan is stored as
|
||||
extrusion vector in the MLINE entity.
|
||||
|
||||
"""
|
||||
vertices = Vec3.list(vertices)
|
||||
if not vertices:
|
||||
return
|
||||
all_vertices = []
|
||||
if len(self):
|
||||
all_vertices.extend(self.get_locations())
|
||||
all_vertices.extend(vertices)
|
||||
self.generate_geometry(all_vertices)
|
||||
|
||||
def update_geometry(self) -> None:
|
||||
"""Regenerate the MLINE geometry based on current settings."""
|
||||
self.generate_geometry(self.get_locations())
|
||||
|
||||
def generate_geometry(self, vertices: list[Vec3]) -> None:
|
||||
"""Regenerate the MLINE geometry for new reference line defined by
|
||||
`vertices`.
|
||||
"""
|
||||
vertices = list(filter_close_vertices(vertices, abs_tol=1e-6))
|
||||
if len(vertices) == 0:
|
||||
self.clear()
|
||||
return
|
||||
elif len(vertices) == 1:
|
||||
self.vertices = [MLineVertex.new(vertices[0], X_AXIS, Y_AXIS)]
|
||||
return
|
||||
|
||||
style = self.style
|
||||
assert style is not None, "valid MLINE style required"
|
||||
if len(style.elements) == 0:
|
||||
raise const.DXFStructureError(f"No line elements defined in {str(style)}.")
|
||||
|
||||
def miter(dir1: Vec3, dir2: Vec3):
|
||||
return ((dir1 + dir2) * 0.5).normalize().orthogonal()
|
||||
|
||||
ucs = UCS.from_z_axis_and_point_in_xz(
|
||||
origin=vertices[0],
|
||||
point=vertices[1],
|
||||
axis=self.dxf.extrusion,
|
||||
)
|
||||
# Transform given vertices into UCS and project them into the
|
||||
# UCS-xy-plane by setting the z-axis to 0:
|
||||
vertices = [v.replace(z=0.0) for v in ucs.points_from_wcs(vertices)]
|
||||
start_angle = style.dxf.start_angle
|
||||
end_angle = style.dxf.end_angle
|
||||
|
||||
line_directions = [
|
||||
(v2 - v1).normalize() for v1, v2 in zip(vertices, vertices[1:])
|
||||
]
|
||||
|
||||
if self.is_closed:
|
||||
line_directions.append((vertices[0] - vertices[-1]).normalize())
|
||||
closing_miter = miter(line_directions[0], line_directions[-1])
|
||||
miter_directions = [closing_miter]
|
||||
else:
|
||||
closing_miter = None
|
||||
line_directions.append(line_directions[-1])
|
||||
miter_directions = [line_directions[0].rotate_deg(start_angle)]
|
||||
|
||||
for d1, d2 in zip(line_directions, line_directions[1:]):
|
||||
miter_directions.append(miter(d1, d2))
|
||||
|
||||
if closing_miter is None:
|
||||
miter_directions.pop()
|
||||
miter_directions.append(line_directions[-1].rotate_deg(end_angle))
|
||||
else:
|
||||
miter_directions.append(closing_miter)
|
||||
|
||||
self.vertices = [
|
||||
MLineVertex.new(v, d, m)
|
||||
for v, d, m in zip(vertices, line_directions, miter_directions)
|
||||
]
|
||||
self._update_parametrization()
|
||||
|
||||
# reverse transformation into WCS
|
||||
for v in self.vertices:
|
||||
v.transform(ucs.matrix)
|
||||
|
||||
def _update_parametrization(self):
|
||||
scale = self.dxf.scale_factor
|
||||
style = self.style
|
||||
|
||||
justification = self.dxf.justification
|
||||
offsets = [e.offset for e in style.elements]
|
||||
min_offset = min(offsets)
|
||||
max_offset = max(offsets)
|
||||
shift = 0
|
||||
if justification == self.TOP:
|
||||
shift = -max_offset
|
||||
elif justification == self.BOTTOM:
|
||||
shift = -min_offset
|
||||
|
||||
for vertex in self.vertices:
|
||||
angle = vertex.line_direction.angle_between(vertex.miter_direction)
|
||||
try:
|
||||
stretch = scale / math.sin(angle)
|
||||
except ZeroDivisionError:
|
||||
stretch = 1.0
|
||||
vertex.line_params = [
|
||||
((element.offset + shift) * stretch, 0.0) for element in style.elements
|
||||
]
|
||||
vertex.fill_params = [tuple() for _ in style.elements]
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Remove all MLINE vertices."""
|
||||
self.vertices.clear()
|
||||
|
||||
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)
|
||||
self.dxf.style_handle = "0"
|
||||
if other:
|
||||
style = other.mline_styles.get(self.dxf.style_name)
|
||||
if style:
|
||||
self.dxf.style_handle = style.dxf.handle
|
||||
return
|
||||
self.dxf.style_name = "Standard"
|
||||
|
||||
def transform(self, m: Matrix44) -> Self:
|
||||
"""Transform MLINE entity by transformation matrix `m` inplace."""
|
||||
for vertex in self.vertices:
|
||||
vertex.transform(m)
|
||||
self.dxf.extrusion = m.transform_direction(self.dxf.extrusion)
|
||||
scale = self.dxf.scale_factor
|
||||
scale_vec = m.transform_direction(Vec3(scale, scale, scale))
|
||||
if math.isclose(scale_vec.x, scale_vec.y, abs_tol=1e-6) and math.isclose(
|
||||
scale_vec.y, scale_vec.z, abs_tol=1e-6
|
||||
):
|
||||
self.dxf.scale_factor = sum(scale_vec) / 3 # average error
|
||||
# None uniform scaling will not be applied to the scale_factor!
|
||||
self.update_geometry()
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def __virtual_entities__(self) -> Iterator[DXFGraphic]:
|
||||
"""Implements the SupportsVirtualEntities protocol.
|
||||
|
||||
This protocol is for consistent internal usage and does not replace
|
||||
the method :meth:`virtual_entities`!
|
||||
"""
|
||||
from ezdxf.render.mline import virtual_entities
|
||||
|
||||
for e in virtual_entities(self):
|
||||
e.set_source_of_copy(self)
|
||||
yield e
|
||||
|
||||
def virtual_entities(self) -> Iterator[DXFGraphic]:
|
||||
"""Yields virtual DXF primitives of the MLINE entity as LINE, ARC and HATCH
|
||||
entities.
|
||||
|
||||
These entities are located at the original positions, but are not stored
|
||||
in the entity database, have no handle and are not assigned to any
|
||||
layout.
|
||||
|
||||
"""
|
||||
return self.__virtual_entities__()
|
||||
|
||||
def explode(self, target_layout: Optional[BaseLayout] = None) -> EntityQuery:
|
||||
"""Explode the MLINE entity as LINE, ARC and HATCH entities into target
|
||||
layout, if target layout is ``None``, the target layout is the layout
|
||||
of the MLINE. This method destroys the source entity.
|
||||
|
||||
Returns an :class:`~ezdxf.query.EntityQuery` container referencing all DXF
|
||||
primitives.
|
||||
|
||||
Args:
|
||||
target_layout: target layout for DXF primitives, ``None`` for same layout
|
||||
as source entity.
|
||||
"""
|
||||
from ezdxf.explode import explode_entity
|
||||
|
||||
return explode_entity(self, target_layout)
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
"""Validity check."""
|
||||
|
||||
def reset_mline_style(name="Standard"):
|
||||
auditor.fixed_error(
|
||||
code=AuditError.RESET_MLINE_STYLE,
|
||||
message=f'Reset MLINESTYLE to "{name}" in {str(self)}.',
|
||||
dxf_entity=self,
|
||||
)
|
||||
self.dxf.style_name = name
|
||||
style = doc.mline_styles.get(name)
|
||||
self.dxf.style_handle = style.dxf.handle
|
||||
|
||||
super().audit(auditor)
|
||||
doc = auditor.doc
|
||||
if doc is None:
|
||||
return
|
||||
|
||||
# Audit associated MLINESTYLE name and handle:
|
||||
style = doc.entitydb.get(self.dxf.style_handle)
|
||||
if not isinstance(style, MLineStyle): # handle is invalid, get style by name
|
||||
style = doc.mline_styles.get(self.dxf.style_name, None)
|
||||
if style is None:
|
||||
reset_mline_style()
|
||||
else: # fix MLINESTYLE handle:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_MLINESTYLE_HANDLE,
|
||||
message=f"Fixed invalid style handle in {str(self)}.",
|
||||
dxf_entity=self,
|
||||
)
|
||||
self.dxf.style_handle = style.dxf.handle
|
||||
else: # update MLINESTYLE name silently
|
||||
self.dxf.style_name = style.dxf.name
|
||||
|
||||
# Get current (maybe fixed) MLINESTYLE:
|
||||
style = self.style
|
||||
assert style is not None, "valid MLINE style required"
|
||||
|
||||
# Update style element count silently:
|
||||
element_count = len(style.elements)
|
||||
self.dxf.style_element_count = element_count
|
||||
|
||||
# Audit vertices:
|
||||
for vertex in self.vertices:
|
||||
if NULLVEC.isclose(vertex.line_direction):
|
||||
break
|
||||
if NULLVEC.isclose(vertex.miter_direction):
|
||||
break
|
||||
if len(vertex.line_params) != element_count:
|
||||
break
|
||||
# Ignore fill parameters.
|
||||
else: # no break
|
||||
return
|
||||
|
||||
# Invalid vertices found:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_MLINE_VERTEX,
|
||||
message=f"Execute geometry update for {str(self)}.",
|
||||
dxf_entity=self,
|
||||
)
|
||||
self.update_geometry()
|
||||
|
||||
def ocs(self) -> OCS:
|
||||
# WCS entity which supports the "extrusion" attribute in a
|
||||
# different way!
|
||||
return OCS()
|
||||
|
||||
|
||||
acdb_mline_style = DefSubclass(
|
||||
"AcDbMlineStyle",
|
||||
{
|
||||
"name": DXFAttr(2, default="Standard"),
|
||||
# Flags (bit-coded):
|
||||
# 1 =Fill on
|
||||
# 2 = Display miters
|
||||
# 16 = Start square end (line) cap
|
||||
# 32 = Start inner arcs cap
|
||||
# 64 = Start round (outer arcs) cap
|
||||
# 256 = End square (line) cap
|
||||
# 512 = End inner arcs cap
|
||||
# 1024 = End round (outer arcs) cap
|
||||
"flags": DXFAttr(70, default=0),
|
||||
# Style description (string, 255 characters maximum):
|
||||
"description": DXFAttr(3, default=""),
|
||||
# Fill color (integer, default = 256):
|
||||
"fill_color": DXFAttr(
|
||||
62,
|
||||
default=256,
|
||||
validator=validator.is_valid_aci_color,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Start angle (real, default is 90 degrees):
|
||||
"start_angle": DXFAttr(51, default=90),
|
||||
# End angle (real, default is 90 degrees):
|
||||
"end_angle": DXFAttr(52, default=90),
|
||||
# 71: Number of elements
|
||||
# 49: Element offset (real, no default).
|
||||
# Multiple entries can exist; one entry for each element
|
||||
# 62: Element color (integer, default = 0).
|
||||
# Multiple entries can exist; one entry for each element
|
||||
# 6: Element linetype (string, default = BYLAYER).
|
||||
# Multiple entries can exist; one entry for each element
|
||||
},
|
||||
)
|
||||
acdb_mline_style_group_codes = group_code_mapping(acdb_mline_style)
|
||||
MLineStyleElement = namedtuple("MLineStyleElement", "offset color linetype")
|
||||
|
||||
|
||||
class MLineStyleElements:
|
||||
def __init__(self, tags: Optional[Tags] = None):
|
||||
self.elements: list[MLineStyleElement] = []
|
||||
if tags:
|
||||
for e in self.parse_tags(tags):
|
||||
data = MLineStyleElement(
|
||||
e.get("offset", 1.0),
|
||||
e.get("color", 0),
|
||||
e.get("linetype", "BYLAYER"),
|
||||
)
|
||||
self.elements.append(data)
|
||||
|
||||
def copy(self) -> MLineStyleElements:
|
||||
elements = MLineStyleElements()
|
||||
# new list of immutable data
|
||||
elements.elements = list(self.elements)
|
||||
return elements
|
||||
|
||||
def __len__(self):
|
||||
return len(self.elements)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.elements[item]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.elements)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter):
|
||||
write_tag = tagwriter.write_tag2
|
||||
write_tag(71, len(self.elements))
|
||||
for offset, color, linetype in self.elements:
|
||||
write_tag(49, offset)
|
||||
write_tag(62, color)
|
||||
write_tag(6, linetype)
|
||||
|
||||
def append(self, offset: float, color: int = 0, linetype: str = "BYLAYER") -> None:
|
||||
"""Append a new line element.
|
||||
|
||||
Args:
|
||||
offset: normal offset from the reference line: if justification is
|
||||
``MLINE_ZERO``, positive values are above and negative values
|
||||
are below the reference line.
|
||||
color: :ref:`ACI` value
|
||||
linetype: linetype name
|
||||
|
||||
"""
|
||||
self.elements.append(
|
||||
MLineStyleElement(float(offset), int(color), str(linetype))
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_tags(tags: Tags) -> Iterator[dict]:
|
||||
collector = None
|
||||
for code, value in tags:
|
||||
if code == 49:
|
||||
if collector is not None:
|
||||
yield collector
|
||||
collector = {"offset": value}
|
||||
elif code == 62:
|
||||
collector["color"] = value # type: ignore
|
||||
elif code == 6:
|
||||
collector["linetype"] = value # type: ignore
|
||||
if collector is not None:
|
||||
yield collector
|
||||
|
||||
def ordered_indices(self) -> list[int]:
|
||||
offsets = [e.offset for e in self.elements]
|
||||
return [offsets.index(value) for value in sorted(offsets)]
|
||||
|
||||
|
||||
@register_entity
|
||||
class MLineStyle(DXFObject):
|
||||
DXFTYPE = "MLINESTYLE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_mline_style)
|
||||
FILL = const.MLINESTYLE_FILL
|
||||
MITER = const.MLINESTYLE_MITER
|
||||
START_SQUARE = const.MLINESTYLE_START_SQUARE
|
||||
START_INNER_ARC = const.MLINESTYLE_START_INNER_ARC
|
||||
START_ROUND = const.MLINESTYLE_START_ROUND
|
||||
END_SQUARE = const.MLINESTYLE_END_SQUARE
|
||||
END_INNER_ARC = const.MLINESTYLE_END_INNER_ARC
|
||||
END_ROUND = const.MLINESTYLE_END_ROUND
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.elements = MLineStyleElements()
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
assert isinstance(entity, MLineStyle)
|
||||
entity.elements = self.elements.copy()
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.subclass_by_index(1)
|
||||
if tags is None:
|
||||
raise const.DXFStructureError(
|
||||
f"missing 'AcDbMLine' subclass in MLINE(#{dxf.handle})"
|
||||
)
|
||||
|
||||
try:
|
||||
# Find index of the count tag:
|
||||
index71 = tags.tag_index(71)
|
||||
except const.DXFValueError:
|
||||
# The count tag does not exist: DXF structure error?
|
||||
pass
|
||||
else:
|
||||
self.elements = MLineStyleElements(tags[index71 + 1 :]) # type: ignore
|
||||
# Remove processed tags:
|
||||
del tags[index71:]
|
||||
processor.fast_load_dxfattribs(dxf, acdb_mline_style_group_codes, tags)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_mline_style.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, acdb_mline_style.attribs.keys())
|
||||
self.elements.export_dxf(tagwriter)
|
||||
|
||||
def update_all(self):
|
||||
"""Update all MLINE entities using this MLINESTYLE.
|
||||
|
||||
The update is required if elements were added or removed or the offset
|
||||
of any element was changed.
|
||||
|
||||
"""
|
||||
if self.doc:
|
||||
handle = self.dxf.handle
|
||||
mlines = (e for e in self.doc.entitydb.values() if e.dxftype() == "MLINE")
|
||||
for mline in mlines:
|
||||
if mline.dxf.style_handle == handle:
|
||||
mline.update_geometry()
|
||||
|
||||
def ordered_indices(self) -> list[int]:
|
||||
return self.elements.ordered_indices()
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
super().audit(auditor)
|
||||
if len(self.elements) == 0:
|
||||
auditor.add_error(
|
||||
code=AuditError.INVALID_MLINESTYLE_ELEMENT_COUNT,
|
||||
message=f"No line elements defined in {str(self)}.",
|
||||
dxf_entity=self,
|
||||
)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
super().register_resources(registry)
|
||||
for element in self.elements:
|
||||
registry.add_linetype(element.linetype)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
assert isinstance(clone, MLineStyle)
|
||||
super().map_resources(clone, mapping)
|
||||
self.elements.elements = [
|
||||
MLineStyleElement(
|
||||
element.offset,
|
||||
element.color,
|
||||
mapping.get_linetype(element.linetype),
|
||||
)
|
||||
for element in self.elements
|
||||
]
|
||||
|
||||
|
||||
class MLineStyleCollection(ObjectCollection[MLineStyle]):
|
||||
def __init__(self, doc: Drawing):
|
||||
super().__init__(doc, dict_name="ACAD_MLINESTYLE", object_type="MLINESTYLE")
|
||||
self.create_required_entries()
|
||||
|
||||
def create_required_entries(self) -> None:
|
||||
if "Standard" not in self:
|
||||
entity: MLineStyle = self.new("Standard")
|
||||
entity.elements.append(0.5, 256)
|
||||
entity.elements.append(-0.5, 256)
|
||||
@@ -0,0 +1,241 @@
|
||||
# Copyright (c) 2021-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import logging
|
||||
from ezdxf.lldxf import validator, const
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
DXFAttr,
|
||||
RETURN_DEFAULT,
|
||||
XType,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.math import NULLVEC, Z_AXIS
|
||||
from .dxfentity import base_class
|
||||
from .dxfgfx import acdb_entity
|
||||
from .factory import register_entity
|
||||
from .polygon import DXFPolygon
|
||||
from .gradient import Gradient, GradientType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.colors import RGB
|
||||
|
||||
__all__ = ["MPolygon"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
acdb_mpolygon = DefSubclass(
|
||||
"AcDbMPolygon",
|
||||
{
|
||||
# MPolygon: version
|
||||
"version": DXFAttr(
|
||||
70,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# x- and y-axis always equal 0, z-axis represents the elevation:
|
||||
"elevation": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch pattern name:
|
||||
"pattern_name": DXFAttr(2, default=""),
|
||||
# Solid fill color as ACI
|
||||
"fill_color": DXFAttr(
|
||||
63,
|
||||
default=const.BYLAYER,
|
||||
optional=True,
|
||||
validator=validator.is_valid_aci_color,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# MPolygon: Solid-fill flag:
|
||||
# 0 = lacks solid fill
|
||||
# 1 = has solid fill
|
||||
"solid_fill": DXFAttr(
|
||||
71,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch style tag is not supported for MPOLYGON!?
|
||||
# I don't know in which order this tag has to be exported.
|
||||
# TrueView does not accept this tag in any place!
|
||||
# BricsCAD supports this tag!
|
||||
"hatch_style": DXFAttr(
|
||||
75,
|
||||
default=const.HATCH_STYLE_NESTED,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
optional=True,
|
||||
),
|
||||
# Hatch pattern type ... see HATCH
|
||||
"pattern_type": DXFAttr(
|
||||
76,
|
||||
default=const.HATCH_TYPE_PREDEFINED,
|
||||
validator=validator.is_in_integer_range(0, 3),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch pattern angle (pattern fill only) in degrees:
|
||||
"pattern_angle": DXFAttr(52, default=0),
|
||||
# Hatch pattern scale or spacing (pattern fill only):
|
||||
"pattern_scale": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# MPolygon: Boundary annotation flag:
|
||||
# 0 = boundary is not an annotated boundary
|
||||
# 1 = boundary is an annotated boundary
|
||||
"annotated_boundary": DXFAttr(
|
||||
73,
|
||||
default=0,
|
||||
optional=True,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Hatch pattern double flag (pattern fill only) .. see HATCH
|
||||
"pattern_double": DXFAttr(
|
||||
77,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# see ... HATCH
|
||||
"pixel_size": DXFAttr(47, optional=True),
|
||||
"n_seed_points": DXFAttr(
|
||||
98,
|
||||
default=0,
|
||||
validator=validator.is_greater_or_equal_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# MPolygon: offset vector in OCS ???
|
||||
"offset_vector": DXFAttr(11, xtype=XType.point2d, default=(0, 0)),
|
||||
# MPolygon: number of degenerate boundary paths (loops), where a
|
||||
# degenerate boundary path is a border that is ignored by the hatch:
|
||||
"degenerated_loops": DXFAttr(99, default=0),
|
||||
},
|
||||
)
|
||||
acdb_mpolygon_group_code = group_code_mapping(acdb_mpolygon)
|
||||
|
||||
|
||||
@register_entity
|
||||
class MPolygon(DXFPolygon):
|
||||
"""DXF MPOLYGON entity
|
||||
|
||||
The MPOLYGON is not a core DXF entity, and requires a CLASS definition.
|
||||
|
||||
"""
|
||||
|
||||
DXFTYPE = "MPOLYGON"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mpolygon)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
||||
LOAD_GROUP_CODES = acdb_mpolygon_group_code
|
||||
|
||||
def preprocess_export(self, tagwriter: AbstractTagWriter) -> bool:
|
||||
if self.paths.has_edge_paths:
|
||||
logger.warning(
|
||||
"MPOLYGON including edge paths are not exported by ezdxf!"
|
||||
)
|
||||
return False
|
||||
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_mpolygon.name)
|
||||
dxf = self.dxf
|
||||
dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[ # tag order is important!
|
||||
"version",
|
||||
"elevation",
|
||||
"extrusion",
|
||||
"pattern_name",
|
||||
"solid_fill",
|
||||
],
|
||||
)
|
||||
self.paths.export_dxf(tagwriter, self.dxftype())
|
||||
# hatch_style not supported?
|
||||
dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
# "hatch_style", # not supported by MPolygon ???
|
||||
"pattern_type",
|
||||
],
|
||||
)
|
||||
if dxf.solid_fill == 0: # export pattern
|
||||
dxf.export_dxf_attribs(
|
||||
tagwriter, ["pattern_angle", "pattern_scale", "pattern_double"]
|
||||
)
|
||||
|
||||
dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"annotated_boundary",
|
||||
"pixel_size",
|
||||
],
|
||||
)
|
||||
if dxf.solid_fill == 0: # export pattern
|
||||
if self.pattern:
|
||||
self.pattern.export_dxf(tagwriter, force=True)
|
||||
else: # required pattern length tag!
|
||||
tagwriter.write_tag2(78, 0)
|
||||
if tagwriter.dxfversion > const.DXF2000:
|
||||
dxf.export_dxf_attribs(tagwriter, "fill_color")
|
||||
dxf.export_dxf_attribs(tagwriter, "offset_vector")
|
||||
self.export_degenerated_loops(tagwriter)
|
||||
self.export_gradient(tagwriter)
|
||||
|
||||
def export_degenerated_loops(self, tagwriter: AbstractTagWriter):
|
||||
self.dxf.export_dxf_attribs(tagwriter, "degenerated_loops")
|
||||
|
||||
def export_gradient(self, tagwriter: AbstractTagWriter):
|
||||
if tagwriter.dxfversion < const.DXF2004:
|
||||
return
|
||||
if self.gradient is None:
|
||||
self.gradient = Gradient(kind=0, num=0, type=0)
|
||||
self.gradient.export_dxf(tagwriter)
|
||||
|
||||
def set_solid_fill(
|
||||
self, color: int = 7, style: int = 1, rgb: Optional[RGB] = None
|
||||
):
|
||||
"""Set :class:`MPolygon` to solid fill mode and removes all gradient and
|
||||
pattern fill related data.
|
||||
|
||||
Args:
|
||||
color: :ref:`ACI`, (0 = BYBLOCK; 256 = BYLAYER)
|
||||
style: hatch style is not supported by MPOLYGON, just for symmetry
|
||||
to HATCH
|
||||
rgb: true color value as (r, g, b)-tuple - has higher priority
|
||||
than `color`. True color support requires DXF R2004+
|
||||
|
||||
"""
|
||||
# remove existing gradient and pattern fill
|
||||
self.gradient = None
|
||||
self.pattern = None
|
||||
self.dxf.solid_fill = 1
|
||||
|
||||
# if a gradient is present, the solid fill_color is ignored
|
||||
self.dxf.fill_color = color
|
||||
self.dxf.pattern_name = "SOLID"
|
||||
self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
|
||||
if rgb is not None:
|
||||
self.set_solid_rgb_gradient(rgb)
|
||||
|
||||
def set_solid_rgb_gradient(self, rgb: RGB):
|
||||
"""Set solid fill color as gradient of a single RGB color.
|
||||
This disables pattern fill!
|
||||
|
||||
(internal API)
|
||||
"""
|
||||
self.gradient = Gradient(kind=1, num=2, type=GradientType.LINEAR)
|
||||
self.gradient.color1 = rgb
|
||||
self.gradient.color2 = rgb
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,154 @@
|
||||
# Copyright (c) 2021-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Iterable, Iterator, Sequence
|
||||
from .mtext import MText, MTextColumns
|
||||
|
||||
__all__ = [
|
||||
"make_static_columns_r2000",
|
||||
"make_dynamic_auto_height_columns_r2000",
|
||||
"make_dynamic_manual_height_columns_r2000",
|
||||
"make_static_columns_r2018",
|
||||
"make_dynamic_auto_height_columns_r2018",
|
||||
"make_dynamic_manual_height_columns_r2018",
|
||||
]
|
||||
|
||||
COLUMN_BREAK = "\\N"
|
||||
|
||||
|
||||
def add_column_breaks(content: Iterable[str]) -> Iterator[str]:
|
||||
content = list(content)
|
||||
for c in content[:-1]:
|
||||
if not c.endswith(COLUMN_BREAK):
|
||||
c += COLUMN_BREAK
|
||||
yield c
|
||||
yield content[-1] # last column without a column break
|
||||
|
||||
|
||||
def make_static_columns_r2000(
|
||||
content: Sequence[str],
|
||||
width: float,
|
||||
gutter_width: float,
|
||||
height: float,
|
||||
dxfattribs=None,
|
||||
) -> MText:
|
||||
if len(content) < 1:
|
||||
raise ValueError("no content")
|
||||
columns = MTextColumns.new_static_columns(
|
||||
len(content), width, gutter_width, height
|
||||
)
|
||||
mtext = MText.new(dxfattribs=dxfattribs)
|
||||
mtext.setup_columns(columns, linked=True)
|
||||
content = list(add_column_breaks(content))
|
||||
mtext.text = content[0]
|
||||
for mt, c in zip(columns.linked_columns, content[1:]):
|
||||
mt.text = c
|
||||
return mtext
|
||||
|
||||
|
||||
def make_dynamic_auto_height_columns_r2000(
|
||||
content: str,
|
||||
width: float,
|
||||
gutter_width: float,
|
||||
height: float,
|
||||
count: int = 1,
|
||||
dxfattribs=None,
|
||||
) -> MText:
|
||||
if not content:
|
||||
raise ValueError("no content")
|
||||
mtext = MText.new(dxfattribs=dxfattribs)
|
||||
mtext.dxf.width = width
|
||||
columns = MTextColumns.new_dynamic_auto_height_columns(
|
||||
count, width, gutter_width, height
|
||||
)
|
||||
set_dynamic_columns_content(content, mtext, columns)
|
||||
return mtext
|
||||
|
||||
|
||||
def make_dynamic_manual_height_columns_r2000(
|
||||
content: str,
|
||||
width: float,
|
||||
gutter_width: float,
|
||||
heights: Sequence[float],
|
||||
dxfattribs=None,
|
||||
) -> MText:
|
||||
if not content:
|
||||
raise ValueError("no content")
|
||||
mtext = MText.new(dxfattribs=dxfattribs)
|
||||
mtext.dxf.width = width
|
||||
columns = MTextColumns.new_dynamic_manual_height_columns(
|
||||
width, gutter_width, heights
|
||||
)
|
||||
set_dynamic_columns_content(content, mtext, columns)
|
||||
return mtext
|
||||
|
||||
|
||||
def set_dynamic_columns_content(
|
||||
content: str, mtext: MText, columns: MTextColumns
|
||||
):
|
||||
mtext.setup_columns(columns, linked=True)
|
||||
# temp. hack: assign whole content to the main column
|
||||
mtext.text = content
|
||||
# for mt, c in zip(mtext.columns.linked_columns, content[1:]):
|
||||
# mt.text = c
|
||||
return mtext
|
||||
|
||||
|
||||
# DXF version R2018
|
||||
|
||||
|
||||
def make_static_columns_r2018(
|
||||
content: Sequence[str],
|
||||
width: float,
|
||||
gutter_width: float,
|
||||
height: float,
|
||||
dxfattribs=None,
|
||||
) -> MText:
|
||||
if len(content) < 1:
|
||||
raise ValueError("no content")
|
||||
columns = MTextColumns.new_static_columns(
|
||||
len(content), width, gutter_width, height
|
||||
)
|
||||
mtext = MText.new(dxfattribs=dxfattribs)
|
||||
mtext.setup_columns(columns, linked=False)
|
||||
mtext.text = "".join(add_column_breaks(content))
|
||||
return mtext
|
||||
|
||||
|
||||
def make_dynamic_auto_height_columns_r2018(
|
||||
content: str,
|
||||
width: float,
|
||||
gutter_width: float,
|
||||
height: float,
|
||||
count: int,
|
||||
dxfattribs=None,
|
||||
) -> MText:
|
||||
columns = MTextColumns.new_dynamic_auto_height_columns(
|
||||
count, width, gutter_width, height
|
||||
)
|
||||
return _make_dynamic_columns_r2018(content, columns, dxfattribs or {})
|
||||
|
||||
|
||||
def make_dynamic_manual_height_columns_r2018(
|
||||
content: str,
|
||||
width: float,
|
||||
gutter_width: float,
|
||||
heights: Sequence[float],
|
||||
dxfattribs=None,
|
||||
) -> MText:
|
||||
columns = MTextColumns.new_dynamic_manual_height_columns(
|
||||
width, gutter_width, heights
|
||||
)
|
||||
return _make_dynamic_columns_r2018(content, columns, dxfattribs or {})
|
||||
|
||||
|
||||
def _make_dynamic_columns_r2018(
|
||||
content: str, columns: MTextColumns, dxfattribs
|
||||
) -> MText:
|
||||
if not content:
|
||||
raise ValueError("no content")
|
||||
# column count is not required for DXF R2018
|
||||
mtext = MText.new(dxfattribs=dxfattribs)
|
||||
mtext.setup_columns(columns, linked=False)
|
||||
mtext.text = content
|
||||
return mtext
|
||||
@@ -0,0 +1,193 @@
|
||||
# Copyright (c) 2018-2023 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterator,
|
||||
cast,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Generic,
|
||||
)
|
||||
from ezdxf.lldxf.const import (
|
||||
DXFValueError,
|
||||
DXFKeyError,
|
||||
INVALID_NAME_CHARACTERS,
|
||||
)
|
||||
from ezdxf.lldxf.validator import make_table_key, is_valid_table_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFObject, Dictionary
|
||||
|
||||
|
||||
def validate_name(name: str) -> str:
|
||||
name = name[:255]
|
||||
if not is_valid_table_name(name):
|
||||
raise DXFValueError(
|
||||
f"table name '{name}' contains invalid characters: {INVALID_NAME_CHARACTERS}"
|
||||
)
|
||||
return name
|
||||
|
||||
|
||||
T = TypeVar("T", bound="DXFObject")
|
||||
|
||||
|
||||
class ObjectCollection(Generic[T]):
|
||||
"""
|
||||
Note:
|
||||
ObjectCollections may contain entries where the name stored in the entity as
|
||||
"name" attribute diverges from the key in the DICTIONARY object e.g. MLEADERSTYLE
|
||||
collection may have entries for "Standard" and "Annotative" but both MLEADERSTYLE
|
||||
objects have the name "Standard".
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
doc: Drawing,
|
||||
dict_name: str = "ACAD_MATERIAL",
|
||||
object_type: str = "MATERIAL",
|
||||
):
|
||||
self.doc: Drawing = doc
|
||||
self.object_dict_name = dict_name
|
||||
self.object_type: str = object_type
|
||||
self.object_dict: Dictionary = doc.rootdict.get_required_dict(dict_name)
|
||||
|
||||
def update_object_dict(self) -> None:
|
||||
self.object_dict = self.doc.rootdict.get_required_dict(self.object_dict_name)
|
||||
|
||||
def create_required_entries(self) -> None:
|
||||
pass
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[str, T]]:
|
||||
return self.object_dict.items()
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.object_dict)
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return self.has_entry(name)
|
||||
|
||||
def __getitem__(self, name: str) -> T:
|
||||
entry = self.get(name)
|
||||
if entry is None:
|
||||
raise DXFKeyError(name)
|
||||
return entry
|
||||
|
||||
@property
|
||||
def handle(self) -> str:
|
||||
"""Returns the DXF handle of the DICTIONARY object."""
|
||||
return self.object_dict.dxf.handle
|
||||
|
||||
@property
|
||||
def is_hard_owner(self) -> bool:
|
||||
"""Returns ``True`` if the collection is hard owner of entities.
|
||||
Hard owned entities will be destroyed by deleting the dictionary.
|
||||
"""
|
||||
return self.object_dict.is_hard_owner
|
||||
|
||||
def has_entry(self, name: str) -> bool:
|
||||
return self.get(name) is not None
|
||||
|
||||
def is_unique_name(self, name: str) -> bool:
|
||||
name = make_table_key(name)
|
||||
for entry_name in self.object_dict.keys():
|
||||
if make_table_key(entry_name) == name:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get(self, name: str, default: Optional[T] = None) -> Optional[T]:
|
||||
"""Get object by name. Object collection entries are case-insensitive.
|
||||
|
||||
Args:
|
||||
name: object name as string
|
||||
default: default value
|
||||
|
||||
"""
|
||||
name = make_table_key(name)
|
||||
for entry_name, obj in self.object_dict.items():
|
||||
if make_table_key(entry_name) == name:
|
||||
return obj
|
||||
return default
|
||||
|
||||
def new(self, name: str) -> T:
|
||||
"""Create a new object of type `self.object_type` and store its handle
|
||||
in the object manager dictionary. Object collection entry names are
|
||||
case-insensitive and limited to 255 characters.
|
||||
|
||||
Args:
|
||||
name: name of new object as string
|
||||
|
||||
Returns:
|
||||
new object of type `self.object_type`
|
||||
|
||||
Raises:
|
||||
DXFValueError: if object name already exist or is invalid
|
||||
|
||||
(internal API)
|
||||
|
||||
"""
|
||||
name = validate_name(name)
|
||||
if not self.is_unique_name(name):
|
||||
raise DXFValueError(f"{self.object_type} entry {name} already exists.")
|
||||
return self._new(name, dxfattribs={"name": name})
|
||||
|
||||
def duplicate_entry(self, name: str, new_name: str) -> T:
|
||||
"""Returns a new table entry `new_name` as copy of `name`,
|
||||
replaces entry `new_name` if already exist.
|
||||
|
||||
Raises:
|
||||
DXFValueError: `name` does not exist
|
||||
|
||||
"""
|
||||
entry = self.get(name)
|
||||
if entry is None:
|
||||
raise DXFValueError(f"entry '{name}' does not exist")
|
||||
new_name = validate_name(new_name)
|
||||
# remove existing entry
|
||||
existing_entry = self.get(new_name)
|
||||
if existing_entry is not None:
|
||||
self.delete(new_name)
|
||||
|
||||
entitydb = self.doc.entitydb
|
||||
if entitydb:
|
||||
new_entry = entitydb.duplicate_entity(entry)
|
||||
else: # only for testing!
|
||||
new_entry = entry.copy()
|
||||
if new_entry.dxf.is_supported("name"):
|
||||
new_entry.dxf.name = new_name
|
||||
self.object_dict.add(new_name, new_entry) # type: ignore
|
||||
owner_handle = self.object_dict.dxf.handle
|
||||
new_entry.dxf.owner = owner_handle
|
||||
new_entry.set_reactors([owner_handle])
|
||||
return new_entry # type: ignore
|
||||
|
||||
def _new(self, name: str, dxfattribs: dict) -> T:
|
||||
objects = self.doc.objects
|
||||
assert objects is not None
|
||||
|
||||
owner = self.object_dict.dxf.handle
|
||||
dxfattribs["owner"] = owner
|
||||
obj = objects.add_dxf_object_with_reactor(
|
||||
self.object_type, dxfattribs=dxfattribs
|
||||
)
|
||||
self.object_dict.add(name, obj)
|
||||
return cast(T, obj)
|
||||
|
||||
def delete(self, name: str) -> None:
|
||||
objects = self.doc.objects
|
||||
assert objects is not None
|
||||
|
||||
obj = self.get(name) # case insensitive
|
||||
if obj is not None:
|
||||
# The underlying DICTIONARY is not case-insensitive implemented,
|
||||
# get real object name if available
|
||||
if obj.dxf.is_supported("name"):
|
||||
name = obj.dxf.get("name", name)
|
||||
self.object_dict.discard(name)
|
||||
objects.delete_entity(obj)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Delete all entries."""
|
||||
self.object_dict.clear()
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (c) 2021-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from ezdxf.lldxf import const
|
||||
from . import factory
|
||||
from .dxfgfx import DXFGraphic
|
||||
from .dxfentity import SubclassProcessor
|
||||
from .copy import default_copy, CopyNotSupported
|
||||
from ezdxf.math import BoundingBox, Vec3
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
|
||||
|
||||
@factory.register_entity
|
||||
class OLE2Frame(DXFGraphic):
|
||||
DXFTYPE = "OLE2FRAME"
|
||||
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.acdb_ole2frame: Optional[Tags] = None
|
||||
|
||||
def copy(self, copy_strategy=default_copy) -> OLE2Frame:
|
||||
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_ole2frame = processor.subclass_by_index(2)
|
||||
return dxf
|
||||
|
||||
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_ole2frame is not None:
|
||||
tagwriter.write_tags(self.acdb_ole2frame)
|
||||
# XDATA export is done by the parent class
|
||||
|
||||
def bbox(self) -> BoundingBox:
|
||||
if self.acdb_ole2frame is not None:
|
||||
v10 = self.acdb_ole2frame.get_first_value(10, None)
|
||||
v11 = self.acdb_ole2frame.get_first_value(11, None)
|
||||
if v10 is not None and v11 is not None:
|
||||
return BoundingBox([Vec3(v10), Vec3(v11)])
|
||||
return BoundingBox()
|
||||
|
||||
def binary_data(self) -> bytes:
|
||||
if self.acdb_ole2frame is not None:
|
||||
return b"".join(value for code, value in self.acdb_ole2frame if code == 310)
|
||||
return b""
|
||||
@@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2019-2021 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Iterable, TYPE_CHECKING, Optional
|
||||
|
||||
from ezdxf.lldxf.tags import Tags, group_tags
|
||||
from ezdxf.math import Vec2, UVec
|
||||
from ezdxf.tools import pattern
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["Pattern", "PatternLine"]
|
||||
|
||||
|
||||
class Pattern:
|
||||
def __init__(self, lines: Optional[Iterable[PatternLine]] = None):
|
||||
self.lines: list[PatternLine] = list(lines) if lines else []
|
||||
|
||||
@classmethod
|
||||
def load_tags(cls, tags: Tags) -> Pattern:
|
||||
grouped_line_tags = group_tags(tags, splitcode=53)
|
||||
return cls(
|
||||
PatternLine.load_tags(line_tags) for line_tags in grouped_line_tags
|
||||
)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Delete all pattern definition lines."""
|
||||
self.lines = []
|
||||
|
||||
def add_line(
|
||||
self,
|
||||
angle: float = 0,
|
||||
base_point: UVec = (0, 0),
|
||||
offset: UVec = (0, 0),
|
||||
dash_length_items: Optional[Iterable[float]] = None,
|
||||
) -> None:
|
||||
"""Create a new pattern definition line and add the line to the
|
||||
:attr:`Pattern.lines` attribute.
|
||||
|
||||
"""
|
||||
assert (
|
||||
dash_length_items is not None
|
||||
), "argument 'dash_length_items' is None"
|
||||
self.lines.append(
|
||||
PatternLine(angle, base_point, offset, dash_length_items)
|
||||
)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter, force=False) -> None:
|
||||
if len(self.lines) or force:
|
||||
tagwriter.write_tag2(78, len(self.lines))
|
||||
for line in self.lines:
|
||||
line.export_dxf(tagwriter)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "[" + ",".join(str(line) for line in self.lines) + "]"
|
||||
|
||||
def as_list(self) -> list:
|
||||
return [line.as_list() for line in self.lines]
|
||||
|
||||
def scale(self, factor: float = 1, angle: float = 0) -> None:
|
||||
"""Scale and rotate pattern.
|
||||
|
||||
Be careful, this changes the base pattern definition, maybe better use
|
||||
:meth:`Hatch.set_pattern_scale` or :meth:`Hatch.set_pattern_angle`.
|
||||
|
||||
Args:
|
||||
factor: scaling factor
|
||||
angle: rotation angle in degrees
|
||||
|
||||
"""
|
||||
scaled_pattern = pattern.scale_pattern(
|
||||
self.as_list(), factor=factor, angle=angle
|
||||
)
|
||||
self.clear()
|
||||
for line in scaled_pattern:
|
||||
self.add_line(*line)
|
||||
|
||||
|
||||
class PatternLine:
|
||||
def __init__(
|
||||
self,
|
||||
angle: float = 0,
|
||||
base_point: UVec = (0, 0),
|
||||
offset: UVec = (0, 0),
|
||||
dash_length_items: Optional[Iterable[float]] = None,
|
||||
):
|
||||
self.angle: float = float(angle) # in degrees
|
||||
self.base_point: Vec2 = Vec2(base_point)
|
||||
self.offset: Vec2 = Vec2(offset)
|
||||
self.dash_length_items: list[float] = (
|
||||
[] if dash_length_items is None else list(dash_length_items)
|
||||
)
|
||||
# dash_length_items = [item0, item1, ...]
|
||||
# item > 0 is line, < 0 is gap, 0.0 = dot;
|
||||
|
||||
@staticmethod
|
||||
def load_tags(tags: Tags) -> PatternLine:
|
||||
p = {53: 0, 43: 0, 44: 0, 45: 0, 46: 0}
|
||||
dash_length_items = []
|
||||
for tag in tags:
|
||||
code, value = tag
|
||||
if code == 49:
|
||||
dash_length_items.append(value)
|
||||
else:
|
||||
p[code] = value
|
||||
return PatternLine(
|
||||
p[53], (p[43], p[44]), (p[45], p[46]), dash_length_items
|
||||
)
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
||||
write_tag = tagwriter.write_tag2
|
||||
write_tag(53, self.angle)
|
||||
write_tag(43, self.base_point.x)
|
||||
write_tag(44, self.base_point.y)
|
||||
write_tag(45, self.offset.x)
|
||||
write_tag(46, self.offset.y)
|
||||
write_tag(79, len(self.dash_length_items))
|
||||
for item in self.dash_length_items:
|
||||
write_tag(49, item)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"[{self.angle}, {self.base_point}, {self.offset}, "
|
||||
f"{self.dash_length_items}]"
|
||||
)
|
||||
|
||||
def as_list(self) -> list:
|
||||
return [
|
||||
self.angle,
|
||||
self.base_point,
|
||||
self.offset,
|
||||
self.dash_length_items,
|
||||
]
|
||||
@@ -0,0 +1,145 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
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 DXF12, SUBCLASS_MARKER
|
||||
from ezdxf.math import Vec3, Matrix44, NULLVEC, Z_AXIS, OCS
|
||||
from ezdxf.math.transformtools import (
|
||||
transform_thickness_and_extrusion_without_ocs,
|
||||
)
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity, acdb_entity_group_codes
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["Point"]
|
||||
|
||||
# Point styling is a global setting, stored in the HEADER section as:
|
||||
# $PDMODE: https://knowledge.autodesk.com/support/autocad/learn-explore/caas/CloudHelp/cloudhelp/2019/ENU/AutoCAD-Core/files/GUID-82F9BB52-D026-4D6A-ABA6-BF29641F459B-htm.html
|
||||
# One of these values
|
||||
# 0 = center dot (.)
|
||||
# 1 = none ( )
|
||||
# 2 = cross (+)
|
||||
# 3 = x-cross (x)
|
||||
# 4 = tick (')
|
||||
# Combined with these bit values
|
||||
# 32 = circle
|
||||
# 64 = Square
|
||||
#
|
||||
# e.g. circle + square+center dot = 32 + 64 + 0 = 96
|
||||
#
|
||||
# $PDSIZE: https://knowledge.autodesk.com/support/autocad/learn-explore/caas/CloudHelp/cloudhelp/2021/ENU/AutoCAD-Core/files/GUID-826CA91D-704B-400B-B784-7FCC9619AFB9-htm.html?st=$PDSIZE
|
||||
# 0 = 5% of draw area height
|
||||
# <0 = Specifies a percentage of the viewport size
|
||||
# >0 = Specifies an absolute size
|
||||
|
||||
acdb_point = DefSubclass(
|
||||
"AcDbPoint",
|
||||
{
|
||||
# Point location:
|
||||
"location": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# Thickness could be negative:
|
||||
"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,
|
||||
),
|
||||
# angle of the x-axis for the UCS in effect when the point was drawn;
|
||||
# used when PDMODE is nonzero:
|
||||
"angle": DXFAttr(50, default=0, optional=True),
|
||||
},
|
||||
)
|
||||
acdb_point_group_codes = group_code_mapping(acdb_point)
|
||||
merged_point_group_codes = merge_group_code_mappings(
|
||||
acdb_entity_group_codes, acdb_point_group_codes # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Point(DXFGraphic):
|
||||
"""DXF POINT entity"""
|
||||
|
||||
DXFTYPE = "POINT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_point)
|
||||
|
||||
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_point_group_codes)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags. (internal API)"""
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_point.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter, ["location", "thickness", "extrusion", "angle"]
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> Point:
|
||||
"""Transform the POINT entity by transformation matrix `m` inplace."""
|
||||
self.dxf.location = m.transform(self.dxf.location)
|
||||
transform_thickness_and_extrusion_without_ocs(self, m)
|
||||
# ignore dxf.angle!
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def translate(self, dx: float, dy: float, dz: float) -> Point:
|
||||
"""Optimized POINT translation about `dx` in x-axis, `dy` in y-axis and
|
||||
`dz` in z-axis.
|
||||
"""
|
||||
self.dxf.location = Vec3(dx, dy, dz) + self.dxf.location
|
||||
# Avoid Matrix44 instantiation if not required:
|
||||
if self.is_post_transform_required:
|
||||
self.post_transform(Matrix44.translate(dx, dy, dz))
|
||||
return self
|
||||
|
||||
def virtual_entities(
|
||||
self, pdsize: float = 1, pdmode: int = 0
|
||||
) -> Iterator[DXFGraphic]:
|
||||
"""Yields the graphical representation of POINT as virtual DXF
|
||||
primitives (LINE and CIRCLE).
|
||||
The dimensionless point is rendered as zero-length line!
|
||||
|
||||
Check for this condition::
|
||||
|
||||
e.dxftype() == 'LINE' and e.dxf.start.isclose(e.dxf.end)
|
||||
|
||||
if the rendering engine can't handle zero-length lines.
|
||||
|
||||
Args:
|
||||
pdsize: point size in drawing units
|
||||
pdmode: point styling mode
|
||||
|
||||
"""
|
||||
from ezdxf.render import point
|
||||
for e in point.virtual_entities(self, pdsize, pdmode):
|
||||
e.set_source_of_copy(self)
|
||||
yield e
|
||||
|
||||
def ocs(self) -> OCS:
|
||||
# WCS entity which supports the "extrusion" attribute in a
|
||||
# different way!
|
||||
return OCS()
|
||||
@@ -0,0 +1,463 @@
|
||||
# Copyright (c) 2019-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Sequence, Optional, Union, TYPE_CHECKING, Iterator
|
||||
from typing_extensions import Self
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from ezdxf.audit import Auditor, AuditError
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf import colors
|
||||
from ezdxf.tools import pattern
|
||||
from ezdxf.math import Vec3, Matrix44
|
||||
from ezdxf.math.transformtools import OCSTransform
|
||||
from .boundary_paths import BoundaryPaths
|
||||
from .dxfns import SubclassProcessor, DXFNamespace
|
||||
from .dxfgfx import DXFGraphic
|
||||
from .gradient import Gradient
|
||||
from .pattern import Pattern, PatternLine
|
||||
from .dxfentity import DXFEntity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf import xref
|
||||
|
||||
RGB = colors.RGB
|
||||
|
||||
__all__ = ["DXFPolygon"]
|
||||
|
||||
PATH_CODES = {
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
40,
|
||||
42,
|
||||
50,
|
||||
51,
|
||||
42,
|
||||
72,
|
||||
73,
|
||||
74,
|
||||
92,
|
||||
93,
|
||||
94,
|
||||
95,
|
||||
96,
|
||||
97,
|
||||
330,
|
||||
}
|
||||
PATTERN_DEFINITION_LINE_CODES = {53, 43, 44, 45, 46, 79, 49}
|
||||
|
||||
|
||||
class DXFPolygon(DXFGraphic):
|
||||
"""Base class for the HATCH and the MPOLYGON entity."""
|
||||
|
||||
LOAD_GROUP_CODES: dict[int, Union[str, list[str]]] = {}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.paths = BoundaryPaths()
|
||||
self.pattern: Optional[Pattern] = None
|
||||
self.gradient: Optional[Gradient] = None
|
||||
self.seeds: list[tuple[float, float]] = [] # not supported/exported by MPOLYGON
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy paths, pattern, gradient, seeds."""
|
||||
assert isinstance(entity, DXFPolygon)
|
||||
entity.paths = copy.deepcopy(self.paths)
|
||||
entity.pattern = copy.deepcopy(self.pattern)
|
||||
entity.gradient = copy.deepcopy(self.gradient)
|
||||
entity.seeds = copy.deepcopy(self.seeds)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
# Copy without subclass marker:
|
||||
tags = Tags(processor.subclasses[2][1:])
|
||||
# Removes boundary path data from tags:
|
||||
tags = self.load_paths(tags)
|
||||
# Removes gradient data from tags:
|
||||
tags = self.load_gradient(tags)
|
||||
# Removes pattern from tags:
|
||||
tags = self.load_pattern(tags)
|
||||
# Removes seeds from tags:
|
||||
tags = self.load_seeds(tags)
|
||||
|
||||
# Load HATCH DXF attributes from remaining tags:
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, self.LOAD_GROUP_CODES, subclass=tags, recover=True
|
||||
)
|
||||
return dxf
|
||||
|
||||
def load_paths(self, tags: Tags) -> Tags:
|
||||
# Find first group code 91 = count of loops, Spline data also contains
|
||||
# group code 91!
|
||||
try:
|
||||
start_index = tags.tag_index(91)
|
||||
except const.DXFValueError:
|
||||
raise const.DXFStructureError(
|
||||
f"{self.dxftype()}: Missing required DXF tag 'Number of "
|
||||
f"boundary paths (loops)' (code=91)."
|
||||
)
|
||||
|
||||
path_tags = tags.collect_consecutive_tags(PATH_CODES, start=start_index + 1)
|
||||
if len(path_tags):
|
||||
self.paths = BoundaryPaths.load_tags(path_tags)
|
||||
end_index = start_index + len(path_tags) + 1
|
||||
del tags[start_index:end_index]
|
||||
return tags
|
||||
|
||||
def load_pattern(self, tags: Tags) -> Tags:
|
||||
try:
|
||||
# Group code 78 = Number of pattern definition lines
|
||||
index = tags.tag_index(78)
|
||||
except const.DXFValueError:
|
||||
# No pattern definition lines found.
|
||||
return tags
|
||||
|
||||
pattern_tags = tags.collect_consecutive_tags(
|
||||
PATTERN_DEFINITION_LINE_CODES, start=index + 1
|
||||
)
|
||||
self.pattern = Pattern.load_tags(pattern_tags)
|
||||
|
||||
# Delete pattern data including length tag 78
|
||||
del tags[index : index + len(pattern_tags) + 1]
|
||||
return tags
|
||||
|
||||
def load_gradient(self, tags: Tags) -> Tags:
|
||||
try:
|
||||
index = tags.tag_index(450)
|
||||
except const.DXFValueError:
|
||||
# No gradient data present
|
||||
return tags
|
||||
|
||||
# Gradient data is always at the end of the AcDbHatch subclass.
|
||||
self.gradient = Gradient.load_tags(tags[index:]) # type: ignore
|
||||
# Remove gradient data from tags
|
||||
del tags[index:]
|
||||
return tags
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
assert isinstance(clone, DXFPolygon)
|
||||
assert clone.doc is not None
|
||||
|
||||
super().map_resources(clone, mapping)
|
||||
db = clone.doc.entitydb
|
||||
for path in clone.paths:
|
||||
handles = [mapping.get_handle(h) for h in path.source_boundary_objects]
|
||||
path.source_boundary_objects = [h for h in handles if h in db]
|
||||
|
||||
def load_seeds(self, tags: Tags) -> Tags:
|
||||
return tags
|
||||
|
||||
@property
|
||||
def has_solid_fill(self) -> bool:
|
||||
"""``True`` if entity has a solid fill. (read only)"""
|
||||
return bool(self.dxf.solid_fill)
|
||||
|
||||
@property
|
||||
def has_pattern_fill(self) -> bool:
|
||||
"""``True`` if entity has a pattern fill. (read only)"""
|
||||
return not bool(self.dxf.solid_fill)
|
||||
|
||||
@property
|
||||
def has_gradient_data(self) -> bool:
|
||||
"""``True`` if entity has a gradient fill. A hatch with gradient fill
|
||||
has also a solid fill. (read only)
|
||||
"""
|
||||
return bool(self.gradient)
|
||||
|
||||
@property
|
||||
def bgcolor(self) -> Optional[RGB]:
|
||||
"""
|
||||
Set pattern fill background color as (r, g, b)-tuple, rgb values
|
||||
in the range [0, 255] (read/write/del)
|
||||
|
||||
usage::
|
||||
|
||||
r, g, b = entity.bgcolor # get pattern fill background color
|
||||
entity.bgcolor = (10, 20, 30) # set pattern fill background color
|
||||
del entity.bgcolor # delete pattern fill background color
|
||||
|
||||
"""
|
||||
try:
|
||||
xdata_bgcolor = self.get_xdata("HATCHBACKGROUNDCOLOR")
|
||||
except const.DXFValueError:
|
||||
return None
|
||||
color = xdata_bgcolor.get_first_value(1071, 0)
|
||||
try:
|
||||
return colors.int2rgb(int(color))
|
||||
except ValueError: # invalid data type
|
||||
return RGB(0, 0, 0)
|
||||
|
||||
@bgcolor.setter
|
||||
def bgcolor(self, rgb: RGB) -> None:
|
||||
color_value = (
|
||||
colors.rgb2int(rgb) | -0b111110000000000000000000000000
|
||||
) # it's magic
|
||||
|
||||
self.discard_xdata("HATCHBACKGROUNDCOLOR")
|
||||
self.set_xdata("HATCHBACKGROUNDCOLOR", [(1071, color_value)])
|
||||
|
||||
@bgcolor.deleter
|
||||
def bgcolor(self) -> None:
|
||||
self.discard_xdata("HATCHBACKGROUNDCOLOR")
|
||||
|
||||
def set_gradient(
|
||||
self,
|
||||
color1: RGB = RGB(0, 0, 0),
|
||||
color2: RGB = RGB(255, 255, 255),
|
||||
rotation: float = 0.0,
|
||||
centered: float = 0.0,
|
||||
one_color: int = 0,
|
||||
tint: float = 0.0,
|
||||
name: str = "LINEAR",
|
||||
) -> None:
|
||||
"""Sets the gradient fill mode and removes all pattern fill related data, requires
|
||||
DXF R2004 or newer. A gradient filled hatch is also a solid filled hatch.
|
||||
|
||||
Valid gradient type names are:
|
||||
|
||||
- "LINEAR"
|
||||
- "CYLINDER"
|
||||
- "INVCYLINDER"
|
||||
- "SPHERICAL"
|
||||
- "INVSPHERICAL"
|
||||
- "HEMISPHERICAL"
|
||||
- "INVHEMISPHERICAL"
|
||||
- "CURVED"
|
||||
- "INVCURVED"
|
||||
|
||||
Args:
|
||||
color1: (r, g, b)-tuple for first color, rgb values as int in
|
||||
the range [0, 255]
|
||||
color2: (r, g, b)-tuple for second color, rgb values as int in
|
||||
the range [0, 255]
|
||||
rotation: rotation angle in degrees
|
||||
centered: determines whether the gradient is centered or not
|
||||
one_color: 1 for gradient from `color1` to tinted `color1`
|
||||
tint: determines the tinted target `color1` for a one color
|
||||
gradient. (valid range 0.0 to 1.0)
|
||||
name: name of gradient type, default "LINEAR"
|
||||
|
||||
"""
|
||||
if self.doc is not None and self.doc.dxfversion < const.DXF2004:
|
||||
raise const.DXFVersionError("Gradient support requires DXF R2004")
|
||||
if name and name not in const.GRADIENT_TYPES:
|
||||
raise const.DXFValueError(f"Invalid gradient type name: {name}")
|
||||
|
||||
self.pattern = None
|
||||
self.dxf.solid_fill = 1
|
||||
self.dxf.pattern_name = "SOLID"
|
||||
self.dxf.pattern_type = const.HATCH_TYPE_PREDEFINED
|
||||
|
||||
gradient = Gradient()
|
||||
gradient.color1 = color1
|
||||
gradient.color2 = color2
|
||||
gradient.one_color = one_color
|
||||
gradient.rotation = rotation
|
||||
gradient.centered = centered
|
||||
gradient.tint = tint
|
||||
gradient.name = name
|
||||
self.gradient = gradient
|
||||
|
||||
def set_pattern_fill(
|
||||
self,
|
||||
name: str,
|
||||
color: int = 7,
|
||||
angle: float = 0.0,
|
||||
scale: float = 1.0,
|
||||
double: int = 0,
|
||||
style: int = 1,
|
||||
pattern_type: int = 1,
|
||||
definition=None,
|
||||
) -> None:
|
||||
"""Sets the pattern fill mode and removes all gradient related data.
|
||||
|
||||
The pattern definition should be designed for a scale factor 1 and a rotation
|
||||
angle of 0 degrees. The predefined hatch pattern like "ANSI33" are scaled
|
||||
according to the HEADER variable $MEASUREMENT for ISO measurement (m, cm, ... ),
|
||||
or imperial units (in, ft, ...), this replicates the behavior of BricsCAD.
|
||||
|
||||
Args:
|
||||
name: pattern name as string
|
||||
color: pattern color as :ref:`ACI`
|
||||
angle: pattern rotation angle in degrees
|
||||
scale: pattern scale factor
|
||||
double: double size flag
|
||||
style: hatch style (0 = normal; 1 = outer; 2 = ignore)
|
||||
pattern_type: pattern type (0 = user-defined;
|
||||
1 = predefined; 2 = custom)
|
||||
definition: list of definition lines and a definition line is a
|
||||
4-tuple [angle, base_point, offset, dash_length_items],
|
||||
see :meth:`set_pattern_definition`
|
||||
|
||||
"""
|
||||
self.gradient = None
|
||||
self.dxf.solid_fill = 0
|
||||
self.dxf.pattern_name = name
|
||||
self.dxf.color = color
|
||||
self.dxf.pattern_scale = float(scale)
|
||||
self.dxf.pattern_angle = float(angle)
|
||||
self.dxf.pattern_double = int(double)
|
||||
self.dxf.hatch_style = style
|
||||
self.dxf.pattern_type = pattern_type
|
||||
|
||||
if definition is None:
|
||||
measurement = 1
|
||||
if self.doc:
|
||||
measurement = self.doc.header.get("$MEASUREMENT", measurement)
|
||||
predefined_pattern = (
|
||||
pattern.ISO_PATTERN if measurement else pattern.IMPERIAL_PATTERN
|
||||
)
|
||||
definition = predefined_pattern.get(name, predefined_pattern["ANSI31"])
|
||||
self.set_pattern_definition(
|
||||
definition,
|
||||
factor=self.dxf.pattern_scale,
|
||||
angle=self.dxf.pattern_angle,
|
||||
)
|
||||
|
||||
def set_pattern_definition(
|
||||
self, lines: Sequence, factor: float = 1, angle: float = 0
|
||||
) -> None:
|
||||
"""Setup pattern definition by a list of definition lines and the
|
||||
definition line is a 4-tuple (angle, base_point, offset, dash_length_items).
|
||||
The pattern definition should be designed for a pattern scale factor of 1 and
|
||||
a pattern rotation angle of 0.
|
||||
|
||||
- angle: line angle in degrees
|
||||
- base-point: (x, y) tuple
|
||||
- offset: (dx, dy) tuple
|
||||
- dash_length_items: list of dash items (item > 0 is a line,
|
||||
item < 0 is a gap and item == 0.0 is a point)
|
||||
|
||||
Args:
|
||||
lines: list of definition lines
|
||||
factor: pattern scale factor
|
||||
angle: rotation angle in degrees
|
||||
|
||||
"""
|
||||
if factor != 1 or angle:
|
||||
lines = pattern.scale_pattern(lines, factor=factor, angle=angle)
|
||||
self.pattern = Pattern(
|
||||
[PatternLine(line[0], line[1], line[2], line[3]) for line in lines]
|
||||
)
|
||||
|
||||
def set_pattern_scale(self, scale: float) -> None:
|
||||
"""Sets the pattern scale factor and scales the pattern definition.
|
||||
|
||||
The method always starts from the original base scale, the
|
||||
:code:`set_pattern_scale(1)` call resets the pattern scale to the original
|
||||
appearance as defined by the pattern designer, but only if the pattern attribute
|
||||
:attr:`dxf.pattern_scale` represents the actual scale, it cannot
|
||||
restore the original pattern scale from the pattern definition itself.
|
||||
|
||||
Args:
|
||||
scale: pattern scale factor
|
||||
|
||||
"""
|
||||
if not self.has_pattern_fill:
|
||||
return
|
||||
dxf = self.dxf
|
||||
self.pattern.scale(factor=1.0 / dxf.pattern_scale * scale) # type: ignore
|
||||
dxf.pattern_scale = scale
|
||||
|
||||
def set_pattern_angle(self, angle: float) -> None:
|
||||
"""Sets the pattern rotation angle and rotates the pattern definition.
|
||||
|
||||
The method always starts from the original base rotation of 0, the
|
||||
:code:`set_pattern_angle(0)` call resets the pattern rotation angle to the
|
||||
original appearance as defined by the pattern designer, but only if the
|
||||
pattern attribute :attr:`dxf.pattern_angle` represents the actual pattern
|
||||
rotation, it cannot restore the original rotation angle from the
|
||||
pattern definition itself.
|
||||
|
||||
Args:
|
||||
angle: pattern rotation angle in degrees
|
||||
|
||||
"""
|
||||
if not self.has_pattern_fill:
|
||||
return
|
||||
dxf = self.dxf
|
||||
self.pattern.scale(angle=angle - dxf.pattern_angle) # type: ignore
|
||||
dxf.pattern_angle = angle % 360.0
|
||||
|
||||
def transform(self, m: Matrix44) -> DXFPolygon:
|
||||
"""Transform entity by transformation matrix `m` inplace."""
|
||||
dxf = self.dxf
|
||||
ocs = OCSTransform(dxf.extrusion, m)
|
||||
|
||||
elevation = Vec3(dxf.elevation).z
|
||||
self.paths.transform(ocs, elevation=elevation)
|
||||
dxf.elevation = ocs.transform_vertex(Vec3(0, 0, elevation)).replace(
|
||||
x=0.0, y=0.0
|
||||
)
|
||||
dxf.extrusion = ocs.new_extrusion
|
||||
if self.pattern:
|
||||
# todo: non-uniform scaling
|
||||
# take the scaling factor of the x-axis
|
||||
factor = ocs.transform_length((self.dxf.pattern_scale, 0, 0))
|
||||
angle = ocs.transform_deg_angle(self.dxf.pattern_angle)
|
||||
# todo: non-uniform pattern scaling is not supported
|
||||
self.pattern.scale(factor, angle)
|
||||
self.dxf.pattern_scale = factor
|
||||
self.dxf.pattern_angle = angle
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def triangulate(self, max_sagitta, min_segments=16) -> Iterator[Sequence[Vec3]]:
|
||||
"""Triangulate the HATCH/MPOLYGON in OCS coordinates, Elevation and offset is
|
||||
applied to all vertices.
|
||||
|
||||
Args:
|
||||
max_sagitta: maximum distance from the center of the curve to the
|
||||
center of the line segment between two approximation points to determine
|
||||
if a segment should be subdivided.
|
||||
min_segments: minimum segment count per Bézier curve
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
"""
|
||||
from ezdxf import path
|
||||
|
||||
elevation = Vec3(self.dxf.elevation)
|
||||
if self.dxf.hasattr("offset"): # MPOLYGON
|
||||
elevation += Vec3(self.dxf.offset) # offset in OCS?
|
||||
boundary_paths = [path.from_hatch_boundary_path(p) for p in self.paths]
|
||||
for vertices in path.triangulate(boundary_paths, max_sagitta, min_segments):
|
||||
yield tuple(elevation + v for v in vertices)
|
||||
|
||||
def render_pattern_lines(self) -> Iterator[tuple[Vec3, Vec3]]:
|
||||
"""Yields the pattern lines in WCS coordinates.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
|
||||
"""
|
||||
from ezdxf.render import hatching
|
||||
|
||||
if self.has_pattern_fill:
|
||||
try:
|
||||
yield from hatching.hatch_entity(self)
|
||||
except hatching.HatchingError:
|
||||
return
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_solid_fill(self, color: int = 7, style: int = 1, rgb: Optional[RGB] = None):
|
||||
...
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
super().audit(auditor)
|
||||
if not self.is_alive:
|
||||
return
|
||||
if not self.paths.is_valid():
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_HATCH_BOUNDARY_PATH,
|
||||
message=f"Deleted entity {str(self)} containing invalid boundary paths."
|
||||
)
|
||||
auditor.trash(self)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,138 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
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 DXF12, SUBCLASS_MARKER
|
||||
from ezdxf.math import NULLVEC, Z_AXIS
|
||||
from ezdxf.math.transformtools import OCSTransform
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import (
|
||||
DXFGraphic,
|
||||
acdb_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.math import Matrix44
|
||||
|
||||
|
||||
__all__ = ["Shape"]
|
||||
|
||||
# Description of the "name" attribute from the DWG documentation: 20.4.37 SHAPE (33)
|
||||
# In DXF the shape name is stored. When reading from DXF, the shape is found by
|
||||
# iterating over all the text styles and when the text style contains a shape file,
|
||||
# iterating over all the shapes until the one with the matching name is found.
|
||||
|
||||
acdb_shape = DefSubclass(
|
||||
"AcDbShape",
|
||||
{
|
||||
# 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 could be negative:
|
||||
"thickness": DXFAttr(39, default=0, optional=True),
|
||||
# Insertion point (in WCS)
|
||||
"insert": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# Shape size:
|
||||
"size": DXFAttr(40, default=1),
|
||||
# Shape name:
|
||||
"name": DXFAttr(2, default=""),
|
||||
# Rotation angle in degrees:
|
||||
"rotation": DXFAttr(50, default=0, optional=True),
|
||||
# Relative X scale factor
|
||||
"xscale": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Oblique angle in degrees:
|
||||
"oblique": DXFAttr(51, 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_shape_group_codes = group_code_mapping(acdb_shape)
|
||||
merged_shape_group_codes = merge_group_code_mappings(
|
||||
acdb_entity_group_codes, acdb_shape_group_codes # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Shape(DXFGraphic):
|
||||
"""DXF SHAPE entity"""
|
||||
|
||||
DXFTYPE = "SHAPE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_shape)
|
||||
|
||||
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_shape_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_shape.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"insert",
|
||||
"size",
|
||||
"name",
|
||||
"thickness",
|
||||
"rotation",
|
||||
"xscale",
|
||||
"oblique",
|
||||
"extrusion",
|
||||
],
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> Shape:
|
||||
"""Transform the SHAPE entity by transformation matrix `m` inplace."""
|
||||
dxf = self.dxf
|
||||
dxf.insert = m.transform(dxf.insert) # DXF Reference: WCS?
|
||||
ocs = OCSTransform(self.dxf.extrusion, m)
|
||||
|
||||
dxf.rotation = ocs.transform_deg_angle(dxf.rotation)
|
||||
dxf.size = ocs.transform_length((0, dxf.size, 0))
|
||||
dxf.x_scale = ocs.transform_length(
|
||||
(dxf.x_scale, 0, 0), reflection=dxf.x_scale
|
||||
)
|
||||
if dxf.hasattr("thickness"):
|
||||
dxf.thickness = ocs.transform_thickness(dxf.thickness)
|
||||
|
||||
dxf.extrusion = ocs.new_extrusion
|
||||
self.post_transform(m)
|
||||
return self
|
||||
@@ -0,0 +1,279 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
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 DXF12, SUBCLASS_MARKER, VERTEXNAMES
|
||||
from ezdxf.math import Matrix44, Z_AXIS, NULLVEC, Vec3
|
||||
from ezdxf.math.transformtools import OCSTransform
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import (
|
||||
DXFGraphic,
|
||||
acdb_entity,
|
||||
elevation_to_z_axis,
|
||||
acdb_entity_group_codes,
|
||||
)
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.entities import DXFNamespace
|
||||
|
||||
__all__ = ["Solid", "Trace", "Face3d"]
|
||||
|
||||
acdb_trace = DefSubclass(
|
||||
"AcDbTrace",
|
||||
{
|
||||
# IMPORTANT: all 4 vertices have to be present in the DXF file,
|
||||
# otherwise AutoCAD shows a DXF structure error and does not load the
|
||||
# file! (SOLID, TRACE and 3DFACE)
|
||||
# 1. corner Solid WCS; Trace OCS
|
||||
"vtx0": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# 2. corner Solid WCS; Trace OCS
|
||||
"vtx1": DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
|
||||
# 3. corner Solid WCS; Trace OCS
|
||||
"vtx2": DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
|
||||
# 4. corner Solid WCS; Trace OCS:
|
||||
# If only three corners are entered to define the SOLID, then the fourth
|
||||
# corner coordinate is the same as the third.
|
||||
"vtx3": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
|
||||
# IMPORTANT: for TRACE and SOLID the last two vertices are in reversed
|
||||
# order: a square has the vertex order 0-1-3-2
|
||||
# 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 could be negative:
|
||||
"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_trace_group_codes = group_code_mapping(acdb_trace)
|
||||
merged_trace_group_codes = merge_group_code_mappings(
|
||||
acdb_entity_group_codes, acdb_trace_group_codes # type: ignore
|
||||
)
|
||||
|
||||
|
||||
class _Base(DXFGraphic):
|
||||
def __getitem__(self, num):
|
||||
return self.dxf.get(VERTEXNAMES[num])
|
||||
|
||||
def __setitem__(self, num, value):
|
||||
return self.dxf.set(VERTEXNAMES[num], value)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Solid(_Base):
|
||||
"""DXF SHAPE entity"""
|
||||
|
||||
DXFTYPE = "SOLID"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_trace)
|
||||
|
||||
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_trace_group_codes)
|
||||
if processor.r12:
|
||||
# Transform elevation attribute from R11 to z-axis values:
|
||||
elevation_to_z_axis(dxf, VERTEXNAMES)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags. (internal API)"""
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_trace.name)
|
||||
if not self.dxf.hasattr("vtx3"):
|
||||
self.dxf.vtx3 = self.dxf.vtx2
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"vtx0",
|
||||
"vtx1",
|
||||
"vtx2",
|
||||
"vtx3",
|
||||
"thickness",
|
||||
"extrusion",
|
||||
],
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> Solid:
|
||||
"""Transform the SOLID/TRACE entity by transformation matrix `m` inplace."""
|
||||
# SOLID and TRACE are OCS entities.
|
||||
dxf = self.dxf
|
||||
ocs = OCSTransform(self.dxf.extrusion, m)
|
||||
for name in VERTEXNAMES:
|
||||
if dxf.hasattr(name):
|
||||
dxf.set(name, ocs.transform_vertex(dxf.get(name)))
|
||||
if dxf.hasattr("thickness"):
|
||||
dxf.thickness = ocs.transform_thickness(dxf.thickness)
|
||||
dxf.extrusion = ocs.new_extrusion
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def wcs_vertices(self, close: bool = False) -> list[Vec3]:
|
||||
"""Returns WCS vertices in correct order,
|
||||
if argument `close` is ``True``, last vertex == first vertex.
|
||||
Does **not** return the duplicated last vertex if the entity represents
|
||||
a triangle.
|
||||
|
||||
"""
|
||||
ocs = self.ocs()
|
||||
return list(ocs.points_to_wcs(self.vertices(close)))
|
||||
|
||||
def vertices(self, close: bool = False) -> list[Vec3]:
|
||||
"""Returns OCS vertices in correct order,
|
||||
if argument `close` is ``True``, last vertex == first vertex.
|
||||
Does **not** return the duplicated last vertex if the entity represents
|
||||
a triangle.
|
||||
|
||||
"""
|
||||
dxf = self.dxf
|
||||
vertices: list[Vec3] = [dxf.vtx0, dxf.vtx1, dxf.vtx2]
|
||||
if dxf.vtx3 != dxf.vtx2: # face is not a triangle
|
||||
vertices.append(dxf.vtx3)
|
||||
|
||||
# adjust weird vertex order of SOLID and TRACE:
|
||||
# 0, 1, 2, 3 -> 0, 1, 3, 2
|
||||
if len(vertices) > 3:
|
||||
vertices[2], vertices[3] = vertices[3], vertices[2]
|
||||
|
||||
if close and not vertices[0].isclose(vertices[-1]):
|
||||
vertices.append(vertices[0])
|
||||
return vertices
|
||||
|
||||
|
||||
@register_entity
|
||||
class Trace(Solid):
|
||||
"""DXF TRACE entity"""
|
||||
|
||||
DXFTYPE = "TRACE"
|
||||
|
||||
|
||||
acdb_face = DefSubclass(
|
||||
"AcDbFace",
|
||||
{
|
||||
# 1. corner WCS:
|
||||
"vtx0": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# 2. corner WCS:
|
||||
"vtx1": DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
|
||||
# 3. corner WCS:
|
||||
"vtx2": DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
|
||||
# 4. corner WCS:
|
||||
# If only three corners are entered to define the SOLID, then the fourth
|
||||
# corner coordinate is the same as the third.
|
||||
"vtx3": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
|
||||
# invisible:
|
||||
# 1 = First edge is invisible
|
||||
# 2 = Second edge is invisible
|
||||
# 4 = Third edge is invisible
|
||||
# 8 = Fourth edge is invisible
|
||||
"invisible_edges": DXFAttr(70, default=0, optional=True),
|
||||
},
|
||||
)
|
||||
acdb_face_group_codes = group_code_mapping(acdb_face)
|
||||
merged_face_group_codes = merge_group_code_mappings(
|
||||
acdb_entity_group_codes, acdb_face_group_codes # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Face3d(_Base):
|
||||
"""DXF 3DFACE entity"""
|
||||
|
||||
# IMPORTANT: for 3DFACE the last two vertices are in regular order:
|
||||
# a square has the vertex order 0-1-2-3
|
||||
|
||||
DXFTYPE = "3DFACE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_face)
|
||||
|
||||
def is_invisible_edge(self, num: int) -> bool:
|
||||
"""Returns True if edge `num` is an invisible edge."""
|
||||
if num < 0 or num > 4:
|
||||
raise ValueError(f"invalid edge: {num}")
|
||||
return bool(self.dxf.invisible_edges & (1 << num))
|
||||
|
||||
def set_edge_visibility(self, num: int, visible: bool = False) -> None:
|
||||
"""Set visibility of edge `num`, status `True` for visible, status
|
||||
`False` for invisible.
|
||||
"""
|
||||
if num < 0 or num >= 4:
|
||||
raise ValueError(f"invalid edge: {num}")
|
||||
if not visible:
|
||||
self.dxf.invisible_edges = self.dxf.invisible_edges | (1 << num)
|
||||
else:
|
||||
self.dxf.invisible_edges = self.dxf.invisible_edges & ~(1 << num)
|
||||
|
||||
def get_edges_visibility(self) -> list[bool]:
|
||||
# if the face is a triangle, a fourth visibility flag
|
||||
# may be present but is ignored
|
||||
return [not self.is_invisible_edge(i) for i in range(4)]
|
||||
|
||||
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_face_group_codes)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_face.name)
|
||||
if not self.dxf.hasattr("vtx3"):
|
||||
self.dxf.vtx3 = self.dxf.vtx2
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter, ["vtx0", "vtx1", "vtx2", "vtx3", "invisible_edges"]
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> Face3d:
|
||||
"""Transform the 3DFACE entity by transformation matrix `m` inplace."""
|
||||
dxf = self.dxf
|
||||
# 3DFACE is a real 3d entity
|
||||
dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3 = m.transform_vertices(
|
||||
(dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3)
|
||||
)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def wcs_vertices(self, close: bool = False) -> list[Vec3]:
|
||||
"""Returns WCS vertices, if argument `close` is ``True``,
|
||||
the first vertex is also returned as closing last vertex.
|
||||
|
||||
Returns 4 vertices when `close` is ``False`` and 5 vertices when `close` is
|
||||
``True``. Some edges may have zero-length. This is a compatibility interface
|
||||
to SOLID and TRACE. The 3DFACE entity is already defined by WCS vertices.
|
||||
"""
|
||||
dxf = self.dxf
|
||||
vertices: list[Vec3] = [dxf.vtx0, dxf.vtx1, dxf.vtx2]
|
||||
vtx3 = dxf.get("vtx3")
|
||||
if (
|
||||
isinstance(vtx3, Vec3) and vtx3 != dxf.vtx2
|
||||
): # face is not a triangle
|
||||
vertices.append(vtx3)
|
||||
if close:
|
||||
vertices.append(vertices[0])
|
||||
return vertices
|
||||
@@ -0,0 +1,201 @@
|
||||
# Copyright (c) 2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional, Iterable
|
||||
from typing_extensions import Self
|
||||
import logging
|
||||
|
||||
from ezdxf.lldxf import const, validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
DXFAttr,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.math import UVec, Vec2, Matrix44, Z_AXIS, NULLVEC
|
||||
from ezdxf.entities import factory
|
||||
from .dxfentity import SubclassProcessor, base_class
|
||||
from .dxfobj import DXFObject
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["SpatialFilter"]
|
||||
|
||||
# The HEADER variable $XCLIPFRAME determines if the clipping path polygon is displayed
|
||||
# and plotted:
|
||||
# 0 - not displayed, not plotted
|
||||
# 1 - displayed, not plotted
|
||||
# 2 - displayed and plotted
|
||||
|
||||
logger = logging.getLogger("ezdxf")
|
||||
AcDbFilter = "AcDbFilter"
|
||||
AcDbSpatialFilter = "AcDbSpatialFilter"
|
||||
|
||||
acdb_filter = DefSubclass(AcDbFilter, {})
|
||||
acdb_spatial_filter = DefSubclass(
|
||||
AcDbSpatialFilter,
|
||||
{
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"origin": DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
|
||||
"is_clipping_enabled": DXFAttr(
|
||||
71, default=1, validator=validator.is_integer_bool, fixer=RETURN_DEFAULT
|
||||
),
|
||||
"has_front_clipping_plane": DXFAttr(
|
||||
72, default=0, validator=validator.is_integer_bool, fixer=RETURN_DEFAULT
|
||||
),
|
||||
"front_clipping_plane_distance": DXFAttr(40, default=0.0),
|
||||
"has_back_clipping_plane": DXFAttr(
|
||||
73, default=0, validator=validator.is_integer_bool, fixer=RETURN_DEFAULT
|
||||
),
|
||||
"back_clipping_plane_distance": DXFAttr(41, default=0.0),
|
||||
},
|
||||
)
|
||||
acdb_spatial_filter_group_codes = group_code_mapping(acdb_spatial_filter)
|
||||
|
||||
|
||||
@factory.register_entity
|
||||
class SpatialFilter(DXFObject):
|
||||
DXFTYPE = "SPATIAL_FILTER"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_filter, acdb_spatial_filter)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = const.DXF2000
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# clipping path vertices in OCS coordinates
|
||||
self._boundary_vertices: tuple[Vec2, ...] = tuple()
|
||||
|
||||
# This matrix is the inverse of the original block reference (insert entity)
|
||||
# transformation. The original block reference transformation is the one that
|
||||
# is applied to all entities in the block when the block reference is regenerated.
|
||||
self._inverse_insert_matrix = Matrix44()
|
||||
|
||||
# This matrix transforms points into the coordinate system of the clip boundary.
|
||||
self._transform_matrix = Matrix44()
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
assert isinstance(entity, SpatialFilter)
|
||||
# immutable data
|
||||
entity._boundary_vertices = self._boundary_vertices
|
||||
entity._inverse_insert_matrix = self._inverse_insert_matrix
|
||||
entity._transform_matrix = self._transform_matrix
|
||||
|
||||
@property
|
||||
def boundary_vertices(self) -> tuple[Vec2, ...]:
|
||||
"""Returns the clipping path vertices in OCS coordinates."""
|
||||
return self._boundary_vertices
|
||||
|
||||
def set_boundary_vertices(self, vertices: Iterable[UVec]) -> None:
|
||||
"""Set the clipping path vertices in OCS coordinates."""
|
||||
self._boundary_vertices = tuple(Vec2(v) for v in vertices)
|
||||
if len(self._boundary_vertices) < 2:
|
||||
raise const.DXFValueError("2 or more vertices required")
|
||||
|
||||
@property
|
||||
def inverse_insert_matrix(self) -> Matrix44:
|
||||
"""Returns the inverse insert matrix.
|
||||
|
||||
This matrix is the inverse of the original block reference (insert entity)
|
||||
transformation. The original block reference transformation is the one that
|
||||
is applied to all entities in the block when the block reference is regenerated.
|
||||
"""
|
||||
return self._inverse_insert_matrix.copy()
|
||||
|
||||
def set_inverse_insert_matrix(self, m: Matrix44) -> None:
|
||||
self._inverse_insert_matrix = m.copy()
|
||||
|
||||
@property
|
||||
def transform_matrix(self) -> Matrix44:
|
||||
"""Returns the transform matrix.
|
||||
|
||||
This matrix transforms points into the coordinate system of the clip boundary.
|
||||
"""
|
||||
return self._transform_matrix.copy()
|
||||
|
||||
def set_transform_matrix(self, m: Matrix44) -> None:
|
||||
self._transform_matrix = m.copy()
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.find_subclass(AcDbSpatialFilter)
|
||||
if tags:
|
||||
try:
|
||||
self._load_boundary_data(tags)
|
||||
except IndexError:
|
||||
logger.warning(
|
||||
f"Not enough matrix values in SPATIAL_FILTER(#{processor.handle})"
|
||||
)
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_spatial_filter_group_codes, subclass=tags
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Required subclass 'AcDbSpatialFilter' in object "
|
||||
f"SPATIAL_FILTER(#{processor.handle}) not present"
|
||||
)
|
||||
return dxf
|
||||
|
||||
def _load_boundary_data(self, tags: Tags) -> None:
|
||||
def to_matrix(v: list[float]) -> Matrix44:
|
||||
# raises IndexError if not enough matrix values exist
|
||||
return Matrix44(
|
||||
# fmt: off
|
||||
[
|
||||
v[0], v[4], v[8], 0.0,
|
||||
v[1], v[5], v[9], 0.0,
|
||||
v[2], v[6], v[10], 0.0,
|
||||
v[3], v[7], v[11], 1.0,
|
||||
]
|
||||
# fmt: on
|
||||
)
|
||||
|
||||
self._boundary_vertices = tuple(Vec2(tag.value) for tag in tags.find_all(10))
|
||||
matrix_values = [float(tag.value) for tag in tags.find_all(40)]
|
||||
# use only last 24 values
|
||||
self._inverse_insert_matrix = to_matrix(matrix_values[-24:-12])
|
||||
self._transform_matrix = to_matrix(matrix_values[-12:])
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
|
||||
def write_matrix(m: Matrix44) -> None:
|
||||
for index in range(3):
|
||||
for value in m.get_col(index):
|
||||
tagwriter.write_tag2(40, value)
|
||||
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(const.SUBCLASS_MARKER, AcDbFilter)
|
||||
tagwriter.write_tag2(const.SUBCLASS_MARKER, AcDbSpatialFilter)
|
||||
tagwriter.write_tag2(70, len(self._boundary_vertices))
|
||||
for vertex in self._boundary_vertices:
|
||||
tagwriter.write_vertex(10, vertex)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter, ["extrusion", "origin", "is_clipping_enabled"]
|
||||
)
|
||||
has_front_clipping = self.dxf.has_front_clipping_plane
|
||||
tagwriter.write_tag2(72, has_front_clipping)
|
||||
if has_front_clipping:
|
||||
# AutoCAD does no accept tag 40, if front clipping is disabled
|
||||
tagwriter.write_tag2(40, self.dxf.front_clipping_plane_distance)
|
||||
has_back_clipping = self.dxf.has_back_clipping_plane
|
||||
tagwriter.write_tag2(73, has_back_clipping)
|
||||
if has_back_clipping:
|
||||
# AutoCAD does no accept tag 41, if back clipping is disabled
|
||||
tagwriter.write_tag2(41, self.dxf.back_clipping_plane_distance)
|
||||
|
||||
write_matrix(self._inverse_insert_matrix)
|
||||
write_matrix(self._transform_matrix)
|
||||
@@ -0,0 +1,629 @@
|
||||
# Copyright (c) 2019-2025 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
List,
|
||||
Iterable,
|
||||
Sequence,
|
||||
cast,
|
||||
Iterator,
|
||||
Optional,
|
||||
)
|
||||
from typing_extensions import TypeAlias, Self
|
||||
import array
|
||||
import copy
|
||||
from ezdxf.audit import AuditError
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import (
|
||||
SUBCLASS_MARKER,
|
||||
DXF2000,
|
||||
DXFValueError,
|
||||
DXFStructureError,
|
||||
)
|
||||
from ezdxf.lldxf.packedtags import VertexArray, Tags
|
||||
from ezdxf.math import (
|
||||
Vec3,
|
||||
UVec,
|
||||
Matrix44,
|
||||
ConstructionEllipse,
|
||||
Z_AXIS,
|
||||
NULLVEC,
|
||||
OCS,
|
||||
uniform_knot_vector,
|
||||
open_uniform_knot_vector,
|
||||
BSpline,
|
||||
required_knot_values,
|
||||
required_fit_points,
|
||||
required_control_points,
|
||||
fit_points_to_cad_cv,
|
||||
round_knots,
|
||||
)
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace, Ellipse
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.audit import Auditor
|
||||
|
||||
__all__ = ["Spline"]
|
||||
|
||||
# From the Autodesk ObjectARX reference:
|
||||
# Objects of the AcDbSpline class use an embedded gelib object to maintain the
|
||||
# actual spline information.
|
||||
#
|
||||
# Book recommendations:
|
||||
#
|
||||
# - "Curves and Surfaces for CAGD" by Gerald Farin
|
||||
# - "Mathematical Elements for Computer Graphics"
|
||||
# by David Rogers and Alan Adams
|
||||
# - "An Introduction To Splines For Use In Computer Graphics & Geometric Modeling"
|
||||
# by Richard H. Bartels, John C. Beatty, and Brian A Barsky
|
||||
#
|
||||
# http://help.autodesk.com/view/OARX/2018/ENU/?guid=OREF-AcDbSpline__setFitData_AcGePoint3dArray__AcGeVector3d__AcGeVector3d__AcGe__KnotParameterization_int_double
|
||||
# Construction of a AcDbSpline entity from fit points:
|
||||
# degree has no effect. A spline with degree=3 is always constructed when
|
||||
# interpolating a series of fit points.
|
||||
|
||||
acdb_spline = DefSubclass(
|
||||
"AcDbSpline",
|
||||
{
|
||||
# Spline flags:
|
||||
# 1 = Closed spline
|
||||
# 2 = Periodic spline
|
||||
# 4 = Rational spline
|
||||
# 8 = Planar
|
||||
# 16 = Linear (planar bit is also set)
|
||||
"flags": DXFAttr(70, default=0),
|
||||
# degree: The degree can't be higher than 11 according to the Autodesk
|
||||
# ObjectARX reference.
|
||||
"degree": DXFAttr(71, default=3, validator=validator.is_positive),
|
||||
"n_knots": DXFAttr(72, xtype=XType.callback, getter="knot_count"),
|
||||
"n_control_points": DXFAttr(
|
||||
73, xtype=XType.callback, getter="control_point_count"
|
||||
),
|
||||
"n_fit_points": DXFAttr(74, xtype=XType.callback, getter="fit_point_count"),
|
||||
"knot_tolerance": DXFAttr(42, default=1e-10, optional=True),
|
||||
"control_point_tolerance": DXFAttr(43, default=1e-10, optional=True),
|
||||
"fit_tolerance": DXFAttr(44, default=1e-10, optional=True),
|
||||
# Start- and end tangents should be normalized, but CAD applications do not
|
||||
# crash if they are not normalized.
|
||||
"start_tangent": DXFAttr(
|
||||
12,
|
||||
xtype=XType.point3d,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
),
|
||||
"end_tangent": DXFAttr(
|
||||
13,
|
||||
xtype=XType.point3d,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
),
|
||||
# Extrusion is the normal vector (omitted if the spline is non-planar)
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 10: Control points (in WCS); one entry per control point
|
||||
# 11: Fit points (in WCS); one entry per fit point
|
||||
# 40: Knot value (one entry per knot)
|
||||
# 41: Weight (if not 1); with multiple group pairs, they are present if all
|
||||
# are not 1
|
||||
},
|
||||
)
|
||||
acdb_spline_group_codes = group_code_mapping(acdb_spline)
|
||||
|
||||
|
||||
class SplineData:
|
||||
def __init__(self, spline: Spline):
|
||||
self.fit_points = spline.fit_points
|
||||
self.control_points = spline.control_points
|
||||
self.knots = spline.knots
|
||||
self.weights = spline.weights
|
||||
|
||||
|
||||
REMOVE_CODES = {10, 11, 40, 41, 72, 73, 74}
|
||||
|
||||
Vertices: TypeAlias = List[Sequence[float]]
|
||||
|
||||
|
||||
@register_entity
|
||||
class Spline(DXFGraphic):
|
||||
"""DXF SPLINE entity"""
|
||||
|
||||
DXFTYPE = "SPLINE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_spline)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
CLOSED = 1 # closed b-spline
|
||||
PERIODIC = 2 # uniform b-spline
|
||||
RATIONAL = 4 # rational b-spline
|
||||
PLANAR = 8 # all spline points in a plane, don't read or set this bit, just ignore like AutoCAD
|
||||
LINEAR = 16 # always set with PLANAR, don't read or set this bit, just ignore like AutoCAD
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.fit_points = VertexArray()
|
||||
self.control_points = VertexArray()
|
||||
self.knots = []
|
||||
self.weights = []
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy data: control_points, fit_points, weights, knot_values."""
|
||||
assert isinstance(entity, Spline)
|
||||
entity._control_points = copy.deepcopy(self._control_points)
|
||||
entity._fit_points = copy.deepcopy(self._fit_points)
|
||||
entity._knots = copy.deepcopy(self._knots)
|
||||
entity._weights = copy.deepcopy(self._weights)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.subclass_by_index(2)
|
||||
if tags:
|
||||
tags = Tags(self.load_spline_data(tags))
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_spline_group_codes, subclass=tags, recover=True
|
||||
)
|
||||
else:
|
||||
raise DXFStructureError(
|
||||
f"missing 'AcDbSpline' subclass in SPLINE(#{dxf.handle})"
|
||||
)
|
||||
return dxf
|
||||
|
||||
def load_spline_data(self, tags) -> Iterator:
|
||||
"""Load and set spline data (fit points, control points, weights,
|
||||
knots) and remove invalid start- and end tangents.
|
||||
Yields the remaining unprocessed tags.
|
||||
"""
|
||||
control_points = []
|
||||
fit_points = []
|
||||
knots = []
|
||||
weights = []
|
||||
for tag in tags:
|
||||
code, value = tag
|
||||
if code == 10:
|
||||
control_points.append(value)
|
||||
elif code == 11:
|
||||
fit_points.append(value)
|
||||
elif code == 40:
|
||||
knots.append(value)
|
||||
elif code == 41:
|
||||
weights.append(value)
|
||||
elif code in (12, 13) and NULLVEC.isclose(value):
|
||||
# Tangent values equal to (0, 0, 0) are invalid and ignored at
|
||||
# the loading stage!
|
||||
pass
|
||||
else:
|
||||
yield tag
|
||||
self.control_points = control_points
|
||||
self.fit_points = fit_points
|
||||
self.knots = knots
|
||||
self.weights = weights
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_spline.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["extrusion", "flags", "degree"])
|
||||
tagwriter.write_tag2(72, self.knot_count())
|
||||
tagwriter.write_tag2(73, self.control_point_count())
|
||||
tagwriter.write_tag2(74, self.fit_point_count())
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"knot_tolerance",
|
||||
"control_point_tolerance",
|
||||
"fit_tolerance",
|
||||
"start_tangent",
|
||||
"end_tangent",
|
||||
],
|
||||
)
|
||||
|
||||
self.export_spline_data(tagwriter)
|
||||
|
||||
def export_spline_data(self, tagwriter: AbstractTagWriter):
|
||||
for value in self._knots:
|
||||
tagwriter.write_tag2(40, value)
|
||||
|
||||
if len(self._weights):
|
||||
for value in self._weights:
|
||||
tagwriter.write_tag2(41, value)
|
||||
|
||||
self._control_points.export_dxf(tagwriter, code=10) # type: ignore
|
||||
self._fit_points.export_dxf(tagwriter, code=11) # type: ignore
|
||||
|
||||
@property
|
||||
def closed(self) -> bool:
|
||||
"""``True`` if spline is closed. A closed spline has a connection from
|
||||
the last control point to the first control point. (read/write)
|
||||
"""
|
||||
return self.get_flag_state(self.CLOSED, name="flags")
|
||||
|
||||
@closed.setter
|
||||
def closed(self, status: bool) -> None:
|
||||
self.set_flag_state(self.CLOSED, state=status, name="flags")
|
||||
|
||||
@property
|
||||
def knots(self) -> list[float]:
|
||||
"""Knot values as :code:`array.array('d')`."""
|
||||
return self._knots
|
||||
|
||||
@knots.setter
|
||||
def knots(self, values: Iterable[float]) -> None:
|
||||
self._knots: list[float] = cast(List[float], array.array("d", values))
|
||||
|
||||
# DXF callback attribute Spline.dxf.n_knots
|
||||
def knot_count(self) -> int:
|
||||
"""Count of knot values."""
|
||||
return len(self._knots)
|
||||
|
||||
@property
|
||||
def weights(self) -> list[float]:
|
||||
"""Control point weights as :code:`array.array('d')`."""
|
||||
return self._weights
|
||||
|
||||
@weights.setter
|
||||
def weights(self, values: Iterable[float]) -> None:
|
||||
self._weights: list[float] = cast(List[float], array.array("d", values))
|
||||
|
||||
@property
|
||||
def control_points(self) -> Vertices:
|
||||
""":class:`~ezdxf.lldxf.packedtags.VertexArray` of control points in
|
||||
:ref:`WCS`.
|
||||
"""
|
||||
return self._control_points
|
||||
|
||||
@control_points.setter
|
||||
def control_points(self, points: Iterable[UVec]) -> None:
|
||||
self._control_points: Vertices = cast(Vertices, VertexArray(Vec3.list(points)))
|
||||
|
||||
# DXF callback attribute Spline.dxf.n_control_points
|
||||
def control_point_count(self) -> int:
|
||||
"""Count of control points."""
|
||||
return len(self.control_points)
|
||||
|
||||
@property
|
||||
def fit_points(self) -> Vertices:
|
||||
""":class:`~ezdxf.lldxf.packedtags.VertexArray` of fit points in
|
||||
:ref:`WCS`.
|
||||
"""
|
||||
return self._fit_points
|
||||
|
||||
@fit_points.setter
|
||||
def fit_points(self, points: Iterable[UVec]) -> None:
|
||||
self._fit_points: Vertices = cast(
|
||||
Vertices,
|
||||
VertexArray(Vec3.list(points)),
|
||||
)
|
||||
|
||||
# DXF callback attribute Spline.dxf.n_fit_points
|
||||
def fit_point_count(self) -> int:
|
||||
"""Count of fit points."""
|
||||
return len(self.fit_points)
|
||||
|
||||
def construction_tool(self) -> BSpline:
|
||||
"""Returns the construction tool :class:`ezdxf.math.BSpline`."""
|
||||
if self.control_point_count():
|
||||
weights = self.weights if len(self.weights) else None
|
||||
|
||||
if len(self.knots):
|
||||
knots = round_knots(self.knots, self.dxf.knot_tolerance)
|
||||
else:
|
||||
knots = None
|
||||
return BSpline(
|
||||
control_points=self.control_points,
|
||||
order=self.dxf.degree + 1,
|
||||
knots=knots,
|
||||
weights=weights,
|
||||
)
|
||||
elif self.fit_point_count():
|
||||
tangents = None
|
||||
if self.dxf.hasattr("start_tangent") and self.dxf.hasattr("end_tangent"):
|
||||
tangents = [self.dxf.start_tangent, self.dxf.end_tangent]
|
||||
# SPLINE from fit points has always a degree of 3!
|
||||
return fit_points_to_cad_cv(
|
||||
self.fit_points,
|
||||
tangents=tangents,
|
||||
)
|
||||
else:
|
||||
raise ValueError("Construction tool requires control- or fit points.")
|
||||
|
||||
def apply_construction_tool(self, s) -> Spline:
|
||||
"""Apply SPLINE data from a :class:`~ezdxf.math.BSpline` construction
|
||||
tool or from a :class:`geomdl.BSpline.Curve` object.
|
||||
|
||||
"""
|
||||
try:
|
||||
self.control_points = s.control_points
|
||||
except AttributeError: # maybe a geomdl.BSpline.Curve class
|
||||
s = BSpline.from_nurbs_python_curve(s)
|
||||
self.control_points = s.control_points
|
||||
|
||||
self.dxf.degree = s.degree
|
||||
self.fit_points = [] # remove fit points
|
||||
self.knots = s.knots()
|
||||
self.weights = s.weights()
|
||||
self.set_flag_state(Spline.RATIONAL, state=bool(len(self.weights)))
|
||||
return self # floating interface
|
||||
|
||||
def flattening(self, distance: float, segments: int = 4) -> Iterator[Vec3]:
|
||||
"""Adaptive recursive flattening. The argument `segments` is the
|
||||
minimum count of approximation segments between two knots, if the
|
||||
distance from the center of the approximation segment to the curve is
|
||||
bigger than `distance` the segment will be subdivided.
|
||||
|
||||
Args:
|
||||
distance: maximum distance from the projected curve point onto the
|
||||
segment chord.
|
||||
segments: minimum segment count between two knots
|
||||
|
||||
"""
|
||||
return self.construction_tool().flattening(distance, segments)
|
||||
|
||||
@classmethod
|
||||
def from_arc(cls, entity: DXFGraphic) -> Spline:
|
||||
"""Create a new SPLINE entity from a CIRCLE, ARC or ELLIPSE entity.
|
||||
|
||||
The new SPLINE entity has no owner, no handle, is not stored in
|
||||
the entity database nor assigned to any layout!
|
||||
|
||||
"""
|
||||
dxftype = entity.dxftype()
|
||||
if dxftype == "ELLIPSE":
|
||||
ellipse = cast("Ellipse", entity).construction_tool()
|
||||
elif dxftype == "CIRCLE":
|
||||
ellipse = ConstructionEllipse.from_arc(
|
||||
center=entity.dxf.get("center", NULLVEC),
|
||||
radius=abs(entity.dxf.get("radius", 1.0)),
|
||||
extrusion=entity.dxf.get("extrusion", Z_AXIS),
|
||||
)
|
||||
elif dxftype == "ARC":
|
||||
ellipse = ConstructionEllipse.from_arc(
|
||||
center=entity.dxf.get("center", NULLVEC),
|
||||
radius=abs(entity.dxf.get("radius", 1.0)),
|
||||
extrusion=entity.dxf.get("extrusion", Z_AXIS),
|
||||
start_angle=entity.dxf.get("start_angle", 0),
|
||||
end_angle=entity.dxf.get("end_angle", 360),
|
||||
)
|
||||
else:
|
||||
raise TypeError("CIRCLE, ARC or ELLIPSE entity required.")
|
||||
|
||||
spline = Spline.new(dxfattribs=entity.graphic_properties(), doc=entity.doc)
|
||||
s = BSpline.from_ellipse(ellipse)
|
||||
spline.dxf.degree = s.degree
|
||||
spline.dxf.flags = Spline.RATIONAL
|
||||
spline.control_points = s.control_points # type: ignore
|
||||
spline.knots = s.knots() # type: ignore
|
||||
spline.weights = s.weights() # type: ignore
|
||||
return spline
|
||||
|
||||
def set_open_uniform(self, control_points: Sequence[UVec], degree: int = 3) -> None:
|
||||
"""Open B-spline with a uniform knot vector, start and end at your first
|
||||
and last control points.
|
||||
|
||||
"""
|
||||
self.dxf.flags = 0
|
||||
self.dxf.degree = degree
|
||||
self.control_points = control_points # type: ignore
|
||||
self.knots = open_uniform_knot_vector(len(control_points), degree + 1)
|
||||
|
||||
def set_uniform(self, control_points: Sequence[UVec], degree: int = 3) -> None:
|
||||
"""B-spline with a uniform knot vector, does NOT start and end at your
|
||||
first and last control points.
|
||||
|
||||
"""
|
||||
self.dxf.flags = 0
|
||||
self.dxf.degree = degree
|
||||
self.control_points = control_points # type: ignore
|
||||
self.knots = uniform_knot_vector(len(control_points), degree + 1)
|
||||
|
||||
def set_closed(self, control_points: Sequence[UVec], degree=3) -> None:
|
||||
"""Closed B-spline with a uniform knot vector, start and end at your
|
||||
first control point.
|
||||
|
||||
"""
|
||||
self.dxf.flags = self.PERIODIC | self.CLOSED
|
||||
self.dxf.degree = degree
|
||||
self.control_points = control_points # type: ignore
|
||||
self.control_points.extend(control_points[:degree])
|
||||
# AutoDesk Developer Docs:
|
||||
# If the spline is periodic, the length of knot vector will be greater
|
||||
# than length of the control array by 1, but this does not work with
|
||||
# BricsCAD.
|
||||
self.knots = uniform_knot_vector(len(self.control_points), degree + 1)
|
||||
|
||||
def set_open_rational(
|
||||
self,
|
||||
control_points: Sequence[UVec],
|
||||
weights: Sequence[float],
|
||||
degree: int = 3,
|
||||
) -> None:
|
||||
"""Open rational B-spline with a uniform knot vector, start and end at
|
||||
your first and last control points, and has additional control
|
||||
possibilities by weighting each control point.
|
||||
|
||||
"""
|
||||
self.set_open_uniform(control_points, degree=degree)
|
||||
self.dxf.flags = self.dxf.flags | self.RATIONAL
|
||||
if len(weights) != len(self.control_points):
|
||||
raise DXFValueError("Control point count must be equal to weights count.")
|
||||
self.weights = weights # type: ignore
|
||||
|
||||
def set_uniform_rational(
|
||||
self,
|
||||
control_points: Sequence[UVec],
|
||||
weights: Sequence[float],
|
||||
degree: int = 3,
|
||||
) -> None:
|
||||
"""Rational B-spline with a uniform knot vector, does NOT start and end
|
||||
at your first and last control points, and has additional control
|
||||
possibilities by weighting each control point.
|
||||
|
||||
"""
|
||||
self.set_uniform(control_points, degree=degree)
|
||||
self.dxf.flags = self.dxf.flags | self.RATIONAL
|
||||
if len(weights) != len(self.control_points):
|
||||
raise DXFValueError("Control point count must be equal to weights count.")
|
||||
self.weights = weights # type: ignore
|
||||
|
||||
def set_closed_rational(
|
||||
self,
|
||||
control_points: Sequence[UVec],
|
||||
weights: Sequence[float],
|
||||
degree: int = 3,
|
||||
) -> None:
|
||||
"""Closed rational B-spline with a uniform knot vector, start and end at
|
||||
your first control point, and has additional control possibilities by
|
||||
weighting each control point.
|
||||
|
||||
"""
|
||||
self.set_closed(control_points, degree=degree)
|
||||
self.dxf.flags = self.dxf.flags | self.RATIONAL
|
||||
weights = list(weights)
|
||||
weights.extend(weights[:degree])
|
||||
if len(weights) != len(self.control_points):
|
||||
raise DXFValueError("Control point count must be equal to weights count.")
|
||||
self.weights = weights
|
||||
|
||||
def transform(self, m: Matrix44) -> Spline:
|
||||
"""Transform the SPLINE entity by transformation matrix `m` inplace."""
|
||||
self._control_points.transform(m) # type: ignore
|
||||
self._fit_points.transform(m) # type: ignore
|
||||
# Transform optional attributes if they exist
|
||||
dxf = self.dxf
|
||||
for name in ("start_tangent", "end_tangent", "extrusion"):
|
||||
if dxf.hasattr(name):
|
||||
dxf.set(name, m.transform_direction(dxf.get(name)))
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def audit(self, auditor: Auditor) -> None:
|
||||
"""Audit the SPLINE entity."""
|
||||
super().audit(auditor)
|
||||
degree = self.dxf.degree
|
||||
name = str(self)
|
||||
|
||||
if degree < 1:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_DEFINITION,
|
||||
message=f"Removed {name} with invalid degree: {degree} < 1.",
|
||||
)
|
||||
auditor.trash(self)
|
||||
return
|
||||
|
||||
n_control_points = len(self.control_points)
|
||||
n_fit_points = len(self.fit_points)
|
||||
|
||||
if n_control_points == 0 and n_fit_points == 0:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_DEFINITION,
|
||||
message=f"Removed {name} without any points (no geometry).",
|
||||
)
|
||||
auditor.trash(self)
|
||||
return
|
||||
|
||||
if n_control_points > 0:
|
||||
self._audit_control_points(auditor)
|
||||
# Ignore fit points if defined by control points
|
||||
elif n_fit_points > 0:
|
||||
self._audit_fit_points(auditor)
|
||||
|
||||
def _audit_control_points(self, auditor: Auditor):
|
||||
name = str(self)
|
||||
order = self.dxf.degree + 1
|
||||
n_control_points = len(self.control_points)
|
||||
|
||||
# Splines with to few control points can't be processed:
|
||||
n_control_points_required = required_control_points(order)
|
||||
if n_control_points < n_control_points_required:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_CONTROL_POINT_COUNT,
|
||||
message=f"Removed {name} with invalid control point count: "
|
||||
f"{n_control_points} < {n_control_points_required}",
|
||||
)
|
||||
auditor.trash(self)
|
||||
return
|
||||
|
||||
n_weights = len(self.weights)
|
||||
n_knots = len(self.knots)
|
||||
n_knots_required = required_knot_values(n_control_points, order)
|
||||
|
||||
if n_knots < n_knots_required:
|
||||
# Can not fix entity: because the knot values are basic
|
||||
# values which define the geometry of SPLINE.
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_KNOT_VALUE_COUNT,
|
||||
message=f"Removed {name} with invalid knot value count: "
|
||||
f"{n_knots} < {n_knots_required}",
|
||||
)
|
||||
auditor.trash(self)
|
||||
return
|
||||
|
||||
if n_weights and n_weights != n_control_points:
|
||||
# Can not fix entity: because the weights are basic
|
||||
# values which define the geometry of SPLINE.
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_WEIGHT_COUNT,
|
||||
message=f"Removed {name} with invalid weight count: "
|
||||
f"{n_weights} != {n_control_points}",
|
||||
)
|
||||
auditor.trash(self)
|
||||
return
|
||||
|
||||
def _audit_fit_points(self, auditor: Auditor):
|
||||
name = str(self)
|
||||
order = self.dxf.degree + 1
|
||||
# Assuming end tangents will be estimated if not present,
|
||||
# like by ezdxf:
|
||||
n_fit_points_required = required_fit_points(order, tangents=True)
|
||||
|
||||
# Splines with to few fit points can't be processed:
|
||||
n_fit_points = len(self.fit_points)
|
||||
if n_fit_points < n_fit_points_required:
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_FIT_POINT_COUNT,
|
||||
message=f"Removed {name} with invalid fit point count: "
|
||||
f"{n_fit_points} < {n_fit_points_required}",
|
||||
)
|
||||
auditor.trash(self)
|
||||
return
|
||||
|
||||
# Knot values have no meaning for splines defined by fit points:
|
||||
if len(self.knots):
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_KNOT_VALUE_COUNT,
|
||||
message=f"Removed unused knot values for {name} "
|
||||
f"defined by fit points.",
|
||||
)
|
||||
self.knots = []
|
||||
|
||||
# Weights have no meaning for splines defined by fit points:
|
||||
if len(self.weights):
|
||||
auditor.fixed_error(
|
||||
code=AuditError.INVALID_SPLINE_WEIGHT_COUNT,
|
||||
message=f"Removed unused weights for {name} " f"defined by fit points.",
|
||||
)
|
||||
self.weights = []
|
||||
|
||||
def ocs(self) -> OCS:
|
||||
# WCS entity which supports the "extrusion" attribute in a
|
||||
# different way!
|
||||
return OCS()
|
||||
@@ -0,0 +1,217 @@
|
||||
# Copyright (c) 2020-2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Iterable, Callable, Optional
|
||||
from typing_extensions import Self
|
||||
|
||||
from ezdxf.entities import factory, DXFGraphic, SeqEnd, DXFEntity
|
||||
from ezdxf.lldxf import const
|
||||
from .copy import default_copy
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFEntity
|
||||
from ezdxf.entitydb import EntityDB
|
||||
from ezdxf import xref
|
||||
|
||||
|
||||
__all__ = ["entity_linker", "LinkedEntities"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
class LinkedEntities(DXFGraphic):
|
||||
"""Super class for common features of the INSERT and the POLYLINE entity.
|
||||
Both have linked entities like the VERTEX or ATTRIB entity and a
|
||||
SEQEND entity.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._sub_entities: list[DXFGraphic] = []
|
||||
self.seqend: Optional[SeqEnd] = None
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy all sub-entities ands SEQEND. (internal API)"""
|
||||
assert isinstance(entity, LinkedEntities)
|
||||
entity._sub_entities = [
|
||||
copy_strategy.copy(e) for e in self._sub_entities
|
||||
]
|
||||
if self.seqend:
|
||||
entity.seqend = copy_strategy.copy(self.seqend)
|
||||
|
||||
def link_entity(self, entity: DXFEntity) -> None:
|
||||
"""Link VERTEX to ATTRIB entities."""
|
||||
assert isinstance(entity, DXFGraphic)
|
||||
entity.set_owner(self.dxf.handle, self.dxf.paperspace)
|
||||
self._sub_entities.append(entity)
|
||||
|
||||
def link_seqend(self, seqend: DXFEntity) -> None:
|
||||
"""Link SEQEND entity. (internal API)"""
|
||||
seqend.dxf.owner = self.dxf.owner
|
||||
if self.seqend is not None: # destroy existing seqend
|
||||
self.seqend.destroy()
|
||||
self.seqend = seqend # type: ignore
|
||||
|
||||
def post_bind_hook(self):
|
||||
"""Create always a SEQEND entity."""
|
||||
if self.seqend is None:
|
||||
self.new_seqend()
|
||||
|
||||
def all_sub_entities(self) -> Iterable[DXFEntity]:
|
||||
"""Yields all sub-entities and SEQEND. (internal API)"""
|
||||
yield from self._sub_entities
|
||||
if self.seqend:
|
||||
yield self.seqend
|
||||
|
||||
def process_sub_entities(self, func: Callable[[DXFEntity], None]):
|
||||
"""Call `func` for all sub-entities and SEQEND. (internal API)"""
|
||||
for entity in self.all_sub_entities():
|
||||
if entity.is_alive:
|
||||
func(entity)
|
||||
|
||||
def add_sub_entities_to_entitydb(self, db: EntityDB) -> None:
|
||||
"""Add sub-entities (VERTEX, ATTRIB, SEQEND) to entity database `db`,
|
||||
called from EntityDB. (internal API)
|
||||
"""
|
||||
|
||||
def add(entity: DXFEntity):
|
||||
entity.doc = self.doc # grant same document
|
||||
db.add(entity)
|
||||
|
||||
if not self.seqend or not self.seqend.is_alive:
|
||||
self.new_seqend()
|
||||
self.process_sub_entities(add)
|
||||
|
||||
def new_seqend(self):
|
||||
"""Create and bind new SEQEND. (internal API)"""
|
||||
attribs = {"layer": self.dxf.layer}
|
||||
if self.doc:
|
||||
seqend = factory.create_db_entry("SEQEND", attribs, self.doc)
|
||||
else:
|
||||
seqend = factory.new("SEQEND", attribs)
|
||||
self.link_seqend(seqend)
|
||||
|
||||
def set_owner(self, owner: Optional[str], paperspace: int = 0):
|
||||
"""Set owner of all sub-entities and SEQEND. (internal API)"""
|
||||
# Loading from file: POLYLINE/INSERT will be added to layout before
|
||||
# vertices/attrib entities are linked, so set_owner() of POLYLINE does
|
||||
# not set owner of vertices at loading time.
|
||||
super().set_owner(owner, paperspace)
|
||||
self.take_ownership()
|
||||
|
||||
def take_ownership(self):
|
||||
"""Take ownership of all sub-entities and SEQEND. (internal API)"""
|
||||
handle = self.dxf.handle
|
||||
paperspace = self.dxf.paperspace
|
||||
for entity in self.all_sub_entities():
|
||||
if entity.is_alive:
|
||||
entity.dxf.owner = handle
|
||||
entity.dxf.paperspace = paperspace
|
||||
|
||||
def remove_dependencies(self, other: Optional[Drawing] = None):
|
||||
"""Remove all dependencies from current document to bind entity to
|
||||
`other` document. (internal API)
|
||||
"""
|
||||
self.process_sub_entities(lambda e: e.remove_dependencies(other))
|
||||
super().remove_dependencies(other)
|
||||
|
||||
def destroy(self) -> None:
|
||||
"""Destroy all data and references."""
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
self.process_sub_entities(func=lambda e: e.destroy())
|
||||
del self._sub_entities
|
||||
del self.seqend
|
||||
super().destroy()
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
super().register_resources(registry)
|
||||
self.process_sub_entities(lambda e: e.register_resources(registry))
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
assert isinstance(clone, LinkedEntities)
|
||||
super().map_resources(clone, mapping)
|
||||
for source, _clone in zip(self.all_sub_entities(), clone.all_sub_entities()):
|
||||
source.map_resources(_clone, mapping)
|
||||
|
||||
|
||||
LINKED_ENTITIES = {"INSERT": "ATTRIB", "POLYLINE": "VERTEX"}
|
||||
|
||||
|
||||
def entity_linker() -> Callable[[DXFEntity], bool]:
|
||||
"""Create an DXF entities linker."""
|
||||
main_entity: Optional[DXFEntity] = None
|
||||
expected_dxftype = ""
|
||||
|
||||
def entity_linker_(entity: DXFEntity) -> bool:
|
||||
"""Collect and link entities which are linked to a parent entity:
|
||||
|
||||
- VERTEX -> POLYLINE
|
||||
- ATTRIB -> INSERT
|
||||
|
||||
Args:
|
||||
entity: examined DXF entity
|
||||
|
||||
Returns:
|
||||
True if `entity` is linked to a parent entity
|
||||
|
||||
"""
|
||||
nonlocal main_entity, expected_dxftype
|
||||
dxftype: str = entity.dxftype()
|
||||
# INSERT & POLYLINE are not linked entities, they are stored in the
|
||||
# entity space.
|
||||
are_linked_entities = False
|
||||
if main_entity is not None:
|
||||
# VERTEX, ATTRIB & SEQEND are linked tags, they are NOT stored in
|
||||
# the entity space.
|
||||
are_linked_entities = True
|
||||
if dxftype == "SEQEND":
|
||||
main_entity.link_seqend(entity) # type: ignore
|
||||
# Marks also the end of the main entity
|
||||
main_entity = None
|
||||
# Check for valid DXF structure:
|
||||
# VERTEX follows POLYLINE
|
||||
# ATTRIB follows INSERT
|
||||
elif dxftype == expected_dxftype:
|
||||
main_entity.link_entity(entity) # type: ignore
|
||||
else:
|
||||
raise const.DXFStructureError(
|
||||
f"Expected DXF entity {dxftype} or SEQEND"
|
||||
)
|
||||
|
||||
elif dxftype in LINKED_ENTITIES:
|
||||
# Only INSERT and POLYLINE have a linked entities structure:
|
||||
if dxftype == "INSERT" and not entity.dxf.get("attribs_follow", 0):
|
||||
# INSERT must not have following ATTRIBS:
|
||||
#
|
||||
# INSERT with no ATTRIBS, attribs_follow == 0
|
||||
# ATTRIB as a stand alone entity, which is a DXF structure
|
||||
# error, but this error should be handled in the audit
|
||||
# process.
|
||||
# ....
|
||||
# INSERT with ATTRIBS, attribs_follow == 1
|
||||
# ATTRIB as connected entity
|
||||
# SEQEND
|
||||
#
|
||||
# Therefore a ATTRIB following an INSERT doesn't mean that
|
||||
# these entities are linked.
|
||||
pass
|
||||
else:
|
||||
main_entity = entity
|
||||
expected_dxftype = LINKED_ENTITIES[dxftype]
|
||||
|
||||
# Attached MTEXT entity - this feature most likely does not exist!
|
||||
elif (dxftype == "MTEXT") and (entity.dxf.handle is None):
|
||||
logger.error(
|
||||
"Found attached MTEXT entity. Please open an issue at github: "
|
||||
"https://github.com/mozman/ezdxf/issues and provide a DXF "
|
||||
"example file."
|
||||
)
|
||||
return are_linked_entities
|
||||
|
||||
return entity_linker_
|
||||
@@ -0,0 +1,154 @@
|
||||
# Copyright (c) 2019-2022, Manfred Moitzi
|
||||
# License: MIT-License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2007
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
DXFAttr,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfobj import DXFObject
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["Sun"]
|
||||
|
||||
acdb_sun = DefSubclass(
|
||||
"AcDbSun",
|
||||
{
|
||||
"version": DXFAttr(90, default=1),
|
||||
"status": DXFAttr(
|
||||
290,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"color": DXFAttr(
|
||||
63,
|
||||
default=7,
|
||||
validator=validator.is_valid_aci_color,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"true_color": DXFAttr(421, default=16777215),
|
||||
"intensity": DXFAttr(40, default=1),
|
||||
"shadows": DXFAttr(
|
||||
291,
|
||||
default=1,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"julian_day": DXFAttr(91, default=2456922),
|
||||
# Time in seconds past midnight:
|
||||
"time": DXFAttr(92, default=43200),
|
||||
"daylight_savings_time": DXFAttr(
|
||||
292,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Shadow type:
|
||||
# 0 = Ray traced shadows
|
||||
# 1 = Shadow maps
|
||||
"shadow_type": DXFAttr(
|
||||
70,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"shadow_map_size": DXFAttr(71, default=256),
|
||||
"shadow_softness": DXFAttr(280, default=1),
|
||||
},
|
||||
)
|
||||
acdb_sun_group_codes = group_code_mapping(acdb_sun)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Sun(DXFObject):
|
||||
"""DXF SUN entity"""
|
||||
|
||||
DXFTYPE = "SUN"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_sun)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2007
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(dxf, acdb_sun_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_sun.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"version",
|
||||
"status",
|
||||
"color",
|
||||
"true_color",
|
||||
"intensity",
|
||||
"shadows",
|
||||
"julian_day",
|
||||
"time",
|
||||
"daylight_savings_time",
|
||||
"shadow_type",
|
||||
"shadow_map_size",
|
||||
"shadow_softness",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# todo: implement SUNSTUDY?
|
||||
acdb_sunstudy = DefSubclass(
|
||||
"AcDbSun",
|
||||
{
|
||||
"version": DXFAttr(90),
|
||||
"name": DXFAttr(1),
|
||||
"description": DXFAttr(2),
|
||||
"output_type": DXFAttr(70),
|
||||
"sheet_set_name": DXFAttr(
|
||||
3
|
||||
), # Included only if Output type is Sheet Set.
|
||||
"use_subset": DXFAttr(
|
||||
290
|
||||
), # Included only if Output type is Sheet Set.
|
||||
"sheet_subset_name": DXFAttr(4),
|
||||
# Included only if Output type is Sheet Set.
|
||||
"dates_from_calender": DXFAttr(291),
|
||||
"date_input_array_size": DXFAttr(91),
|
||||
# represents the number of dates picked
|
||||
# 90 Julian day; represents the date. One entry for each date picked.
|
||||
# 90 Seconds past midnight; represents the time of day. One entry for each date picked.
|
||||
"range_of_dates": DXFAttr(292),
|
||||
# 93 Start time. If range of dates flag is true.
|
||||
# 94 End time. If range of dates flag is true.
|
||||
# 95 Interval in seconds. If range of dates flag is true.
|
||||
"hours_count": DXFAttr(73),
|
||||
# 290 Hour. One entry for every hour as specified by the number of hours entry above.
|
||||
"page_setup_wizard_handle": DXFAttr(
|
||||
340
|
||||
), # Page setup wizard hard pointer ID
|
||||
"view_handle": DXFAttr(341), # View hard pointer ID
|
||||
"visual_style_handle": DXFAttr(342), # Visual Style ID
|
||||
"shade_plot_type": DXFAttr(74),
|
||||
"viewports_per_page": DXFAttr(75),
|
||||
"row_count": DXFAttr(76), # Number of rows for viewport distribution
|
||||
"column_count": DXFAttr(77), # Number of columns for viewport distribution
|
||||
"spacing": DXFAttr(40),
|
||||
"lock_viewports": DXFAttr(293),
|
||||
"label_viewports": DXFAttr(294),
|
||||
"text_style_handle": DXFAttr(343),
|
||||
},
|
||||
)
|
||||
acdb_sunstudy_group_codes = group_code_mapping(acdb_sunstudy)
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from ezdxf.lldxf.attributes import DXFAttr, DXFAttributes, DefSubclass
|
||||
from ezdxf.lldxf import const
|
||||
from .dxfentity import SubclassProcessor, DXFEntity
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["TableHead"]
|
||||
|
||||
base_class = DefSubclass(
|
||||
None,
|
||||
{
|
||||
"name": DXFAttr(2),
|
||||
"handle": DXFAttr(5),
|
||||
"owner": DXFAttr(330),
|
||||
},
|
||||
)
|
||||
|
||||
acdb_symbol_table = DefSubclass(
|
||||
"AcDbSymbolTable",
|
||||
{
|
||||
"count": DXFAttr(70, default=0),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@register_entity
|
||||
class TableHead(DXFEntity):
|
||||
"""The table head structure is only maintained for export and not for
|
||||
internal usage, ezdxf ignores an inconsistent table head at runtime.
|
||||
|
||||
"""
|
||||
|
||||
DXFTYPE = "TABLE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
dxf.name = processor.base_class.get_first_value(2)
|
||||
# Stored max table count is not required:
|
||||
dxf.count = 0
|
||||
return dxf
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
||||
assert self.dxf.handle, (
|
||||
"TABLE needs a handle, maybe loaded from DXF R12 without handle!"
|
||||
)
|
||||
tagwriter.write_tag2(const.STRUCTURE_MARKER, self.DXFTYPE)
|
||||
tagwriter.write_tag2(2, self.dxf.name)
|
||||
if tagwriter.dxfversion >= const.DXF2000:
|
||||
tagwriter.write_tag2(5, self.dxf.handle)
|
||||
if self.has_extension_dict:
|
||||
self.extension_dict.export_dxf(tagwriter) # type: ignore
|
||||
tagwriter.write_tag2(const.OWNER_CODE, self.dxf.owner)
|
||||
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_symbol_table.name)
|
||||
tagwriter.write_tag2(70, self.dxf.count)
|
||||
# There is always one exception:
|
||||
if self.dxf.name == "DIMSTYLE":
|
||||
tagwriter.write_tag2(const.SUBCLASS_MARKER, "AcDbDimStyleTable")
|
||||
else: # DXF R12
|
||||
# TABLE does not need a handle at all
|
||||
tagwriter.write_tag2(70, self.dxf.count)
|
||||
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
import abc
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.math import Matrix44
|
||||
from ezdxf.entities.dxfgfx import DXFEntity
|
||||
|
||||
__all__ = ["TemporaryTransformation", "TransformByBlockReference"]
|
||||
|
||||
|
||||
class TemporaryTransformation:
|
||||
__slots__ = ("_matrix",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._matrix: Matrix44 | None = None
|
||||
|
||||
def get_matrix(self) -> Matrix44 | None:
|
||||
return self._matrix
|
||||
|
||||
def set_matrix(self, m: Matrix44 | None) -> None:
|
||||
self._matrix = m
|
||||
|
||||
def add_matrix(self, m: Matrix44) -> None:
|
||||
matrix = self.get_matrix()
|
||||
if matrix is not None:
|
||||
m = matrix @ m
|
||||
self.set_matrix(m)
|
||||
|
||||
@abc.abstractmethod
|
||||
def apply_transformation(self, entity: DXFEntity) -> bool: ...
|
||||
|
||||
|
||||
class TransformByBlockReference(TemporaryTransformation):
|
||||
__slots__ = ("_matrix",)
|
||||
|
||||
def apply_transformation(self, entity: DXFEntity) -> bool:
|
||||
from ezdxf.transform import transform_entity_by_blockref
|
||||
|
||||
m = self.get_matrix()
|
||||
if m is None:
|
||||
return False
|
||||
|
||||
if transform_entity_by_blockref(entity, m):
|
||||
self.set_matrix(None)
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,483 @@
|
||||
# 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 math
|
||||
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
merge_group_code_mappings,
|
||||
)
|
||||
from ezdxf.enums import (
|
||||
TextEntityAlignment,
|
||||
MAP_TEXT_ENUM_TO_ALIGN_FLAGS,
|
||||
MAP_TEXT_ALIGN_FLAGS_TO_ENUM,
|
||||
)
|
||||
from ezdxf.math import Vec3, UVec, Matrix44, NULLVEC, Z_AXIS
|
||||
from ezdxf.math.transformtools import OCSTransform
|
||||
from ezdxf.audit import Auditor
|
||||
from ezdxf.tools.text import plain_text
|
||||
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import (
|
||||
DXFGraphic,
|
||||
acdb_entity,
|
||||
elevation_to_z_axis,
|
||||
acdb_entity_group_codes,
|
||||
)
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["Text", "acdb_text", "acdb_text_group_codes"]
|
||||
|
||||
acdb_text = DefSubclass(
|
||||
"AcDbText",
|
||||
{
|
||||
# First alignment point (in OCS):
|
||||
"insert": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# Text height
|
||||
"height": DXFAttr(
|
||||
40,
|
||||
default=2.5,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Text content as string:
|
||||
"text": DXFAttr(
|
||||
1,
|
||||
default="",
|
||||
validator=validator.is_valid_one_line_text,
|
||||
fixer=validator.fix_one_line_text,
|
||||
),
|
||||
# Text rotation in degrees (optional)
|
||||
"rotation": DXFAttr(50, default=0, optional=True),
|
||||
# Oblique angle in degrees, vertical = 0 deg (optional)
|
||||
"oblique": DXFAttr(51, default=0, optional=True),
|
||||
# Text style name (optional), given text style must have an entry in the
|
||||
# text-styles tables.
|
||||
"style": DXFAttr(7, default="Standard", optional=True),
|
||||
# Relative X scale factor—width (optional)
|
||||
# This value is also adjusted when fit-type text is used
|
||||
"width": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
optional=True,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Text generation flags (optional)
|
||||
# 2 = backward (mirror-x),
|
||||
# 4 = upside down (mirror-y)
|
||||
"text_generation_flag": DXFAttr(
|
||||
71,
|
||||
default=0,
|
||||
optional=True,
|
||||
validator=validator.is_one_of({0, 2, 4, 6}),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Horizontal text justification type (optional) horizontal justification
|
||||
# 0 = Left
|
||||
# 1 = Center
|
||||
# 2 = Right
|
||||
# 3 = Aligned (if vertical alignment = 0)
|
||||
# 4 = Middle (if vertical alignment = 0)
|
||||
# 5 = Fit (if vertical alignment = 0)
|
||||
# This value is meaningful only if the value of a 72 or 73 group is nonzero
|
||||
# (if the justification is anything other than baseline/left)
|
||||
"halign": DXFAttr(
|
||||
72,
|
||||
default=0,
|
||||
optional=True,
|
||||
validator=validator.is_in_integer_range(0, 6),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Second alignment point (in OCS) (optional)
|
||||
"align_point": DXFAttr(11, xtype=XType.point3d, optional=True),
|
||||
# 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 in extrusion direction, only supported for SHX font in
|
||||
# AutoCAD/BricsCAD (optional), can be negative
|
||||
"thickness": DXFAttr(39, default=0, optional=True),
|
||||
# Extrusion direction (optional)
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_text_group_codes = group_code_mapping(acdb_text)
|
||||
acdb_text2 = DefSubclass(
|
||||
"AcDbText",
|
||||
{
|
||||
# Vertical text justification type (optional)
|
||||
# 0 = Baseline
|
||||
# 1 = Bottom
|
||||
# 2 = Middle
|
||||
# 3 = Top
|
||||
"valign": DXFAttr(
|
||||
73,
|
||||
default=0,
|
||||
optional=True,
|
||||
validator=validator.is_in_integer_range(0, 4),
|
||||
fixer=RETURN_DEFAULT,
|
||||
)
|
||||
},
|
||||
)
|
||||
acdb_text2_group_codes = group_code_mapping(acdb_text2)
|
||||
merged_text_group_codes = merge_group_code_mappings(
|
||||
acdb_entity_group_codes, # type: ignore
|
||||
acdb_text_group_codes,
|
||||
acdb_text2_group_codes,
|
||||
)
|
||||
|
||||
|
||||
# Formatting codes:
|
||||
# %%d: '°'
|
||||
# %%u in TEXT start underline formatting until next %%u or until end of line
|
||||
|
||||
|
||||
@register_entity
|
||||
class Text(DXFGraphic):
|
||||
"""DXF TEXT entity"""
|
||||
|
||||
DXFTYPE = "TEXT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_text, acdb_text2)
|
||||
# horizontal align values
|
||||
LEFT = 0
|
||||
CENTER = 1
|
||||
RIGHT = 2
|
||||
# vertical align values
|
||||
BASELINE = 0
|
||||
BOTTOM = 1
|
||||
MIDDLE = 2
|
||||
TOP = 3
|
||||
# text generation flags
|
||||
MIRROR_X = 2
|
||||
MIRROR_Y = 4
|
||||
BACKWARD = MIRROR_X
|
||||
UPSIDE_DOWN = MIRROR_Y
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
"""Loading interface. (internal API)"""
|
||||
dxf = super(DXFGraphic, self).load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.simple_dxfattribs_loader(dxf, merged_text_group_codes)
|
||||
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:
|
||||
"""Export entity specific data as DXF tags. (internal API)"""
|
||||
super().export_entity(tagwriter)
|
||||
self.export_acdb_text(tagwriter)
|
||||
self.export_acdb_text2(tagwriter)
|
||||
|
||||
def export_acdb_text(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export TEXT data as DXF tags. (internal API)"""
|
||||
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",
|
||||
"text_generation_flag",
|
||||
"halign",
|
||||
"align_point",
|
||||
"extrusion",
|
||||
],
|
||||
)
|
||||
|
||||
def export_acdb_text2(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export TEXT data as DXF tags. (internal API)"""
|
||||
if tagwriter.dxfversion > const.DXF12:
|
||||
tagwriter.write_tag2(const.SUBCLASS_MARKER, acdb_text2.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, "valign")
|
||||
|
||||
def set_placement(
|
||||
self,
|
||||
p1: UVec,
|
||||
p2: Optional[UVec] = None,
|
||||
align: Optional[TextEntityAlignment] = None,
|
||||
) -> Text:
|
||||
"""Set text alignment and location.
|
||||
|
||||
The alignments :attr:`ALIGNED` and :attr:`FIT`
|
||||
are special, they require a second alignment point, the text is aligned
|
||||
on the virtual line between these two points and sits vertically at the
|
||||
baseline.
|
||||
|
||||
- :attr:`ALIGNED`: Text is stretched or compressed
|
||||
to fit exactly between `p1` and `p2` and the text height is also
|
||||
adjusted to preserve height/width ratio.
|
||||
- :attr:`FIT`: Text is stretched or compressed to fit
|
||||
exactly between `p1` and `p2` but only the text width is adjusted,
|
||||
the text height is fixed by the :attr:`dxf.height` attribute.
|
||||
- :attr:`MIDDLE`: also a special adjustment, centered
|
||||
text like :attr:`MIDDLE_CENTER`, but vertically
|
||||
centred at the total height of the text.
|
||||
|
||||
Args:
|
||||
p1: first alignment point as (x, y[, z])
|
||||
p2: second alignment point as (x, y[, z]), required for :attr:`ALIGNED`
|
||||
and :attr:`FIT` else ignored
|
||||
align: new alignment as enum :class:`~ezdxf.enums.TextEntityAlignment`,
|
||||
``None`` to preserve the existing alignment.
|
||||
|
||||
"""
|
||||
if align is None:
|
||||
align = self.get_align_enum()
|
||||
else:
|
||||
assert isinstance(align, TextEntityAlignment)
|
||||
self.set_align_enum(align)
|
||||
self.dxf.insert = p1
|
||||
if align in (TextEntityAlignment.ALIGNED, TextEntityAlignment.FIT):
|
||||
if p2 is None:
|
||||
raise const.DXFValueError(
|
||||
f"Alignment '{str(align)}' requires a second alignment point."
|
||||
)
|
||||
else:
|
||||
p2 = p1
|
||||
self.dxf.align_point = p2
|
||||
return self
|
||||
|
||||
def get_placement(self) -> tuple[TextEntityAlignment, Vec3, Optional[Vec3]]:
|
||||
"""Returns a tuple (`align`, `p1`, `p2`), `align` is the alignment
|
||||
enum :class:`~ezdxf.enum.TextEntityAlignment`, `p1` is the
|
||||
alignment point, `p2` is only relevant if `align` is :attr:`ALIGNED` or
|
||||
:attr:`FIT`, otherwise it is ``None``.
|
||||
|
||||
"""
|
||||
p1 = Vec3(self.dxf.insert)
|
||||
# Except for "LEFT" is the "align point" the real insert point:
|
||||
# If the required "align point" is not present use "insert"!
|
||||
p2 = Vec3(self.dxf.get("align_point", p1))
|
||||
align = self.get_align_enum()
|
||||
if align is TextEntityAlignment.LEFT:
|
||||
return align, p1, None
|
||||
if align in (TextEntityAlignment.FIT, TextEntityAlignment.ALIGNED):
|
||||
return align, p1, p2
|
||||
return align, p2, None
|
||||
|
||||
def set_align_enum(self, align=TextEntityAlignment.LEFT) -> Text:
|
||||
"""Just for experts: Sets the text alignment without setting the
|
||||
alignment points, set adjustment points attr:`dxf.insert` and
|
||||
:attr:`dxf.align_point` manually.
|
||||
|
||||
Args:
|
||||
align: :class:`~ezdxf.enums.TextEntityAlignment`
|
||||
|
||||
"""
|
||||
halign, valign = MAP_TEXT_ENUM_TO_ALIGN_FLAGS[align]
|
||||
self.dxf.halign = halign
|
||||
self.dxf.valign = valign
|
||||
return self
|
||||
|
||||
def get_align_enum(self) -> TextEntityAlignment:
|
||||
"""Returns the current text alignment as :class:`~ezdxf.enums.TextEntityAlignment`,
|
||||
see also :meth:`set_placement`.
|
||||
"""
|
||||
halign = self.dxf.get("halign", 0)
|
||||
valign = self.dxf.get("valign", 0)
|
||||
if halign > 2:
|
||||
valign = 0
|
||||
return MAP_TEXT_ALIGN_FLAGS_TO_ENUM.get(
|
||||
(halign, valign), TextEntityAlignment.LEFT
|
||||
)
|
||||
|
||||
def transform(self, m: Matrix44) -> Text:
|
||||
"""Transform the TEXT entity by transformation matrix `m` inplace."""
|
||||
dxf = self.dxf
|
||||
if not dxf.hasattr("align_point"):
|
||||
dxf.align_point = dxf.insert
|
||||
ocs = OCSTransform(self.dxf.extrusion, m)
|
||||
dxf.insert = ocs.transform_vertex(dxf.insert)
|
||||
dxf.align_point = ocs.transform_vertex(dxf.align_point)
|
||||
old_rotation = dxf.rotation
|
||||
new_rotation = ocs.transform_deg_angle(old_rotation)
|
||||
x_scale = ocs.transform_length(Vec3.from_deg_angle(old_rotation))
|
||||
y_scale = ocs.transform_length(Vec3.from_deg_angle(old_rotation + 90.0))
|
||||
|
||||
if not ocs.scale_uniform:
|
||||
oblique_vec = Vec3.from_deg_angle(old_rotation + 90.0 - dxf.oblique)
|
||||
new_oblique_deg = (
|
||||
new_rotation
|
||||
+ 90.0
|
||||
- ocs.transform_direction(oblique_vec).angle_deg
|
||||
)
|
||||
dxf.oblique = new_oblique_deg
|
||||
y_scale *= math.cos(math.radians(new_oblique_deg))
|
||||
|
||||
dxf.width *= x_scale / y_scale
|
||||
dxf.height *= y_scale
|
||||
dxf.rotation = new_rotation
|
||||
|
||||
if dxf.hasattr("thickness"): # can be negative
|
||||
dxf.thickness = ocs.transform_thickness(dxf.thickness)
|
||||
dxf.extrusion = ocs.new_extrusion
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def translate(self, dx: float, dy: float, dz: float) -> Text:
|
||||
"""Optimized TEXT/ATTRIB/ATTDEF translation about `dx` in x-axis, `dy`
|
||||
in y-axis and `dz` in z-axis, returns `self`.
|
||||
|
||||
"""
|
||||
ocs = self.ocs()
|
||||
dxf = self.dxf
|
||||
vec = Vec3(dx, dy, dz)
|
||||
|
||||
dxf.insert = ocs.from_wcs(vec + ocs.to_wcs(dxf.insert))
|
||||
if dxf.hasattr("align_point"):
|
||||
dxf.align_point = ocs.from_wcs(vec + ocs.to_wcs(dxf.align_point))
|
||||
# Avoid Matrix44 instantiation if not required:
|
||||
if self.is_post_transform_required:
|
||||
self.post_transform(Matrix44.translate(dx, dy, dz))
|
||||
return self
|
||||
|
||||
def remove_dependencies(self, other: Optional[Drawing] = None) -> None:
|
||||
"""Remove all dependencies from actual document.
|
||||
|
||||
(internal API)
|
||||
"""
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
super().remove_dependencies()
|
||||
has_style = other is not None and (self.dxf.style in other.styles)
|
||||
if not has_style:
|
||||
self.dxf.style = "Standard"
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
"""Register required resources to the resource registry."""
|
||||
super().register_resources(registry)
|
||||
if self.dxf.hasattr("style"):
|
||||
registry.add_text_style(self.dxf.style)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
"""Translate resources from self to the copied entity."""
|
||||
super().map_resources(clone, mapping)
|
||||
if clone.dxf.hasattr("style"):
|
||||
clone.dxf.style = mapping.get_text_style(clone.dxf.style)
|
||||
|
||||
def plain_text(self) -> str:
|
||||
"""Returns text content without formatting codes."""
|
||||
return plain_text(self.dxf.text)
|
||||
|
||||
def audit(self, auditor: Auditor):
|
||||
"""Validity check."""
|
||||
super().audit(auditor)
|
||||
auditor.check_text_style(self)
|
||||
|
||||
@property
|
||||
def is_backward(self) -> bool:
|
||||
"""Get/set text generation flag BACKWARDS, for mirrored text along the
|
||||
x-axis.
|
||||
"""
|
||||
return bool(self.dxf.text_generation_flag & const.BACKWARD)
|
||||
|
||||
@is_backward.setter
|
||||
def is_backward(self, state) -> None:
|
||||
self.set_flag_state(const.BACKWARD, state, "text_generation_flag")
|
||||
|
||||
@property
|
||||
def is_upside_down(self) -> bool:
|
||||
"""Get/set text generation flag UPSIDE_DOWN, for mirrored text along
|
||||
the y-axis.
|
||||
|
||||
"""
|
||||
return bool(self.dxf.text_generation_flag & const.UPSIDE_DOWN)
|
||||
|
||||
@is_upside_down.setter
|
||||
def is_upside_down(self, state) -> None:
|
||||
self.set_flag_state(const.UPSIDE_DOWN, state, "text_generation_flag")
|
||||
|
||||
def wcs_transformation_matrix(self) -> Matrix44:
|
||||
return text_transformation_matrix(self)
|
||||
|
||||
def font_name(self) -> str:
|
||||
"""Returns the font name of the associated :class:`Textstyle`."""
|
||||
font_name = "arial.ttf"
|
||||
style_name = self.dxf.style
|
||||
if self.doc:
|
||||
try:
|
||||
style = self.doc.styles.get(style_name)
|
||||
font_name = style.dxf.font
|
||||
except ValueError:
|
||||
pass
|
||||
return font_name
|
||||
|
||||
def fit_length(self) -> float:
|
||||
"""Returns the text length for alignments :attr:`TextEntityAlignment.FIT`
|
||||
and :attr:`TextEntityAlignment.ALIGNED`, defined by the distance from
|
||||
the insertion point to the align point or 0 for all other alignments.
|
||||
|
||||
"""
|
||||
length = 0.0
|
||||
align, p1, p2 = self.get_placement()
|
||||
if align in (TextEntityAlignment.FIT, TextEntityAlignment.ALIGNED):
|
||||
# text is stretch between p1 and p2
|
||||
length = p1.distance(p2)
|
||||
return length
|
||||
|
||||
|
||||
def text_transformation_matrix(entity: Text) -> Matrix44:
|
||||
"""Apply rotation, width factor, translation to the insertion point
|
||||
and if necessary transformation from OCS to WCS.
|
||||
"""
|
||||
angle = math.radians(entity.dxf.rotation)
|
||||
width_factor = entity.dxf.width
|
||||
align, p1, p2 = entity.get_placement()
|
||||
mirror_x = -1 if entity.is_backward else 1
|
||||
mirror_y = -1 if entity.is_upside_down else 1
|
||||
oblique = math.radians(entity.dxf.oblique)
|
||||
location = p1
|
||||
if align in (TextEntityAlignment.ALIGNED, TextEntityAlignment.FIT):
|
||||
width_factor = 1.0 # text goes from p1 to p2, no stretching applied
|
||||
location = p1.lerp(p2, factor=0.5)
|
||||
angle = (p2 - p1).angle # override stored angle
|
||||
|
||||
m = Matrix44()
|
||||
if oblique:
|
||||
m *= Matrix44.shear_xy(angle_x=oblique)
|
||||
sx = width_factor * mirror_x
|
||||
sy = mirror_y
|
||||
if sx != 1 or sy != 1:
|
||||
m *= Matrix44.scale(sx, sy, 1)
|
||||
if angle:
|
||||
m *= Matrix44.z_rotate(angle)
|
||||
if location:
|
||||
m *= Matrix44.translate(location.x, location.y, location.z)
|
||||
|
||||
ocs = entity.ocs()
|
||||
if ocs.transform: # to WCS
|
||||
m *= ocs.matrix
|
||||
return m
|
||||
@@ -0,0 +1,248 @@
|
||||
# Copyright (c) 2019-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import logging
|
||||
from ezdxf.lldxf import validator, const
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
RETURN_DEFAULT,
|
||||
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 .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.fonts import fonts
|
||||
|
||||
__all__ = ["Textstyle"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
acdb_style = DefSubclass(
|
||||
"AcDbTextStyleTableRecord",
|
||||
{
|
||||
"name": DXFAttr(
|
||||
2,
|
||||
default="Standard",
|
||||
validator=validator.is_valid_table_name,
|
||||
),
|
||||
# Flags: Standard flag values (bit-coded values):
|
||||
# 1 = If set, this entry describes a shape
|
||||
# 4 = Vertical text
|
||||
# 16 = If set, table entry is externally dependent on a xref
|
||||
# 32 = If both this bit and bit 16 are set, the externally dependent xref ...
|
||||
# 64 = If set, the table entry was referenced by at least one entity in ...
|
||||
# Vertical text works only for SHX fonts in AutoCAD and BricsCAD
|
||||
"flags": DXFAttr(70, default=0),
|
||||
# Fixed height, 0 if not fixed
|
||||
"height": DXFAttr(
|
||||
40,
|
||||
default=0,
|
||||
validator=validator.is_greater_or_equal_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Width factor: a.k.a. "Stretch"
|
||||
"width": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
validator=validator.is_greater_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Oblique angle in degree, 0 = vertical
|
||||
"oblique": DXFAttr(50, default=0),
|
||||
# Generation flags:
|
||||
# 2 = backward
|
||||
# 4 = mirrored in Y
|
||||
"generation_flags": DXFAttr(71, default=0),
|
||||
# Last height used:
|
||||
"last_height": DXFAttr(42, default=2.5),
|
||||
# Primary font file name:
|
||||
# ATTENTION: The font file name can be an empty string and the font family
|
||||
# may be stored in XDATA! See also posts at the (unrelated) issue #380.
|
||||
"font": DXFAttr(3, default=const.DEFAULT_TEXT_FONT),
|
||||
# Big font name, blank if none
|
||||
"bigfont": DXFAttr(4, default=""),
|
||||
},
|
||||
)
|
||||
acdb_style_group_codes = group_code_mapping(acdb_style)
|
||||
|
||||
|
||||
# XDATA: This is not a reliable source for font data!
|
||||
# 1001 <ctrl> ACAD
|
||||
# 1000 <str> Arial ; font-family sometimes an empty string!
|
||||
# 1071 <int> 34 ; flags
|
||||
# ----
|
||||
# "Arial" "normal" flags = 34 = 0b00:00000000:00000000:00100010
|
||||
# "Arial" "italic" flags = 16777250 = 0b01:00000000:00000000:00100010
|
||||
# "Arial" "bold" flags = 33554466 = 0b10:00000000:00000000:00100010
|
||||
# "Arial" "bold+italic" flags = 50331682 = 0b11:00000000:00000000:00100010
|
||||
|
||||
|
||||
@register_entity
|
||||
class Textstyle(DXFEntity):
|
||||
"""DXF STYLE entity"""
|
||||
|
||||
DXFTYPE = "STYLE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_style)
|
||||
ITALIC = 0b01000000000000000000000000
|
||||
BOLD = 0b10000000000000000000000000
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.simple_dxfattribs_loader(dxf, acdb_style_group_codes) # type: ignore
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_style.name)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"flags",
|
||||
"height",
|
||||
"width",
|
||||
"oblique",
|
||||
"generation_flags",
|
||||
"last_height",
|
||||
"font",
|
||||
"bigfont",
|
||||
],
|
||||
)
|
||||
|
||||
@property
|
||||
def has_extended_font_data(self) -> bool:
|
||||
"""Returns ``True`` if extended font data is present."""
|
||||
return self.has_xdata("ACAD")
|
||||
|
||||
def get_extended_font_data(self) -> tuple[str, bool, bool]:
|
||||
"""Returns extended font data as tuple (font-family, italic-flag,
|
||||
bold-flag).
|
||||
|
||||
The extended font data is optional and not reliable! Returns
|
||||
("", ``False``, ``False``) if extended font data is not present.
|
||||
|
||||
"""
|
||||
family = ""
|
||||
italic = False
|
||||
bold = False
|
||||
try:
|
||||
xdata = self.get_xdata("ACAD")
|
||||
except const.DXFValueError:
|
||||
pass
|
||||
else:
|
||||
if len(xdata) > 1:
|
||||
group_code, value = xdata[0]
|
||||
if group_code == 1000:
|
||||
family = value
|
||||
group_code, value = xdata[1]
|
||||
if group_code == 1071:
|
||||
italic = bool(self.ITALIC & value)
|
||||
bold = bool(self.BOLD & value)
|
||||
return family, italic, bold
|
||||
|
||||
def set_extended_font_data(
|
||||
self, family: str = "", *, italic=False, bold=False
|
||||
) -> None:
|
||||
"""Set extended font data, the font-family name `family` is not
|
||||
validated by `ezdxf`. Overwrites existing data.
|
||||
"""
|
||||
if self.has_xdata("ACAD"):
|
||||
self.discard_xdata("ACAD")
|
||||
|
||||
flags = 34 # unknown default flags
|
||||
if italic:
|
||||
flags += self.ITALIC
|
||||
if bold:
|
||||
flags += self.BOLD
|
||||
self.set_xdata("ACAD", [(1000, family), (1071, flags)])
|
||||
|
||||
def discard_extended_font_data(self):
|
||||
"""Discard extended font data."""
|
||||
self.discard_xdata("ACAD")
|
||||
|
||||
@property
|
||||
def is_backward(self) -> bool:
|
||||
"""Get/set text generation flag BACKWARDS, for mirrored text along the
|
||||
x-axis.
|
||||
"""
|
||||
return self.get_flag_state(const.BACKWARD, "generation_flags")
|
||||
|
||||
@is_backward.setter
|
||||
def is_backward(self, state) -> None:
|
||||
self.set_flag_state(const.BACKWARD, state, "generation_flags")
|
||||
|
||||
@property
|
||||
def is_upside_down(self) -> bool:
|
||||
"""Get/set text generation flag UPSIDE_DOWN, for mirrored text along
|
||||
the y-axis.
|
||||
|
||||
"""
|
||||
return self.get_flag_state(const.UPSIDE_DOWN, "generation_flags")
|
||||
|
||||
@is_upside_down.setter
|
||||
def is_upside_down(self, state) -> None:
|
||||
self.set_flag_state(const.UPSIDE_DOWN, state, "generation_flags")
|
||||
|
||||
@property
|
||||
def is_vertical_stacked(self) -> bool:
|
||||
"""Get/set style flag VERTICAL_STACKED, for vertical stacked text."""
|
||||
return self.get_flag_state(const.VERTICAL_STACKED, "flags")
|
||||
|
||||
@is_vertical_stacked.setter
|
||||
def is_vertical_stacked(self, state) -> None:
|
||||
self.set_flag_state(const.VERTICAL_STACKED, state, "flags")
|
||||
|
||||
@property
|
||||
def is_shape_file(self) -> bool:
|
||||
"""``True`` if entry describes a shape."""
|
||||
return self.dxf.name == "" and bool(self.dxf.flags & 1)
|
||||
|
||||
def make_font(
|
||||
self,
|
||||
cap_height: Optional[float] = None,
|
||||
width_factor: Optional[float] = None,
|
||||
) -> fonts.AbstractFont:
|
||||
"""Returns a font abstraction :class:`~ezdxf.tools.fonts.AbstractFont`
|
||||
for this text style. Returns a font for a cap height of 1, if the
|
||||
text style has auto height (:attr:`Textstyle.dxf.height` is 0) and
|
||||
the given `cap_height` is ``None`` or 0.
|
||||
Uses the :attr:`Textstyle.dxf.width` attribute if the given `width_factor`
|
||||
is ``None`` or 0, the default value is 1.
|
||||
The attribute :attr:`Textstyle.dxf.big_font` is ignored.
|
||||
"""
|
||||
from ezdxf.fonts import fonts
|
||||
|
||||
ttf = ""
|
||||
if self.has_extended_font_data:
|
||||
family, italic, bold = self.get_extended_font_data()
|
||||
if family:
|
||||
text_style = "Italic" if italic else "Regular"
|
||||
text_weight = 700 if bold else 400
|
||||
font_face = fonts.FontFace(
|
||||
family=family, style=text_style, weight=text_weight
|
||||
)
|
||||
ttf = fonts.find_font_file_name(font_face)
|
||||
else:
|
||||
ttf = self.dxf.get("font", const.DEFAULT_TTF)
|
||||
if ttf == "":
|
||||
ttf = const.DEFAULT_TTF
|
||||
if cap_height is None or cap_height == 0.0:
|
||||
cap_height = self.dxf.height
|
||||
if cap_height == 0.0:
|
||||
cap_height = 1.0
|
||||
if width_factor is None or width_factor == 0.0:
|
||||
width_factor = self.dxf.width
|
||||
return fonts.make_font(ttf, cap_height, width_factor) # type: ignore
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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,
|
||||
)
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000
|
||||
from ezdxf.math import NULLVEC, Z_AXIS, X_AXIS
|
||||
from ezdxf.math.transformtools import transform_extrusion
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.math import Matrix44
|
||||
from ezdxf import xref
|
||||
|
||||
|
||||
__all__ = ["Tolerance"]
|
||||
|
||||
acdb_tolerance = DefSubclass(
|
||||
"AcDbFcf",
|
||||
{
|
||||
"dimstyle": DXFAttr(
|
||||
3,
|
||||
default="Standard",
|
||||
validator=validator.is_valid_table_name,
|
||||
),
|
||||
# Insertion point (in WCS):
|
||||
"insert": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# String representing the visual representation of the tolerance:
|
||||
"content": DXFAttr(1, default=""),
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# X-axis direction vector (in WCS):
|
||||
"x_axis_vector": DXFAttr(
|
||||
11,
|
||||
xtype=XType.point3d,
|
||||
default=X_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_tolerance_group_codes = group_code_mapping(acdb_tolerance)
|
||||
|
||||
|
||||
@register_entity
|
||||
class Tolerance(DXFGraphic):
|
||||
"""DXF TOLERANCE entity"""
|
||||
|
||||
DXFTYPE = "TOLERANCE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_tolerance)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_tolerance_group_codes, subclass=2, recover=True
|
||||
)
|
||||
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_tolerance.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
["dimstyle", "insert", "content", "extrusion", "x_axis_vector"],
|
||||
)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
super().register_resources(registry)
|
||||
registry.add_dim_style(self.dxf.dimstyle)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
super().map_resources(clone, mapping)
|
||||
clone.dxf.dimstyle = mapping.get_dim_style(self.dxf.dimstyle)
|
||||
|
||||
def transform(self, m: Matrix44) -> Tolerance:
|
||||
"""Transform the TOLERANCE entity by transformation matrix `m` inplace."""
|
||||
self.dxf.insert = m.transform(self.dxf.insert)
|
||||
self.dxf.x_axis_vector = m.transform_direction(self.dxf.x_axis_vector)
|
||||
self.dxf.extrusion, _ = transform_extrusion(self.dxf.extrusion, m)
|
||||
self.post_transform(m)
|
||||
return self
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.math import UCS, NULLVEC, X_AXIS, Y_AXIS
|
||||
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.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
|
||||
__all__ = ["UCSTableEntry"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
acdb_ucs = DefSubclass(
|
||||
"AcDbUCSTableRecord",
|
||||
{
|
||||
"name": DXFAttr(2, validator=validator.is_valid_table_name),
|
||||
"flags": DXFAttr(70, default=0),
|
||||
"origin": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
"xaxis": DXFAttr(
|
||||
11,
|
||||
xtype=XType.point3d,
|
||||
default=X_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"yaxis": DXFAttr(
|
||||
12,
|
||||
xtype=XType.point3d,
|
||||
default=Y_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_ucs_group_codes = group_code_mapping(acdb_ucs)
|
||||
|
||||
|
||||
@register_entity
|
||||
class UCSTableEntry(DXFEntity):
|
||||
"""DXF UCS table entity"""
|
||||
|
||||
DXFTYPE = "UCS"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_ucs)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.simple_dxfattribs_loader(dxf, acdb_ucs_group_codes) # type: ignore
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_ucs.name)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter, ["name", "flags", "origin", "xaxis", "yaxis"]
|
||||
)
|
||||
|
||||
def ucs(self) -> UCS:
|
||||
"""Returns an :class:`ezdxf.math.UCS` object for this UCS table entry."""
|
||||
return UCS(
|
||||
origin=self.dxf.origin,
|
||||
ux=self.dxf.xaxis,
|
||||
uy=self.dxf.yaxis,
|
||||
)
|
||||
@@ -0,0 +1,446 @@
|
||||
# Copyright (c) 2019-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Union, Iterable, 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,
|
||||
)
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000, DXFTypeError
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.math import NULLVEC, Z_AXIS, UVec, Matrix44, Vec3
|
||||
from .dxfentity import base_class, SubclassProcessor, DXFEntity
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .dxfobj import DXFObject
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
from ezdxf.math.transformtools import (
|
||||
InsertTransformationError,
|
||||
InsertCoordinateSystem,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf import xref
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PdfUnderlay",
|
||||
"DwfUnderlay",
|
||||
"DgnUnderlay",
|
||||
"PdfDefinition",
|
||||
"DgnDefinition",
|
||||
"DwfDefinition",
|
||||
"Underlay",
|
||||
"UnderlayDefinition",
|
||||
]
|
||||
|
||||
acdb_underlay = DefSubclass(
|
||||
"AcDbUnderlayReference",
|
||||
{
|
||||
# Hard reference to underlay definition object
|
||||
"underlay_def_handle": DXFAttr(340),
|
||||
"insert": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# Scale x factor:
|
||||
"scale_x": DXFAttr(
|
||||
41,
|
||||
default=1,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Scale y factor:
|
||||
"scale_y": DXFAttr(
|
||||
42,
|
||||
default=1,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Scale z factor:
|
||||
"scale_z": DXFAttr(
|
||||
43,
|
||||
default=1,
|
||||
validator=validator.is_not_zero,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Rotation angle in degrees:
|
||||
"rotation": DXFAttr(50, default=0),
|
||||
"extrusion": DXFAttr(
|
||||
210,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
optional=True,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Underlay display properties:
|
||||
# 1 = Clipping is on
|
||||
# 2 = Underlay is on
|
||||
# 4 = Monochrome
|
||||
# 8 = Adjust for background
|
||||
"flags": DXFAttr(280, default=10),
|
||||
# Contrast value (20-100; default = 100)
|
||||
"contrast": DXFAttr(
|
||||
281,
|
||||
default=100,
|
||||
validator=validator.is_in_integer_range(20, 101),
|
||||
fixer=validator.fit_into_integer_range(20, 101),
|
||||
),
|
||||
# Fade value (0-80; default = 0)
|
||||
"fade": DXFAttr(
|
||||
282,
|
||||
default=0,
|
||||
validator=validator.is_in_integer_range(0, 81),
|
||||
fixer=validator.fit_into_integer_range(0, 81),
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_underlay_group_codes = group_code_mapping(acdb_underlay)
|
||||
|
||||
|
||||
class Underlay(DXFGraphic):
|
||||
"""Virtual UNDERLAY entity."""
|
||||
|
||||
# DXFTYPE = 'UNDERLAY'
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_underlay)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._boundary_path: list[UVec] = []
|
||||
self._underlay_def: Optional[UnderlayDefinition] = None
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
assert isinstance(entity, Underlay)
|
||||
entity._boundary_path = list(self._boundary_path)
|
||||
entity._underlay_def = self._underlay_def
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.subclass_by_index(2)
|
||||
|
||||
if tags:
|
||||
tags = Tags(self.load_boundary_path(tags))
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_underlay_group_codes, subclass=tags
|
||||
)
|
||||
if len(self.boundary_path) < 2:
|
||||
self.dxf = dxf
|
||||
self.reset_boundary_path()
|
||||
else:
|
||||
raise const.DXFStructureError(
|
||||
f"missing 'AcDbUnderlayReference' subclass in "
|
||||
f"{self.DXFTYPE}(#{dxf.handle})"
|
||||
)
|
||||
return dxf
|
||||
|
||||
def load_boundary_path(self, tags: Tags) -> Iterable:
|
||||
path = []
|
||||
for tag in tags:
|
||||
if tag.code == 11:
|
||||
path.append(tag.value)
|
||||
else:
|
||||
yield tag
|
||||
self._boundary_path = path
|
||||
|
||||
def post_load_hook(self, doc: Drawing) -> None:
|
||||
super().post_load_hook(doc)
|
||||
db = doc.entitydb
|
||||
self._underlay_def = db.get(self.dxf.get("underlay_def_handle", None)) # type: ignore
|
||||
|
||||
def post_bind_hook(self):
|
||||
assert isinstance(self.dxf.handle, str)
|
||||
underlay_def = self._underlay_def
|
||||
if (
|
||||
isinstance(underlay_def, UnderlayDefinition)
|
||||
and self.doc is underlay_def.doc # it's not a xref copy!
|
||||
):
|
||||
underlay_def.append_reactor_handle(self.dxf.handle)
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_underlay.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"underlay_def_handle",
|
||||
"insert",
|
||||
"scale_x",
|
||||
"scale_y",
|
||||
"scale_z",
|
||||
"rotation",
|
||||
"extrusion",
|
||||
"flags",
|
||||
"contrast",
|
||||
"fade",
|
||||
],
|
||||
)
|
||||
self.export_boundary_path(tagwriter)
|
||||
|
||||
def export_boundary_path(self, tagwriter: AbstractTagWriter):
|
||||
for vertex in self.boundary_path:
|
||||
tagwriter.write_vertex(11, vertex[:2])
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
super().register_resources(registry)
|
||||
if isinstance(self._underlay_def, UnderlayDefinition):
|
||||
registry.add_handle(self._underlay_def.dxf.handle)
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
assert isinstance(clone, Underlay)
|
||||
super().map_resources(clone, mapping)
|
||||
underlay_def_copy = self.map_underlay_def(clone, mapping)
|
||||
clone._underlay_def = underlay_def_copy
|
||||
clone.dxf.underlay_def_handle = underlay_def_copy.dxf.handle
|
||||
underlay_def_copy.append_reactor_handle(clone.dxf.handle)
|
||||
|
||||
def map_underlay_def(
|
||||
self, clone: Underlay, mapping: xref.ResourceMapper
|
||||
) -> UnderlayDefinition:
|
||||
underlay_def = self._underlay_def
|
||||
assert isinstance(underlay_def, UnderlayDefinition)
|
||||
|
||||
underlay_def_copy = mapping.get_reference_of_copy(underlay_def.dxf.handle)
|
||||
assert isinstance(underlay_def_copy, UnderlayDefinition)
|
||||
|
||||
doc = clone.doc
|
||||
assert doc is not None
|
||||
|
||||
underlay_dict = doc.rootdict.get_required_dict(underlay_def.acad_dict_name)
|
||||
if underlay_dict.find_key(underlay_def_copy): # entry already exist
|
||||
return underlay_def_copy
|
||||
|
||||
# create required dictionary entry
|
||||
key = doc.objects.next_underlay_key(lambda k: k not in underlay_dict)
|
||||
underlay_dict.take_ownership(key, underlay_def_copy)
|
||||
return underlay_def_copy
|
||||
|
||||
def set_underlay_def(self, underlay_def: UnderlayDefinition) -> None:
|
||||
self._underlay_def = underlay_def
|
||||
self.dxf.underlay_def_handle = underlay_def.dxf.handle
|
||||
underlay_def.append_reactor_handle(self.dxf.handle)
|
||||
|
||||
def get_underlay_def(self) -> Optional[UnderlayDefinition]:
|
||||
return self._underlay_def
|
||||
|
||||
@property
|
||||
def boundary_path(self):
|
||||
return self._boundary_path
|
||||
|
||||
@boundary_path.setter
|
||||
def boundary_path(self, vertices: Iterable[UVec]) -> None:
|
||||
self.set_boundary_path(vertices)
|
||||
|
||||
@property
|
||||
def clipping(self) -> bool:
|
||||
return bool(self.dxf.flags & const.UNDERLAY_CLIPPING)
|
||||
|
||||
@clipping.setter
|
||||
def clipping(self, state: bool) -> None:
|
||||
self.set_flag_state(const.UNDERLAY_CLIPPING, state)
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return bool(self.dxf.flags & const.UNDERLAY_ON)
|
||||
|
||||
@on.setter
|
||||
def on(self, state: bool) -> None:
|
||||
self.set_flag_state(const.UNDERLAY_ON, state)
|
||||
|
||||
@property
|
||||
def monochrome(self) -> bool:
|
||||
return bool(self.dxf.flags & const.UNDERLAY_MONOCHROME)
|
||||
|
||||
@monochrome.setter
|
||||
def monochrome(self, state: bool) -> None:
|
||||
self.set_flag_state(const.UNDERLAY_MONOCHROME, state)
|
||||
|
||||
@property
|
||||
def adjust_for_background(self) -> bool:
|
||||
return bool(self.dxf.flags & const.UNDERLAY_ADJUST_FOR_BG)
|
||||
|
||||
@adjust_for_background.setter
|
||||
def adjust_for_background(self, state: bool):
|
||||
self.set_flag_state(const.UNDERLAY_ADJUST_FOR_BG, state)
|
||||
|
||||
@property
|
||||
def scaling(self) -> tuple[float, float, float]:
|
||||
return self.dxf.scale_x, self.dxf.scale_y, self.dxf.scale_z
|
||||
|
||||
@scaling.setter
|
||||
def scaling(self, scale: Union[float, tuple]):
|
||||
if isinstance(scale, (float, int)):
|
||||
x, y, z = scale, scale, scale
|
||||
else:
|
||||
x, y, z = scale
|
||||
self.dxf.scale_x = x
|
||||
self.dxf.scale_y = y
|
||||
self.dxf.scale_z = z
|
||||
|
||||
def set_boundary_path(self, vertices: Iterable[UVec]) -> None:
|
||||
# path coordinates as drawing coordinates but unscaled
|
||||
vertices = list(vertices)
|
||||
if len(vertices):
|
||||
self._boundary_path = vertices
|
||||
self.clipping = True
|
||||
else:
|
||||
self.reset_boundary_path()
|
||||
|
||||
def reset_boundary_path(self) -> None:
|
||||
"""Removes the clipping path."""
|
||||
self._boundary_path = []
|
||||
self.clipping = False
|
||||
|
||||
def destroy(self) -> None:
|
||||
if not self.is_alive:
|
||||
return
|
||||
|
||||
if self._underlay_def:
|
||||
self._underlay_def.discard_reactor_handle(self.dxf.handle)
|
||||
del self._boundary_path
|
||||
super().destroy()
|
||||
|
||||
def transform(self, m: Matrix44) -> Underlay:
|
||||
"""Transform UNDERLAY entity by transformation matrix `m` inplace.
|
||||
|
||||
Unlike the transformation matrix `m`, the UNDERLAY entity can not
|
||||
represent a non-orthogonal target coordinate system and an
|
||||
:class:`InsertTransformationError` will be raised in that case.
|
||||
|
||||
"""
|
||||
dxf = self.dxf
|
||||
source_system = InsertCoordinateSystem(
|
||||
insert=Vec3(dxf.insert),
|
||||
scale=(dxf.scale_x, dxf.scale_y, dxf.scale_z),
|
||||
rotation=dxf.rotation,
|
||||
extrusion=dxf.extrusion,
|
||||
)
|
||||
try:
|
||||
target_system = source_system.transform(m)
|
||||
except InsertTransformationError:
|
||||
raise InsertTransformationError(
|
||||
"UNDERLAY entity can not represent a non-orthogonal target coordinate system."
|
||||
)
|
||||
dxf.insert = target_system.insert
|
||||
dxf.rotation = target_system.rotation
|
||||
dxf.extrusion = target_system.extrusion
|
||||
dxf.scale_x = target_system.scale_factor_x
|
||||
dxf.scale_y = target_system.scale_factor_y
|
||||
dxf.scale_z = target_system.scale_factor_z
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
|
||||
@register_entity
|
||||
class PdfUnderlay(Underlay):
|
||||
"""DXF PDFUNDERLAY entity"""
|
||||
|
||||
DXFTYPE = "PDFUNDERLAY"
|
||||
|
||||
|
||||
@register_entity
|
||||
class PdfReference(Underlay):
|
||||
"""PDFREFERENCE ia a synonym for PDFUNDERLAY, ezdxf creates always PDFUNDERLAY
|
||||
entities.
|
||||
"""
|
||||
|
||||
DXFTYPE = "PDFREFERENCE"
|
||||
|
||||
|
||||
@register_entity
|
||||
class DwfUnderlay(Underlay):
|
||||
"""DXF DWFUNDERLAY entity"""
|
||||
|
||||
DXFTYPE = "DWFUNDERLAY"
|
||||
|
||||
|
||||
@register_entity
|
||||
class DgnUnderlay(Underlay):
|
||||
"""DXF DGNUNDERLAY entity"""
|
||||
|
||||
DXFTYPE = "DGNUNDERLAY"
|
||||
|
||||
|
||||
acdb_underlay_def = DefSubclass(
|
||||
"AcDbUnderlayDefinition",
|
||||
{
|
||||
"filename": DXFAttr(1), # File name of underlay
|
||||
"name": DXFAttr(2),
|
||||
# underlay name - pdf=page number to display; dgn=default; dwf=????
|
||||
},
|
||||
)
|
||||
acdb_underlay_def_group_codes = group_code_mapping(acdb_underlay_def)
|
||||
|
||||
|
||||
# (PDF|DWF|DGN)DEFINITION - requires entry in objects table ACAD_(PDF|DWF|DGN)DEFINITIONS,
|
||||
# ACAD_(PDF|DWF|DGN)DEFINITIONS do not exist by default
|
||||
class UnderlayDefinition(DXFObject):
|
||||
"""Virtual UNDERLAY DEFINITION entity."""
|
||||
|
||||
DXFTYPE = "UNDERLAYDEFINITION"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_underlay_def)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_underlay_def_group_codes, subclass=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_underlay_def.name)
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["filename", "name"])
|
||||
|
||||
@property
|
||||
def file_format(self) -> str:
|
||||
return self.DXFTYPE[:3]
|
||||
|
||||
@property
|
||||
def entity_name(self) -> str:
|
||||
return self.file_format + "UNDERLAY"
|
||||
|
||||
@property
|
||||
def acad_dict_name(self) -> str:
|
||||
return f"ACAD_{self.file_format}DEFINITIONS"
|
||||
|
||||
def post_new_hook(self):
|
||||
self.set_reactors([self.dxf.owner])
|
||||
|
||||
|
||||
@register_entity
|
||||
class PdfDefinition(UnderlayDefinition):
|
||||
"""DXF PDFDEFINITION entity"""
|
||||
|
||||
DXFTYPE = "PDFDEFINITION"
|
||||
|
||||
|
||||
@register_entity
|
||||
class DwfDefinition(UnderlayDefinition):
|
||||
"""DXF DWFDEFINITION entity"""
|
||||
|
||||
DXFTYPE = "DWFDEFINITION"
|
||||
|
||||
|
||||
@register_entity
|
||||
class DgnDefinition(UnderlayDefinition):
|
||||
"""DXF DGNDEFINITION entity"""
|
||||
|
||||
DXFTYPE = "DGNDEFINITION"
|
||||
@@ -0,0 +1,176 @@
|
||||
# Copyright (c) 2019-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import logging
|
||||
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER, DXF2000, DXF2007, DXF2010
|
||||
from ezdxf.math import Vec3, NULLVEC
|
||||
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.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["View"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
acdb_view = DefSubclass(
|
||||
"AcDbViewTableRecord",
|
||||
{
|
||||
"name": DXFAttr(2, validator=validator.is_valid_table_name),
|
||||
"flags": DXFAttr(70, default=0),
|
||||
"height": DXFAttr(40, default=1),
|
||||
"width": DXFAttr(41, default=1),
|
||||
"center": DXFAttr(10, xtype=XType.point2d, default=NULLVEC),
|
||||
"direction": DXFAttr(
|
||||
11,
|
||||
xtype=XType.point3d,
|
||||
default=Vec3(1, 1, 1),
|
||||
validator=validator.is_not_null_vector,
|
||||
),
|
||||
"target": DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
|
||||
"focal_length": DXFAttr(42, default=50),
|
||||
"front_clipping": DXFAttr(43, default=0),
|
||||
"back_clipping": DXFAttr(44, default=0),
|
||||
"view_twist": DXFAttr(50, default=0),
|
||||
"view_mode": DXFAttr(71, default=0),
|
||||
# Render mode:
|
||||
# 0 = 2D Optimized (classic 2D)
|
||||
# 1 = Wireframe
|
||||
# 2 = Hidden line
|
||||
# 3 = Flat shaded
|
||||
# 4 = Gouraud shaded
|
||||
# 5 = Flat shaded with wireframe
|
||||
# 6 = Gouraud shaded with wireframe
|
||||
"render_mode": DXFAttr(
|
||||
281,
|
||||
default=0,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_in_integer_range(0, 7),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# 1 if there is an UCS associated to this view, 0 otherwise.
|
||||
"ucs": DXFAttr(
|
||||
72,
|
||||
default=0,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"ucs_origin": DXFAttr(110, xtype=XType.point3d, dxfversion=DXF2000),
|
||||
"ucs_xaxis": DXFAttr(
|
||||
111,
|
||||
xtype=XType.point3d,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_not_null_vector,
|
||||
),
|
||||
"ucs_yaxis": DXFAttr(
|
||||
112,
|
||||
xtype=XType.point3d,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_not_null_vector,
|
||||
),
|
||||
# 0 = UCS is not orthographic
|
||||
# 1 = Top
|
||||
# 2 = Bottom
|
||||
# 3 = Front
|
||||
# 4 = Back
|
||||
# 5 = Left
|
||||
# 6 = Right
|
||||
"ucs_ortho_type": DXFAttr(
|
||||
79,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_in_integer_range(0, 7),
|
||||
fixer=lambda x: 0,
|
||||
),
|
||||
"elevation": DXFAttr(146, dxfversion=DXF2000, default=0),
|
||||
# handle of AcDbUCSTableRecord if UCS is a named UCS. If not present,
|
||||
# then UCS is unnamed:
|
||||
"ucs_handle": DXFAttr(345, dxfversion=DXF2000),
|
||||
# handle of AcDbUCSTableRecord of base UCS if UCS is orthographic (79 code
|
||||
# is non-zero). If not present and 79 code is non-zero, then base UCS is
|
||||
# taken to be WORLD
|
||||
"base_ucs_handle": DXFAttr(346, dxfversion=DXF2000),
|
||||
# 1 if the camera is plottable
|
||||
"camera_plottable": DXFAttr(
|
||||
73,
|
||||
default=0,
|
||||
dxfversion=DXF2007,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"background_handle": DXFAttr(332, optional=True, dxfversion=DXF2007),
|
||||
"live_selection_handle": DXFAttr(
|
||||
334, optional=True, dxfversion=DXF2007
|
||||
),
|
||||
"visual_style_handle": DXFAttr(348, optional=True, dxfversion=DXF2007),
|
||||
"sun_handle": DXFAttr(361, optional=True, dxfversion=DXF2010),
|
||||
},
|
||||
)
|
||||
acdb_view_group_codes = group_code_mapping(acdb_view)
|
||||
|
||||
|
||||
@register_entity
|
||||
class View(DXFEntity):
|
||||
"""DXF VIEW entity"""
|
||||
|
||||
DXFTYPE = "VIEW"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_view)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.simple_dxfattribs_loader(dxf, acdb_view_group_codes) # type: ignore
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_view.name)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"flags",
|
||||
"height",
|
||||
"width",
|
||||
"center",
|
||||
"direction",
|
||||
"target",
|
||||
"focal_length",
|
||||
"front_clipping",
|
||||
"back_clipping",
|
||||
"view_twist",
|
||||
"view_mode",
|
||||
"render_mode",
|
||||
"ucs",
|
||||
"ucs_origin",
|
||||
"ucs_xaxis",
|
||||
"ucs_yaxis",
|
||||
"ucs_ortho_type",
|
||||
"elevation",
|
||||
"ucs_handle",
|
||||
"base_ucs_handle",
|
||||
"camera_plottable",
|
||||
"background_handle",
|
||||
"live_selection_handle",
|
||||
"visual_style_handle",
|
||||
"sun_handle",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,681 @@
|
||||
# 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 math
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf import const
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.types import DXFTag, DXFVertex
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.lldxf.const import (
|
||||
DXF12,
|
||||
SUBCLASS_MARKER,
|
||||
DXFStructureError,
|
||||
DXFValueError,
|
||||
DXFTableEntryError,
|
||||
)
|
||||
from ezdxf.math import (
|
||||
Vec3,
|
||||
Vec2,
|
||||
NULLVEC,
|
||||
X_AXIS,
|
||||
Y_AXIS,
|
||||
Z_AXIS,
|
||||
Matrix44,
|
||||
BoundingBox2d,
|
||||
)
|
||||
from ezdxf.tools import set_flag_state
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf import xref
|
||||
|
||||
__all__ = ["Viewport"]
|
||||
|
||||
acdb_viewport = DefSubclass(
|
||||
"AcDbViewport",
|
||||
{
|
||||
# DXF reference: Center point (in WCS)
|
||||
# Correction to the DXF reference:
|
||||
# This point represents the center of the viewport in paper space units
|
||||
# (DCS), but is stored as 3D point inclusive z-axis!
|
||||
"center": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
# Width in paper space units:
|
||||
"width": DXFAttr(40, default=1),
|
||||
# Height in paper space units:
|
||||
"height": DXFAttr(41, default=1),
|
||||
# Viewport status field: (according to the DXF Reference)
|
||||
# -1 = On, but is fully off-screen, or is one of the viewports that is not
|
||||
# active because the $MAXACTVP count is currently being exceeded.
|
||||
# 0 = Off
|
||||
# <positive value> = On and active. The value indicates the order of
|
||||
# stacking for the viewports, where 1 is the "active" viewport, 2 is the
|
||||
# next, and so on. The "active" viewport determines how the paperspace layout
|
||||
# is presented as a whole (location & zoom state)
|
||||
"status": DXFAttr(68, default=0),
|
||||
# Viewport id: (according to the DXF Reference)
|
||||
# Special VIEWPORT id == 1, this viewport defines the area of the layout
|
||||
# which is currently shown in the layout tab by the CAD application.
|
||||
# I guess this is meant by "active viewport" and therefore it is most likely
|
||||
# that this id is always 1.
|
||||
# This "active viewport" is mandatory for a valid DXF file.
|
||||
# BricsCAD set this id to -1 if the viewport is off and 'status' (group code 68)
|
||||
# is not present.
|
||||
"id": DXFAttr(69, default=2),
|
||||
# DXF reference: View center point (in WCS):
|
||||
# Correction to the DXF reference:
|
||||
# This point represents the center point in model space (WCS) stored as
|
||||
# 2D point!
|
||||
"view_center_point": DXFAttr(12, xtype=XType.point2d, default=NULLVEC),
|
||||
"snap_base_point": DXFAttr(13, xtype=XType.point2d, default=NULLVEC),
|
||||
"snap_spacing": DXFAttr(14, xtype=XType.point2d, default=Vec2(10, 10)),
|
||||
"grid_spacing": DXFAttr(15, xtype=XType.point2d, default=Vec2(10, 10)),
|
||||
# View direction vector (WCS):
|
||||
"view_direction_vector": DXFAttr(16, xtype=XType.point3d, default=Z_AXIS),
|
||||
# View target point (in WCS):
|
||||
"view_target_point": DXFAttr(17, xtype=XType.point3d, default=NULLVEC),
|
||||
"perspective_lens_length": DXFAttr(42, default=50),
|
||||
"front_clip_plane_z_value": DXFAttr(43, default=0),
|
||||
"back_clip_plane_z_value": DXFAttr(44, default=0),
|
||||
# View height (in model space units):
|
||||
"view_height": DXFAttr(45, default=1),
|
||||
"snap_angle": DXFAttr(50, default=0),
|
||||
"view_twist_angle": DXFAttr(51, default=0),
|
||||
"circle_zoom": DXFAttr(72, default=100),
|
||||
# 331: Frozen layer object ID/handle (multiple entries may exist) (optional)
|
||||
# Viewport status bit-coded flags:
|
||||
# 1 (0x1) = Enables perspective mode
|
||||
# 2 (0x2) = Enables front clipping
|
||||
# 4 (0x4) = Enables back clipping
|
||||
# 8 (0x8) = Enables UCS follow
|
||||
# 16 (0x10) = Enables front clip not at eye
|
||||
# 32 (0x20) = Enables UCS icon visibility
|
||||
# 64 (0x40) = Enables UCS icon at origin
|
||||
# 128 (0x80) = Enables fast zoom
|
||||
# 256 (0x100) = Enables snap mode
|
||||
# 512 (0x200) = Enables grid mode
|
||||
# 1024 (0x400) = Enables isometric snap style
|
||||
# 2048 (0x800) = Enables hide plot mode
|
||||
# 4096 (0x1000) = kIsoPairTop. If set and kIsoPairRight is not set, then
|
||||
# isopair top is enabled. If both kIsoPairTop and kIsoPairRight are set,
|
||||
# then isopair left is enabled
|
||||
# 8192 (0x2000) = kIsoPairRight. If set and kIsoPairTop is not set, then
|
||||
# isopair right is enabled
|
||||
# 16384 (0x4000) = Enables viewport zoom locking
|
||||
# 32768 (0x8000) = Currently always enabled
|
||||
# 65536 (0x10000) = Enables non-rectangular clipping
|
||||
# 131072 (0x20000) = Turns the viewport off
|
||||
# 262144 (0x40000) = Enables the display of the grid beyond the drawing
|
||||
# limits
|
||||
# 524288 (0x80000) = Enable adaptive grid display
|
||||
# 1048576 (0x100000) = Enables subdivision of the grid below the set grid
|
||||
# spacing when the grid display is adaptive
|
||||
# 2097152 (0x200000) = Enables grid follows workplane switching
|
||||
"flags": DXFAttr(90, default=0),
|
||||
# Clipping viewports: the following handle point to a graphical entity
|
||||
# located in the paperspace. Known supported entities:
|
||||
# LWPOLYLINE (2D POLYLINE), CIRCLE, ELLIPSE, closed SPLINE
|
||||
# Extract bounding- or clipping path: ezdxf.render.make_path()
|
||||
"clipping_boundary_handle": DXFAttr(340, default="0", optional=True),
|
||||
# Plot style sheet name assigned to this viewport
|
||||
"plot_style_name": DXFAttr(1, default=""),
|
||||
# Render mode:
|
||||
# 0 = 2D Optimized (classic 2D)
|
||||
# 1 = Wireframe
|
||||
# 2 = Hidden line
|
||||
# 3 = Flat shaded
|
||||
# 4 = Gouraud shaded
|
||||
# 5 = Flat shaded with wireframe
|
||||
# 6 = Gouraud shaded with wireframe
|
||||
"render_mode": DXFAttr(
|
||||
281,
|
||||
default=0,
|
||||
validator=validator.is_in_integer_range(0, 7),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"ucs_per_viewport": DXFAttr(
|
||||
71,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"ucs_icon": DXFAttr(74, default=0),
|
||||
"ucs_origin": DXFAttr(110, xtype=XType.point3d, default=NULLVEC),
|
||||
"ucs_x_axis": DXFAttr(
|
||||
111,
|
||||
xtype=XType.point3d,
|
||||
default=X_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"ucs_y_axis": DXFAttr(
|
||||
112,
|
||||
xtype=XType.point3d,
|
||||
default=Y_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Handle of AcDbUCSTableRecord if UCS is a named UCS.
|
||||
# If not present, then UCS is unnamed:
|
||||
"ucs_handle": DXFAttr(345),
|
||||
# Handle of AcDbUCSTableRecord of base UCS if UCS is orthographic (79 code
|
||||
# is non-zero). If not present and 79 code is non-zero, then base UCS is
|
||||
# taken to be WORLD:
|
||||
"base_ucs_handle": DXFAttr(346, optional=True),
|
||||
# UCS ortho type:
|
||||
# 0 = not orthographic
|
||||
# 1 = Top
|
||||
# 2 = Bottom
|
||||
# 3 = Front
|
||||
# 4 = Back
|
||||
# 5 = Left
|
||||
# 6 = Right
|
||||
"ucs_ortho_type": DXFAttr(
|
||||
79,
|
||||
default=0,
|
||||
validator=validator.is_in_integer_range(0, 7),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"elevation": DXFAttr(146, default=0),
|
||||
# Shade plot mode:
|
||||
# 0 = As Displayed
|
||||
# 1 = Wireframe
|
||||
# 2 = Hidden
|
||||
# 3 = Rendered
|
||||
"shade_plot_mode": DXFAttr(
|
||||
170,
|
||||
dxfversion="AC1018",
|
||||
validator=validator.is_in_integer_range(0, 4),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Frequency of major grid lines compared to minor grid lines
|
||||
"grid_frequency": DXFAttr(61, dxfversion="AC1021"),
|
||||
"background_handle": DXFAttr(332, dxfversion="AC1021", optional=True),
|
||||
"shade_plot_handle": DXFAttr(333, dxfversion="AC1021", optional=True),
|
||||
"visual_style_handle": DXFAttr(348, dxfversion="AC1021", optional=True),
|
||||
"default_lighting_flag": DXFAttr(
|
||||
292, dxfversion="AC1021", default=1, optional=True
|
||||
),
|
||||
# Default lighting type:
|
||||
# 0 = One distant light
|
||||
# 1 = Two distant lights
|
||||
"default_lighting_type": DXFAttr(
|
||||
282,
|
||||
default=0,
|
||||
dxfversion="AC1021",
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"view_brightness": DXFAttr(141, dxfversion="AC1021"),
|
||||
"view_contrast": DXFAttr(142, dxfversion="AC1021"),
|
||||
# as AutoCAD Color Index
|
||||
"ambient_light_color_1": DXFAttr(
|
||||
63,
|
||||
dxfversion="AC1021",
|
||||
validator=validator.is_valid_aci_color,
|
||||
),
|
||||
# as True Color:
|
||||
"ambient_light_color_2": DXFAttr(421, dxfversion="AC1021"),
|
||||
# as True Color:
|
||||
"ambient_light_color_3": DXFAttr(431, dxfversion="AC1021"),
|
||||
"sun_handle": DXFAttr(361, dxfversion="AC1021", optional=True),
|
||||
# The following attributes are mentioned in the DXF reference but may not really exist:
|
||||
# "Soft pointer reference to viewport object (for layer VP property override)"
|
||||
"ref_vp_object_1": DXFAttr(335, dxfversion="AC1021"), # soft-pointer
|
||||
"ref_vp_object_2": DXFAttr(343, dxfversion="AC1021"), # hard-pointer
|
||||
"ref_vp_object_3": DXFAttr(344, dxfversion="AC1021"), # hard-pointer
|
||||
"ref_vp_object_4": DXFAttr(91, dxfversion="AC1021"), # this is not a pointer!
|
||||
},
|
||||
)
|
||||
acdb_viewport_group_codes = group_code_mapping(acdb_viewport)
|
||||
# Note:
|
||||
# The ZOOM XP factor is calculated with the following formula:
|
||||
# group_41 / group_45 (or pspace_height / mspace_height).
|
||||
|
||||
FROZEN_LAYER_GROUP_CODE = 331
|
||||
|
||||
|
||||
@register_entity
|
||||
class Viewport(DXFGraphic):
|
||||
"""DXF VIEWPORT entity"""
|
||||
|
||||
DXFTYPE = "VIEWPORT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_viewport)
|
||||
|
||||
# Notes to viewport_id:
|
||||
# The id of the first viewport has to be 1, which is the definition of
|
||||
# paper space. For the following viewports it seems only important, that
|
||||
# the id is greater than 1.
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._frozen_layers: list[str] = []
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
assert isinstance(entity, Viewport)
|
||||
entity._frozen_layers = list(self._frozen_layers)
|
||||
|
||||
@property
|
||||
def frozen_layers(self) -> list[str]:
|
||||
"""Set/get frozen layers as list of layer names."""
|
||||
return self._frozen_layers
|
||||
|
||||
@frozen_layers.setter
|
||||
def frozen_layers(self, names: Iterable[str]):
|
||||
self._frozen_layers = list(names)
|
||||
|
||||
def _layer_index(self, layer_name: str) -> int:
|
||||
name_key = validator.make_table_key(layer_name)
|
||||
for index, name in enumerate(self._frozen_layers):
|
||||
if name_key == validator.make_table_key(name):
|
||||
return index
|
||||
return -1
|
||||
|
||||
def freeze(self, layer_name: str) -> None:
|
||||
"""Freeze `layer_name` in this viewport."""
|
||||
index = self._layer_index(layer_name)
|
||||
if index == -1:
|
||||
self._frozen_layers.append(layer_name)
|
||||
|
||||
def is_frozen(self, layer_name: str) -> bool:
|
||||
"""Returns ``True`` if `layer_name` id frozen in this viewport."""
|
||||
return self._layer_index(layer_name) != -1
|
||||
|
||||
def thaw(self, layer_name: str) -> None:
|
||||
"""Thaw `layer_name` in this viewport."""
|
||||
index = self._layer_index(layer_name)
|
||||
if index != -1:
|
||||
del self._frozen_layers[index]
|
||||
|
||||
@property
|
||||
def is_visible(self) -> bool:
|
||||
# VIEWPORT id == 1 or status == 1, this viewport defines the "active viewport"
|
||||
# which is the area currently shown in the layout tab by the CAD
|
||||
# application.
|
||||
# BricsCAD set id to -1 if the viewport is off and 'status' (group
|
||||
# code 68) is not present.
|
||||
# status: -1= off-screen, 0= off, 1= "active viewport"
|
||||
if self.dxf.hasattr("status"):
|
||||
return self.dxf.status > 0
|
||||
return self.dxf.id > 1
|
||||
|
||||
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_viewport_group_codes, subclass=2, log=False
|
||||
)
|
||||
if processor.r12:
|
||||
self.load_xdata_into_dxf_namespace()
|
||||
else:
|
||||
if len(tags):
|
||||
tags = self.load_frozen_layer_handles(tags)
|
||||
if len(tags):
|
||||
processor.log_unprocessed_tags(tags, subclass=acdb_viewport.name)
|
||||
return dxf
|
||||
|
||||
def post_load_hook(self, doc: Drawing):
|
||||
super().post_load_hook(doc)
|
||||
bag: list[str] = []
|
||||
db = doc.entitydb
|
||||
for handle in self._frozen_layers:
|
||||
try:
|
||||
bag.append(db[handle].dxf.name)
|
||||
except KeyError: # ignore non-existing layers
|
||||
pass
|
||||
self._frozen_layers = bag
|
||||
|
||||
def load_frozen_layer_handles(self, tags: Tags) -> Tags:
|
||||
unprocessed_tags = Tags()
|
||||
for tag in tags:
|
||||
if tag.code == FROZEN_LAYER_GROUP_CODE:
|
||||
self._frozen_layers.append(tag.value)
|
||||
else:
|
||||
unprocessed_tags.append(tag)
|
||||
return unprocessed_tags
|
||||
|
||||
def load_xdata_into_dxf_namespace(self) -> None:
|
||||
try:
|
||||
tags = [v for c, v in self.xdata.get_xlist("ACAD", "MVIEW")] # type: ignore
|
||||
except DXFValueError:
|
||||
return
|
||||
tags = tags[3:-2]
|
||||
dxf = self.dxf
|
||||
flags = 0
|
||||
flags = set_flag_state(flags, const.VSF_FAST_ZOOM, bool(tags[11]))
|
||||
flags = set_flag_state(flags, const.VSF_SNAP_MODE, bool(tags[13]))
|
||||
flags = set_flag_state(flags, const.VSF_GRID_MODE, bool(tags[14]))
|
||||
flags = set_flag_state(flags, const.VSF_ISOMETRIC_SNAP_STYLE, bool(tags[15]))
|
||||
flags = set_flag_state(flags, const.VSF_HIDE_PLOT_MODE, bool(tags[24]))
|
||||
try:
|
||||
dxf.view_target_point = tags[0]
|
||||
dxf.view_direction_vector = tags[1]
|
||||
dxf.view_twist_angle = tags[2]
|
||||
dxf.view_height = tags[3]
|
||||
dxf.view_center_point = tags[4], tags[5]
|
||||
dxf.perspective_lens_length = tags[6]
|
||||
dxf.front_clip_plane_z_value = tags[7]
|
||||
dxf.back_clip_plane_z_value = tags[8]
|
||||
dxf.render_mode = tags[9] # view_mode == render_mode ?
|
||||
dxf.circle_zoom = tags[10]
|
||||
# fast zoom flag : tag[11]
|
||||
dxf.ucs_icon = tags[12]
|
||||
# snap mode flag = tags[13]
|
||||
# grid mode flag = tags[14]
|
||||
# isometric snap style = tags[15]
|
||||
# dxf.snap_isopair = tags[16] ???
|
||||
dxf.snap_angle = tags[17]
|
||||
dxf.snap_base_point = tags[18], tags[19]
|
||||
dxf.snap_spacing = tags[20], tags[21]
|
||||
dxf.grid_spacing = tags[22], tags[23]
|
||||
# hide plot flag = tags[24]
|
||||
except IndexError: # internal exception
|
||||
raise DXFStructureError("Invalid viewport entity - missing data")
|
||||
dxf.flags = flags
|
||||
self._frozen_layers = tags[26:]
|
||||
self.xdata.discard("ACAD") # type: ignore
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
if tagwriter.dxfversion == DXF12:
|
||||
self.export_acdb_viewport_r12(tagwriter)
|
||||
else:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_viewport.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"center",
|
||||
"width",
|
||||
"height",
|
||||
"status",
|
||||
"id",
|
||||
"view_center_point",
|
||||
"snap_base_point",
|
||||
"snap_spacing",
|
||||
"grid_spacing",
|
||||
"view_direction_vector",
|
||||
"view_target_point",
|
||||
"perspective_lens_length",
|
||||
"front_clip_plane_z_value",
|
||||
"back_clip_plane_z_value",
|
||||
"view_height",
|
||||
"snap_angle",
|
||||
"view_twist_angle",
|
||||
"circle_zoom",
|
||||
],
|
||||
)
|
||||
if len(self.frozen_layers):
|
||||
assert self.doc is not None, "valid DXF document required"
|
||||
layers = self.doc.layers
|
||||
for layer_name in self.frozen_layers:
|
||||
try:
|
||||
layer = layers.get(layer_name)
|
||||
except DXFTableEntryError:
|
||||
pass
|
||||
else:
|
||||
tagwriter.write_tag2(FROZEN_LAYER_GROUP_CODE, layer.dxf.handle)
|
||||
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"flags",
|
||||
"clipping_boundary_handle",
|
||||
"plot_style_name",
|
||||
"render_mode",
|
||||
"ucs_per_viewport",
|
||||
"ucs_icon",
|
||||
"ucs_origin",
|
||||
"ucs_x_axis",
|
||||
"ucs_y_axis",
|
||||
"ucs_handle",
|
||||
"base_ucs_handle",
|
||||
"ucs_ortho_type",
|
||||
"elevation",
|
||||
"shade_plot_mode",
|
||||
"grid_frequency",
|
||||
"background_handle",
|
||||
"shade_plot_handle",
|
||||
"visual_style_handle",
|
||||
"default_lighting_flag",
|
||||
"default_lighting_type",
|
||||
"view_brightness",
|
||||
"view_contrast",
|
||||
"ambient_light_color_1",
|
||||
"ambient_light_color_2",
|
||||
"ambient_light_color_3",
|
||||
"sun_handle",
|
||||
"ref_vp_object_1",
|
||||
"ref_vp_object_2",
|
||||
"ref_vp_object_3",
|
||||
"ref_vp_object_4",
|
||||
],
|
||||
)
|
||||
|
||||
def export_acdb_viewport_r12(self, tagwriter: AbstractTagWriter):
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"center",
|
||||
"width",
|
||||
"height",
|
||||
"status",
|
||||
"id",
|
||||
],
|
||||
)
|
||||
tagwriter.write_tags(self.dxftags())
|
||||
|
||||
def dxftags(self) -> Tags:
|
||||
def flag(flag):
|
||||
return 1 if self.dxf.flags & flag else 0
|
||||
|
||||
dxf = self.dxf
|
||||
tags = [
|
||||
DXFTag(1001, "ACAD"),
|
||||
DXFTag(1000, "MVIEW"),
|
||||
DXFTag(1002, "{"),
|
||||
DXFTag(1070, 16), # extended data version, always 16 for R11/12
|
||||
DXFVertex(1010, dxf.view_target_point),
|
||||
DXFVertex(1010, dxf.view_direction_vector),
|
||||
DXFTag(1040, dxf.view_twist_angle),
|
||||
DXFTag(1040, dxf.view_height),
|
||||
DXFTag(1040, dxf.view_center_point[0]),
|
||||
DXFTag(
|
||||
1040,
|
||||
dxf.view_center_point[1],
|
||||
),
|
||||
DXFTag(1040, dxf.perspective_lens_length),
|
||||
DXFTag(1040, dxf.front_clip_plane_z_value),
|
||||
DXFTag(1040, dxf.back_clip_plane_z_value),
|
||||
DXFTag(1070, dxf.render_mode),
|
||||
DXFTag(1070, dxf.circle_zoom),
|
||||
DXFTag(1070, flag(const.VSF_FAST_ZOOM)),
|
||||
DXFTag(1070, dxf.ucs_icon),
|
||||
DXFTag(1070, flag(const.VSF_SNAP_MODE)),
|
||||
DXFTag(1070, flag(const.VSF_GRID_MODE)),
|
||||
DXFTag(1070, flag(const.VSF_ISOMETRIC_SNAP_STYLE)),
|
||||
DXFTag(1070, 0), # snap isopair ???
|
||||
DXFTag(1040, dxf.snap_angle),
|
||||
DXFTag(1040, dxf.snap_base_point[0]),
|
||||
DXFTag(1040, dxf.snap_base_point[1]),
|
||||
DXFTag(1040, dxf.snap_spacing[0]),
|
||||
DXFTag(1040, dxf.snap_spacing[1]),
|
||||
DXFTag(1040, dxf.grid_spacing[0]),
|
||||
DXFTag(1040, dxf.grid_spacing[1]),
|
||||
DXFTag(1070, flag(const.VSF_HIDE_PLOT_MODE)),
|
||||
DXFTag(1002, "{"), # start frozen layer list
|
||||
]
|
||||
tags.extend(DXFTag(1003, layer_name) for layer_name in self.frozen_layers)
|
||||
tags.extend(
|
||||
[
|
||||
DXFTag(1002, "}"), # end of frozen layer list
|
||||
DXFTag(1002, "}"), # MVIEW
|
||||
]
|
||||
)
|
||||
return Tags(tags)
|
||||
|
||||
def register_resources(self, registry: xref.Registry) -> None:
|
||||
assert self.doc is not None
|
||||
super().register_resources(registry)
|
||||
# The clipping path entity should not be added here!
|
||||
registry.add_handle(self.dxf.get("ucs_handle"))
|
||||
registry.add_handle(self.dxf.get("base_ucs_handle"))
|
||||
registry.add_handle(self.dxf.get("visual_style_handle"))
|
||||
registry.add_handle(self.dxf.get("background_handle"))
|
||||
registry.add_handle(self.dxf.get("shade_plot_handle"))
|
||||
registry.add_handle(self.dxf.get("sun_handle"))
|
||||
|
||||
def map_resources(self, clone: Self, mapping: xref.ResourceMapper) -> None:
|
||||
assert isinstance(clone, Viewport)
|
||||
super().map_resources(clone, mapping)
|
||||
|
||||
mapping.map_existing_handle(
|
||||
self, clone, "clipping_boundary_handle", optional=True
|
||||
)
|
||||
mapping.map_existing_handle(self, clone, "ucs_handle", optional=True)
|
||||
mapping.map_existing_handle(self, clone, "base_ucs_handle", optional=True)
|
||||
mapping.map_existing_handle(self, clone, "visual_style_handle", optional=True)
|
||||
mapping.map_existing_handle(self, clone, "sun_handle", optional=True)
|
||||
# VIEWPORT entity is hard owner of the SUN object
|
||||
clone.take_sun_ownership()
|
||||
clone.frozen_layers = [mapping.get_layer(name) for name in self.frozen_layers]
|
||||
|
||||
# I have no information to what entities the background- and the shade_plot
|
||||
# handles are pointing to and I don't have any examples for that!
|
||||
mapping.map_existing_handle(self, clone, "background_handle", optional=True)
|
||||
mapping.map_existing_handle(self, clone, "shade_plot_handle", optional=True)
|
||||
|
||||
# No information if these attributes really exist or any examples where these
|
||||
# attributes are used. BricsCAD does not create these attributes when using
|
||||
# viewport layer overrides:
|
||||
for num in range(1, 5):
|
||||
clone.dxf.discard(f"ref_vp_object_{num}")
|
||||
|
||||
def take_sun_ownership(self) -> None:
|
||||
assert self.doc is not None
|
||||
sun = self.doc.entitydb.get(self.dxf.get("sun_handle"))
|
||||
if sun:
|
||||
sun.dxf.owner = self.dxf.handle
|
||||
|
||||
def rename_frozen_layer(self, old_name: str, new_name: str) -> None:
|
||||
assert self.doc is not None, "valid DXF document required"
|
||||
key = self.doc.layers.key
|
||||
old_key = key(old_name)
|
||||
self.frozen_layers = [
|
||||
(name if key(name) != old_key else new_name) for name in self.frozen_layers
|
||||
]
|
||||
|
||||
def clipping_rect_corners(self) -> list[Vec2]:
|
||||
"""Returns the default rectangular clipping path as list of
|
||||
vertices. Use function :func:`ezdxf.path.make_path` to get also
|
||||
non-rectangular shaped clipping paths if defined.
|
||||
"""
|
||||
center = self.dxf.center
|
||||
cx = center.x
|
||||
cy = center.y
|
||||
width2 = self.dxf.width / 2
|
||||
height2 = self.dxf.height / 2
|
||||
return [
|
||||
Vec2(cx - width2, cy - height2),
|
||||
Vec2(cx + width2, cy - height2),
|
||||
Vec2(cx + width2, cy + height2),
|
||||
Vec2(cx - width2, cy + height2),
|
||||
]
|
||||
|
||||
def clipping_rect(self) -> tuple[Vec2, Vec2]:
|
||||
"""Returns the lower left and the upper right corner of the clipping
|
||||
rectangle in paperspace coordinates.
|
||||
"""
|
||||
corners = self.clipping_rect_corners()
|
||||
return corners[0], corners[2]
|
||||
|
||||
@property
|
||||
def has_extended_clipping_path(self) -> bool:
|
||||
"""Returns ``True`` if a non-rectangular clipping path is defined."""
|
||||
_flag = self.dxf.flags & const.VSF_NON_RECTANGULAR_CLIPPING
|
||||
if _flag:
|
||||
handle = self.dxf.clipping_boundary_handle
|
||||
return handle != "0"
|
||||
return False
|
||||
|
||||
def get_scale(self) -> float:
|
||||
"""Returns the scaling factor from modelspace to viewport."""
|
||||
msp_height = self.dxf.view_height
|
||||
if abs(msp_height) < 1e-12:
|
||||
return 0.0
|
||||
vp_height = self.dxf.height
|
||||
return vp_height / msp_height
|
||||
|
||||
@property
|
||||
def is_top_view(self) -> bool:
|
||||
"""Returns ``True`` if the viewport is a top view."""
|
||||
view_direction: Vec3 = self.dxf.view_direction_vector
|
||||
return view_direction.is_null or view_direction.isclose(Z_AXIS)
|
||||
|
||||
def get_view_center_point(self) -> Vec3:
|
||||
# TODO: Is there a flag or attribute that determines which of these points is
|
||||
# the center point?
|
||||
center_point = Vec3(self.dxf.view_center_point)
|
||||
if center_point.is_null:
|
||||
center_point = Vec3(self.dxf.view_target_point)
|
||||
return center_point
|
||||
|
||||
def get_transformation_matrix(self) -> Matrix44:
|
||||
"""Returns the transformation matrix from modelspace to paperspace coordinates."""
|
||||
# supports only top-view viewports!
|
||||
scale = self.get_scale()
|
||||
rotation_angle: float = self.dxf.view_twist_angle
|
||||
msp_center_point: Vec3 = self.get_view_center_point()
|
||||
offset: Vec3 = self.dxf.center - (msp_center_point * scale)
|
||||
m = Matrix44.scale(scale)
|
||||
if rotation_angle:
|
||||
m @= Matrix44.z_rotate(math.radians(rotation_angle))
|
||||
return m @ Matrix44.translate(offset.x, offset.y, 0)
|
||||
|
||||
def get_aspect_ratio(self) -> float:
|
||||
"""Returns the aspect ratio of the viewport, return 0.0 if width or
|
||||
height is zero.
|
||||
"""
|
||||
try:
|
||||
return self.dxf.width / self.dxf.height
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
def get_modelspace_limits(self) -> tuple[float, float, float, float]:
|
||||
"""Returns the limits of the modelspace to view in drawing units
|
||||
as tuple (min_x, min_y, max_x, max_y).
|
||||
"""
|
||||
msp_center_point: Vec3 = self.get_view_center_point()
|
||||
msp_height: float = self.dxf.view_height
|
||||
rotation_angle: float = self.dxf.view_twist_angle
|
||||
ratio = self.get_aspect_ratio()
|
||||
if ratio == 0.0:
|
||||
raise ValueError("invalid viewport parameters width or height")
|
||||
|
||||
w2 = msp_height * ratio * 0.5
|
||||
h2 = msp_height * 0.5
|
||||
if rotation_angle:
|
||||
frame = Vec2.list(((-w2, -h2), (w2, -h2), (w2, h2), (-w2, h2)))
|
||||
angle = math.radians(rotation_angle)
|
||||
bbox = BoundingBox2d(v.rotate(angle) + msp_center_point for v in frame)
|
||||
return bbox.extmin.x, bbox.extmin.y, bbox.extmax.x, bbox.extmax.y
|
||||
else:
|
||||
mx, my, _ = msp_center_point
|
||||
return mx - w2, my - h2, mx + w2, my + h2
|
||||
@@ -0,0 +1,235 @@
|
||||
# Copyright (c) 2019-2023, 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.lldxf.const import SUBCLASS_MARKER, DXF2000, DXFStructureError
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
DXFAttr,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfobj import DXFObject
|
||||
from .factory import register_entity
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace, DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["VisualStyle"]
|
||||
|
||||
acdb_visualstyle = DefSubclass(
|
||||
"AcDbVisualStyle",
|
||||
{
|
||||
"description": DXFAttr(2),
|
||||
# Style type:
|
||||
# 0 = Flat
|
||||
# 1 = FlatWithEdges
|
||||
# 2 = Gouraud
|
||||
# 3 = GouraudWithEdges
|
||||
# 4 = 2dWireframe
|
||||
# 5 = Wireframe
|
||||
# 6 = Hidden
|
||||
# 7 = Basic
|
||||
# 8 = Realistic
|
||||
# 9 = Conceptual
|
||||
# 10 = Modeling
|
||||
# 11 = Dim
|
||||
# 12 = Brighten
|
||||
# 13 = Thicken
|
||||
# 14 = Linepattern
|
||||
# 15 = Facepattern
|
||||
# 16 = ColorChange
|
||||
# 20 = JitterOff
|
||||
# 21 = OverhangOff
|
||||
# 22 = EdgeColorOff
|
||||
# 23 = Shades of Gray
|
||||
# 24 = Sketchy
|
||||
# 25 = X-Ray
|
||||
# 26 = Shaded with edges
|
||||
# 27 = Shaded
|
||||
"style_type": DXFAttr(
|
||||
70, validator=validator.is_in_integer_range(0, 28)
|
||||
),
|
||||
# Face lighting type:
|
||||
# 0 = Invisible
|
||||
# 1 = Visible
|
||||
# 2 = Phong
|
||||
# 3 = Gooch
|
||||
"face_lighting_model": DXFAttr(
|
||||
71, validator=validator.is_in_integer_range(0, 4)
|
||||
),
|
||||
# Face lighting quality:
|
||||
# 0 = No lighting
|
||||
# 1 = Per face lighting
|
||||
# 2 = Per vertex lighting
|
||||
# 3 = Unknown
|
||||
"face_lighting_quality": DXFAttr(
|
||||
72, validator=validator.is_in_integer_range(0, 4)
|
||||
),
|
||||
# Face color mode:
|
||||
# 0 = No color
|
||||
# 1 = Object color
|
||||
# 2 = Background color
|
||||
# 3 = Custom color
|
||||
# 4 = Mono color
|
||||
# 5 = Tinted
|
||||
# 6 = Desaturated
|
||||
"face_color_mode": DXFAttr(
|
||||
73, validator=validator.is_in_integer_range(0, 6)
|
||||
),
|
||||
# Face modifiers:
|
||||
# 0 = No modifiers
|
||||
# 1 = Opacity
|
||||
# 2 = Specular
|
||||
# 3 = Unknown
|
||||
"face_modifiers": DXFAttr(
|
||||
90, validator=validator.is_in_integer_range(0, 4)
|
||||
),
|
||||
"face_opacity_level": DXFAttr(40),
|
||||
"face_specular_level": DXFAttr(41),
|
||||
"color1": DXFAttr(62),
|
||||
"color2": DXFAttr(63),
|
||||
"face_style_mono_color": DXFAttr(421),
|
||||
# Edge style model:
|
||||
# 0 = No edges
|
||||
# 1 = Isolines
|
||||
# 2 = Facet edges
|
||||
"edge_style_model": DXFAttr(
|
||||
74, validator=validator.is_in_integer_range(0, 3)
|
||||
),
|
||||
"edge_style": DXFAttr(91),
|
||||
"edge_intersection_color": DXFAttr(64),
|
||||
"edge_obscured_color": DXFAttr(65),
|
||||
"edge_obscured_linetype": DXFAttr(75),
|
||||
"edge_intersection_linetype": DXFAttr(175),
|
||||
"edge_crease_angle": DXFAttr(42),
|
||||
"edge_modifiers": DXFAttr(92),
|
||||
"edge_color": DXFAttr(66),
|
||||
"edge_opacity_level": DXFAttr(43),
|
||||
"edge_width": DXFAttr(76),
|
||||
"edge_overhang": DXFAttr(77),
|
||||
"edge_jitter": DXFAttr(78),
|
||||
"edge_silhouette_color": DXFAttr(67),
|
||||
"edge_silhouette_width": DXFAttr(79),
|
||||
"edge_halo_gap": DXFAttr(170),
|
||||
"edge_isoline_count": DXFAttr(171),
|
||||
"edge_hide_precision": DXFAttr(290), # flag
|
||||
"edge_style_apply": DXFAttr(174), # flag
|
||||
"style_display_settings": DXFAttr(93),
|
||||
"brightness": DXFAttr(44),
|
||||
"shadow_type": DXFAttr(173),
|
||||
"unknown1": DXFAttr(177), # required if xdata is present?
|
||||
"internal_use_only_flag": DXFAttr(
|
||||
291
|
||||
), # visual style only use internal
|
||||
# Xdata must follow tag 291 (AutoCAD -> 'Xdata wasn't read' error)
|
||||
# 70: Xdata count (count of tag groups)
|
||||
# any code, value: multiple tags build a tag group
|
||||
# 176, 1: end of group marker
|
||||
# e.g. (291, 0) (70, 2) (62, 7) (420, 16777215) (176, 1) (90, 1) (176, 1)
|
||||
},
|
||||
)
|
||||
acdb_visualstyle_group_codes = group_code_mapping(acdb_visualstyle)
|
||||
|
||||
|
||||
# undocumented Xdata in DXF R2018
|
||||
@register_entity
|
||||
class VisualStyle(DXFObject):
|
||||
"""DXF VISUALSTYLE entity"""
|
||||
|
||||
DXFTYPE = "VISUALSTYLE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_visualstyle)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000 # official supported in R2007
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.acad_xdata = None # to preserve AutoCAD xdata
|
||||
|
||||
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
|
||||
"""Copy acad internal data."""
|
||||
assert isinstance(entity, VisualStyle)
|
||||
entity.acad_xdata = copy.deepcopy(self.acad_xdata)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
tags = processor.subclass_by_index(1)
|
||||
if tags:
|
||||
self.acad_xdata = self.store_acad_xdata(tags)
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_visualstyle_group_codes, subclass=tags
|
||||
)
|
||||
else:
|
||||
raise DXFStructureError(
|
||||
f"missing 'AcDbVisualStyle' subclass in VISUALSTYLE(#{dxf.handle})"
|
||||
)
|
||||
|
||||
return dxf
|
||||
|
||||
@staticmethod
|
||||
def store_acad_xdata(tags: Tags):
|
||||
try:
|
||||
index = tags.tag_index(291)
|
||||
except IndexError:
|
||||
return None
|
||||
else: # store tags after 291
|
||||
index += 1
|
||||
xdata = tags[index:]
|
||||
del tags[index:] # remove xdata from subclass
|
||||
return xdata
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
"""Export entity specific data as DXF tags."""
|
||||
super().export_entity(tagwriter)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_visualstyle.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"description",
|
||||
"style_type",
|
||||
"face_lighting_model",
|
||||
"face_lighting_quality",
|
||||
"face_color_mode",
|
||||
"face_modifiers",
|
||||
"face_opacity_level",
|
||||
"face_specular_level",
|
||||
"color1",
|
||||
"color2",
|
||||
"face_style_mono_color",
|
||||
"edge_style_model",
|
||||
"edge_style",
|
||||
"edge_intersection_color",
|
||||
"edge_obscured_color",
|
||||
"edge_obscured_linetype",
|
||||
"edge_intersection_linetype",
|
||||
"edge_crease_angle",
|
||||
"edge_modifiers",
|
||||
"edge_color",
|
||||
"edge_opacity_level",
|
||||
"edge_width",
|
||||
"edge_overhang",
|
||||
"edge_jitter",
|
||||
"edge_silhouette_color",
|
||||
"edge_silhouette_width",
|
||||
"edge_halo_gap",
|
||||
"edge_isoline_count",
|
||||
"edge_hide_precision",
|
||||
"edge_style_apply",
|
||||
"style_display_settings",
|
||||
"brightness",
|
||||
"shadow_type",
|
||||
"unknown1",
|
||||
"internal_use_only_flag",
|
||||
],
|
||||
)
|
||||
if self.acad_xdata:
|
||||
tagwriter.write_tags(self.acad_xdata)
|
||||
@@ -0,0 +1,259 @@
|
||||
# Copyright (c) 2019-2022, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import logging
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER, DXF2000, DXF2007
|
||||
from ezdxf.lldxf.validator import is_valid_vport_name
|
||||
from ezdxf.math import Vec2, NULLVEC, Z_AXIS
|
||||
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.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["VPort"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
acdb_vport = DefSubclass(
|
||||
"AcDbViewportTableRecord",
|
||||
{
|
||||
"name": DXFAttr(2, validator=is_valid_vport_name),
|
||||
"flags": DXFAttr(70, default=0),
|
||||
"lower_left": DXFAttr(10, xtype=XType.point2d, default=Vec2(0, 0)),
|
||||
"upper_right": DXFAttr(11, xtype=XType.point2d, default=Vec2(1, 1)),
|
||||
"center": DXFAttr(12, xtype=XType.point2d, default=Vec2(0, 0)),
|
||||
"snap_base": DXFAttr(13, xtype=XType.point2d, default=Vec2(0, 0)),
|
||||
"snap_spacing": DXFAttr(
|
||||
14, xtype=XType.point2d, default=Vec2(0.5, 0.5)
|
||||
),
|
||||
"grid_spacing": DXFAttr(
|
||||
15, xtype=XType.point2d, default=Vec2(0.5, 0.5)
|
||||
),
|
||||
"direction": DXFAttr(
|
||||
16,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"target": DXFAttr(17, xtype=XType.point3d, default=NULLVEC),
|
||||
# height: DXF reference error: listed as group code 45
|
||||
"height": DXFAttr(40, default=1000),
|
||||
"aspect_ratio": DXFAttr(41, default=1.34),
|
||||
"focal_length": DXFAttr(42, default=50),
|
||||
"front_clipping": DXFAttr(43, default=0),
|
||||
"back_clipping": DXFAttr(44, default=0),
|
||||
"snap_rotation": DXFAttr(50, default=0),
|
||||
"view_twist": DXFAttr(51, default=0),
|
||||
"view_mode": DXFAttr(71, default=0),
|
||||
"circle_sides": DXFAttr(72, default=1000),
|
||||
"fast_zoom": DXFAttr(73, default=1), # removed in R2007
|
||||
# ucs_icon:
|
||||
# bit 0: 0=hide, 1=show
|
||||
# bit 1: 0=display in lower left corner, 1=display at origin
|
||||
"ucs_icon": DXFAttr(74, default=3), # show at origin
|
||||
"snap_on": DXFAttr(75, default=0), # removed in R2007
|
||||
"grid_on": DXFAttr(76, default=0), # removed in R2007
|
||||
"snap_style": DXFAttr(77, default=0), # removed in R2007
|
||||
"snap_isopair": DXFAttr(78, default=0), # removed in R2007
|
||||
# R2000: 331 or 441 (optional) - ignored by ezdxf
|
||||
# Soft or hard-pointer ID/handle to frozen layer objects;
|
||||
# repeats for each frozen layers
|
||||
# 70: Bit flags and perspective mode
|
||||
# CTB-File?
|
||||
"plot_style_sheet": DXFAttr(1, dxfversion=DXF2007),
|
||||
# Render mode:
|
||||
# 0 = 2D Optimized (classic 2D)
|
||||
# 1 = Wireframe
|
||||
# 2 = Hidden line
|
||||
# 3 = Flat shaded
|
||||
# 4 = Gouraud shaded
|
||||
# 5 = Flat shaded with wireframe
|
||||
# 6 = Gouraud shaded with wireframe
|
||||
# All rendering modes other than 2D Optimized engage the new 3D graphics
|
||||
# pipeline. These values directly correspond to the SHADEMODE command and
|
||||
# the AcDbAbstractViewTableRecord::RenderMode enum
|
||||
"render_mode": DXFAttr(
|
||||
281,
|
||||
default=0,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_in_integer_range(0, 7),
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
# Value of UCSVP for this viewport. If set to 1, then viewport stores its
|
||||
# own UCS which will become the current UCS whenever the viewport is
|
||||
# activated. If set to 0, UCS will not change when this viewport is
|
||||
# activated
|
||||
"ucs_vp": DXFAttr(
|
||||
65,
|
||||
dxfversion=DXF2000,
|
||||
default=0,
|
||||
validator=validator.is_integer_bool,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
"ucs_origin": DXFAttr(110, xtype=XType.point3d, dxfversion=DXF2000),
|
||||
"ucs_xaxis": DXFAttr(
|
||||
111,
|
||||
xtype=XType.point3d,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_not_null_vector,
|
||||
),
|
||||
"ucs_yaxis": DXFAttr(
|
||||
112,
|
||||
xtype=XType.point3d,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_not_null_vector,
|
||||
),
|
||||
# Handle of AcDbUCSTableRecord if UCS is a named UCS. If not present,
|
||||
# then UCS is unnamed:
|
||||
"ucs_handle": DXFAttr(345, dxfversion=DXF2000),
|
||||
# Handle of AcDbUCSTableRecord of base UCS if UCS is orthographic (79 code
|
||||
# is non-zero). If not present and 79 code is non-zero, then base UCS is
|
||||
# taken to be WORLD:
|
||||
"base_ucs_handle": DXFAttr(346, dxfversion=DXF2000),
|
||||
# UCS ortho type:
|
||||
# 0 = UCS is not orthographic
|
||||
# 1 = Top
|
||||
# 2 = Bottom
|
||||
# 3 = Front
|
||||
# 4 = Back
|
||||
# 5 = Left
|
||||
# 6 = Right
|
||||
"ucs_ortho_type": DXFAttr(
|
||||
79,
|
||||
dxfversion=DXF2000,
|
||||
validator=validator.is_in_integer_range(0, 7),
|
||||
),
|
||||
"elevation": DXFAttr(146, dxfversion=DXF2000, default=0),
|
||||
"unknown1": DXFAttr(60, dxfversion=DXF2000),
|
||||
"shade_plot_setting": DXFAttr(170, dxfversion=DXF2007),
|
||||
"major_grid_lines": DXFAttr(61, dxfversion=DXF2007),
|
||||
# Handle to background object
|
||||
"background_handle": DXFAttr(332, dxfversion=DXF2007, optional=True),
|
||||
# Handle to shade plot object
|
||||
"shade_plot_handle": DXFAttr(333, dxfversion=DXF2007, optional=True),
|
||||
# Handle to visual style object
|
||||
"visual_style_handle": DXFAttr(348, dxfversion=DXF2007, optional=True),
|
||||
"default_lighting_on": DXFAttr(
|
||||
292,
|
||||
dxfversion=DXF2007,
|
||||
validator=validator.is_integer_bool,
|
||||
),
|
||||
# Default lighting type:
|
||||
# 0 = One distant light
|
||||
# 1 = Two distant lights
|
||||
"default_lighting_type": DXFAttr(
|
||||
282,
|
||||
dxfversion=DXF2007,
|
||||
validator=validator.is_integer_bool,
|
||||
),
|
||||
"brightness": DXFAttr(141, dxfversion=DXF2000),
|
||||
"contrast": DXFAttr(142, dxfversion=DXF2000),
|
||||
"ambient_color_aci": DXFAttr(63, dxfversion=DXF2000, optional=True),
|
||||
"ambient_true_color": DXFAttr(421, dxfversion=DXF2000, optional=True),
|
||||
"ambient_color_name": DXFAttr(431, dxfversion=DXF2000, optional=True),
|
||||
# Hard-pointer handle to sun object:
|
||||
"sun_handle": DXFAttr(361, dxfversion=DXF2007, optional=True),
|
||||
},
|
||||
)
|
||||
acdb_vport_group_codes = group_code_mapping(acdb_vport)
|
||||
|
||||
|
||||
@register_entity
|
||||
class VPort(DXFEntity):
|
||||
"""DXF VIEW entity"""
|
||||
|
||||
DXFTYPE = "VPORT"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_symbol_table_record, acdb_vport)
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_vport_group_codes, subclass=2
|
||||
)
|
||||
return dxf
|
||||
|
||||
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
|
||||
super().export_entity(tagwriter)
|
||||
dxfversion = tagwriter.dxfversion
|
||||
if dxfversion > DXF12:
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_symbol_table_record.name)
|
||||
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_vport.name)
|
||||
self.dxf.export_dxf_attribs(
|
||||
tagwriter,
|
||||
[
|
||||
"name",
|
||||
"flags",
|
||||
"lower_left",
|
||||
"upper_right",
|
||||
"center",
|
||||
"snap_base",
|
||||
"snap_spacing",
|
||||
"grid_spacing",
|
||||
"direction",
|
||||
"target",
|
||||
"height",
|
||||
"aspect_ratio",
|
||||
"focal_length",
|
||||
"front_clipping",
|
||||
"back_clipping",
|
||||
"snap_rotation",
|
||||
"view_twist",
|
||||
"view_mode",
|
||||
"circle_sides",
|
||||
"fast_zoom",
|
||||
"ucs_icon",
|
||||
"snap_on",
|
||||
"grid_on",
|
||||
"snap_style",
|
||||
"snap_isopair",
|
||||
"plot_style_sheet",
|
||||
"render_mode",
|
||||
"ucs_vp",
|
||||
"ucs_origin",
|
||||
"ucs_xaxis",
|
||||
"ucs_yaxis",
|
||||
"ucs_handle",
|
||||
"base_ucs_handle",
|
||||
"ucs_ortho_type",
|
||||
"elevation",
|
||||
"unknown1",
|
||||
"shade_plot_setting",
|
||||
"major_grid_lines",
|
||||
"background_handle",
|
||||
"shade_plot_handle",
|
||||
"visual_style_handle",
|
||||
"default_lighting_on",
|
||||
"default_lighting_type",
|
||||
"brightness",
|
||||
"contrast",
|
||||
"ambient_color_aci",
|
||||
"ambient_true_color",
|
||||
"ambient_color_name",
|
||||
],
|
||||
)
|
||||
|
||||
def reset_wcs(self) -> None:
|
||||
"""Reset coordinate system to the :ref:`WCS`."""
|
||||
self.dxf.ucs_vp = 1
|
||||
self.dxf.ucs_origin = (0, 0, 0)
|
||||
self.dxf.ucs_xaxis = (1, 0, 0)
|
||||
self.dxf.ucs_yaxis = (0, 1, 0)
|
||||
self.dxf.ucs_ortho_type = 0
|
||||
self.dxf.discard("ucs_handle")
|
||||
self.dxf.discard("base_ucs_handle")
|
||||
@@ -0,0 +1,531 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Any,
|
||||
MutableSequence,
|
||||
MutableMapping,
|
||||
Iterator,
|
||||
Union,
|
||||
Optional,
|
||||
)
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
|
||||
from ezdxf.math import Vec3, Matrix44
|
||||
from ezdxf.lldxf.types import dxftag, VALID_XDATA_GROUP_CODES, DXFTag
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.lldxf.const import XDATA_MARKER, DXFValueError, DXFTypeError
|
||||
from ezdxf.lldxf.tags import (
|
||||
xdata_list,
|
||||
remove_named_list_from_xdata,
|
||||
get_named_list_from_xdata,
|
||||
NotFoundException,
|
||||
)
|
||||
from ezdxf.tools import take2
|
||||
from ezdxf import options
|
||||
from ezdxf.lldxf.repair import filter_invalid_xdata_group_codes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFEntity
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["XData", "XDataUserList", "XDataUserDict"]
|
||||
logger = logging.getLogger("ezdxf")
|
||||
|
||||
|
||||
def has_valid_xdata_group_codes(tags: Tags) -> bool:
|
||||
return all(tag.code in VALID_XDATA_GROUP_CODES for tag in tags)
|
||||
|
||||
|
||||
class XData:
|
||||
def __init__(self, xdata: Optional[Iterable[Tags]] = None):
|
||||
self.data: dict[str, Tags] = dict()
|
||||
if xdata is not None:
|
||||
for data in xdata:
|
||||
self._add(data)
|
||||
|
||||
@classmethod
|
||||
def safe_init(cls, xdata: Iterable[Tags]):
|
||||
return cls(
|
||||
[Tags(filter_invalid_xdata_group_codes(tags)) for tags in xdata]
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __contains__(self, appid: str) -> bool:
|
||||
"""Returns ``True`` if DXF tags for `appid` exist."""
|
||||
return appid in self.data
|
||||
|
||||
def update_keys(self):
|
||||
"""Update APPID keys. (internal API)"""
|
||||
self.data = {tags[0].value: tags for tags in self.data.values()}
|
||||
|
||||
def _add(self, tags: Tags) -> None:
|
||||
tags = Tags(tags)
|
||||
if len(tags):
|
||||
appid = tags[0].value
|
||||
if appid in self.data:
|
||||
logger.info(f"Duplicate XDATA appid {appid} in one entity")
|
||||
if has_valid_xdata_group_codes(tags):
|
||||
self.data[appid] = tags
|
||||
else:
|
||||
raise DXFValueError(f"found invalid XDATA group code in {tags}")
|
||||
|
||||
def add(
|
||||
self, appid: str, tags: Iterable[Union[tuple[int, Any], DXFTag]]
|
||||
) -> None:
|
||||
"""Add a list of DXF tags for `appid`. The `tags` argument is an
|
||||
iterable of (group code, value) tuples, where the group code has to be
|
||||
an integer value. The mandatory XDATA marker (1001, appid) is added
|
||||
automatically if front of the tags if missing.
|
||||
|
||||
Each entity can contain only one list of tags for each `appid`.
|
||||
Adding a second list of tags for the same `appid` replaces the
|
||||
existing list of tags.
|
||||
|
||||
The valid XDATA group codes are restricted to some specific values in
|
||||
the range from 1000 to 1071, for more information see also the
|
||||
internals about :ref:`xdata_internals`.
|
||||
|
||||
"""
|
||||
data = Tags(dxftag(code, value) for code, value in tags)
|
||||
if len(data) == 0 or data[0] != (XDATA_MARKER, appid):
|
||||
data.insert(0, dxftag(XDATA_MARKER, appid))
|
||||
self._add(data)
|
||||
|
||||
def get(self, appid: str) -> Tags:
|
||||
"""Returns the DXF tags as :class:`~ezdxf.lldxf.tags.Tags` list
|
||||
stored by `appid`.
|
||||
|
||||
Raises:
|
||||
DXFValueError: no data for `appid` exist
|
||||
|
||||
"""
|
||||
if appid in self.data:
|
||||
return self.data[appid]
|
||||
else:
|
||||
raise DXFValueError(appid)
|
||||
|
||||
def discard(self, appid):
|
||||
"""Delete DXF tags for `appid`. None existing appids are silently
|
||||
ignored.
|
||||
"""
|
||||
if appid in self.data:
|
||||
del self.data[appid]
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
||||
for appid, tags in self.data.items():
|
||||
if options.filter_invalid_xdata_group_codes:
|
||||
tags = Tags(filter_invalid_xdata_group_codes(tags))
|
||||
tagwriter.write_tags(tags)
|
||||
|
||||
def has_xlist(self, appid: str, name: str) -> bool:
|
||||
"""Returns ``True`` if list `name` from XDATA `appid` exists.
|
||||
|
||||
Args:
|
||||
appid: APPID
|
||||
name: list name
|
||||
|
||||
"""
|
||||
try:
|
||||
self.get_xlist(appid, name)
|
||||
except DXFValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_xlist(self, appid: str, name: str) -> list[tuple]:
|
||||
"""Get list `name` from XDATA `appid`.
|
||||
|
||||
Args:
|
||||
appid: APPID
|
||||
name: list name
|
||||
|
||||
Returns: list of DXFTags including list name and curly braces '{' '}' tags
|
||||
|
||||
Raises:
|
||||
DXFKeyError: XDATA `appid` does not exist
|
||||
DXFValueError: list `name` does not exist
|
||||
|
||||
"""
|
||||
xdata = self.get(appid)
|
||||
try:
|
||||
return get_named_list_from_xdata(name, xdata)
|
||||
except NotFoundException:
|
||||
raise DXFValueError(
|
||||
f'No data list "{name}" not found for APPID "{appid}"'
|
||||
)
|
||||
|
||||
def set_xlist(self, appid: str, name: str, tags: Iterable) -> None:
|
||||
"""Create new list `name` of XDATA `appid` with `xdata_tags` and
|
||||
replaces list `name` if already exists.
|
||||
|
||||
Args:
|
||||
appid: APPID
|
||||
name: list name
|
||||
tags: list content as DXFTags or (code, value) tuples, list name and
|
||||
curly braces '{' '}' tags will be added
|
||||
"""
|
||||
if appid not in self.data:
|
||||
data = [(XDATA_MARKER, appid)]
|
||||
data.extend(xdata_list(name, tags))
|
||||
self.add(appid, data)
|
||||
else:
|
||||
self.replace_xlist(appid, name, tags)
|
||||
|
||||
def discard_xlist(self, appid: str, name: str) -> None:
|
||||
"""Deletes list `name` from XDATA `appid`. Ignores silently if XDATA
|
||||
`appid` or list `name` not exist.
|
||||
|
||||
Args:
|
||||
appid: APPID
|
||||
name: list name
|
||||
|
||||
"""
|
||||
try:
|
||||
xdata = self.get(appid)
|
||||
except DXFValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
tags = remove_named_list_from_xdata(name, xdata)
|
||||
except NotFoundException:
|
||||
pass
|
||||
else:
|
||||
self.add(appid, tags)
|
||||
|
||||
def replace_xlist(self, appid: str, name: str, tags: Iterable) -> None:
|
||||
"""Replaces list `name` of existing XDATA `appid` by `tags`. Appends
|
||||
new list if list `name` do not exist, but raises `DXFValueError` if
|
||||
XDATA `appid` do not exist.
|
||||
|
||||
Low level interface, if not sure use `set_xdata_list()` instead.
|
||||
|
||||
Args:
|
||||
appid: APPID
|
||||
name: list name
|
||||
tags: list content as DXFTags or (code, value) tuples, list name and
|
||||
curly braces '{' '}' tags will be added
|
||||
Raises:
|
||||
DXFValueError: XDATA `appid` do not exist
|
||||
|
||||
"""
|
||||
xdata = self.get(appid)
|
||||
try:
|
||||
data = remove_named_list_from_xdata(name, xdata)
|
||||
except NotFoundException:
|
||||
data = xdata
|
||||
xlist = xdata_list(name, tags)
|
||||
data.extend(xlist)
|
||||
self.add(appid, data)
|
||||
|
||||
def transform(self, m: Matrix44) -> None:
|
||||
"""Transform XDATA tags with group codes 1011, 1012, 1013, 1041 and
|
||||
1042 inplace. For more information see :ref:`xdata_internals` Internals.
|
||||
|
||||
"""
|
||||
transformed_data = dict()
|
||||
for key, tags in self.data.items():
|
||||
transformed_data[key] = Tags(transform_xdata_tags(tags, m))
|
||||
self.data = transformed_data
|
||||
|
||||
|
||||
def transform_xdata_tags(tags: Tags, m: Matrix44) -> Iterator[DXFTag]:
|
||||
for tag in tags:
|
||||
code, value = tag
|
||||
if code == 1011:
|
||||
# move, scale, rotate and mirror
|
||||
yield dxftag(code, m.transform(Vec3(value)))
|
||||
elif code == 1012:
|
||||
# scale, rotate and mirror
|
||||
yield dxftag(code, m.transform_direction(Vec3(value)))
|
||||
elif code == 1013:
|
||||
# rotate and mirror
|
||||
vec = Vec3(value)
|
||||
length = vec.magnitude
|
||||
if length > 1e-12:
|
||||
vec = m.transform_direction(vec).normalize(length)
|
||||
yield dxftag(code, vec)
|
||||
else:
|
||||
yield tag
|
||||
elif code == 1041 or code == 1042:
|
||||
# scale distance and factor, works only for uniform scaling
|
||||
vec = m.transform_direction(Vec3(value, 0, 0))
|
||||
yield dxftag(code, vec.magnitude)
|
||||
else:
|
||||
yield tag
|
||||
|
||||
|
||||
class XDataUserList(MutableSequence):
|
||||
"""Manage named XDATA lists as a list-like object.
|
||||
|
||||
Stores just a few data types with fixed group codes:
|
||||
|
||||
1000 str
|
||||
1010 Vec3
|
||||
1040 float
|
||||
1071 32bit int
|
||||
|
||||
"""
|
||||
|
||||
converter = {
|
||||
1000: str,
|
||||
1010: Vec3,
|
||||
1040: float,
|
||||
1071: int,
|
||||
}
|
||||
group_codes = {
|
||||
str: 1000,
|
||||
Vec3: 1010,
|
||||
float: 1040,
|
||||
int: 1071,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, xdata: Optional[XData] = None, name="DefaultList", appid="EZDXF"
|
||||
):
|
||||
"""Setup a XDATA user list `name` for the given `appid`.
|
||||
|
||||
The data is stored in the given `xdata` object, or in a new created
|
||||
:class:`XData` instance if ``None``.
|
||||
Changes of the content has to be committed at the end to be stored in
|
||||
the underlying `xdata` object.
|
||||
|
||||
Args:
|
||||
xdata (XData): underlying :class:`XData` instance, if ``None`` a
|
||||
new one will be created
|
||||
name (str): name of the user list
|
||||
appid (str): application specific AppID
|
||||
|
||||
"""
|
||||
if xdata is None:
|
||||
xdata = XData()
|
||||
self.xdata = xdata
|
||||
self._appid = str(appid)
|
||||
self._name = str(name)
|
||||
try:
|
||||
data = xdata.get_xlist(self._appid, self._name)
|
||||
except DXFValueError:
|
||||
data = []
|
||||
self._data: list = self._parse_list(data)
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def entity(
|
||||
cls, entity: DXFEntity, name="DefaultList", appid="EZDXF"
|
||||
) -> Iterator[XDataUserList]:
|
||||
"""Context manager to manage a XDATA list `name` for a given DXF
|
||||
`entity`. Appends the user list to the existing :class:`XData` instance
|
||||
or creates new :class:`XData` instance.
|
||||
|
||||
Args:
|
||||
entity (DXFEntity): target DXF entity for the XDATA
|
||||
name (str): name of the user list
|
||||
appid (str): application specific AppID
|
||||
|
||||
"""
|
||||
xdata = entity.xdata
|
||||
if xdata is None:
|
||||
xdata = XData()
|
||||
entity.xdata = xdata
|
||||
xlist = cls(xdata, name, appid)
|
||||
yield xlist
|
||||
xlist.commit()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.commit()
|
||||
|
||||
def __str__(self):
|
||||
"""Return str(self)."""
|
||||
return str(self._data)
|
||||
|
||||
def insert(self, index: int, value) -> None:
|
||||
self._data.insert(index, value)
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Get self[item]."""
|
||||
return self._data[item]
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
"""Set self[item] to value."""
|
||||
self._data.__setitem__(item, value)
|
||||
|
||||
def __delitem__(self, item):
|
||||
"""Delete self[item]."""
|
||||
self._data.__delitem__(item)
|
||||
|
||||
def _parse_list(self, tags: Iterable[tuple]) -> list:
|
||||
data = list(tags)
|
||||
content = []
|
||||
for code, value in data[2:-1]:
|
||||
factory = self.converter.get(code)
|
||||
if factory:
|
||||
content.append(factory(value))
|
||||
else:
|
||||
raise DXFValueError(f"unsupported group code: {code}")
|
||||
return content
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Returns len(self)."""
|
||||
return len(self._data)
|
||||
|
||||
def commit(self) -> None:
|
||||
"""Store all changes to the underlying :class:`XData` instance.
|
||||
This call is not required if using the :meth:`entity` context manager.
|
||||
|
||||
Raises:
|
||||
DXFValueError: invalid chars ``"\\n"`` or ``"\\r"`` in a string
|
||||
DXFTypeError: invalid data type
|
||||
|
||||
"""
|
||||
data = []
|
||||
for value in self._data:
|
||||
if isinstance(value, str):
|
||||
if len(value) > 255: # XDATA limit for group code 1000
|
||||
raise DXFValueError("string too long, max. 255 characters")
|
||||
if "\n" in value or "\r" in value:
|
||||
raise DXFValueError(
|
||||
"found invalid line break '\\n' or '\\r'"
|
||||
)
|
||||
code = self.group_codes.get(type(value))
|
||||
if code:
|
||||
data.append(dxftag(code, value))
|
||||
else:
|
||||
raise DXFTypeError(f"invalid type: {type(value)}")
|
||||
self.xdata.set_xlist(self._appid, self._name, data)
|
||||
|
||||
|
||||
class XDataUserDict(MutableMapping):
|
||||
"""Manage named XDATA lists as a dict-like object.
|
||||
|
||||
Uses XDataUserList to store key, value pairs in XDATA.
|
||||
|
||||
This class does not create the required AppID table entry, only the
|
||||
default AppID "EZDXF" exist by default.
|
||||
|
||||
Implements the :class:`MutableMapping` interface.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, xdata: Optional[XData] = None, name="DefaultDict", appid="EZDXF"
|
||||
):
|
||||
"""Setup a XDATA user dict `name` for the given `appid`.
|
||||
|
||||
The data is stored in the given `xdata` object, or in a new created
|
||||
:class:`XData` instance if ``None``.
|
||||
Changes of the content has to be committed at the end to be stored in
|
||||
the underlying `xdata` object.
|
||||
|
||||
Args:
|
||||
xdata (XData): underlying :class:`XData` instance, if ``None`` a
|
||||
new one will be created
|
||||
name (str): name of the user list
|
||||
appid (str): application specific AppID
|
||||
|
||||
"""
|
||||
self._xlist = XDataUserList(xdata, name, appid)
|
||||
self._user_dict: dict[str, Any] = self._parse_xlist()
|
||||
|
||||
def _parse_xlist(self) -> dict:
|
||||
if self._xlist:
|
||||
return dict(take2(self._xlist))
|
||||
else:
|
||||
return dict()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.commit()
|
||||
|
||||
def __str__(self):
|
||||
"""Return str(self)."""
|
||||
return str(self._user_dict)
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def entity(
|
||||
cls, entity: DXFEntity, name="DefaultDict", appid="EZDXF"
|
||||
) -> Iterator[XDataUserDict]:
|
||||
"""Context manager to manage a XDATA dict `name` for a given DXF
|
||||
`entity`. Appends the user dict to the existing :class:`XData` instance
|
||||
or creates new :class:`XData` instance.
|
||||
|
||||
Args:
|
||||
entity (DXFEntity): target DXF entity for the XDATA
|
||||
name (str): name of the user list
|
||||
appid (str): application specific AppID
|
||||
|
||||
"""
|
||||
xdata = entity.xdata
|
||||
if xdata is None:
|
||||
xdata = XData()
|
||||
entity.xdata = xdata
|
||||
xdict = cls(xdata, name, appid)
|
||||
yield xdict
|
||||
xdict.commit()
|
||||
|
||||
@property
|
||||
def xdata(self):
|
||||
return self._xlist.xdata
|
||||
|
||||
def __len__(self):
|
||||
"""Returns len(self)."""
|
||||
return len(self._user_dict)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get self[key]."""
|
||||
return self._user_dict[key]
|
||||
|
||||
def __setitem__(self, key, item):
|
||||
"""Set self[key] to value, key has to be a string.
|
||||
|
||||
Raises:
|
||||
DXFTypeError: key is not a string
|
||||
|
||||
"""
|
||||
if not isinstance(key, str):
|
||||
raise DXFTypeError("key is not a string")
|
||||
self._user_dict[key] = item
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Delete self[key]."""
|
||||
del self._user_dict[key]
|
||||
|
||||
def __iter__(self):
|
||||
"""Implement iter(self)."""
|
||||
return iter(self._user_dict)
|
||||
|
||||
def discard(self, key):
|
||||
"""Delete self[key], without raising a :class:`KeyError` if `key` does
|
||||
not exist.
|
||||
|
||||
"""
|
||||
try:
|
||||
del self._user_dict[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def commit(self) -> None:
|
||||
"""Store all changes to the underlying :class:`XData` instance.
|
||||
This call is not required if using the :meth:`entity` context manager.
|
||||
|
||||
Raises:
|
||||
DXFValueError: invalid chars ``"\\n"`` or ``"\\r"`` in a string
|
||||
DXFTypeError: invalid data type
|
||||
|
||||
"""
|
||||
xlist = self._xlist
|
||||
xlist.clear()
|
||||
for key, value in self._user_dict.items():
|
||||
xlist.append(key)
|
||||
xlist.append(value)
|
||||
xlist.commit()
|
||||
@@ -0,0 +1,241 @@
|
||||
# Copyright (c) 2019-2023 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Union, Optional
|
||||
from ezdxf.lldxf.tags import Tags
|
||||
from ezdxf.lldxf.const import DXFStructureError
|
||||
from ezdxf.lldxf.const import (
|
||||
ACAD_XDICTIONARY,
|
||||
XDICT_HANDLE_CODE,
|
||||
APP_DATA_MARKER,
|
||||
)
|
||||
from .copy import default_copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.document import Drawing
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
from ezdxf.entities import (
|
||||
Dictionary,
|
||||
DXFEntity,
|
||||
DXFObject,
|
||||
Placeholder,
|
||||
DictionaryVar,
|
||||
XRecord,
|
||||
)
|
||||
|
||||
__all__ = ["ExtensionDict"]
|
||||
|
||||
|
||||
# Example for table head and -entries with extension dicts:
|
||||
# AutodeskSamples\lineweights.dxf
|
||||
|
||||
|
||||
class ExtensionDict:
|
||||
"""Stores extended data of entities in app data 'ACAD_XDICTIONARY', app
|
||||
data contains just one entry to a hard-owned DICTIONARY objects, which is
|
||||
not shared with other entities, each entity copy has its own extension
|
||||
dictionary and the extension dictionary is destroyed when the owner entity
|
||||
is deleted from database.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_xdict",)
|
||||
|
||||
def __init__(self, xdict: Union[str, Dictionary]):
|
||||
# 1st loading stage: xdict as string -> handle to dict
|
||||
# 2nd loading stage: xdict as DXF Dictionary
|
||||
self._xdict = xdict
|
||||
|
||||
@property
|
||||
def dictionary(self) -> Dictionary:
|
||||
"""Returns the underlying :class:`~ezdxf.entities.Dictionary` object."""
|
||||
xdict = self._xdict
|
||||
assert xdict is not None, "destroyed extension dictionary"
|
||||
assert not isinstance(xdict, str), f"dictionary handle #{xdict} not resolved"
|
||||
return xdict
|
||||
|
||||
@property
|
||||
def handle(self) -> str:
|
||||
"""Returns the handle of the underlying :class:`~ezdxf.entities.Dictionary`
|
||||
object.
|
||||
"""
|
||||
return self.dictionary.dxf.handle
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
"""Get self[key]."""
|
||||
return self.dictionary[key]
|
||||
|
||||
def __setitem__(self, key: str, value):
|
||||
"""Set self[key] to value.
|
||||
|
||||
Only DXF objects stored in the OBJECTS section are allowed as content
|
||||
of the extension dictionary. DXF entities stored in layouts are not
|
||||
allowed.
|
||||
|
||||
Raises:
|
||||
DXFTypeError: invalid DXF type
|
||||
|
||||
"""
|
||||
self.dictionary[key] = value
|
||||
|
||||
def __delitem__(self, key: str):
|
||||
"""Delete self[key], destroys referenced entity."""
|
||||
del self.dictionary[key]
|
||||
|
||||
def __contains__(self, key: str):
|
||||
"""Return `key` in self."""
|
||||
return key in self.dictionary
|
||||
|
||||
def __len__(self):
|
||||
"""Returns count of extension dictionary entries."""
|
||||
return len(self.dictionary)
|
||||
|
||||
def keys(self):
|
||||
"""Returns a :class:`KeysView` of all extension dictionary keys."""
|
||||
return self.dictionary.keys()
|
||||
|
||||
def items(self):
|
||||
"""Returns an :class:`ItemsView` for all extension dictionary entries as
|
||||
(key, entity) pairs. An entity can be a handle string if the entity
|
||||
does not exist.
|
||||
"""
|
||||
return self.dictionary.items()
|
||||
|
||||
def get(self, key: str, default=None) -> Optional[DXFEntity]:
|
||||
"""Return extension dictionary entry `key`."""
|
||||
return self.dictionary.get(key, default)
|
||||
|
||||
def discard(self, key: str) -> None:
|
||||
"""Discard extension dictionary entry `key`."""
|
||||
return self.dictionary.discard(key)
|
||||
|
||||
@classmethod
|
||||
def new(cls, owner_handle: str, doc: Drawing):
|
||||
xdict = doc.objects.add_dictionary(
|
||||
owner=owner_handle,
|
||||
# All data in the extension dictionary belongs only to the owner
|
||||
hard_owned=True,
|
||||
)
|
||||
return cls(xdict)
|
||||
|
||||
def copy(self, copy_strategy=default_copy) -> ExtensionDict:
|
||||
"""Deep copy of the extension dictionary all entries are virtual
|
||||
entities.
|
||||
"""
|
||||
new_xdict = copy_strategy.copy(self.dictionary)
|
||||
return ExtensionDict(new_xdict)
|
||||
|
||||
@property
|
||||
def is_alive(self):
|
||||
"""Returns ``True`` if the underlying :class:`~ezdxf.entities.Dictionary`
|
||||
object is not deleted.
|
||||
"""
|
||||
# Can not check if _xdict (as handle or Dictionary) really exist:
|
||||
return self._xdict is not None
|
||||
|
||||
@property
|
||||
def has_valid_dictionary(self):
|
||||
"""Returns ``True`` if the underlying :class:`~ezdxf.entities.Dictionary`
|
||||
really exist and is valid.
|
||||
"""
|
||||
xdict = self._xdict
|
||||
if xdict is None or isinstance(xdict, str):
|
||||
return False
|
||||
return xdict.is_alive
|
||||
|
||||
def update_owner(self, handle: str) -> None:
|
||||
"""Update owner tag of underlying :class:`~ezdxf.entities.Dictionary`
|
||||
object.
|
||||
|
||||
Internal API.
|
||||
"""
|
||||
assert self.is_alive, "destroyed extension dictionary"
|
||||
self.dictionary.dxf.owner = handle
|
||||
|
||||
@classmethod
|
||||
def from_tags(cls, tags: Tags):
|
||||
assert tags is not None
|
||||
# Expected DXF structure:
|
||||
# [(102, '{ACAD_XDICTIONARY', (360, handle), (102, '}')]
|
||||
if len(tags) != 3 or tags[1].code != XDICT_HANDLE_CODE:
|
||||
raise DXFStructureError("ACAD_XDICTIONARY error.")
|
||||
return cls(tags[1].value)
|
||||
|
||||
def load_resources(self, doc: Drawing) -> None:
|
||||
handle = self._xdict
|
||||
assert isinstance(handle, str)
|
||||
self._xdict = doc.entitydb.get(handle) # type: ignore
|
||||
|
||||
def export_dxf(self, tagwriter: AbstractTagWriter) -> None:
|
||||
assert self._xdict is not None
|
||||
xdict = self._xdict
|
||||
handle = xdict if isinstance(xdict, str) else xdict.dxf.handle
|
||||
tagwriter.write_tag2(APP_DATA_MARKER, ACAD_XDICTIONARY)
|
||||
tagwriter.write_tag2(XDICT_HANDLE_CODE, handle)
|
||||
tagwriter.write_tag2(APP_DATA_MARKER, "}")
|
||||
|
||||
def destroy(self):
|
||||
"""Destroy the underlying :class:`~ezdxf.entities.Dictionary` object."""
|
||||
if self.has_valid_dictionary:
|
||||
self._xdict.destroy()
|
||||
self._xdict = None
|
||||
|
||||
def add_dictionary(self, name: str, hard_owned: bool = True) -> Dictionary:
|
||||
"""Create a new :class:`~ezdxf.entities.Dictionary` object as
|
||||
extension dictionary entry `name`.
|
||||
"""
|
||||
dictionary = self.dictionary
|
||||
doc = dictionary.doc
|
||||
assert doc is not None, "valid DXF document required"
|
||||
new_dict = doc.objects.add_dictionary(
|
||||
owner=dictionary.dxf.handle,
|
||||
hard_owned=hard_owned,
|
||||
)
|
||||
dictionary[name] = new_dict
|
||||
return new_dict
|
||||
|
||||
def add_xrecord(self, name: str) -> XRecord:
|
||||
"""Create a new :class:`~ezdxf.entities.XRecord` object as
|
||||
extension dictionary entry `name`.
|
||||
"""
|
||||
dictionary = self.dictionary
|
||||
doc = dictionary.doc
|
||||
assert doc is not None, "valid DXF document required"
|
||||
xrecord = doc.objects.add_xrecord(dictionary.dxf.handle)
|
||||
dictionary[name] = xrecord
|
||||
return xrecord
|
||||
|
||||
def add_dictionary_var(self, name: str, value: str) -> DictionaryVar:
|
||||
"""Create a new :class:`~ezdxf.entities.DictionaryVar` object as
|
||||
extension dictionary entry `name`.
|
||||
"""
|
||||
dictionary = self.dictionary
|
||||
doc = dictionary.doc
|
||||
assert doc is not None, "valid DXF document required"
|
||||
dict_var = doc.objects.add_dictionary_var(dictionary.dxf.handle, value)
|
||||
dictionary[name] = dict_var
|
||||
return dict_var
|
||||
|
||||
def add_placeholder(self, name: str) -> Placeholder:
|
||||
"""Create a new :class:`~ezdxf.entities.Placeholder` object as
|
||||
extension dictionary entry `name`.
|
||||
"""
|
||||
dictionary = self.dictionary
|
||||
doc = dictionary.doc
|
||||
assert doc is not None, "valid DXF document required"
|
||||
placeholder = doc.objects.add_placeholder(dictionary.dxf.handle)
|
||||
dictionary[name] = placeholder
|
||||
return placeholder
|
||||
|
||||
def link_dxf_object(self, name: str, obj: DXFObject) -> None:
|
||||
"""Link `obj` to the extension dictionary as entry `name`.
|
||||
|
||||
Linked objects are owned by the extensions dictionary and therefore
|
||||
cannot be a graphical entity, which have to be owned by a
|
||||
:class:`~ezdxf.layouts.BaseLayout`.
|
||||
|
||||
Raises:
|
||||
DXFTypeError: `obj` has invalid DXF type
|
||||
|
||||
"""
|
||||
self.dictionary.link_dxf_object(name, obj)
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) 2019-2022 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from ezdxf.lldxf import validator
|
||||
from ezdxf.lldxf.attributes import (
|
||||
DXFAttr,
|
||||
DXFAttributes,
|
||||
DefSubclass,
|
||||
XType,
|
||||
RETURN_DEFAULT,
|
||||
group_code_mapping,
|
||||
)
|
||||
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000
|
||||
from ezdxf.math import Vec3, Matrix44, NULLVEC, Z_AXIS
|
||||
from .dxfentity import base_class, SubclassProcessor
|
||||
from .dxfgfx import DXFGraphic, acdb_entity
|
||||
from .factory import register_entity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.entities import DXFNamespace
|
||||
from ezdxf.lldxf.tagwriter import AbstractTagWriter
|
||||
|
||||
__all__ = ["Ray", "XLine"]
|
||||
|
||||
acdb_xline = DefSubclass(
|
||||
"AcDbXline",
|
||||
{
|
||||
"start": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
|
||||
"unit_vector": DXFAttr(
|
||||
11,
|
||||
xtype=XType.point3d,
|
||||
default=Z_AXIS,
|
||||
validator=validator.is_not_null_vector,
|
||||
fixer=RETURN_DEFAULT,
|
||||
),
|
||||
},
|
||||
)
|
||||
acdb_xline_group_codes = group_code_mapping(acdb_xline)
|
||||
|
||||
|
||||
@register_entity
|
||||
class XLine(DXFGraphic):
|
||||
"""DXF XLINE entity"""
|
||||
|
||||
DXFTYPE = "XLINE"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_xline)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
XLINE_SUBCLASS = "AcDbXline"
|
||||
|
||||
def load_dxf_attribs(
|
||||
self, processor: Optional[SubclassProcessor] = None
|
||||
) -> DXFNamespace:
|
||||
dxf = super().load_dxf_attribs(processor)
|
||||
if processor:
|
||||
processor.fast_load_dxfattribs(
|
||||
dxf, acdb_xline_group_codes, subclass=2, recover=True
|
||||
)
|
||||
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, self.XLINE_SUBCLASS)
|
||||
self.dxf.export_dxf_attribs(tagwriter, ["start", "unit_vector"])
|
||||
|
||||
def transform(self, m: Matrix44) -> XLine:
|
||||
"""Transform the XLINE/RAY entity by transformation matrix `m` inplace."""
|
||||
self.dxf.start = m.transform(self.dxf.start)
|
||||
self.dxf.unit_vector = m.transform_direction(
|
||||
self.dxf.unit_vector
|
||||
).normalize()
|
||||
self.post_transform(m)
|
||||
return self
|
||||
|
||||
def translate(self, dx: float, dy: float, dz: float) -> XLine:
|
||||
"""Optimized XLINE/RAY translation about `dx` in x-axis, `dy` in
|
||||
y-axis and `dz` in z-axis.
|
||||
|
||||
"""
|
||||
self.dxf.start = Vec3(dx, dy, dz) + self.dxf.start
|
||||
# Avoid Matrix44 instantiation if not required:
|
||||
if self.is_post_transform_required:
|
||||
self.post_transform(Matrix44.translate(dx, dy, dz))
|
||||
return self
|
||||
|
||||
|
||||
@register_entity
|
||||
class Ray(XLine):
|
||||
"""DXF Ray entity"""
|
||||
|
||||
DXFTYPE = "RAY"
|
||||
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_xline)
|
||||
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
|
||||
XLINE_SUBCLASS = "AcDbRay"
|
||||
Reference in New Issue
Block a user