/
xref.py
1664 lines (1401 loc) · 62.6 KB
/
xref.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# 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)