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

385 lines
15 KiB
Python

# Copyright (c) 2011-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterator, cast, Optional
import logging
from ezdxf.lldxf.const import DXFKeyError, DXFValueError, DXFInternalEzdxfError
from ezdxf.lldxf.const import (
MODEL_SPACE_R2000,
PAPER_SPACE_R2000,
TMP_PAPER_SPACE_NAME,
)
from ezdxf.lldxf.validator import is_valid_table_name
from .layout import Layout, Modelspace, Paperspace
from ezdxf.entities import DXFEntity, BlockRecord
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.document import Drawing
from ezdxf.entities import Dictionary, DXFLayout
logger = logging.getLogger("ezdxf")
def key(name: str) -> str:
"""AutoCAD uses case insensitive layout names, but stores the name case
sensitive."""
return name.upper()
MODEL = key("Model")
class Layouts:
def __init__(self, doc: Drawing):
"""Default constructor. (internal API)"""
self.doc = doc
# Store layout names in normalized form: key(name)
self._layouts: dict[str, Layout] = {}
# key: layout name as original case-sensitive string; value: DXFLayout()
self._dxf_layouts: Dictionary = cast(
"Dictionary", self.doc.rootdict["ACAD_LAYOUT"]
)
@classmethod
def setup(cls, doc: Drawing):
"""Constructor from scratch. (internal API)"""
layouts = Layouts(doc)
layouts.setup_modelspace()
layouts.setup_paperspace()
return layouts
def __len__(self) -> int:
"""Returns count of existing layouts, including the modelspace
layout."""
return len(self._layouts)
def __contains__(self, name: str) -> bool:
"""Returns ``True`` if layout `name` exist."""
assert isinstance(name, str), type(str)
return key(name) in self._layouts
def __iter__(self) -> Iterator[Layout]:
"""Returns iterable of all layouts as :class:`~ezdxf.layouts.Layout`
objects, including the modelspace layout.
"""
return iter(self._layouts.values())
def _add_layout(self, name: str, layout: Layout):
dxf_layout = layout.dxf_layout
dxf_layout.dxf.name = name
dxf_layout.dxf.owner = self._dxf_layouts.dxf.handle
self._layouts[key(name)] = layout
self._dxf_layouts[name] = dxf_layout
def _discard(self, layout: Layout):
name = layout.name
self._dxf_layouts.discard(name)
del self._layouts[key(name)]
def append_layout(self, layout: Layout) -> None:
"""Append an existing (copied) paperspace layout as last layout tab."""
index = 1
base_layout_name = layout.dxf.name
layout_name = base_layout_name
while layout_name in self:
index += 1
layout_name = base_layout_name + f" ({index})"
layout.dxf.taborder = len(self._layouts) + 1
self._add_layout(layout_name, layout)
def setup_modelspace(self):
"""Modelspace setup. (internal API)"""
self._new_special(
Modelspace, "Model", MODEL_SPACE_R2000, dxfattribs={"taborder": 0}
)
def setup_paperspace(self):
"""First layout setup. (internal API)"""
self._new_special(
Paperspace, "Layout1", PAPER_SPACE_R2000, dxfattribs={"taborder": 1}
)
def _new_special(self, cls, name: str, block_name: str, dxfattribs: dict) -> Layout:
if name in self._layouts:
raise DXFValueError(f'Layout "{name}" already exists')
dxfattribs["owner"] = self._dxf_layouts.dxf.handle
layout = cls.new(name, block_name, self.doc, dxfattribs=dxfattribs)
self._add_layout(name, layout)
return layout
def unique_paperspace_name(self) -> str:
"""Returns a unique paperspace name. (internal API)"""
blocks = self.doc.blocks
count = 0
while "*Paper_Space%d" % count in blocks:
count += 1
return "*Paper_Space%d" % count
def new(self, name: str, dxfattribs=None) -> Paperspace:
"""Returns a new :class:`~ezdxf.layouts.Paperspace` layout.
Args:
name: layout name as shown in tabs in :term:`CAD` applications
dxfattribs: additional DXF attributes for the
:class:`~ezdxf.entities.layout.DXFLayout` entity
Raises:
DXFValueError: Invalid characters in layout name.
DXFValueError: Layout `name` already exist.
"""
assert isinstance(name, str), type(str)
if not is_valid_table_name(name):
raise DXFValueError("Layout name contains invalid characters.")
if name in self:
raise DXFValueError(f'Layout "{name}" already exist.')
dxfattribs = dict(dxfattribs or {}) # copy attribs
dxfattribs["owner"] = self._dxf_layouts.dxf.handle
dxfattribs.setdefault("taborder", len(self._layouts) + 1)
block_name = self.unique_paperspace_name()
layout = Paperspace.new(name, block_name, self.doc, dxfattribs=dxfattribs)
# Default extents are ok!
# Reset limits to (0, 0) and (paper width, paper height)
layout.reset_limits()
self._add_layout(name, layout)
return layout # type: ignore
@classmethod
def load(cls, doc: "Drawing") -> "Layouts":
"""Constructor if loading from file. (internal API)"""
layouts = cls(doc)
layouts.setup_from_rootdict()
# DXF R12: block/block_record for *Model_Space and *Paper_Space
# already exist:
if len(layouts) < 2: # restore missing DXF Layouts
layouts.restore("Model", MODEL_SPACE_R2000, taborder=0)
layouts.restore("Layout1", PAPER_SPACE_R2000, taborder=1)
return layouts
def restore(self, name: str, block_record_name: str, taborder: int) -> None:
"""Restore layout from block if DXFLayout does not exist.
(internal API)"""
if name in self:
return
block_layout = self.doc.blocks.get(block_record_name)
self._new_from_block_layout(name, block_layout, taborder)
def _new_from_block_layout(self, name, block_layout, taborder: int) -> "Layout":
dxfattribs = {
"owner": self._dxf_layouts.dxf.handle,
"name": name,
"block_record_handle": block_layout.block_record_handle,
"taborder": taborder,
}
dxf_layout = cast(
"DXFLayout",
self.doc.objects.new_entity("LAYOUT", dxfattribs=dxfattribs),
)
if key(name) == MODEL:
layout = Modelspace.load(dxf_layout, self.doc)
else:
layout = Paperspace.load(dxf_layout, self.doc)
self._add_layout(name, layout)
return layout
def setup_from_rootdict(self) -> None:
"""Setup layout manager from root dictionary. (internal API)"""
layout: Layout
for name, dxf_layout in self._dxf_layouts.items():
if isinstance(dxf_layout, str):
logger.debug(f"ignore missing LAYOUT(#{dxf_layout}) entity '{name}'")
continue
if key(name) == MODEL:
layout = Modelspace(dxf_layout, self.doc)
else:
layout = Paperspace(dxf_layout, self.doc)
# assert name == layout.dxf.name
self._layouts[key(name)] = layout
def modelspace(self) -> Modelspace:
"""Returns the :class:`~ezdxf.layouts.Modelspace` layout."""
return cast(Modelspace, self.get("Model"))
def names(self) -> list[str]:
"""Returns a list of all layout names, all names in original case
sensitive form."""
return [layout.name for layout in self._layouts.values()]
def get(self, name: Optional[str]) -> Layout:
"""Returns :class:`~ezdxf.layouts.Layout` by `name`, case insensitive
"Model" == "MODEL".
Args:
name: layout name as shown in tab, e.g. ``'Model'`` for modelspace
"""
name = name or self.names_in_taborder()[1] # first paperspace layout
return self._layouts[key(name)]
def rename(self, old_name: str, new_name: str) -> None:
"""Rename a layout from `old_name` to `new_name`.
Can not rename layout ``'Model'`` and the new name of a layout must
not exist.
Args:
old_name: actual layout name, case insensitive
new_name: new layout name, case insensitive
Raises:
DXFValueError: try to rename ``'Model'``
DXFValueError: Layout `new_name` already exist.
"""
assert isinstance(old_name, str), type(old_name)
assert isinstance(new_name, str), type(new_name)
if key(old_name) == MODEL:
raise DXFValueError("Can not rename model space.")
if new_name in self:
raise DXFValueError(f'Layout "{new_name}" already exist.')
if old_name not in self:
raise DXFValueError(f'Layout "{old_name}" does not exist.')
layout = self.get(old_name)
self._discard(layout)
layout.rename(new_name)
self._add_layout(new_name, layout)
def names_in_taborder(self) -> list[str]:
"""Returns all layout names in tab order as shown in :term:`CAD`
applications."""
names = [
(layout.dxf.taborder, layout.name) for layout in self._layouts.values()
]
return [name for order, name in sorted(names)]
def get_layout_for_entity(self, entity: DXFEntity) -> Layout:
"""Returns the owner layout for a DXF `entity`."""
owner = entity.dxf.owner
if owner is None:
raise DXFKeyError("No associated layout, owner is None.")
return self.get_layout_by_key(entity.dxf.owner)
def get_layout_by_key(self, layout_key: str) -> Layout:
"""Returns a layout by its `layout_key`. (internal API)"""
assert isinstance(layout_key, str), type(layout_key)
error_msg = f'Layout with key "{layout_key}" does not exist.'
try:
block_record = self.doc.entitydb[layout_key]
except KeyError:
raise DXFKeyError(error_msg)
if not isinstance(block_record, BlockRecord):
# VERTEX, ATTRIB, SEQEND are owned by the parent entity
raise DXFKeyError(error_msg)
try:
dxf_layout = self.doc.entitydb[block_record.dxf.layout]
except KeyError:
raise DXFKeyError(error_msg)
return self.get(dxf_layout.dxf.name)
def get_active_layout_key(self):
"""Returns layout kay for the active paperspace layout.
(internal API)"""
active_layout_block_record = self.doc.block_records.get(PAPER_SPACE_R2000)
return active_layout_block_record.dxf.handle
def set_active_layout(self, name: str) -> None:
"""Set layout `name` as active paperspace layout."""
assert isinstance(name, str), type(name)
if key(name) == MODEL: # reserved layout name
raise DXFValueError("Can not set model space as active layout")
# raises KeyError if layout 'name' does not exist
new_active_layout = self.get(name)
old_active_layout_key = self.get_active_layout_key()
if old_active_layout_key == new_active_layout.layout_key:
return # layout 'name' is already the active layout
blocks = self.doc.blocks
new_active_paper_space_name = new_active_layout.block_record_name
blocks.rename_block(PAPER_SPACE_R2000, TMP_PAPER_SPACE_NAME)
blocks.rename_block(new_active_paper_space_name, PAPER_SPACE_R2000)
blocks.rename_block(TMP_PAPER_SPACE_NAME, new_active_paper_space_name)
def delete(self, name: str) -> None:
"""Delete layout `name` and destroy all entities in that layout.
Args:
name (str): layout name as shown in tabs
Raises:
DXFKeyError: if layout `name` do not exists
DXFValueError: deleting modelspace layout is not possible
DXFValueError: deleting last paperspace layout is not possible
"""
assert isinstance(name, str), type(name)
if key(name) == MODEL:
raise DXFValueError("Can not delete modelspace layout.")
layout = self.get(name)
if len(self) < 3:
raise DXFValueError("Can not delete last paperspace layout.")
if layout.layout_key == self.get_active_layout_key():
# Layout `name` is the active layout:
for layout_name in self._layouts:
# Set any other paperspace layout as active layout
if layout_name not in (key(name), MODEL):
self.set_active_layout(layout_name)
break
self._discard(layout)
layout.destroy()
def active_layout(self) -> Paperspace:
"""Returns the active paperspace layout."""
for layout in self:
if layout.is_active_paperspace:
return cast(Paperspace, layout)
raise DXFInternalEzdxfError("No active paperspace layout found.")
def audit(self, auditor: Auditor):
from ezdxf.audit import AuditError
doc = auditor.doc
# Find/remove orphaned LAYOUT objects:
layouts = (o for o in doc.objects if o.dxftype() == "LAYOUT")
for layout in layouts:
name = layout.dxf.get("name")
if name not in self:
auditor.fixed_error(
code=AuditError.ORPHANED_LAYOUT_ENTITY,
message=f'Removed orphaned {str(layout)} "{name}"',
)
doc.objects.delete_entity(layout)
# Find/remove orphaned paperspace BLOCK_RECORDS named: *Paper_Space...
psp_br_handles = {
br.dxf.handle
for br in doc.block_records
if br.dxf.name.lower().startswith("*paper_space")
}
psp_layout_br_handles = {
layout.dxf.block_record_handle
for layout in self._layouts.values()
if key(layout.name) != MODEL
}
mismatch = psp_br_handles.difference(psp_layout_br_handles)
if len(mismatch):
for handle in mismatch:
br = doc.entitydb.get(handle)
name = br.dxf.get("name") # type: ignore
auditor.fixed_error(
code=AuditError.ORPHANED_PAPER_SPACE_BLOCK_RECORD_ENTITY,
message=f'Removed orphaned layout {str(br)} "{name}"',
)
if name in doc.blocks:
doc.blocks.delete_block(name)
else:
doc.block_records.remove(name)
# Does not check the LAYOUT content this is done in the BlockSection,
# because the content of layouts is stored as blocks.