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

863 lines
31 KiB
Python

# Copyright (c) 2019-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Union,
cast,
Optional,
)
import logging
from ezdxf.math import Vec2, UVec
from ezdxf.entitydb import EntitySpace
from ezdxf.lldxf import const
from ezdxf.lldxf.validator import make_table_key
from .base import BaseLayout
if TYPE_CHECKING:
from ezdxf.document import Drawing
from ezdxf.layouts import BlockLayout
from ezdxf.entities import GeoData, Viewport, DXFLayout, DXFGraphic, BlockRecord
logger = logging.getLogger("ezdxf")
def get_block_entity_space(doc: Drawing, block_record_handle: str) -> EntitySpace:
block_record = doc.entitydb[block_record_handle]
return block_record.entity_space # type: ignore
class Layout(BaseLayout):
"""
Layout representation - base class for :class:`Modelspace` and
:class:`Paperspace`
Every layout consist of a LAYOUT entity in the OBJECTS section, an
associated BLOCK in the BLOCKS section and a BLOCK_RECORD_TABLE entry.
layout_key: handle of the BLOCK_RECORD, every layout entity has this
handle as owner attribute (entity.dxf.owner)
There are 3 different layout types:
1. :class:`Modelspace`
2. active :class:`Paperspace` layout
3. inactive :class:`Paperspace` layout
Internal Structure
For every layout exist a :class:`BlockLayout` object in
:class:`BlocksSection` and a :class:`Layout` object
(as :class:`Modelspace` or :class:`Paperspace`) in :class:`Layouts`.
The entity space of the :class:`BlockLayout` object and the entity space
of the :class:`Layout` object are the same object.
"""
# plot_layout_flags of LAYOUT entity
PLOT_VIEWPORT_BORDERS = 1
SHOW_PLOT_STYLES = 2
PLOT_CENTERED = 4
PLOT_HIDDEN = 8
USE_STANDARD_SCALE = 16
PLOT_PLOTSTYLES = 32
SCALE_LINEWEIGHTS = 64
PRINT_LINEWEIGHTS = 128
DRAW_VIEWPORTS_FIRST = 512
MODEL_TYPE = 1024
UPDATE_PAPER = 2048
ZOOM_TO_PAPER_ON_UPDATE = 4096
INITIALIZING = 8192
PREV_PLOT_INIT = 16384
def __init__(self, layout: DXFLayout, doc: Drawing):
self.dxf_layout = layout
handle = layout.dxf.get("block_record_handle", "0")
try:
block_record = doc.entitydb[handle]
except KeyError:
block_record = _find_layout_block_record(layout) # type: ignore
if block_record is None:
raise const.DXFStructureError(
f"required BLOCK_RECORD #{handle} for layout '{layout.dxf.name}' "
f"does not exist"
)
if block_record.dxftype() != "BLOCK_RECORD":
raise const.DXFStructureError(
f"expected BLOCK_RECORD(#{handle}) for layout '{layout.dxf.name}' "
f"has invalid entity type: {block_record.dxftype()}"
)
# link is maybe broken
block_record.dxf.layout = layout.dxf.handle
super().__init__(block_record) # type: ignore
@classmethod
def new(cls, name: str, block_name: str, doc: Drawing, dxfattribs=None) -> Layout:
"""Returns the required structures for a new layout:
- a :class:`BlockLayout` with BLOCK_RECORD, BLOCK and ENDBLK entities
- LAYOUT entity in the objects section
Args:
name: layout name as shown in tabs of CAD applications e.g. 'Layout2'
block_name: layout block name e.g. '*Paper_Space2'
doc: drawing document
dxfattribs: additional DXF attributes for LAYOUT entity
(internal API)
"""
block_layout: BlockLayout = doc.blocks.new(block_name)
dxfattribs = dxfattribs or {}
dxfattribs.update(
{
"name": name,
"block_record_handle": block_layout.block_record_handle,
}
)
dxf_layout = doc.objects.new_entity("LAYOUT", dxfattribs=dxfattribs)
return cls(dxf_layout, doc) # type: ignore
@classmethod
def load(cls, layout: DXFLayout, doc: Drawing):
"""Loading interface. (internal API)"""
_layout = cls(layout, doc)
_layout._repair_owner_tags()
return _layout
@property
def name(self) -> str:
"""Layout name as shown in tabs of :term:`CAD` applications."""
return self.dxf_layout.dxf.name
@property # dynamic DXF attribute dispatching, e.g. DXFLayout.dxf.layout_flags
def dxf(self) -> Any:
"""Returns the DXF name space attribute of the associated
:class:`~ezdxf.entities.DXFLayout` object.
This enables direct access to the underlying LAYOUT entity,
e.g. ``Layout.dxf.layout_flags``
"""
return self.dxf_layout.dxf
@property
def block_record_name(self) -> str:
"""Returns the name of the associated BLOCK_RECORD as string."""
return self.block_record.dxf.name
def _repair_owner_tags(self) -> None:
"""Set `owner` and `paperspace` attributes of entities hosted by this
layout to correct values.
"""
layout_key = self.layout_key
paperspace = 0 if self.is_modelspace else 1
for entity in self:
if entity.dxf.owner != layout_key:
entity.dxf.owner = layout_key
if entity.dxf.paperspace != paperspace:
entity.dxf.paperspace = paperspace
def __contains__(self, entity: Union[DXFGraphic, str]) -> bool:
"""Returns ``True`` if `entity` is stored in this layout.
Args:
entity: :class:`DXFGraphic` object or handle as hex string
"""
if isinstance(entity, str): # entity is a handle string
entity = self.entitydb[entity] # type: ignore
return entity.dxf.owner == self.layout_key # type: ignore
def destroy(self) -> None:
"""Delete all entities and the layout itself from entity database and
all linked structures.
(internal API)
"""
self.doc.objects.delete_entity(self.dxf_layout)
# super() deletes block_record and associated entity space
super().destroy()
@property
def plot_layout_flags(self) -> int:
return self.dxf_layout.dxf.plot_layout_flags
def reset_extents(
self, extmin=(+1e20, +1e20, +1e20), extmax=(-1e20, -1e20, -1e20)
) -> None:
"""Reset `extents`_ to given values or the AutoCAD default values.
"Drawing extents are the bounds of the area occupied by objects."
(Quote Autodesk Knowledge Network)
Args:
extmin: minimum extents or (+1e20, +1e20, +1e20) as default value
extmax: maximum extents or (-1e20, -1e20, -1e20) as default value
"""
dxf = self.dxf_layout.dxf
dxf.extmin = extmin
dxf.extmax = extmax
def reset_limits(self, limmin=None, limmax=None) -> None:
"""Reset `limits`_ to given values or the AutoCAD default values.
"Sets an invisible rectangular boundary in the drawing area that can
limit the grid display and limit clicking or entering point locations."
(Quote Autodesk Knowledge Network)
The :class:`Paperspace` class has an additional method
:meth:`~Paperspace.reset_paper_limits` to deduce the default limits from
the paper size settings.
Args:
limmin: minimum limits or (0, 0) as default
limmax: maximum limits or (paper width, paper height) as default value
"""
dxf = self.dxf_layout.dxf
if limmin is None:
limmin = (0, 0)
if limmax is None:
limmax = (dxf.paper_width, dxf.paper_height)
dxf.limmin = limmin
dxf.limmax = limmax
def set_plot_type(self, value: int = 5) -> None:
"""
=== ============================================================
0 last screen display
1 drawing extents
2 drawing limits
3 view specific (defined by :attr:`Layout.dxf.plot_view_name`)
4 window specific (defined by :meth:`Layout.set_plot_window_limits`)
5 layout information (default)
=== ============================================================
Args:
value: plot type
Raises:
DXFValueError: for `value` out of range
"""
if 0 <= int(value) <= 5:
self.dxf.plot_type = value
else:
raise const.DXFValueError("Plot type value out of range (0-5).")
def set_plot_style(self, name: str = "ezdxf.ctb", show: bool = False) -> None:
"""Set plot style file of type `.ctb`.
Args:
name: plot style filename
show: show plot style effect in preview? (AutoCAD specific attribute)
"""
self.dxf_layout.dxf.current_style_sheet = name
self.use_plot_styles(True)
self.show_plot_styles(show)
def get_plot_style_filename(self) -> str:
return self.dxf_layout.dxf.current_style_sheet
def set_plot_window(
self,
lower_left: tuple[float, float] = (0, 0),
upper_right: tuple[float, float] = (0, 0),
) -> None:
"""Set plot window size in (scaled) paper space units.
Args:
lower_left: lower left corner as 2D point
upper_right: upper right corner as 2D point
"""
x1, y1 = lower_left
x2, y2 = upper_right
dxf = self.dxf_layout.dxf
dxf.plot_window_x1 = x1
dxf.plot_window_y1 = y1
dxf.plot_window_x2 = x2
dxf.plot_window_y2 = y2
self.set_plot_type(4)
def get_plot_unit_scale_factor(self) -> float:
denom = self.dxf.scale_denominator
numerator = self.dxf.scale_numerator
scale = 1.0
if denom:
scale = numerator / denom
if self.dxf.plot_paper_units == 1:
return scale # mm
else:
return scale * 25.4 # inch
# plot layout flags setter
def plot_viewport_borders(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_VIEWPORT_BORDERS, state)
def show_plot_styles(self, state: bool = True) -> None:
self.set_plot_flags(self.SHOW_PLOT_STYLES, state)
def plot_centered(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_CENTERED, state)
def plot_hidden(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_HIDDEN, state)
def use_standard_scale(self, state: bool = True) -> None:
self.set_plot_flags(self.USE_STANDARD_SCALE, state)
def use_plot_styles(self, state: bool = True) -> None:
self.set_plot_flags(self.PLOT_PLOTSTYLES, state)
def scale_lineweights(self, state: bool = True) -> None:
self.set_plot_flags(self.SCALE_LINEWEIGHTS, state)
def print_lineweights(self, state: bool = True) -> None:
self.set_plot_flags(self.PRINT_LINEWEIGHTS, state)
def draw_viewports_first(self, state: bool = True) -> None:
self.set_plot_flags(self.DRAW_VIEWPORTS_FIRST, state)
def model_type(self, state: bool = True) -> None:
self.set_plot_flags(self.MODEL_TYPE, state)
def update_paper(self, state: bool = True) -> None:
self.set_plot_flags(self.UPDATE_PAPER, state)
def zoom_to_paper_on_update(self, state: bool = True) -> None:
self.set_plot_flags(self.ZOOM_TO_PAPER_ON_UPDATE, state)
def plot_flags_initializing(self, state: bool = True) -> None:
self.set_plot_flags(self.INITIALIZING, state)
def prev_plot_init(self, state: bool = True) -> None:
self.set_plot_flags(self.PREV_PLOT_INIT, state)
def set_plot_flags(self, flag, state: bool = True) -> None:
self.dxf_layout.set_flag_state(flag, state=state, name="plot_layout_flags")
class Modelspace(Layout):
""":class:`Modelspace` - not deletable, all entities of this layout are
stored in the ENTITIES section of the DXF file, the associated
"*Model_Space" block is empty, block name is fixed as "*Model_Space",
the name is fixed as "Model".
"""
@property
def name(self) -> str:
"""Name of modelspace is fixed as "Model"."""
return "Model"
def new_geodata(self, dxfattribs=None) -> GeoData:
"""Creates a new :class:`GeoData` entity and replaces existing ones.
The GEODATA entity resides in the OBJECTS section and not in the
modelspace, it is linked to the modelspace by an
:class:`~ezdxf.entities.ExtensionDict` located in BLOCK_RECORD of the
modelspace.
The GEODATA entity requires DXF R2010. The DXF reference does not
document if other layouts than the modelspace supports geo referencing,
so I assume getting/setting geo data may only make sense for the
modelspace.
Args:
dxfattribs: DXF attributes for :class:`~ezdxf.entities.GeoData` entity
"""
if self.doc.dxfversion < const.DXF2010:
raise const.DXFValueError("GEODATA entity requires DXF R2010 or later.")
if dxfattribs is None:
dxfattribs = {}
xdict = self.get_extension_dict()
geodata = self.doc.objects.add_geodata(
owner=xdict.dictionary.dxf.handle,
dxfattribs=dxfattribs,
)
xdict["ACAD_GEOGRAPHICDATA"] = geodata
return geodata
def get_geodata(self) -> Optional[GeoData]:
"""Returns the :class:`~ezdxf.entities.GeoData` entity associated to
the modelspace or ``None``.
"""
try:
xdict = self.block_record.get_extension_dict()
except AttributeError:
return None
try:
return xdict["ACAD_GEOGRAPHICDATA"]
except const.DXFKeyError:
return None
class Paperspace(Layout):
"""There are two kind of paperspace layouts:
1. Active Layout - all entities of this layout are stored in the ENTITIES
section, the associated "*Paper_Space" block is empty, block name
"*Paper_Space" is mandatory and also marks the active layout, the layout
name can be an arbitrary string.
2. Inactive Layout - all entities of this layouts are stored in the
associated BLOCK called "*Paper_SpaceN", where "N" is an arbitrary
number, I don't know if the block name schema "*Paper_SpaceN" is
mandatory, the layout name can be an arbitrary string.
There is no different handling for active layouts and inactive layouts in
`ezdxf`, this differentiation is just for AutoCAD important and it is not
documented in the DXF reference.
"""
def rename(self, name: str) -> None:
"""Rename layout to `name`, changes the name displayed in tabs by
CAD applications, not the internal BLOCK name. (internal API)
Use method :meth:`~ezdxf.layouts.Layouts.rename` of the
:meth:`~ezdxf.layouts.Layouts` class to rename paper space
layouts.
"""
self.dxf_layout.dxf.name = name
def viewports(self) -> list[Viewport]:
"""Get all VIEWPORT entities defined in this paperspace layout."""
return [e for e in self if e.is_alive and e.dxftype() == "VIEWPORT"] # type: ignore
def main_viewport(self) -> Optional[Viewport]:
"""Returns the main viewport of this paper space layout, or ``None``
if no main viewport exist.
"""
# Theory: the first VP found is the main VP of the layout, the attributes status
# and id are ignored by BricsCAD and AutoCAD!?
for viewport in self.viewports():
dxf = viewport.dxf
if dxf.hasattr("status") and dxf.status == 1:
return viewport
if dxf.id == 1:
return viewport
return None
def add_viewport(
self,
center: UVec,
size: tuple[float, float],
view_center_point: UVec,
view_height: float,
status: int = 2,
dxfattribs=None,
) -> Viewport:
"""Add a new :class:`~ezdxf.entities.Viewport` entity.
Viewport :attr:`status`:
- -1 is on, but is fully off-screen, or is one of the viewports that is not
active because the $MAXACTVP count is currently being exceeded.
- 0 is off
- any value>0 is on and active. The value indicates the order of
stacking for the viewports, where 1 is the "active viewport", 2 is the
next, ...
"""
dxfattribs = dxfattribs or {}
width, height = size
attribs = {
"center": center, # center in paperspace
"width": width, # width in paperspace
"height": height, # height in paperspace
"status": status,
"layer": "VIEWPORTS",
# use separated layer to turn off for plotting
"view_center_point": view_center_point, # in modelspace
"view_height": view_height, # in modelspace
}
attribs.update(dxfattribs)
viewport = cast("Viewport", self.new_entity("VIEWPORT", attribs))
viewport.dxf.id = self.get_next_viewport_id()
return viewport
def get_next_viewport_id(self):
viewports = self.viewports()
if viewports:
return max(vp.dxf.id for vp in viewports) + 1
return 2
def reset_viewports(self) -> None:
"""Delete all existing viewports, and create a new main viewport."""
# remove existing viewports
for viewport in self.viewports():
self.delete_entity(viewport)
self.add_new_main_viewport()
def reset_main_viewport(self, center: UVec = None, size: UVec = None) -> Viewport:
"""Reset the main viewport of this paper space layout to the given
values, or reset them to the default values, deduced from the paper
settings. Creates a new main viewport if none exist.
Ezdxf does not create a main viewport by default, because CAD
applications don't require one.
Args:
center: center of the viewport in paper space units
size: viewport size as (width, height) tuple in paper space units
"""
viewport = self.main_viewport()
if viewport is None:
viewport = self.add_new_main_viewport()
default_center, default_size = self.default_viewport_config()
if center is None:
center = default_center
if size is None:
size = default_size
viewport.dxf.center = center
width, height = size
viewport.dxf.width = width
viewport.dxf.height = height
return viewport
def default_viewport_config(
self,
) -> tuple[tuple[float, float], tuple[float, float]]:
dxf = self.dxf_layout.dxf
if dxf.plot_paper_units == 0: # inches
unit_factor = 25.4
else: # mm
unit_factor = 1.0
# all paper parameters in mm!
# all viewport parameters in paper space units inch/mm + scale factor!
scale_factor = dxf.scale_denominator / dxf.scale_numerator
def paper_units(value):
return value / unit_factor * scale_factor
paper_width = paper_units(dxf.paper_width)
paper_height = paper_units(dxf.paper_height)
# plot origin offset
x_offset = paper_units(dxf.plot_origin_x_offset)
y_offset = paper_units(dxf.plot_origin_y_offset)
# printing area
printable_width = (
paper_width - paper_units(dxf.left_margin) - paper_units(dxf.right_margin)
)
printable_height = (
paper_height - paper_units(dxf.bottom_margin) - paper_units(dxf.top_margin)
)
# AutoCAD viewport (window) size
vp_width = paper_width * 1.1
vp_height = paper_height * 1.1
# center of printing area
center = (
printable_width / 2 - x_offset,
printable_height / 2 - y_offset,
)
return center, (vp_width, vp_height)
def add_new_main_viewport(self) -> Viewport:
"""Add a new main viewport."""
center, size = self.default_viewport_config()
vp_height = size[1]
# create 'main' viewport
main_viewport = self.add_viewport(
center=center, # no influence to 'main' viewport?
size=size, # I don't get it, just use paper size!
view_center_point=center, # same as center
view_height=vp_height, # view height in paper space units
status=1, # main viewport
)
if len(self.entity_space) > 1:
# move main viewport to index 0 of entity space
_vp = self.entity_space.pop()
assert _vp is main_viewport
self.entity_space.insert(0, main_viewport)
main_viewport.dxf.id = 1 # set as main viewport
main_viewport.dxf.flags = 557088 # AutoCAD default value
self.set_current_viewport_handle(main_viewport.dxf.handle)
return main_viewport
def set_current_viewport_handle(self, handle: str) -> None:
self.dxf_layout.dxf.viewport_handle = handle
def page_setup(
self,
size: tuple[float, float] = (297, 210),
margins: tuple[float, float, float, float] = (0, 0, 0, 0),
units: str = "mm",
offset: tuple[float, float] = (0, 0),
rotation: int = 0,
scale: Union[int, tuple[float, float]] = 16,
name: str = "ezdxf",
device: str = "DWG to PDF.pc3",
) -> None:
"""Setup plot settings and paper size and reset viewports.
All parameters in given `units` (mm or inch).
Reset paper limits, extents and viewports.
Args:
size: paper size as (width, height) tuple
margins: (top, right, bottom, left) hint: clockwise
units: "mm" or "inch"
offset: plot origin offset is 2D point
rotation: see table Rotation
scale: integer in range [0, 32] defines a standard scale type or
as tuple(numerator, denominator) e.g. (1, 50) for scale 1:50
name: paper name prefix "{name}_({width}_x_{height}_{unit})"
device: device .pc3 configuration file or system printer name
=== ============
int Rotation
=== ============
0 no rotation
1 90 degrees counter-clockwise
2 upside-down
3 90 degrees clockwise
=== ============
"""
if int(rotation) not in (0, 1, 2, 3):
raise const.DXFValueError("valid rotation values: 0-3")
if isinstance(scale, tuple):
standard_scale = 16
scale_num, scale_denom = scale
elif isinstance(scale, int):
standard_scale = scale
scale_num, scale_denom = const.STD_SCALES.get(standard_scale, (1.0, 1.0))
else:
raise const.DXFTypeError(
"Scale has to be an int or a tuple(numerator, denominator)"
)
if scale_num == 0:
raise const.DXFValueError("Scale numerator can't be 0.")
if scale_denom == 0:
raise const.DXFValueError("Scale denominator can't be 0.")
paper_width, paper_height = size
margin_top, margin_right, margin_bottom, margin_left = margins
units = units.lower()
if units.startswith("inch"):
units = "Inches"
plot_paper_units = 0
unit_factor = 25.4 # inch to mm
elif units == "mm":
units = "MM"
plot_paper_units = 1
unit_factor = 1.0
else:
raise const.DXFValueError('Supported units: "mm" and "inch"')
# Setup PLOTSETTINGS
# all paper sizes in mm
dxf = self.dxf_layout.dxf
if not dxf.hasattr("plot_layout_flags"):
dxf.plot_layout_flags = dxf.get_default("plot_layout_flags")
self.use_standard_scale(False) # works best, don't know why
dxf.page_setup_name = ""
dxf.plot_configuration_file = device
dxf.paper_size = f"{name}_({paper_width:.2f}_x_{paper_height:.2f}_{units})"
dxf.left_margin = margin_left * unit_factor
dxf.bottom_margin = margin_bottom * unit_factor
dxf.right_margin = margin_right * unit_factor
dxf.top_margin = margin_top * unit_factor
dxf.paper_width = paper_width * unit_factor
dxf.paper_height = paper_height * unit_factor
dxf.scale_numerator = scale_num
dxf.scale_denominator = scale_denom
dxf.plot_paper_units = plot_paper_units
dxf.plot_rotation = rotation
x_offset, y_offset = offset
dxf.plot_origin_x_offset = x_offset * unit_factor # conversion to mm
dxf.plot_origin_y_offset = y_offset * unit_factor # conversion to mm
dxf.standard_scale_type = standard_scale
dxf.unit_factor = 1.0 / unit_factor # 1/1 for mm; 1/25.4 ... for inch
# Setup Layout
self.reset_paper_limits()
self.reset_extents()
self.reset_viewports()
def reset_paper_limits(self) -> None:
"""Set paper limits to default values, all values in paperspace units
but without plot scale (?).
"""
dxf = self.dxf_layout.dxf
if dxf.plot_paper_units == 0: # inch
unit_factor = 25.4
else: # mm
unit_factor = 1.0
# all paper sizes are stored in mm
paper_width = dxf.paper_width / unit_factor # in plot paper units
paper_height = dxf.paper_height / unit_factor # in plot paper units
left_margin = dxf.left_margin / unit_factor
bottom_margin = dxf.bottom_margin / unit_factor
x_offset = dxf.plot_origin_x_offset / unit_factor
y_offset = dxf.plot_origin_y_offset / unit_factor
# plot origin is the lower left corner of the printable paper area
# limits are the paper borders relative to the plot origin
shift_x = left_margin + x_offset
shift_y = bottom_margin + y_offset
dxf.limmin = (-shift_x, -shift_y) # paper space units
dxf.limmax = (paper_width - shift_x, paper_height - shift_y)
def get_paper_limits(self) -> tuple[Vec2, Vec2]:
"""Returns paper limits in plot paper units, relative to the plot origin.
plot origin = lower left corner of printable area + plot origin offset
Returns:
tuple (Vec2(x1, y1), Vec2(x2, y2)), lower left corner is (x1, y1),
upper right corner is (x2, y2).
"""
return Vec2(self.dxf.limmin), Vec2(self.dxf.limmax)
def page_setup_r12(
self,
size: tuple[float, float] = (297, 210),
margins: tuple[float, float, float, float] = (0, 0, 0, 0),
units: str = "mm",
offset: tuple[float, float] = (0, 0),
rotation: float = 0,
scale: Union[int, tuple[float, float]] = 16,
) -> None:
# remove existing viewports
for viewport in self.viewports():
self.delete_entity(viewport)
if int(rotation) not in (0, 1, 2, 3):
raise const.DXFValueError("Valid rotation values: 0-3")
if isinstance(scale, int):
scale_num, scale_denom = const.STD_SCALES.get(scale, (1, 1))
else:
scale_num, scale_denom = scale
if scale_num == 0:
raise const.DXFValueError("Scale numerator can't be 0.")
if scale_denom == 0:
raise const.DXFValueError("Scale denominator can't be 0.")
scale_factor = scale_denom / scale_num
# TODO: don't know how to set inch or mm mode in R12
units = units.lower()
if units.startswith("inch"):
units = "Inches"
plot_paper_units = 0
unit_factor = 25.4 # inch to mm
elif units == "mm":
units = "MM"
plot_paper_units = 1
unit_factor = 1.0
else:
raise const.DXFValueError('Supported units: "mm" and "inch"')
# all viewport parameters are scaled paper space units
def paper_units(value):
return value * scale_factor
margin_top = paper_units(margins[0])
margin_right = paper_units(margins[1])
margin_bottom = paper_units(margins[2])
margin_left = paper_units(margins[3])
paper_width = paper_units(size[0])
paper_height = paper_units(size[1])
self.doc.header["$PLIMMIN"] = (0, 0)
self.doc.header["$PLIMMAX"] = (paper_width, paper_height)
self.doc.header["$PEXTMIN"] = (0, 0, 0)
self.doc.header["$PEXTMAX"] = (paper_width, paper_height, 0)
# printing area
printable_width = paper_width - margin_left - margin_right
printable_height = paper_height - margin_bottom - margin_top
# AutoCAD viewport (window) size
vp_width = paper_width * 1.1
vp_height = paper_height * 1.1
# center of printing area
center = (printable_width / 2, printable_height / 2)
# create 'main' viewport
main_viewport = self.add_viewport(
center=center, # no influence to 'main' viewport?
size=(vp_width, vp_height), # I don't get it, just use paper size!
view_center_point=center, # same as center
view_height=vp_height, # view height in paper space units
status=1, # main viewport
)
main_viewport.dxf.id = 1 # set as main viewport
main_viewport.dxf.render_mode = 1000 # AutoDesk default (view mode?)
def get_paper_limits_r12(self) -> tuple[Vec2, Vec2]:
"""Returns paper limits in plot paper units."""
limmin = self.doc.header.get("$PLIMMIN", (0, 0))
limmax = self.doc.header.get("$PLIMMAX", (0, 0))
return Vec2(limmin), Vec2(limmax)
def _find_layout_block_record(layout: DXFLayout) -> BlockRecord | None:
"""Find and link the lost BLOCK_RECORD for the given LAYOUT entity."""
def link_layout(block_record: BlockRecord) -> None:
logger.info(
f"fixing broken links for '{layout.dxf.name}' between {str(layout)} and {str(block_record)}"
)
layout.dxf.block_record_handle = block_record.dxf.handle
block_record.dxf.layout = layout.dxf.handle
doc = layout.doc
assert doc is not None
if layout.dxf.name == "Model": # modelspace layout
key = make_table_key("*Model_Space")
for block_record in doc.tables.block_records:
if make_table_key(block_record.dxf.name) == key:
link_layout(block_record)
return block_record
return None
# paperspace layout
search_key = make_table_key("*Paper_Space")
paper_space_records = [
block_record
for block_record in doc.tables.block_records
if make_table_key(block_record.dxf.name).startswith(search_key)
]
def search(key):
for block_record in paper_space_records:
layout_handle = block_record.dxf.get("layout", "0")
if doc.entitydb.get(layout_handle) is key:
link_layout(block_record)
return block_record
return None
# first search all records for the lost record
block_record = search(layout)
if block_record is None:
# link layout to the next orphaned record
block_record = search(None)
return block_record