Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/entities/dimension.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

1231 lines
46 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Union, Iterable, Iterator
from typing_extensions import Self
import math
import logging
from ezdxf.audit import AuditError
from ezdxf.lldxf import validator, const
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf.const import (
DXF12,
SUBCLASS_MARKER,
DXF2010,
DXF2000,
DXF2007,
DXF2004,
DXFValueError,
DXFTableEntryError,
DXFTypeError,
)
from ezdxf.lldxf.types import get_xcode_for
from ezdxf.math import Vec3, Matrix44, NULLVEC, Z_AXIS
from ezdxf.math.transformtools import OCSTransform, NonUniformScalingError
from ezdxf.tools import take2
from ezdxf.render.arrows import ARROWS
from ezdxf.explode import explode_entity
from ezdxf.entitydb import EntitySpace
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import DXFGraphic, acdb_entity
from .factory import register_entity
from .dimstyleoverride import DimStyleOverride
from .copy import default_copy, CopyNotSupported
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace, DXFEntity, DimStyle
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.layouts import BaseLayout, BlockLayout
from ezdxf.audit import Auditor
from ezdxf.query import EntityQuery
from ezdxf.math import OCS
from ezdxf import xref
logger = logging.getLogger("ezdxf")
ADSK_CONSTRAINTS = "*ADSK_CONSTRAINTS"
__all__ = [
"Dimension",
"ArcDimension",
"RadialDimensionLarge",
"OverrideMixin",
"DXF_DIMENSION_TYPES",
"register_override_handles",
]
DXF_DIMENSION_TYPES = {"DIMENSION", "ARC_DIMENSION", "LARGE_RADIAL_DIMENSION"}
acdb_dimension = DefSubclass(
"AcDbDimension",
{
# Version number: 0 = 2010
"version": DXFAttr(280, default=0, dxfversion=DXF2010),
# Name of the block that contains the entities that make up the dimension
# picture. Dimensions in block references share the same geometry block,
# therefore deleting the geometry block is very dangerous!
# Important: DIMENSION constraints do not have this group code 2:
"geometry": DXFAttr(2, validator=validator.is_valid_block_name),
# Dimension style name:
"dimstyle": DXFAttr(
3,
default="Standard",
validator=validator.is_valid_table_name,
# Do not fix automatically, but audit fixes value as 'Standard'
),
# definition point for all dimension types in WCS:
"defpoint": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# Midpoint of dimension text in OCS:
"text_midpoint": DXFAttr(11, xtype=XType.point3d),
# Insertion point for clones of a dimension (Baseline and Continue?) (in OCS)
# located in AcDbDimension? Another error in the DXF reference?
"insert": DXFAttr(12, xtype=XType.point3d, default=NULLVEC, optional=True),
# Dimension type:
# Important: Dimensional constraints do not have group code 70
# Values 06 are integer values that represent the dimension type.
# Values 32, 64, and 128 are bit values, which are added to the integer
# values (value 32 is always set in R13 and later releases)
# 0 = Rotated, horizontal, or vertical;
# 1 = Aligned
# 2 = Angular;
# 3 = Diameter;
# 4 = Radius
# 5 = Angular 3 point and ARC_DIMENSION < DXF R2018
# 6 = Ordinate
# 8 = ARC_DIMENSION >= DXF R2018
# 32 = Indicates that the block reference (group code 2) is referenced by
# this dimension only
# 64 = Ordinate type. This is a bit value (bit 7) used only with integer
# value 6. If set, ordinate is X-type; if not set, ordinate is Y-type
# 128 = This is a bit value (bit 8) added to the other group 70 values if
# the dimension text has been positioned at a user-defined location
# rather than at the default location.
"dimtype": DXFAttr(70, default=0),
# Attachment point:
# 1 = Top left
# 2 = Top center
# 3 = Top right
# 4 = Middle left
# 5 = Middle center
# 6 = Middle right
# 7 = Bottom left
# 8 = Bottom center
# 9 = Bottom right
"attachment_point": DXFAttr(
71,
default=5,
dxfversion=DXF2000,
validator=validator.is_in_integer_range(0, 10),
fixer=RETURN_DEFAULT,
),
# Dimension text line-spacing style
# 1 (or missing) = At least (taller characters will override)
# 2 = Exact (taller characters will not override)
"line_spacing_style": DXFAttr(
72,
default=1,
dxfversion=DXF2000,
optional=True,
validator=validator.is_in_integer_range(1, 3),
fixer=RETURN_DEFAULT,
),
# Dimension text-line spacing factor:
# Percentage of default (3-on-5) line spacing to be applied. Valid values
# range from 0.25 to 4.00
"line_spacing_factor": DXFAttr(
41,
dxfversion=DXF2000,
optional=True,
validator=validator.is_in_float_range(0.25, 4.00),
fixer=validator.fit_into_float_range(0.25, 4.00),
),
# Actual measurement (optional; read-only value)
"actual_measurement": DXFAttr(42, dxfversion=DXF2000, optional=True),
"unknown1": DXFAttr(73, dxfversion=DXF2000, optional=True),
"flip_arrow_1": DXFAttr(
74,
dxfversion=DXF2000,
optional=True,
validator=validator.is_integer_bool,
fixer=validator.fix_integer_bool,
),
"flip_arrow_2": DXFAttr(
75,
dxfversion=DXF2000,
optional=True,
validator=validator.is_integer_bool,
fixer=validator.fix_integer_bool,
),
# Dimension text explicitly entered by the user
# default is the measurement.
# If null or "<>", the dimension measurement is drawn as the text,
# if " " (one blank space), the text is suppressed.
# Anything else is drawn as the text.
"text": DXFAttr(1, default="", optional=True),
# Linear dimension types with an oblique angle have an optional group
# code 52. When added to the rotation angle of the linear dimension (group
# code 50), it gives the angle of the extension lines
# DXF reference error: wrong subclass AcDbAlignedDimension
"oblique_angle": DXFAttr(52, default=0, optional=True),
# The optional group code 53 is the rotation angle of the dimension
# text away from its default orientation (the direction of the dimension
# line)
"text_rotation": DXFAttr(53, default=0, optional=True),
# All dimension types have an optional 51 group code, which
# indicates the horizontal direction for the dimension entity.
# The dimension entity determines the orientation of dimension text and
# lines for horizontal, vertical, and rotated linear dimensions.
# This group value is the negative of the angle between the OCS X axis and
# the UCS X axis. It is always in the XY plane of the OCS
"horizontal_direction": 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_dimension_group_codes = group_code_mapping(acdb_dimension)
acdb_dimension_dummy = DefSubclass(
"AcDbDimensionDummy",
{
# Definition point for linear and angular dimensions (in WCS)
# 'defpoint' (10,20,30) specifies the dimension line location.
# 'text_midpoint' (11,21,31) specifies the midpoint of the dimension text.
# (13,23,33) specifies the start point of the first extension line:
"defpoint2": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
# (14,24,34) specifies the start point of the second extension line:
"defpoint3": DXFAttr(14, xtype=XType.point3d, default=NULLVEC),
# Angle of rotated, horizontal, or vertical dimensions:
"angle": DXFAttr(50, default=0),
# Definition point for diameter, radius, and angular dimensions (in WCS)
"defpoint4": DXFAttr(15, xtype=XType.point3d, default=NULLVEC),
# Leader length for radius and diameter dimensions
"leader_length": DXFAttr(40),
# Point defining dimension arc for angular dimensions (in OCS)
# 'defpoint2' (13,23,33) and 'defpoint3' (14,24,34) specify the endpoints
# of the line used to determine the first extension line.
# 'defpoint' (10,20,30) and 'defpoint4' (15,25,35) specify the endpoints of
# the line used to determine the second extension line.
# 'defpoint5' (16,26,36) specifies the location of the dimension line arc.
# 'text_midpoint' (11,21,31) specifies the midpoint of the dimension text.
"defpoint5": DXFAttr(16, xtype=XType.point3d, default=NULLVEC),
},
)
acdb_dimension_dummy_group_codes = group_code_mapping(acdb_dimension_dummy)
# noinspection PyUnresolvedReferences
class OverrideMixin:
def get_dim_style(self) -> DimStyle:
"""Returns the associated :class:`DimStyle` entity."""
assert self.doc is not None, "valid DXF document required" # type: ignore
dim_style_name = self.dxf.dimstyle # type: ignore
# raises ValueError if not exist, but all dim styles in use should
# exist!
return self.doc.dimstyles.get(dim_style_name) # type: ignore
def dim_style_attributes(self) -> DXFAttributes:
"""Returns all valid DXF attributes (internal API)."""
return self.get_dim_style().DXFATTRIBS
def dim_style_attr_names_to_handles(self, data: dict, dxfversion: str) -> dict:
"""`Ezdxf` uses internally only resource names for arrows, linetypes
and text styles, but DXF 2000 and later requires handles for these
resources, this method translates resource names into related handles.
(e.g. 'dimtxsty': 'FancyStyle' -> 'dimtxsty_handle', <handle of FancyStyle>)
Args:
data: dictionary of overridden DimStyle attributes as names (ezdxf)
dxfversion: target DXF version
Returns: dictionary with resource names replaced by handles
Raises:
DXFTableEntry: text style or line type does not exist
DXFKeyError: referenced block does not exist
(internal API)
"""
data = dict(data)
blocks = self.doc.blocks # type: ignore
def set_arrow_handle(attrib_name, block_name):
attrib_name += "_handle"
if block_name in ARROWS:
# Create all arrows on demand
block_name = ARROWS.create_block(blocks, block_name)
if block_name == "_CLOSEDFILLED": # special arrow
handle = "0" # set special #0 handle for closed filled arrow
else:
block = blocks[block_name]
handle = block.block_record_handle
data[attrib_name] = handle
def set_linetype_handle(attrib_name, linetype_name):
try:
ltype = self.doc.linetypes.get(linetype_name)
except DXFTableEntryError:
logger.warning(f'Required line type "{linetype_name}" does not exist.')
else:
data[attrib_name + "_handle"] = ltype.dxf.handle
if dxfversion > DXF12:
# transform block names into block record handles
for attrib_name in ("dimblk", "dimblk1", "dimblk2", "dimldrblk"):
try:
block_name = data.pop(attrib_name)
except KeyError:
pass
else:
set_arrow_handle(attrib_name, block_name)
# replace 'dimtxsty' attribute by 'dimtxsty_handle'
try:
dimtxsty = data.pop("dimtxsty")
except KeyError:
pass
else:
txtstyle = self.doc.styles.get(dimtxsty) # type: ignore
data["dimtxsty_handle"] = txtstyle.dxf.handle
if dxfversion >= DXF2007:
# transform linetype names into LTYPE entry handles
for attrib_name in ("dimltype", "dimltex1", "dimltex2"):
try:
linetype_name = data.pop(attrib_name)
except KeyError:
pass
else:
set_linetype_handle(attrib_name, linetype_name)
return data
def set_acad_dstyle(self, data: dict) -> None:
"""Set XDATA section ACAD:DSTYLE, to override DIMSTYLE attributes for
this DIMENSION entity.
Args:
data: ``dict`` with DIMSTYLE attribute names as keys.
(internal API)
"""
assert self.doc is not None, "valid DXF document required" # type: ignore
# ezdxf uses internally only resource names for arrows, line types and
# text styles, but DXF 2000 and later requires handles for these
# resources:
actual_dxfversion = self.doc.dxfversion # type: ignore
data = self.dim_style_attr_names_to_handles(data, actual_dxfversion)
tags = []
dim_style_attributes = self.dim_style_attributes()
for key, value in data.items():
if key not in dim_style_attributes:
logger.debug(f'Ignore unknown DIMSTYLE attribute: "{key}"')
continue
dxf_attr = dim_style_attributes.get(key)
# Skip internal and virtual tags:
if dxf_attr and dxf_attr.code > 0:
if dxf_attr.dxfversion > actual_dxfversion:
logger.debug(
f'Unsupported DIMSTYLE attribute "{key}" for '
f"DXF version {self.doc.acad_release}" # type: ignore
)
continue
code = dxf_attr.code
tags.append((1070, code))
if code == 5:
# DimStyle 'dimblk' has group code 5 but is not a handle, only used
# for DXF R12
tags.append((1000, value))
else:
tags.append((get_xcode_for(code), value))
if len(tags):
self.set_xdata_list("ACAD", "DSTYLE", tags) # type: ignore
def dim_style_attr_handles_to_names(self, data: dict) -> dict:
"""`Ezdxf` uses internally only resource names for arrows, line types
and text styles, but DXF 2000 and later requires handles for these
resources, this method translates resource handles into related names.
(e.g. 'dimtxsty_handle', <handle of FancyStyle> -> 'dimtxsty': 'FancyStyle')
Args:
data: dictionary of overridden DimStyle attributes as handles,
requires DXF R2000+
Returns: dictionary with resource as handles replaced by names
Raises:
DXFTableEntry: text style or line type does not exist
DXFKeyError: referenced block does not exist
(internal API)
"""
data = dict(data)
db = self.doc.entitydb # type: ignore
def set_arrow_name(attrib_name: str, handle: str):
# Special handle for default arrow CLOSEDFILLED:
if handle == "0":
# Special name for default arrow CLOSEDFILLED:
data[attrib_name] = ""
return
try:
block_record = db[handle]
except KeyError:
logger.warning(
f"Required arrow block #{handle} does not exist, "
f"ignoring {attrib_name.upper()} override."
)
return
name = block_record.dxf.name
# Translate block name into ACAD standard name _OPEN30 -> OPEN30
if name.startswith("_"):
acad_arrow_name = name[1:]
if ARROWS.is_acad_arrow(acad_arrow_name):
name = acad_arrow_name
data[attrib_name] = name
def set_ltype_name(attrib_name: str, handle: str):
try:
ltype = db[handle]
except KeyError:
logger.warning(
f"Required line type #{handle} does not exist, "
f"ignoring {attrib_name.upper()} override."
)
else:
data[attrib_name] = ltype.dxf.name
# transform block record handles into block names
for attrib_name in ("dimblk", "dimblk1", "dimblk2", "dimldrblk"):
blkrec_handle = data.pop(attrib_name + "_handle", None)
if blkrec_handle:
set_arrow_name(attrib_name, blkrec_handle)
# replace 'dimtxsty_handle' attribute by 'dimtxsty'
dimtxsty_handle = data.pop("dimtxsty_handle", None)
if dimtxsty_handle:
try:
txtstyle = db[dimtxsty_handle]
except KeyError:
logger.warning(
f"Required text style #{dimtxsty_handle} does not exist, "
f"ignoring DIMTXSTY override."
)
else:
data["dimtxsty"] = txtstyle.dxf.name
# transform linetype handles into LTYPE entry names
for attrib_name in ("dimltype", "dimltex1", "dimltex2"):
handle = data.pop(attrib_name + "_handle", None)
if handle:
set_ltype_name(attrib_name, handle)
return data
def get_acad_dstyle(self, dim_style: DimStyle) -> 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)
"""
try:
data = self.get_xdata_list("ACAD", "DSTYLE") # type: ignore
except DXFValueError:
return {}
attribs = {}
codes = dim_style.CODE_TO_DXF_ATTRIB
for code_tag, value_tag in take2(data):
group_code = code_tag.value
value = value_tag.value
if group_code in codes:
attribs[codes[group_code]] = value
return self.dim_style_attr_handles_to_names(attribs)
@register_entity
class Dimension(DXFGraphic, OverrideMixin):
"""DXF DIMENSION entity"""
DXFTYPE = "DIMENSION"
DXFATTRIBS = DXFAttributes(
base_class, acdb_entity, acdb_dimension, acdb_dimension_dummy
)
LINEAR = 0
ALIGNED = 1
ANGULAR = 2
DIAMETER = 3
RADIUS = 4
ANGULAR_3P = 5
ORDINATE = 6
ARC = 8
ORDINATE_TYPE = 64
USER_LOCATION_OVERRIDE = 128
# WARNING for destroy() method:
# Do not destroy associated anonymous block, if DIMENSION is used in a
# block, the same geometry block maybe used by multiple block references.
def __init__(self) -> None:
super().__init__()
# store the content of the geometry block for virtual entities
self.virtual_block_content: Optional[EntitySpace] = None
def copy(self, copy_strategy=default_copy) -> Dimension:
virtual_copy = super().copy(copy_strategy=copy_strategy)
# The new virtual copy can not reference the same geometry block as the
# original dimension entity:
virtual_copy.dxf.discard("geometry")
return virtual_copy
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
assert isinstance(entity, Dimension)
if self.virtual_block_content:
# another copy of a virtual entity:
virtual_content = EntitySpace(
copy_strategy.copy(e) for e in self.virtual_block_content
)
else:
# entity is a new virtual copy of self and can not share the same
# geometry block to be independently transformable:
virtual_content = EntitySpace(self.virtual_entities())
# virtual_entities() returns the entities already translated
# to the insert location:
entity.dxf.discard("insert")
entity.virtual_block_content = virtual_content
def post_bind_hook(self):
"""Called after binding a virtual dimension entity to a document.
This method is not called at the loading stage and virtual dimension
entities do not exist at the loading stage!
"""
doc = self.doc
if self.virtual_block_content and doc is not None:
# create a new geometry block:
block = doc.blocks.new_anonymous_block(type_char="D")
# move virtual block content to the new geometry block:
for entity in self.virtual_block_content:
block.add_entity(entity)
self.dxf.geometry = block.name
# unlink virtual block content:
self.virtual_block_content = None
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_dimension_group_codes, 2, recover=True
)
processor.fast_load_dxfattribs(
dxf, acdb_dimension_dummy_group_codes, 3, log=False
)
# Ignore possible 5. subclass AcDbRotatedDimension, which has no
# content.
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
if tagwriter.dxfversion == DXF12:
self.dxf.export_dxf_attribs(
tagwriter,
[
"geometry",
"dimstyle",
"defpoint",
"text_midpoint",
"insert",
"dimtype",
"text",
"defpoint2",
"defpoint3",
"defpoint4",
"defpoint5",
"leader_length",
"angle",
"horizontal_direction",
"oblique_angle",
"text_rotation",
],
)
return
# else DXF2000+
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_dimension.name)
dim_type = self.dimtype
self.dxf.export_dxf_attribs(
tagwriter,
[
"version",
"geometry",
"dimstyle",
"defpoint",
"text_midpoint",
"insert",
"dimtype",
"attachment_point",
"line_spacing_style",
"line_spacing_factor",
"actual_measurement",
"unknown1",
"flip_arrow_1",
"flip_arrow_2",
"text",
"oblique_angle",
"text_rotation",
"horizontal_direction",
"extrusion",
],
)
# Processing by dimtype works only for the original DIMENSION entity.
# Until DXF R2018 dimtype 5 was shared between ARC_DIMENSION and
# angular & angular3p DIMENSION, which have different subclass
# structures!
if self.dxftype() != "DIMENSION":
return
if dim_type == 0: # linear
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbAlignedDimension")
self.dxf.export_dxf_attribs(tagwriter, ["defpoint2", "defpoint3", "angle"])
# empty but required subclass
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbRotatedDimension")
elif dim_type == 1: # aligned
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbAlignedDimension")
self.dxf.export_dxf_attribs(tagwriter, ["defpoint2", "defpoint3", "angle"])
elif dim_type == 2: # angular & angular3p
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDb2LineAngularDimension")
self.dxf.export_dxf_attribs(
tagwriter, ["defpoint2", "defpoint3", "defpoint4", "defpoint5"]
)
elif dim_type == 3: # diameter
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbDiametricDimension")
self.dxf.export_dxf_attribs(tagwriter, ["defpoint4", "leader_length"])
elif dim_type == 4: # radius
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbRadialDimension")
self.dxf.export_dxf_attribs(tagwriter, ["defpoint4", "leader_length"])
elif dim_type == 5: # angular & angular3p
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDb3PointAngularDimension")
self.dxf.export_dxf_attribs(
tagwriter, ["defpoint2", "defpoint3", "defpoint4", "defpoint5"]
)
elif dim_type == 6: # ordinate
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbOrdinateDimension")
self.dxf.export_dxf_attribs(tagwriter, ["defpoint2", "defpoint3"])
def register_resources(self, registry: xref.Registry) -> None:
assert self.doc is not None
super().register_resources(registry)
registry.add_dim_style(self.dxf.dimstyle)
geometry = self.dxf.geometry
if self.doc.block_records.has_entry(geometry):
registry.add_block_name(geometry)
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:
super().map_resources(clone, mapping)
clone.dxf.dimstyle = mapping.get_dim_style(self.dxf.dimstyle)
clone.dxf.geometry = mapping.get_block_name(self.dxf.geometry)
# 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, Dimension)
self_override.map_resources_r12(clone, mapping)
@property
def dimtype(self) -> int:
""":attr:`dxf.dimtype` without binary flags (32, 62, 128)."""
# undocumented ARC_DIMENSION = 8 (DXF R2018)
return self.dxf.dimtype & 15
# Special DIMENSION - Dimensional constraints
# No information in the DXF reference:
# layer name is "*ADSK_CONSTRAINTS"
# missing group code 2 - geometry block name
# has reactor to ACDBASSOCDEPENDENCY object
# Autodesk example: architectural_example-imperial.dxf
@property
def is_dimensional_constraint(self) -> bool:
"""Returns ``True`` if the DIMENSION entity is a dimensional
constraint object.
"""
dxf = self.dxf
return dxf.layer == ADSK_CONSTRAINTS and not dxf.hasattr("geometry")
def get_geometry_block(self) -> Optional[BlockLayout]:
"""Returns :class:`~ezdxf.layouts.BlockLayout` of associated anonymous
dimension block, which contains the entities that make up the dimension
picture. Returns ``None`` if block name is not set or the BLOCK itself
does not exist
"""
block_name = self.dxf.get("geometry", "*")
return self.doc.blocks.get(block_name) # type: ignore
def get_measurement(self) -> Union[float, Vec3]:
"""Returns the actual dimension measurement in :ref:`WCS` units, no
scaling applied for linear dimensions. Returns angle in degrees for
angular dimension from 2 lines and angular dimension from 3 points.
Returns vector from origin to feature location for ordinate dimensions.
"""
tool = MEASUREMENT_TOOLS.get(self.dimtype)
if tool:
return tool(self)
else:
raise TypeError(f"Unknown DIMENSION type {self.dimtype}.")
def override(self) -> DimStyleOverride:
"""Returns the :class:`~ezdxf.entities.DimStyleOverride` object."""
return DimStyleOverride(self)
def render(self) -> None:
"""Renders the graphical representation of the DIMENSION entity as DXF
primitives (TEXT, LINE, ARC, ...) into an anonymous content BLOCK.
"""
if self.is_virtual:
raise DXFTypeError("can not render virtual entity")
# Do not delete existing anonymous block, it is maybe referenced
# by a dimension entity in another block reference! Dimensions in block
# references share the same geometry block!
self.override().render()
def transform(self, m: Matrix44) -> Dimension:
"""Transform the DIMENSION entity by transformation matrix `m` inplace.
Raises ``NonUniformScalingError()`` for non uniform scaling.
"""
def transform_if_exist(name: str, func):
if dxf.hasattr(name):
dxf.set(name, func(dxf.get(name)))
dxf = self.dxf
ocs = OCSTransform(self.dxf.extrusion, m)
for vertex_name in ("text_midpoint", "defpoint5", "insert"):
transform_if_exist(vertex_name, ocs.transform_vertex)
for angle_name in ("text_rotation", "horizontal_direction", "angle"):
transform_if_exist(angle_name, ocs.transform_deg_angle)
for vertex_name in ("defpoint", "defpoint2", "defpoint3", "defpoint4"):
transform_if_exist(vertex_name, m.transform)
dxf.extrusion = ocs.new_extrusion
# ignore cloned geometry, this would transform the block content
# multiple times:
if not dxf.hasattr("insert"):
self._transform_block_content(m)
self.post_transform(m)
return self
def _block_content(self) -> Iterable[DXFGraphic]:
if self.virtual_block_content or self.is_virtual:
content = self.virtual_block_content
else:
content = self.get_geometry_block() # type: ignore
return content or [] # type: ignore
def _transform_block_content(self, m: Matrix44) -> None:
for entity in self._block_content():
try:
entity.transform(m)
except (NotImplementedError, NonUniformScalingError):
pass # ignore transformation errors
def __virtual_entities__(self) -> Iterator[DXFGraphic]:
"""Implements the SupportsVirtualEntities protocol."""
def ocs_to_wcs(e: DXFGraphic, elevation: float):
# - OCS entities have to get the extrusion vector and the
# elevation of the DIMENSION entity
# - WCS entities have to be transformed to the WCS
dxftype = e.dxftype()
dxf = e.dxf
if dxftype == "LINE":
dxf.start = ocs.to_wcs(dxf.start.replace(z=elevation))
dxf.end = ocs.to_wcs(dxf.end.replace(z=elevation))
elif dxftype == "MTEXT":
e.convert_rotation_to_text_direction() # type: ignore
dxf.extrusion = ocs.uz
dxf.text_direction = ocs.to_wcs(dxf.text_direction)
dxf.insert = ocs.to_wcs(dxf.insert.replace(z=elevation))
elif dxftype == "POINT":
dxf.location = ocs.to_wcs(dxf.location.replace(z=elevation))
else: # OCS entities
dxf.extrusion = ocs.uz
# set elevation:
if dxf.hasattr("insert"): # INSERT, TEXT
dxf.insert = dxf.insert.replace(z=elevation)
elif dxf.hasattr("center"): # ARC, CIRCLE
dxf.center = dxf.center.replace(z=elevation)
elif dxftype == "SOLID":
# AutoCAD uses the SOLID entity to render the "solid fill"
# arrow directly without using a block reference as usual. >:(
for vtx_name in const.VERTEXNAMES:
point = dxf.get(vtx_name, NULLVEC).replace(z=elevation)
dxf.set(vtx_name, point)
ocs = self.ocs()
dim_elevation = self.dxf.text_midpoint.z
transform = False
insert = self.dxf.get("insert", None)
if insert:
transform = True
insert = Vec3(ocs.to_wcs(insert))
m = Matrix44.translate(insert.x, insert.y, insert.z)
for entity in self._block_content():
try:
copy = entity.copy(copy_strategy=default_copy)
except CopyNotSupported:
continue
if ocs.transform:
# All block content entities are located in the OCS defined by
# the DIMENSION entity, even the WCS entities LINE, MTEXT and
# POINT:
ocs_to_wcs(copy, dim_elevation)
if transform:
copy.transform(m)
yield copy
def virtual_entities(self) -> Iterator[DXFGraphic]:
"""Yields the graphical representation of the anonymous content BLOCK as virtual
DXF primitives (LINE, ARC, TEXT, ...).
These virtual entities are located at the original location of the DIMENSION entity,
but they 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:
"""Explodes the graphical representation of the DIMENSION entity as DXF
primitives (LINE, ARC, TEXT, ...) into the target layout, ``None`` for the same
layout as the source DIMENSION entity.
Returns an :class:`~ezdxf.query.EntityQuery` container containing all DXF
primitives.
Args:
target_layout: target layout for the DXF primitives, ``None`` for same
layout as source DIMENSION entity.
"""
return explode_entity(self, target_layout)
def destroy(self) -> None:
# Let virtual content just go out of scope:
del self.virtual_block_content
super().destroy()
def __referenced_blocks__(self) -> Iterable[str]:
"""Support for "ReferencedBlocks" protocol."""
if self.doc:
block_name = self.dxf.get("geometry", None)
if block_name:
block = self.doc.blocks.get(block_name)
if block is not None:
return (block.block_record_handle,)
return tuple()
def audit(self, auditor: Auditor) -> None:
super().audit(auditor)
doc = auditor.doc
dxf = self.dxf
if (
not self.is_dimensional_constraint
and dxf.get("geometry", "*") not in doc.blocks
):
auditor.fixed_error(
code=AuditError.UNDEFINED_BLOCK,
message=f"Removed {str(self)} without valid geometry block.",
)
auditor.trash(self)
return
dimstyle = dxf.get("dimstyle", "Standard")
if not doc.dimstyles.has_entry(dimstyle):
auditor.fixed_error(
code=AuditError.INVALID_DIMSTYLE,
message=f"Replaced invalid DIMSTYLE '{dimstyle}' by 'Standard'.",
)
dxf.discard("dimstyle")
# AutoCAD ignores invalid data in the XDATA section, no need to
# check or repair. Ezdxf also ignores invalid XDATA overrides.
def register_override_handles(entity: DXFEntity, registry: xref.Registry) -> None:
override_tags = entity.get_xdata_list("ACAD", "DSTYLE")
for code, value in override_tags:
if code == 1005:
registry.add_handle(value)
acdb_arc_dimension = DefSubclass(
"AcDbArcDimension",
{
# start point of the 1st extension line:
"defpoint2": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
# start point of the 2ndt extension line:
"defpoint3": DXFAttr(14, xtype=XType.point3d, default=NULLVEC),
# center of arc:
"defpoint4": DXFAttr(15, xtype=XType.point3d, default=NULLVEC),
"start_angle": DXFAttr(40), # radians, unknown meaning
"end_angle": DXFAttr(41), # radians, unknown meaning
"is_partial": DXFAttr(70, validator=validator.is_integer_bool),
"has_leader": DXFAttr(71, validator=validator.is_integer_bool),
"leader_point1": DXFAttr(16, xtype=XType.point3d, default=NULLVEC),
"leader_point2": DXFAttr(17, xtype=XType.point3d, default=NULLVEC),
},
)
acdb_arc_dimension_group_codes = group_code_mapping(acdb_arc_dimension)
@register_entity
class ArcDimension(Dimension):
"""DXF ARC_DIMENSION entity"""
# dimtype is 5 for DXF version <= R2013
# dimtype is 8 for DXF version >= R2018
DXFTYPE = "ARC_DIMENSION"
DXFATTRIBS = DXFAttributes(
base_class, acdb_entity, acdb_dimension, acdb_arc_dimension
)
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
# Skip Dimension loader:
dxf = super(Dimension, self).load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_dimension_group_codes, 2, recover=True
)
processor.fast_load_dxfattribs(
dxf, acdb_arc_dimension_group_codes, 3, recover=True
)
return dxf
def versioned_dimtype(self, dxfversion: str) -> int:
if dxfversion > const.DXF2013:
return (self.dxf.dimtype & 0xFFF0) | 8
else:
return (self.dxf.dimtype & 0xFFF0) | 5
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
dimtype = self.dxf.dimtype # preserve original dimtype
self.dxf.dimtype = self.versioned_dimtype(tagwriter.dxfversion)
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbArcDimension")
self.dxf.export_dxf_attribs(
tagwriter,
[
"defpoint2",
"defpoint3",
"defpoint4",
"start_angle",
"end_angle",
"is_partial",
"has_leader",
"leader_point1",
"leader_point2",
],
)
self.dxf.dimtype = dimtype # restore original dimtype
def transform(self, m: Matrix44) -> Self:
"""Transform the ARC_DIMENSION entity by transformation matrix `m` inplace.
Raises ``NonUniformScalingError()`` for non uniform scaling.
"""
def transform_if_exist(name: str, func):
if dxf.hasattr(name):
dxf.set(name, func(dxf.get(name)))
dxf = self.dxf
super().transform(m)
for vertex_name in ("leader_point1", "leader_point2"):
transform_if_exist(vertex_name, m.transform)
return self
acdb_radial_dimension_large = DefSubclass(
"AcDbRadialDimensionLarge",
{
# center_point = def_point from subclass AcDbDimension
"chord_point": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
"override_center": DXFAttr(14, xtype=XType.point3d, default=NULLVEC),
"jog_point": DXFAttr(15, xtype=XType.point3d, default=NULLVEC),
"unknown2": DXFAttr(40),
},
)
acdb_radial_dimension_large_group_codes = group_code_mapping(
acdb_radial_dimension_large
)
# Undocumented DXF entity - OpenDesignAlliance DWG Specification:
# chapter 20.4.30
@register_entity
class RadialDimensionLarge(Dimension):
"""DXF LARGE_RADIAL_DIMENSION entity"""
DXFTYPE = "LARGE_RADIAL_DIMENSION"
DXFATTRIBS = DXFAttributes(
base_class, acdb_entity, acdb_dimension, acdb_radial_dimension_large
)
MIN_DXF_VERSION_FOR_EXPORT = DXF2004
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
# Skip Dimension loader:
dxf = super(Dimension, self).load_dxf_attribs(processor)
if processor:
processor.fast_load_dxfattribs(
dxf, acdb_dimension_group_codes, 2, recover=True
)
processor.fast_load_dxfattribs(
dxf, acdb_radial_dimension_large_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, "AcDbRadialDimensionLarge")
self.dxf.export_dxf_attribs(
tagwriter,
["chord_point", "override_center", "jog_point", "unknown2"],
)
def transform(self, m: Matrix44) -> Self:
"""Transform the LARGE_RADIAL_DIMENSION entity by transformation matrix
`m` inplace.
Raises ``NonUniformScalingError()`` for non uniform scaling.
"""
def transform_if_exist(name: str, func):
if dxf.hasattr(name):
dxf.set(name, func(dxf.get(name)))
dxf = self.dxf
super().transform(m)
# todo: are these WCS points?
for vertex_name in ("chord_point", "override_center", "Jog_point"):
transform_if_exist(vertex_name, m.transform)
return self
# XDATA extension - meaning unknown
# 1001, ACAD_DSTYLE_DIMRADIAL_EXTENSION
# 1070, 387
# 1070, 1
# 1070, 388
# 1040, 0.0
# 1070, 390
# 1040, 0.0
# todo: DIMASSOC
acdb_dim_assoc = DefSubclass(
"AcDbDimAssoc",
{
# Handle of dimension object:
"dimension": DXFAttr(330),
# Associativity flag (bit-coded)
# 1 = First point reference
# 2 = Second point reference
# 4 = Third point reference
# 8 = Fourth point reference
"point_flag": DXFAttr(90),
"trans_space": DXFAttr(
70,
validator=validator.is_integer_bool,
fixer=validator.fix_integer_bool,
),
# Rotated Dimension type (parallel, perpendicular)
# Autodesk gone crazy: subclass AcDbOsnapPointRef with group code 1!!!!!
# }), DefSubclass('AcDbOsnapPointRef', {
"rotated_dim_type": DXFAttr(71),
# Object Osnap type:
# 0 = None
# 1 = Endpoint
# 2 = Midpoint
# 3 = Center
# 4 = Node
# 5 = Quadrant
# 6 = Intersection
# 7 = Insertion
# 8 = Perpendicular
# 9 = Tangent
# 10 = Nearest
# 11 = Apparent intersection
# 12 = Parallel
# 13 = Start point
"osnap_type": DXFAttr(
72,
validator=validator.is_in_integer_range(0, 14),
fixer=validator.fit_into_integer_range(0, 14),
),
# ID of main object (geometry)
"object_id": DXFAttr(331),
# Subtype of main object (edge, face)
"object_subtype": DXFAttr(73),
# GsMarker of main object (index)
"object_gs_marker": DXFAttr(91),
# Handle (string) of Xref object
"object_xref_id": DXFAttr(301),
# Geometry parameter for Near Osnap
"near_param": DXFAttr(40),
# Osnap point in WCS
"osnap_point": DXFAttr(10, xtype=XType.point3d),
# ID of intersection object (geometry)
"intersect_id": DXFAttr(332),
# Subtype of intersection object (edge/face)
"intersect_subtype": DXFAttr(74),
# GsMarker of intersection object (index)
"intersect_gs_marker": DXFAttr(92),
# Handle (string) of intersection Xref object
"intersect_xref_id": DXFAttr(302),
# hasLastPointRef flag (true/false)
"has_last_point_ref": DXFAttr(
75,
validator=validator.is_integer_bool,
fixer=validator.fix_integer_bool,
),
},
)
def measure_linear_distance(dim: Dimension) -> float:
dxf = dim.dxf
return linear_measurement(
dxf.defpoint2,
dxf.defpoint3,
math.radians(dxf.get("angle", 0)),
dim.ocs(),
)
def measure_diameter_or_radius(dim: Dimension) -> float:
p1 = Vec3(dim.dxf.defpoint)
p2 = Vec3(dim.dxf.defpoint4)
return (p2 - p1).magnitude
def measure_angle_between_two_lines(dim: Dimension) -> float:
dxf = dim.dxf
p1 = Vec3(dxf.defpoint2) # 1. point of 1. extension line
p2 = Vec3(dxf.defpoint3) # 2. point of 1. extension line
p3 = Vec3(dxf.defpoint4) # 1. point of 2. extension line
p4 = Vec3(dxf.defpoint) # 2. point of 2. extension line
dir1 = p2 - p1 # direction of 1. extension line
dir2 = p4 - p3 # direction of 2. extension line
return angle_between(dir1, dir2)
def measure_angle_between_three_points(dim: Dimension) -> float:
dxf = dim.dxf
p1 = Vec3(dxf.defpoint4) # center
p2 = Vec3(dxf.defpoint2) # 1. extension line
p3 = Vec3(dxf.defpoint3) # 2. extension line
dir1 = p2 - p1 # direction of 1. extension line
dir2 = p3 - p1 # direction of 2. extension line
return angle_between(dir1, dir2)
def get_feature_location(dim: Dimension) -> Vec3:
origin = Vec3(dim.dxf.defpoint)
feature_location = Vec3(dim.dxf.defpoint2)
return feature_location - origin
def angle_between(v1: Vec3, v2: Vec3) -> float:
angle = v2.angle_deg - v1.angle_deg
return angle + 360 if angle < 0 else angle
def linear_measurement(
p1: Vec3, p2: Vec3, angle: float = 0, ocs: Optional[OCS] = None
) -> float:
"""Returns distance from `p1` to `p2` projected onto ray defined by
`angle`, `angle` in radians in the xy-plane.
"""
if ocs is not None and ocs.uz != (0, 0, 1):
p1 = ocs.to_wcs(p1)
p2 = ocs.to_wcs(p2)
# angle in OCS xy-plane
ocs_direction = Vec3.from_angle(angle)
measurement_direction = ocs.to_wcs(ocs_direction)
else:
# angle in WCS xy-plane
measurement_direction = Vec3.from_angle(angle)
t1 = measurement_direction.project(p1)
t2 = measurement_direction.project(p2)
return (t2 - t1).magnitude
# TODO: add ARC_DIMENSION dimtype=8 support
MEASUREMENT_TOOLS = {
const.DIM_LINEAR: measure_linear_distance,
const.DIM_ALIGNED: measure_linear_distance,
const.DIM_ANGULAR: measure_angle_between_two_lines,
const.DIM_DIAMETER: measure_diameter_or_radius,
const.DIM_RADIUS: measure_diameter_or_radius,
const.DIM_ANGULAR_3P: measure_angle_between_three_points,
const.DIM_ORDINATE: get_feature_location,
}