1665 lines
63 KiB
Python
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))
|