# 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"