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

1665 lines
63 KiB
Python

# Copyright (c) 2023-2024, Manfred Moitzi
# License: MIT License
""" Resource management module for transferring DXF resources between documents.
"""
from __future__ import annotations
from typing import Optional, Sequence, Callable, Iterable
from typing_extensions import Protocol, TypeAlias
import enum
import pathlib
import logging
import os
import ezdxf
from ezdxf.lldxf import const, validator, types
from ezdxf.lldxf.tags import Tags
from ezdxf.lldxf.validator import DXFInfo
from ezdxf.document import Drawing
from ezdxf.layouts import BaseLayout, Paperspace, BlockLayout
from ezdxf.entities import (
is_graphic_entity,
is_dxf_object,
DXFEntity,
DXFClass,
factory,
BlockRecord,
Layer,
Linetype,
Textstyle,
DimStyle,
UCSTableEntry,
Material,
MLineStyle,
MLeaderStyle,
Block,
EndBlk,
Insert,
DXFLayout,
VisualStyle,
)
from ezdxf.entities.copy import CopyStrategy, CopySettings
from ezdxf.math import UVec, Vec3
__all__ = [
"define",
"attach",
"embed",
"detach",
"write_block",
"load_modelspace",
"load_paperspace",
"Registry",
"ResourceMapper",
"ConflictPolicy",
"Loader",
"dxf_info",
"DXFInfo",
"XrefError",
"XrefDefinitionError",
"EntityError",
"LayoutError",
]
logger = logging.getLogger("ezdxf")
NO_BLOCK = "0"
DEFAULT_LINETYPES = {"CONTINUOUS", "BYLAYER", "BYBLOCK"}
DEFAULT_LAYER = "0"
STANDARD = "STANDARD"
FilterFunction: TypeAlias = Callable[[DXFEntity], bool]
LoadFunction: TypeAlias = Callable[[str], Drawing]
# I prefer to see the debug messages stored in the object, because I mostly debug test
# code and pytest does not show logging or print messages by default.
def _log_debug_messages(messages: Iterable[str]) -> None:
for msg in messages:
logger.debug(msg)
class XrefError(Exception):
"""base exception for the xref module"""
pass
class XrefDefinitionError(XrefError):
pass
class EntityError(XrefError):
pass
class LayoutError(XrefError):
pass
class InternalError(XrefError):
pass
class ConflictPolicy(enum.Enum):
"""These conflict policies define how to handle resource name conflicts.
.. versionadded:: 1.1
Attributes:
KEEP: Keeps the existing resource name of the target document and ignore the
resource from the source document.
XREF_PREFIX: This policy handles the resource import like CAD applications by
**always** renaming the loaded resources to `<xref>$0$<name>`, where `xref`
is the name of source document, the `$0$` part is a number to create a
unique resource name and `<name>` is the name of the resource itself.
NUM_PREFIX: This policy renames the loaded resources to `$0$<name>` only if the
resource `<name>` already exists. The `$0$` prefix is a number to create a
unique resource name and `<name>` is the name of the resource itself.
"""
KEEP = enum.auto()
XREF_PREFIX = enum.auto()
NUM_PREFIX = enum.auto()
def dxf_info(filename: str | os.PathLike) -> DXFInfo:
"""Scans the HEADER section of a DXF document and returns a :class:`DXFInfo`
object, which contains information about the DXF version, text encoding, drawing
units and insertion base point.
Raises:
IOError: not a DXF file or a generic IO error
"""
filename = str(filename)
if validator.is_binary_dxf_file(filename):
with open(filename, "rb") as fp:
# The HEADER section of a DXF R2018 file has a length of ~5300 bytes.
data = fp.read(8192)
return validator.binary_dxf_info(data)
if validator.is_dxf_file(filename):
# the relevant information has 7-bit ASCII encoding
with open(filename, "rt", errors="ignore") as fp:
return validator.dxf_info(fp)
else:
raise IOError("Not a DXF files.")
# Exceptions from the ConflictPolicy
# ----------------------------------
# Resources named "STANDARD" will be preserved (KEEP).
# Materials "GLOBAL", "BYLAYER" and "BYBLOCK" will be preserved (KEEP).
# Plot style "NORMAL" will be preserved (KEEP).
# Layers "0", "DEFPOINTS" and special Autodesk layers starting with "*" will be preserved (KEEP).
# Linetypes "CONTINUOUS", "BYLAYER" and "BYBLOCK" will be preserved (KEEP)
# Special blocks like arrow heads will be preserved (KEEP).
# Anonymous blocks get a new arbitrary name following the rules of anonymous block names.
# Notes about DXF files as XREFs
# ------------------------------
# AutoCAD cannot use DXF R12 files as external references (BricsCAD can)!
# AutoCAD may use DXF R2000+ as external references, but does not accept DXF files
# created by ezdxf nor BricsCAD, which opened for itself are total valid DXF documents.
#
# Autodesk DWG TrueView V2022:
# > Error: Unable to load <absolute file path>.
# > Drawing may need recovery.
#
# Using the RECOVER command of BricsCAD and rewriting the DXF files by BricsCAD does
# not work. Replacing the XREF by a newly created DXF file by BricsCAD does not work
# either.
#
# BricsCAD accepts any DXF/DWG file as XREF!
# Idea for automated object loading from the OBJECTS section:
# -----------------------------------------------------------
# EXT_DICT = Extension DICTIONARY; ROOT_DICT = "unnamed" DICTIONARY
# Pointer and owner handles in XRECORD or unknown objects and entities have well-defined
# group codes, if the creator app follow the rules of the DXF reference.
#
# Object types:
# A. object owner path ends at a graphical entity, e.g. LINE -> EXT_DICT -> XRECORD
# B. owner path of an object ends at the root dictionary and contains only objects
# from the OBJECTS section, e.g. ROOT_DICT -> DICTIONARY -> MATERIAL
# or ROOT_DICT -> XRECORD -> CUSTOM_OBJECT -> XRECORD.
# The owner path of object type B cannot contain a graphical entity because the owner
# of a graphical entity is always a BLOCK_RECORD.
# C. owner path ends at an object that does not exist or with an owner handle "0",
# so the last owner handle of the owner path is invalid, this seems to be an invalid
# construct
#
# Automated object loading with reconstruction of the owner path:
# ---------------------------------------------------------------
# example MATERIAL object:
# - find the owner path of the source MATERIAL object:
# ROOT_DICT -> DICTIONARY (material collection) -> MATERIAL
# 1 does the parent object of MATERIAL in the target doc exist:
# 2 YES: add MATERIAL to the owner DICTIONARY (conflict policy!)
# 3 NO:
# 4 create the immediate parent DICTIONARY of MATERIAL in the target dict and add
# it to the ROOT_DICT of the target doc
# GOTO 2 add the MATERIAL to the new owner DICTIONARY
#
# Top management layer of the OBJECTS section
# -------------------------------------------
# Create a standard mapping for the ROOT_DICT and its entries (DICTIONARY objects)
# from the source doc to the target doc. I think these are always basic management
# structures which shouldn't be duplicated.
def define(doc: Drawing, block_name: str, filename: str, overlay=False) -> None:
"""Add an external reference (xref) definition to a document.
XREF attachment types:
- attached: the XREF that's inserted into this drawing is also present in a
document to which this document is inserted as an XREF.
- overlay: the XREF that's inserted into this document is **not** present in a
document to which this document is inserted as an XREF.
Args:
doc: host document
block_name: name of the xref block
filename: external reference filename
overlay: creates an XREF overlay if ``True`` and an XREF attachment otherwise
Raises:
XrefDefinitionError: block with same name exist
.. versionadded:: 1.1
"""
if block_name in doc.blocks:
raise XrefDefinitionError(f"block '{block_name}' already exist")
doc.blocks.new(
name=block_name,
dxfattribs={
"flags": make_xref_flags(overlay),
"xref_path": filename,
},
)
def make_xref_flags(overlay: bool) -> int:
if overlay:
return const.BLK_XREF_OVERLAY | const.BLK_EXTERNAL
else:
return const.BLK_XREF | const.BLK_EXTERNAL
def attach(
doc: Drawing,
*,
block_name: str,
filename: str,
insert: UVec = (0, 0, 0),
scale: float = 1.0,
rotation: float = 0.0,
overlay=False,
) -> Insert:
"""Attach the file `filename` to the host document as external reference (XREF) and
creates a default block reference for the XREF in the modelspace of the document.
The function raises an :class:`XrefDefinitionError` exception if the block definition
already exist, but an XREF can be inserted multiple times by adding additional block
references::
msp.add_blockref(block_name, insert=another_location)
.. important::
If the XREF has different drawing units than the host document, the scale
factor between these units must be applied as a uniform scale factor to the
block reference! Unfortunately the XREF drawing units can only be detected by
scanning the HEADER section of a document by the function :func:`dxf_info` and
is therefore not done automatically by this function.
Advice: always use the same units for all drawings of a project!
Args:
doc: host DXF document
block_name: name of the XREF definition block
filename: file name of the XREF
insert: location of the default block reference
scale: uniform scaling factor
rotation: rotation angle in degrees
overlay: creates an XREF overlay if ``True`` and an XREF attachment otherwise
Returns:
Insert: default block reference for the XREF
Raises:
XrefDefinitionError: block with same name exist
.. versionadded:: 1.1
"""
define(doc, block_name, filename, overlay=overlay)
dxfattribs = dict()
if rotation:
dxfattribs["rotation"] = float(rotation)
if scale != 1.0:
scale = float(scale)
dxfattribs["xscale"] = scale
dxfattribs["yscale"] = scale
dxfattribs["zscale"] = scale
location = Vec3(insert)
msp = doc.modelspace()
return msp.add_blockref(block_name, insert=location, dxfattribs=dxfattribs)
def find_xref(xref_filename: str, search_paths: Sequence[pathlib.Path]) -> pathlib.Path:
"""Returns the path of the XREF file.
Args:
xref_filename: filename of the XREF, absolute or relative path
search_paths: search paths where to look for the XREF file
.. versionadded:: 1.1
"""
filepath = pathlib.Path(xref_filename)
# 1. check absolute xref_filename
if filepath.exists():
return filepath
name = filepath.name
for path in search_paths:
if not path.is_dir():
path = path.parent
search_path = path.resolve()
# 2. check relative xref path to search path
filepath = search_path / xref_filename
if filepath.exists():
return filepath
# 3. check if the file is in the search folder
filepath = search_path / name
if filepath.exists():
return filepath
return pathlib.Path(xref_filename)
def embed(
xref: BlockLayout,
*,
load_fn: Optional[LoadFunction] = None,
search_paths: Iterable[pathlib.Path | str] = tuple(),
conflict_policy=ConflictPolicy.XREF_PREFIX,
) -> None:
"""Loads the modelspace of the XREF as content into a block layout.
The loader function loads the XREF as `Drawing` object, by default the
function :func:`ezdxf.readfile` is used to load DXF files. To load DWG files use the
:func:`~ezdxf.addons.odafc.readfile` function from the :mod:`ezdxf.addons.odafc`
add-on. The :func:`ezdxf.recover.readfile` function is very robust for reading DXF
files with errors.
If the XREF path isn't absolute the XREF is searched in the folder of the host DXF
document and in the `search_path` folders.
Args:
xref: :class:`BlockLayout` of the XREF document
load_fn: function to load the content of the XREF as `Drawing` object
search_paths: list of folders to search for XREFS, default is the folder of the
host document or the current directory if no filepath is set
conflict_policy: how to resolve name conflicts
Raises:
XrefDefinitionError: argument `xref` is not a XREF definition
FileNotFoundError: XREF file not found
DXFVersionError: cannot load a XREF with a newer DXF version than the host
document, try the :mod:`~ezdxf.addons.odafc` add-on to downgrade the XREF
document or upgrade the host document
.. versionadded:: 1.1
"""
assert isinstance(xref, BlockLayout), "expected BLOCK definition of XREF"
target_doc = xref.doc
assert target_doc is not None, "valid DXF document required"
block = xref.block
assert isinstance(block, Block)
if not block.is_xref:
raise XrefDefinitionError("argument 'xref' is not a XREF definition")
xref_path: str = block.dxf.get("xref_path", "")
if not xref_path:
raise XrefDefinitionError("no xref path defined")
_search_paths = [pathlib.Path(p) for p in search_paths]
_search_paths.insert(0, target_doc.get_abs_filepath())
filepath = find_xref(xref_path, _search_paths)
if not filepath.exists():
raise FileNotFoundError(f"file not found: '{filepath}'")
if load_fn:
source_doc = load_fn(str(filepath))
else:
source_doc = ezdxf.readfile(filepath)
if source_doc.dxfversion > target_doc.dxfversion:
raise const.DXFVersionError(
"cannot embed a XREF with a newer DXF version into the host document"
)
loader = Loader(source_doc, target_doc, conflict_policy=conflict_policy)
loader.load_modelspace(xref)
loader.execute(xref_prefix=xref.name)
# reset XREF flags:
block.set_flag_state(const.BLK_XREF | const.BLK_EXTERNAL, state=False)
# update BLOCK origin:
origin = source_doc.header.get("$INSBASE")
if origin:
block.dxf.base_point = Vec3(origin)
def detach(
block: BlockLayout, *, xref_filename: str | os.PathLike, overlay=False
) -> Drawing:
"""Write the content of `block` into the modelspace of a new DXF document and
convert `block` to an external reference (XREF). The new DXF document has to be
written by the caller: :code:`xref_doc.saveas(xref_filename)`.
This way it is possible to convert the DXF document to DWG by the
:mod:`~ezdxf.addons.odafc` add-on if necessary::
xref_doc = xref.detach(my_block, "my_block.dwg")
odafc.export_dwg(xref_doc, "my_block.dwg")
It's recommended to clean up the entity database of the host document afterwards::
doc.entitydb.purge()
The function does not create any block references. These references should already
exist and do not need to be changed since references to blocks and XREFs are the
same.
Args:
block: block definition to detach
xref_filename: name of the external referenced file
overlay: creates an XREF overlay if ``True`` and an XREF attachment otherwise
.. versionadded:: 1.1
"""
source_doc = block.doc
assert source_doc is not None, "valid DXF document required"
target_doc = ezdxf.new(dxfversion=source_doc.dxfversion, units=block.units)
loader = Loader(source_doc, target_doc, conflict_policy=ConflictPolicy.KEEP)
loader.load_block_layout_into(block, target_doc.modelspace())
loader.execute()
target_doc.header["$INSBASE"] = block.base_point
block_to_xref(block, xref_filename, overlay=overlay)
return target_doc
def block_to_xref(
block: BlockLayout, xref_filename: str | os.PathLike, *, overlay=False
) -> None:
"""Convert a block definition into an external reference.
(internal API)
"""
block.delete_all_entities()
block_entity = block.block
assert block_entity is not None, "invalid BlockLayout"
block_entity.dxf.xref_path = str(xref_filename)
block_entity.dxf.flags = make_xref_flags(overlay)
def write_block(entities: Sequence[DXFEntity], *, origin: UVec = (0, 0, 0)) -> Drawing:
"""Write `entities` into the modelspace of a new DXF document.
This function is called "write_block" because the new DXF document can be used as
an external referenced block. This function is similar to the WBLOCK command in CAD
applications.
Virtual entities are not supported, because each entity needs a real database- and
owner handle.
Args:
entities: DXF entities to write
origin: block origin, defines the point in the modelspace which will be inserted
at the insert location of the block reference
Raises:
EntityError: virtual entities are not supported
.. versionadded:: 1.1
"""
if len(entities) == 0:
return ezdxf.new()
if any(e.dxf.owner is None for e in entities):
raise EntityError("virtual entities are not supported")
source_doc = entities[0].doc
assert source_doc is not None, "expected a valid source document"
target_doc = ezdxf.new(dxfversion=source_doc.dxfversion, units=source_doc.units)
loader = Loader(source_doc, target_doc)
loader.add_command(LoadEntities(entities, target_doc.modelspace()))
loader.execute()
target_doc.header["$INSBASE"] = Vec3(origin)
return target_doc
def load_modelspace(
sdoc: Drawing,
tdoc: Drawing,
filter_fn: Optional[FilterFunction] = None,
conflict_policy=ConflictPolicy.KEEP,
) -> None:
"""Loads the modelspace content of the source document into the modelspace
of the target document. The filter function `filter_fn` gets every source entity as
input and returns ``True`` to load the entity or ``False`` otherwise.
Args:
sdoc: source document
tdoc: target document
filter_fn: optional function to filter entities from the source modelspace
conflict_policy: how to resolve name conflicts
.. versionadded:: 1.1
"""
loader = Loader(sdoc, tdoc, conflict_policy=conflict_policy)
loader.load_modelspace(filter_fn=filter_fn)
loader.execute()
def load_paperspace(
psp: Paperspace,
tdoc: Drawing,
filter_fn: Optional[FilterFunction] = None,
conflict_policy=ConflictPolicy.KEEP,
) -> None:
"""Loads the paperspace layout `psp` into the target document. The filter function
`filter_fn` gets every source entity as input and returns ``True`` to load the
entity or ``False`` otherwise.
Args:
psp: paperspace layout to load
tdoc: target document
filter_fn: optional function to filter entities from the source paperspace layout
conflict_policy: how to resolve name conflicts
.. versionadded:: 1.1
"""
if psp.doc is tdoc:
raise LayoutError("Source paperspace layout cannot be from target document.")
loader = Loader(psp.doc, tdoc, conflict_policy=conflict_policy)
loader.load_paperspace_layout(psp, filter_fn=filter_fn)
loader.execute()
class Registry(Protocol):
def add_entity(self, entity: DXFEntity, block_key: str = NO_BLOCK) -> None:
...
def add_block(self, block_record: BlockRecord) -> None:
...
def add_handle(self, handle: Optional[str]) -> None:
...
def add_layer(self, name: str) -> None:
...
def add_linetype(self, name: str) -> None:
...
def add_text_style(self, name: str) -> None:
...
def add_dim_style(self, name: str) -> None:
...
def add_block_name(self, name: str) -> None:
...
def add_appid(self, name: str) -> None:
...
class ResourceMapper(Protocol):
def get_handle(self, handle: str, default="0") -> str:
...
def get_reference_of_copy(self, handle: str) -> Optional[DXFEntity]:
...
def get_layer(self, name: str) -> str:
...
def get_linetype(self, name: str) -> str:
...
def get_text_style(self, name: str) -> str:
...
def get_dim_style(self, name: str) -> str:
...
def get_block_name(self, name: str) -> str:
...
def map_resources_of_copy(self, entity: DXFEntity) -> None:
...
def map_pointers(self, tags: Tags, new_owner_handle: str = "") -> None:
...
def map_acad_dict_entry(
self, dict_name: str, entry_name: str, entity: DXFEntity
) -> tuple[str, DXFEntity]:
...
def map_existing_handle(
self, source: DXFEntity, clone: DXFEntity, attrib_name: str, *, optional=False
) -> None:
...
class LoadingCommand:
def register_resources(self, registry: Registry) -> None:
pass
def execute(self, transfer: _Transfer) -> None:
pass
class LoadEntities(LoadingCommand):
"""Loads all given entities into the target layout."""
def __init__(
self, entities: Sequence[DXFEntity], target_layout: BaseLayout
) -> None:
self.entities = entities
if not isinstance(target_layout, BaseLayout):
raise LayoutError(f"invalid target layout type: {type(target_layout)}")
self.target_layout = target_layout
def register_resources(self, registry: Registry) -> None:
for e in self.entities:
registry.add_entity(e, block_key=e.dxf.owner)
def execute(self, transfer: _Transfer) -> None:
target_layout = self.target_layout
for entity in self.entities:
clone = transfer.get_entity_copy(entity)
if clone is None:
transfer.debug(f"xref:cannot copy {str(entity)}")
continue
if is_graphic_entity(clone):
target_layout.add_entity(clone) # type: ignore
else:
transfer.debug(
f"found non-graphic entity {str(clone)} as layout content"
)
if isinstance(target_layout, Paperspace):
_reorganize_paperspace_viewports(target_layout)
def _reorganize_paperspace_viewports(paperspace: Paperspace) -> None:
main_vp = paperspace.main_viewport()
if main_vp is None:
main_vp = paperspace.add_new_main_viewport()
# destroy loaded main VIEWPORT entities
for vp in paperspace.viewports():
if vp.dxf.id == 1 and vp is not main_vp:
paperspace.delete_entity(vp)
paperspace.set_current_viewport_handle(main_vp.dxf.handle)
class LoadPaperspaceLayout(LoadingCommand):
"""Loads a paperspace layout as a new paperspace layout into the target document.
If a paperspace layout with same name already exists the layout will be renamed
to "<layout name> (x)" where x is 2 or the next free number.
"""
def __init__(self, psp: Paperspace, filter_fn: Optional[FilterFunction]) -> None:
if not isinstance(psp, Paperspace):
raise LayoutError(f"invalid paperspace layout type: {type(psp)}")
self.paperspace_layout = psp
self.filter_fn = filter_fn
def source_entities(self) -> list[DXFEntity]:
filter_fn = self.filter_fn
if filter_fn:
return [e for e in self.paperspace_layout if filter_fn(e)]
else:
return list(self.paperspace_layout)
def register_resources(self, registry: Registry) -> None:
registry.add_entity(self.paperspace_layout.dxf_layout)
block_key = self.paperspace_layout.layout_key
for e in self.source_entities():
registry.add_entity(e, block_key=block_key)
def execute(self, transfer: _Transfer) -> None:
source_dxf_layout = self.paperspace_layout.dxf_layout
target_dxf_layout = transfer.get_reference_of_copy(source_dxf_layout.dxf.handle)
assert isinstance(target_dxf_layout, DXFLayout)
target_layout = transfer.registry.target_doc.paperspace(
target_dxf_layout.dxf.name
)
for entity in self.source_entities():
clone = transfer.get_entity_copy(entity)
if clone and is_graphic_entity(clone):
target_layout.add_entity(clone) # type: ignore
else:
transfer.debug(
f"found non-graphic entity {str(clone)} as layout content"
)
_reorganize_paperspace_viewports(target_layout)
class LoadBlockLayout(LoadingCommand):
"""Loads a block layout as a new block layout into the target document. If a block
layout with the same name exists the conflict policy will be applied.
"""
def __init__(self, block: BlockLayout) -> None:
if not isinstance(block, BlockLayout):
raise LayoutError(f"invalid block layout type: {type(block)}")
self.block_layout = block
def register_resources(self, registry: Registry) -> None:
block_record = self.block_layout.block_record
if isinstance(block_record, BlockRecord):
registry.add_entity(block_record)
class LoadResources(LoadingCommand):
"""Loads table entries into the target document. If a table entry with the same name
exists the conflict policy will be applied.
"""
def __init__(self, entities: Sequence[DXFEntity]) -> None:
self.entities = entities
def register_resources(self, registry: Registry) -> None:
for e in self.entities:
registry.add_entity(e, block_key=NO_BLOCK)
class Loader:
"""Load entities and resources from the source DXF document `sdoc` into the
target DXF document.
Args:
sdoc: source DXF document
tdoc: target DXF document
conflict_policy: :class:`ConflictPolicy`
"""
def __init__(
self, sdoc: Drawing, tdoc: Drawing, conflict_policy=ConflictPolicy.KEEP
) -> None:
assert isinstance(sdoc, Drawing), "a valid source document is mandatory"
assert isinstance(tdoc, Drawing), "a valid target document is mandatory"
assert sdoc is not tdoc, "source and target document cannot be the same"
if tdoc.dxfversion < sdoc.dxfversion:
logger.warning(
"target document has older DXF version than the source document"
)
self.sdoc: Drawing = sdoc
self.tdoc: Drawing = tdoc
self.conflict_policy = conflict_policy
self._commands: list[LoadingCommand] = []
def add_command(self, command: LoadingCommand) -> None:
self._commands.append(command)
def load_modelspace(
self,
target_layout: Optional[BaseLayout] = None,
filter_fn: Optional[FilterFunction] = None,
) -> None:
"""Loads the content of the modelspace of the source document into a layout of
the target document, the modelspace of the target document is the default target
layout. The filter function `filter_fn` is used to skip source entities, the
function should return ``False`` for entities to ignore and ``True`` otherwise.
Args:
target_layout: target layout can be any layout: modelspace, paperspace
layout or block layout.
filter_fn: function to filter source entities
"""
if target_layout is None:
target_layout = self.tdoc.modelspace()
elif not isinstance(target_layout, BaseLayout):
raise LayoutError(f"invalid target layout type: {type(target_layout)}")
if target_layout.doc is not self.tdoc:
raise LayoutError(
f"given target layout does not belong to the target document"
)
if filter_fn is None:
entities = list(self.sdoc.modelspace())
else:
entities = [e for e in self.sdoc.modelspace() if filter_fn(e)]
self.add_command(LoadEntities(entities, target_layout))
def load_paperspace_layout(
self,
psp: Paperspace,
filter_fn: Optional[FilterFunction] = None,
) -> None:
"""Loads a paperspace layout as a new paperspace layout into the target document.
If a paperspace layout with same name already exists the layout will be renamed
to "<layout name> (2)" or "<layout name> (3)" and so on. The filter function
`filter_fn` is used to skip source entities, the function should return ``False``
for entities to ignore and ``True`` otherwise.
The content of the modelspace which may be displayed through a VIEWPORT entity
will **not** be loaded!
Args:
psp: the source paperspace layout
filter_fn: function to filter source entities
"""
if not isinstance(psp, Paperspace):
raise const.DXFTypeError(f"invalid paperspace layout type: {type(psp)}")
if psp.doc is not self.sdoc:
raise LayoutError(
f"given paperspace layout does not belong to the source document"
)
self.add_command(LoadPaperspaceLayout(psp, filter_fn))
def load_paperspace_layout_into(
self,
psp: Paperspace,
target_layout: BaseLayout,
filter_fn: Optional[FilterFunction] = None,
) -> None:
"""Loads the content of a paperspace layout into an existing layout of the target
document. The filter function `filter_fn` is used to skip source entities, the
function should return ``False`` for entities to ignore and ``True`` otherwise.
The content of the modelspace which may be displayed through a
VIEWPORT entity will **not** be loaded!
Args:
psp: the source paperspace layout
target_layout: target layout can be any layout: modelspace, paperspace
layout or block layout.
filter_fn: function to filter source entities
"""
if not isinstance(psp, Paperspace):
raise LayoutError(f"invalid paperspace layout type: {type(psp)}")
if not isinstance(target_layout, BaseLayout):
raise LayoutError(f"invalid target layout type: {type(target_layout)}")
if psp.doc is not self.sdoc:
raise LayoutError(
f"given paperspace layout does not belong to the source document"
)
if target_layout.doc is not self.tdoc:
raise LayoutError(
f"given target layout does not belong to the target document"
)
if filter_fn is None:
entities = list(psp)
else:
entities = [e for e in psp if filter_fn(e)]
self.add_command(LoadEntities(entities, target_layout))
def load_block_layout(
self,
block_layout: BlockLayout,
) -> None:
"""Loads a block layout (block definition) as a new block layout into the target
document. If a block layout with the same name exists the conflict policy will
be applied. This method cannot load modelspace or paperspace layouts.
Args:
block_layout: the source block layout
"""
if not isinstance(block_layout, BlockLayout):
raise LayoutError(f"invalid block layout type: {type(block_layout)}")
if block_layout.doc is not self.sdoc:
raise LayoutError(
f"given block layout does not belong to the source document"
)
self.add_command(LoadBlockLayout(block_layout))
def load_block_layout_into(
self,
block_layout: BlockLayout,
target_layout: BaseLayout,
) -> None:
"""Loads the content of a block layout (block definition) into an existing layout
of the target document. This method cannot load the content of
modelspace or paperspace layouts.
Args:
block_layout: the source block layout
target_layout: target layout can be any layout: modelspace, paperspace
layout or block layout.
"""
if not isinstance(block_layout, BlockLayout):
raise LayoutError(f"invalid block layout type: {type(block_layout)}")
if not isinstance(target_layout, BaseLayout):
raise LayoutError(f"invalid target layout type: {type(target_layout)}")
if block_layout.doc is not self.sdoc:
raise LayoutError(
f"given block layout does not belong to the source document"
)
if target_layout.doc is not self.tdoc:
raise LayoutError(
f"given target layout does not belong to the target document"
)
self.add_command(LoadEntities(list(block_layout), target_layout))
def load_layers(self, names: Sequence[str]) -> None:
"""Loads the layers defined by the argument `names` into the target document.
In the case of a name conflict the conflict policy will be applied.
"""
entities = _get_table_entries(names, self.sdoc.layers)
self.add_command(LoadResources(entities))
def load_linetypes(self, names: Sequence[str]) -> None:
"""Loads the linetypes defined by the argument `names` into the target document.
In the case of a name conflict the conflict policy will be applied.
"""
entities = _get_table_entries(names, self.sdoc.linetypes)
self.add_command(LoadResources(entities))
def load_text_styles(self, names: Sequence[str]) -> None:
"""Loads the TEXT styles defined by the argument `names` into the target document.
In the case of a name conflict the conflict policy will be applied.
"""
entities = _get_table_entries(names, self.sdoc.styles)
self.add_command(LoadResources(entities))
def load_dim_styles(self, names: Sequence[str]) -> None:
"""Loads the DIMENSION styles defined by the argument `names` into the target
document. In the case of a name conflict the conflict policy will be applied.
"""
entities = _get_table_entries(names, self.sdoc.dimstyles)
self.add_command(LoadResources(entities))
def load_mline_styles(self, names: Sequence[str]) -> None:
"""Loads the MLINE styles defined by the argument `names` into the target
document. In the case of a name conflict the conflict policy will be applied.
"""
entities = _get_table_entries(names, self.sdoc.mline_styles)
self.add_command(LoadResources(entities))
def load_mleader_styles(self, names: Sequence[str]) -> None:
"""Loads the MULTILEADER styles defined by the argument `names` into the target
document. In the case of a name conflict the conflict policy will be applied.
"""
entities = _get_table_entries(names, self.sdoc.mleader_styles)
self.add_command(LoadResources(entities))
def load_materials(self, names: Sequence[str]) -> None:
"""Loads the MATERIALS defined by the argument `names` into the target
document. In the case of a name conflict the conflict policy will be applied.
"""
entities = _get_table_entries(names, self.sdoc.materials)
self.add_command(LoadResources(entities))
def execute(self, xref_prefix: str = "") -> None:
"""Execute all loading commands. The `xref_prefix` string is used as XREF name
when the conflict policy :attr:`ConflictPolicy.XREF_PREFIX` is applied.
"""
registry = _Registry(self.sdoc, self.tdoc)
debug = ezdxf.options.debug
for cmd in self._commands:
cmd.register_resources(registry)
if debug:
_log_debug_messages(registry.debug_messages)
cpm = CopyMachine(self.tdoc)
cpm.copy_blocks(registry.source_blocks)
transfer = _Transfer(
registry=registry,
copies=cpm.copies,
objects=cpm.objects,
handle_mapping=cpm.handle_mapping,
conflict_policy=self.conflict_policy,
copy_errors=cpm.copy_errors,
)
if xref_prefix:
transfer.xref_prefix = str(xref_prefix)
transfer.add_object_copies(cpm.objects)
transfer.register_classes(cpm.classes)
transfer.register_table_resources()
transfer.register_object_resources()
transfer.redirect_handle_mapping()
transfer.map_object_resources()
transfer.map_entity_resources()
transfer.copy_settings()
for cmd in self._commands:
cmd.execute(transfer)
transfer.finalize()
def _get_table_entries(names: Iterable[str], table) -> list[DXFEntity]:
entities: list[DXFEntity] = []
for name in names:
try:
entry = table.get(name)
if entry:
entities.append(entry)
except const.DXFTableEntryError:
pass
return entities
class _Registry:
def __init__(self, sdoc: Drawing, tdoc: Drawing) -> None:
self.source_doc = sdoc
self.target_doc = tdoc
# source_blocks:
# - key is the owner handle (layout key)
# - value is a dict(): key is source-entity-handle, value is source-entity
# Storing the entities to copy in a dict() guarantees that each entity is only
# copied once and a dict() preserves the order which a set() doesn't and
# that is nice for testing.
# - entry NO_BLOCK (layout key "0") contains table entries and DXF objects
self.source_blocks: dict[str, dict[str, DXFEntity]] = {NO_BLOCK: {}}
self.appids: set[str] = set()
self.debug_messages: list[str] = []
def debug(self, msg: str) -> None:
self.debug_messages.append(msg)
def add_entity(self, entity: DXFEntity, block_key: str = NO_BLOCK) -> None:
assert entity is not None, "internal error: entity is None"
block = self.source_blocks.setdefault(block_key, {})
entity_handle = entity.dxf.handle
if entity_handle in block:
return
block[entity_handle] = entity
entity.register_resources(self)
def add_block(self, block_record: BlockRecord) -> None:
# add resource entity BLOCK_RECORD to NO_BLOCK
self.add_entity(block_record)
# block content in block <block_key>
block_handle = block_record.dxf.handle
self.add_entity(block_record.block, block_handle) # type: ignore
for entity in block_record.entity_space:
self.add_entity(entity, block_handle)
self.add_entity(block_record.endblk, block_handle) # type: ignore
def add_handle(self, handle: Optional[str]) -> None:
"""Add resource by handle (table entry or object), cannot add graphic entities.
Raises:
EntityError: cannot add graphic entity
"""
if handle is None or handle == "0":
return
entity = self.source_doc.entitydb.get(handle)
if entity is None:
self.debug(f"source entity #{handle} does not exist")
return
if is_graphic_entity(entity):
raise EntityError(f"cannot add graphic entity: {str(entity)}")
self.add_entity(entity)
def add_layer(self, name: str) -> None:
if name == DEFAULT_LAYER:
# Layer name "0" gets never mangled and always exist in the target document.
return
try:
layer = self.source_doc.layers.get(name)
self.add_entity(layer)
except const.DXFTableEntryError:
self.debug(f"source layer '{name}' does not exist")
def add_linetype(self, name: str) -> None:
# These linetype names get never mangled and always exist in the target document.
if name.upper() in DEFAULT_LINETYPES:
return
try:
linetype = self.source_doc.linetypes.get(name)
self.add_entity(linetype)
except const.DXFTableEntryError:
self.debug(f"source linetype '{name}' does not exist")
def add_text_style(self, name) -> None:
try:
text_style = self.source_doc.styles.get(name)
self.add_entity(text_style)
except const.DXFTableEntryError:
self.debug(f"source text style '{name}' does not exist")
def add_dim_style(self, name: str) -> None:
try:
dim_style = self.source_doc.dimstyles.get(name)
self.add_entity(dim_style)
except const.DXFTableEntryError:
self.debug(f"source dimension style '{name}' does not exist")
def add_block_name(self, name: str) -> None:
try:
block_record = self.source_doc.block_records.get(name)
self.add_entity(block_record)
except const.DXFTableEntryError:
self.debug(f"source block '{name}' does not exist")
def add_appid(self, name: str) -> None:
self.appids.add(name.upper())
class _Transfer:
def __init__(
self,
registry: _Registry,
copies: dict[str, dict[str, DXFEntity]],
objects: dict[str, DXFEntity],
handle_mapping: dict[str, str],
*,
conflict_policy=ConflictPolicy.KEEP,
copy_errors: set[str],
) -> None:
self.registry = registry
# entry NO_BLOCK (layout key "0") contains table entries
self.copied_blocks = copies
self.copied_objects = objects
self.copy_errors = copy_errors
self.conflict_policy = conflict_policy
self.xref_prefix = get_xref_name(registry.source_doc)
self.layer_mapping: dict[str, str] = {}
self.linetype_mapping: dict[str, str] = {}
self.text_style_mapping: dict[str, str] = {}
self.dim_style_mapping: dict[str, str] = {}
self.block_name_mapping: dict[str, str] = {}
self.handle_mapping: dict[str, str] = handle_mapping
self._replace_handles: dict[str, str] = {}
self.debug_messages: list[str] = []
def debug(self, msg: str) -> None:
self.debug_messages.append(msg)
def get_handle(self, handle: str, default="0") -> str:
return self.handle_mapping.get(handle, default)
def get_reference_of_copy(self, handle: str) -> Optional[DXFEntity]:
handle_of_copy = self.handle_mapping.get(handle)
if handle_of_copy:
return self.registry.target_doc.entitydb.get(handle_of_copy)
return None
def get_layer(self, name: str) -> str:
return self.layer_mapping.get(name, name)
def get_linetype(self, name: str) -> str:
return self.linetype_mapping.get(name, name)
def get_text_style(self, name: str) -> str:
return self.text_style_mapping.get(name, name)
def get_dim_style(self, name: str) -> str:
return self.dim_style_mapping.get(name, name)
def get_block_name(self, name: str) -> str:
return self.block_name_mapping.get(name, name)
def get_entity_copy(self, entity: DXFEntity) -> Optional[DXFEntity]:
"""Returns the copy of graphic entities."""
try:
return self.copied_blocks[entity.dxf.owner][entity.dxf.handle]
except KeyError:
pass
return None
def map_resources_of_copy(self, entity: DXFEntity) -> None:
clone = self.get_entity_copy(entity)
if clone:
entity.map_resources(clone, self)
elif entity.dxf.handle in self.copy_errors:
pass
else:
raise InternalError(f"copy of {entity} not found")
def map_pointers(self, tags: Tags, new_owner_handle: str = "") -> None:
for index, tag in enumerate(tags):
if types.is_translatable_pointer(tag):
handle = self.get_handle(tag.value, default="0")
tags[index] = types.DXFTag(tag.code, handle)
if new_owner_handle and types.is_hard_owner(tag):
copied_object = self.registry.target_doc.entitydb.get(handle)
if copied_object is None:
continue
copied_object.dxf.owner = new_owner_handle
def map_acad_dict_entry(
self, dict_name: str, entry_name: str, entity: DXFEntity
) -> tuple[str, DXFEntity]:
"""Map and add `entity` to a top level ACAD dictionary `dict_name` in root
dictionary.
"""
tdoc = self.registry.target_doc
acad_dict = tdoc.rootdict.get_required_dict(dict_name)
existing_entry = acad_dict.get(entry_name)
# DICTIONARY entries are only renamed if they exist, this is different to TABLE
# entries:
if isinstance(existing_entry, DXFEntity):
if self.conflict_policy == ConflictPolicy.KEEP:
return entry_name, existing_entry
elif self.conflict_policy == ConflictPolicy.XREF_PREFIX:
entry_name = get_unique_dict_key(
entry_name, self.xref_prefix, acad_dict
)
elif self.conflict_policy == ConflictPolicy.NUM_PREFIX:
entry_name = get_unique_dict_key(entry_name, "", acad_dict)
loaded_entry = self.get_reference_of_copy(entity.dxf.handle)
if loaded_entry is None:
return "", entity
acad_dict.add(entry_name, loaded_entry) # type: ignore
loaded_entry.dxf.owner = acad_dict.dxf.handle
return entry_name, loaded_entry
def map_existing_handle(
self, source: DXFEntity, clone: DXFEntity, attrib_name: str, *, optional=False
) -> None:
"""Map handle attribute if the original handle exist and the references entity
was really copied.
"""
handle = source.dxf.get(attrib_name, "")
if not handle:
return # handle does not exist in source entity
new_handle = self.get_handle(handle)
if new_handle and new_handle != "0": # copied entity exist
clone.dxf.set(attrib_name, new_handle)
else:
if optional: # discard handle attribute
clone.dxf.discard(attrib_name)
else: # set mandatory attribute to null-ptr
clone.dxf.set(attrib_name, "0")
def register_table_resources(self) -> None:
"""Register copied table-entries in resource tables of the target document."""
self.register_appids()
# process copied table-entries, layout key is "0":
for source_entity_handle, entity in self.copied_blocks[NO_BLOCK].items():
if entity.dxf.owner is not None:
continue # already processed!
# add copied table-entries to tables in the target document
if isinstance(entity, Layer):
self.add_layer_entry(entity)
elif isinstance(entity, Linetype):
self.add_linetype_entry(entity)
elif isinstance(entity, Textstyle):
if entity.is_shape_file:
self.add_shape_file_entry(entity)
else:
self.add_text_style_entry(entity)
elif isinstance(entity, DimStyle):
self.add_dim_style_entry(entity)
elif isinstance(entity, BlockRecord):
self.add_block_record_entry(entity, source_entity_handle)
elif isinstance(entity, UCSTableEntry):
self.add_ucs_entry(entity)
def register_object_resources(self) -> None:
"""Register copied objects in object collections of the target document."""
# Note: BricsCAD does not rename conflicting entries in object collections and
# always keeps the existing entry:
# - MATERIAL
# - MLINESTYLE
# - MLEADERSTYLE
# Ezdxf does rename conflicting entries according to self.conflict_policy,
# exceptions are only the system entries.
tdoc = self.registry.target_doc
for _, entity in self.copied_objects.items():
if isinstance(entity, Material):
self.add_collection_entry(
tdoc.materials,
entity,
system_entries={"GLOBAL", "BYLAYER", "BYBLOCK"},
)
elif isinstance(entity, MLineStyle):
self.add_collection_entry(
tdoc.mline_styles,
entity,
system_entries={
STANDARD,
},
)
elif isinstance(entity, MLeaderStyle):
self.add_collection_entry(
tdoc.mleader_styles,
entity,
system_entries={
STANDARD,
},
)
elif isinstance(entity, VisualStyle):
self.add_visualstyle_entry(entity)
elif isinstance(entity, DXFLayout):
self.create_empty_paperspace_layout(entity)
# TODO:
# add GROUP to ACAD_GROUP dictionary
# add SCALE to ACAD_SCALELIST dictionary
# add TABLESTYLE to ACAD_TABLESTYLE dictionary
def replace_handle_mapping(self, old_target, new_target) -> None:
self._replace_handles[old_target] = new_target
def redirect_handle_mapping(self) -> None:
"""Redirect handle mapping from copied entity to a handle of an existing entity
in the target document.
"""
temp_mapping: dict[str, str] = {}
replace_handles = self._replace_handles
# redirect source entity -> new target entity
for source_handle, target_handle in self.handle_mapping.items():
if target_handle in replace_handles:
# build temp mapping, while iterating dict
temp_mapping[source_handle] = replace_handles[target_handle]
for source_handle, new_target_handle in temp_mapping.items():
self.handle_mapping[source_handle] = new_target_handle
def register_appids(self) -> None:
tdoc = self.registry.target_doc
for appid in self.registry.appids:
try:
tdoc.appids.new(appid)
except const.DXFTableEntryError:
pass
def register_classes(self, classes: Sequence[DXFClass]) -> None:
self.registry.target_doc.classes.register(classes)
def map_entity_resources(self) -> None:
source_db = self.registry.source_doc.entitydb
for block_key, block in self.copied_blocks.items():
for source_entity_handle, clone in block.items():
source_entity = source_db.get(source_entity_handle)
if source_entity is None:
raise InternalError("database error, source entity not found")
if clone is not None and clone.is_alive:
source_entity.map_resources(clone, self)
def map_object_resources(self) -> None:
source_db = self.registry.source_doc.entitydb
for source_object_handle, clone in self.copied_objects.items():
source_entity = source_db.get(source_object_handle)
if source_entity is None:
raise InternalError("database error, source object not found")
if clone is not None and clone.is_alive:
source_entity.map_resources(clone, self)
def add_layer_entry(self, layer: Layer) -> None:
tdoc = self.registry.target_doc
layer_name = layer.dxf.name.upper()
# special layers - only copy if not exist
if layer_name in ("0", "DEFPOINTS") or validator.is_adsk_special_layer(
layer_name
):
try:
special = tdoc.layers.get(layer_name)
except const.DXFTableEntryError:
special = None
if special:
# map copied layer handle to existing special layer
self.replace_handle_mapping(layer.dxf.handle, special.dxf.handle)
layer.destroy()
return
old_name = layer.dxf.name
self.add_table_entry(tdoc.layers, layer)
if layer.is_alive:
self.layer_mapping[old_name] = layer.dxf.name
def add_linetype_entry(self, linetype: Linetype) -> None:
tdoc = self.registry.target_doc
if linetype.dxf.name.upper() in DEFAULT_LINETYPES:
standard = tdoc.linetypes.get(linetype.dxf.name)
self.replace_handle_mapping(linetype.dxf.handle, standard.dxf.handle)
linetype.destroy()
return
old_name = linetype.dxf.name
self.add_table_entry(tdoc.linetypes, linetype)
if linetype.is_alive:
self.linetype_mapping[old_name] = linetype.dxf.name
def add_text_style_entry(self, text_style: Textstyle) -> None:
tdoc = self.registry.target_doc
old_name = text_style.dxf.name
self.add_table_entry(tdoc.styles, text_style)
if text_style.is_alive:
self.text_style_mapping[old_name] = text_style.dxf.name
def add_shape_file_entry(self, text_style: Textstyle) -> None:
# A shape file (SHX file) entry is a special text style entry which name is "".
shape_file_name = text_style.dxf.font
if not shape_file_name:
return
tdoc = self.registry.target_doc
shape_file = tdoc.styles.find_shx(shape_file_name)
if shape_file is None:
shape_file = tdoc.styles.add_shx(shape_file_name)
self.replace_handle_mapping(text_style.dxf.handle, shape_file.dxf.handle)
def add_dim_style_entry(self, dim_style: DimStyle) -> None:
tdoc = self.registry.target_doc
old_name = dim_style.dxf.name
self.add_table_entry(tdoc.dimstyles, dim_style)
if dim_style.is_alive:
self.dim_style_mapping[old_name] = dim_style.dxf.name
def add_block_record_entry(self, block_record: BlockRecord, handle: str) -> None:
tdoc = self.registry.target_doc
block_name = block_record.dxf.name.upper()
old_name = block_record.dxf.name
# is anonymous block name?
if len(block_name) > 1 and block_name[0] == "*":
# anonymous block names are always translated to another non-existing
# anonymous block name in the target document of the same type:
# e.g. "*D01" -> "*D0815"
block_record.dxf.name = tdoc.blocks.anonymous_block_name(block_name[1])
tdoc.block_records.add_entry(block_record)
else:
# Standard arrow blocks are handled the same way as every other block,
# tested with BricsCAD V23:
# e.g. arrow block "_Dot" is loaded as "<xref-name>$0$_Dot"
self.add_table_entry(tdoc.block_records, block_record)
if block_record.is_alive:
self.block_name_mapping[old_name] = block_record.dxf.name
self.restore_block_content(block_record, handle)
tdoc.blocks.add(block_record) # create BlockLayout
def restore_block_content(self, block_record: BlockRecord, handle: str) -> None:
content = self.copied_blocks.get(handle, dict())
block: Optional[Block] = None
endblk: Optional[EndBlk] = None
for entity in content.values():
if isinstance(entity, (Block, EndBlk)):
if isinstance(entity, Block):
block = entity
else:
endblk = entity
elif is_graphic_entity(entity):
block_record.add_entity(entity) # type: ignore
else:
name = block_record.dxf.name
msg = f"skipping non-graphic DXF entity in BLOCK_RECORD('{name}', #{handle}): {str(entity)}"
logging.warning(msg) # this is a DXF structure error
self.debug(msg)
if isinstance(block, Block) and isinstance(endblk, EndBlk):
block_record.set_block(block, endblk)
else:
raise InternalError("invalid BLOCK_RECORD copy")
def add_ucs_entry(self, ucs: UCSTableEntry) -> None:
# name mapping is not supported for UCS table entries
tdoc = self.registry.target_doc
self.add_table_entry(tdoc.ucs, ucs)
def add_table_entry(self, table, entity: DXFEntity) -> None:
name = entity.dxf.name
if self.conflict_policy == ConflictPolicy.KEEP:
if table.has_entry(name):
existing_entry = table.get(name)
self.replace_handle_mapping(
entity.dxf.handle, existing_entry.dxf.handle
)
entity.destroy()
return
elif self.conflict_policy == ConflictPolicy.XREF_PREFIX:
# always rename
entity.dxf.name = get_unique_table_name(name, self.xref_prefix, table)
elif self.conflict_policy == ConflictPolicy.NUM_PREFIX:
if table.has_entry(name): # rename only if exist
entity.dxf.name = get_unique_table_name(name, "", table)
table.add_entry(entity)
def add_collection_entry(
self, collection, entry: DXFEntity, system_entries: set[str]
) -> None:
# Note: BricsCAD does not rename conflicting entries in object collections and
# always keeps the existing entry:
# - MATERIAL
# - MLINESTYLE
# - MLEADERSTYLE
# Ezdxf does rename conflicting entries according to self.conflict_policy,
# exceptions are only the given `system_entries`.
name = entry.dxf.name
if name.upper() in system_entries:
special = collection.object_dict.get(name)
if special:
self.replace_handle_mapping(entry.dxf.handle, special.dxf.handle)
entry.destroy()
return
if self.conflict_policy == ConflictPolicy.KEEP:
existing_entry = collection.get(name)
if existing_entry:
self.replace_handle_mapping(entry.dxf.handle, existing_entry.dxf.handle)
entry.destroy()
return
elif self.conflict_policy == ConflictPolicy.XREF_PREFIX:
# always rename
entry.dxf.name = get_unique_table_name(name, self.xref_prefix, collection)
elif self.conflict_policy == ConflictPolicy.NUM_PREFIX:
if collection.has_entry(name): # rename only if exist
entry.dxf.name = get_unique_table_name(name, "", collection)
collection.object_dict.add(entry.dxf.name, entry)
# a resource collection is hard owner
entry.dxf.owner = collection.handle
def add_visualstyle_entry(self, visualstyle: VisualStyle) -> None:
visualstyle_dict = self.registry.target_doc.rootdict.get_required_dict(
"ACAD_VISUALSTYLE"
)
name = visualstyle.dxf.description
existing_entry = visualstyle_dict.get(name)
if existing_entry: # keep existing
self.replace_handle_mapping(
visualstyle.dxf.handle, existing_entry.dxf.handle
)
visualstyle.destroy()
else: # add new entry; rename policy is not supported
visualstyle_dict.take_ownership(name, visualstyle)
def create_empty_paperspace_layout(self, layout: DXFLayout) -> None:
tdoc = self.registry.target_doc
# The layout content will not be copied automatically!
# create new empty block layout:
block_name = tdoc.layouts.unique_paperspace_name()
block_layout = tdoc.blocks.new(block_name)
# link block layout and layout entity:
layout.dxf.block_record_handle = block_layout.block_record_handle
block_layout.block_record.dxf.layout = layout.dxf.handle
paperspace = Paperspace(layout, tdoc)
tdoc.layouts.append_layout(paperspace)
def add_object_copies(self, copies: dict[str, DXFEntity]) -> None:
"""Add copied DXF objects to the OBJECTS section of the target document."""
objects = self.registry.target_doc.objects
for _, obj in copies.items():
if obj and obj.is_alive:
objects.add_object(obj) # type: ignore
def copy_settings(self):
self.copy_raster_vars()
self.copy_wipeout_vars()
def copy_raster_vars(self):
sdoc = self.registry.source_doc
tdoc = self.registry.target_doc
if (
"ACAD_IMAGE_VARS" not in sdoc.rootdict # not required
or "ACAD_IMAGE_VARS" in tdoc.rootdict # do not replace existing values
):
return
frame, quality, units = sdoc.objects.get_raster_variables()
tdoc.objects.set_raster_variables(frame=frame, quality=quality, units=units)
def copy_wipeout_vars(self):
sdoc = self.registry.source_doc
tdoc = self.registry.target_doc
if (
"ACAD_WIPEOUT_VARS" not in sdoc.rootdict # not required
or "ACAD_WIPEOUT_VARS" in tdoc.rootdict # do not replace existing values
):
return
tdoc.objects.set_wipeout_variables(
frame=sdoc.objects.get_wipeout_frame_setting()
)
def finalize(self) -> None:
# remove replaced entities:
self.registry.target_doc.entitydb.purge()
for msg in self.debug_messages:
logger.log(logging.INFO, msg)
def get_xref_name(doc: Drawing) -> str:
if doc.filename:
return pathlib.Path(doc.filename).stem
return ""
def is_anonymous_block_name(name: str) -> bool:
return len(name) > 1 and name.startswith("*")
def get_unique_table_name(name: str, xref: str, table) -> str:
index: int = 0
while True:
new_name = f"{xref}${index}${name}"
if not table.has_entry(new_name):
return new_name
index += 1
def get_unique_dict_key(key: str, xref: str, dictionary) -> str:
index: int = 0
while True:
new_key = f"{xref}${index}${key}"
if new_key not in dictionary:
return new_key
index += 1
class CopyMachine:
def __init__(self, tdoc: Drawing) -> None:
self.target_doc = tdoc
self.copies: dict[str, dict[str, DXFEntity]] = {}
self.classes: list[DXFClass] = []
self.objects: dict[str, DXFEntity] = {}
self.copy_errors: set[str] = set()
self.copy_strategy = CopyStrategy(CopySettings(set_source_of_copy=False))
# mapping from the source entity handle to the handle of the copied entity
self.handle_mapping: dict[str, str] = {}
def copy_blocks(self, blocks: dict[str, dict[str, DXFEntity]]) -> None:
for handle, block in blocks.items():
self.copies[handle] = self.copy_block(block)
def copy_block(self, block: dict[str, DXFEntity]) -> dict[str, DXFEntity]:
copies: dict[str, DXFEntity] = {}
tdoc = self.target_doc
handle_mapping = self.handle_mapping
for handle, entity in block.items():
if isinstance(entity, DXFClass):
self.copy_dxf_class(entity)
continue
clone = self.copy_entity(entity)
if clone is None:
continue
factory.bind(clone, tdoc)
handle_mapping[handle] = clone.dxf.handle
# Get handle mapping for in-object copies: DICTIONARY
if hasattr(entity, "get_handle_mapping"):
self.handle_mapping.update(entity.get_handle_mapping(clone))
if is_dxf_object(clone):
self.objects[handle] = clone
else:
copies[handle] = clone
return copies
def copy_entity(self, entity: DXFEntity) -> Optional[DXFEntity]:
try:
return entity.copy(copy_strategy=self.copy_strategy)
except const.DXFError:
self.copy_errors.add(entity.dxf.handle)
return None
def copy_dxf_class(self, cls: DXFClass) -> None:
self.classes.append(cls.copy(copy_strategy=self.copy_strategy))