1351 lines
49 KiB
Python
1351 lines
49 KiB
Python
# Copyright (c) 2018-2022, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Iterable,
|
|
Optional,
|
|
Any,
|
|
cast,
|
|
)
|
|
import math
|
|
import abc
|
|
from ezdxf.math import (
|
|
Vec3,
|
|
Vec2,
|
|
UVec,
|
|
ConstructionLine,
|
|
ConstructionBox,
|
|
ConstructionArc,
|
|
)
|
|
from ezdxf.math import UCS, PassTroughUCS, xround, Z_AXIS
|
|
from ezdxf.lldxf import const
|
|
from ezdxf.enums import TextEntityAlignment
|
|
from ezdxf._options import options
|
|
from ezdxf.lldxf.const import DXFValueError, DXFUndefinedBlockError
|
|
from ezdxf.tools import suppress_zeros
|
|
from ezdxf.render.arrows import ARROWS
|
|
from ezdxf.entities import DimStyleOverride, Dimension
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.document import Drawing
|
|
from ezdxf.entities import Textstyle
|
|
from ezdxf.eztypes import GenericLayoutType
|
|
|
|
|
|
class TextBox(ConstructionBox):
|
|
"""Text boundaries representation."""
|
|
|
|
def __init__(
|
|
self,
|
|
center: Vec2 = Vec2(0, 0),
|
|
width: float = 0.0,
|
|
height: float = 0.0,
|
|
angle: float = 0.0,
|
|
hgap: float = 0.0, # horizontal gap - width
|
|
vgap: float = 0.0, # vertical gap - height
|
|
):
|
|
super().__init__(center, width + 2.0 * hgap, height + 2.0 * vgap, angle)
|
|
|
|
|
|
PLUS_MINUS = "±"
|
|
_TOLERANCE_COMMON = r"\A{align};{txt}{{\H{fac:.2f}x;"
|
|
TOLERANCE_TEMPLATE1 = _TOLERANCE_COMMON + r"{tol}}}"
|
|
TOLERANCE_TEMPLATE2 = _TOLERANCE_COMMON + r"\S{upr}^ {lwr};}}"
|
|
LIMITS_TEMPLATE = r"{{\H{fac:.2f}x;\S{upr}^ {lwr};}}"
|
|
|
|
|
|
def OptionalVec2(v) -> Optional[Vec2]:
|
|
if v is not None:
|
|
return Vec2(v)
|
|
else:
|
|
return None
|
|
|
|
|
|
def sign_char(value: float) -> str:
|
|
if value < 0.0:
|
|
return "-"
|
|
elif value > 0:
|
|
return "+"
|
|
else:
|
|
return " "
|
|
|
|
|
|
def format_text(
|
|
value: float,
|
|
dimrnd: Optional[float] = None,
|
|
dimdec: int = 2,
|
|
dimzin: int = 0,
|
|
dimdsep: str = ".",
|
|
) -> str:
|
|
if dimrnd is not None:
|
|
value = xround(value, dimrnd)
|
|
|
|
if dimdec is None:
|
|
fmt = "{:f}"
|
|
# Remove pending zeros for undefined decimal places:
|
|
# '{:f}'.format(0) -> '0.000000'
|
|
dimzin = dimzin | 8
|
|
else:
|
|
fmt = "{:." + str(dimdec) + "f}"
|
|
text = fmt.format(value)
|
|
|
|
leading = bool(dimzin & 4)
|
|
pending = bool(dimzin & 8)
|
|
text = suppress_zeros(text, leading, pending)
|
|
if dimdsep != ".":
|
|
text = text.replace(".", dimdsep)
|
|
return text
|
|
|
|
|
|
def apply_dimpost(text: str, dimpost: str) -> str:
|
|
if "<>" in dimpost:
|
|
fmt = dimpost.replace("<>", "{}", 1)
|
|
return fmt.format(text)
|
|
else:
|
|
raise DXFValueError(f'Invalid dimpost string: "{dimpost}"')
|
|
|
|
|
|
class Tolerance: # and Limits
|
|
def __init__(
|
|
self,
|
|
dim_style: DimStyleOverride,
|
|
cap_height: float = 1.0,
|
|
width_factor: float = 1.0,
|
|
dim_scale: float = 1.0,
|
|
):
|
|
self.text_width_factor = width_factor
|
|
self.dim_scale = dim_scale
|
|
get = dim_style.get
|
|
# Appends tolerances to dimension text.
|
|
# enabling DIMTOL disables DIMLIM.
|
|
self.has_tolerance = bool(get("dimtol", 0))
|
|
self.has_limits = False
|
|
if not self.has_tolerance:
|
|
# Limits generates dimension limits as the default text.
|
|
self.has_limits = bool(get("dimlim", 0))
|
|
|
|
# Scale factor for the text height of fractions and tolerance values
|
|
# relative to the dimension text height
|
|
self.text_scale_factor: float = get("dimtfac", 0.5)
|
|
|
|
self.text_decimal_separator = dim_style.get_decimal_separator()
|
|
|
|
# Default MTEXT line spacing for tolerances (BricsCAD)
|
|
self.line_spacing: float = 1.35
|
|
|
|
# Sets the minimum (or lower) tolerance limit for dimension text when
|
|
# DIMTOL or DIMLIM is on.
|
|
# DIMTM accepts signed values.
|
|
# If DIMTOL is on and DIMTP and DIMTM are set to the same value, a
|
|
# tolerance value is drawn.
|
|
# If DIMTM and DIMTP values differ, the upper tolerance is drawn above
|
|
# the lower, and a plus sign is added to the DIMTP value if it is
|
|
# positive.
|
|
# For DIMTM, the program uses the negative of the value you enter
|
|
# (adding a minus sign if you specify a positive number and a plus sign
|
|
# if you specify a negative number).
|
|
self.minimum: float = get("dimtm", 0.0)
|
|
|
|
# Sets the maximum (or upper) tolerance limit for dimension text when
|
|
# DIMTOL or DIMLIM is on.
|
|
# DIMTP accepts signed values.
|
|
# If DIMTOL is on and DIMTP and DIMTM are set to the same value, a
|
|
# tolerance value is drawn.
|
|
# If DIMTM and DIMTP values differ, the upper tolerance is drawn above
|
|
# the lower and a plus sign is added to the DIMTP value if it is
|
|
# positive.
|
|
self.maximum: float = get("dimtp", 0.0)
|
|
|
|
# Number of decimal places to display in tolerance values
|
|
# Same value for linear and angular measurements!
|
|
self.decimal_places: int = get("dimtdec", 2)
|
|
|
|
# Vertical justification for tolerance values relative to the nominal dimension text
|
|
# 0 = Bottom
|
|
# 1 = Middle
|
|
# 2 = Top
|
|
self.valign: int = get("dimtolj", 0)
|
|
|
|
# Same as DIMZIN for tolerances (self.text_suppress_zeros)
|
|
# Same value for linear and angular measurements!
|
|
self.suppress_zeros: int = get("dimtzin", 0)
|
|
self.text: str = ""
|
|
self.text_height: float = 0.0
|
|
self.text_width: float = 0.0
|
|
self.text_upper: str = ""
|
|
self.text_lower: str = ""
|
|
self.char_height: float = cap_height * self.text_scale_factor * self.dim_scale
|
|
if self.has_tolerance:
|
|
self.init_tolerance()
|
|
elif self.has_limits:
|
|
self.init_limits()
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return self.has_tolerance or self.has_limits
|
|
|
|
def disable(self):
|
|
self.has_tolerance = False
|
|
self.has_limits = False
|
|
|
|
def init_tolerance(self):
|
|
# The tolerance values are stored in the dimension style, they are
|
|
# independent from the actual measurement:
|
|
# Single tolerance value +/- value
|
|
if self.minimum == self.maximum:
|
|
self.text_height = self.char_height
|
|
self.text_width = self.get_text_width(self.text, self.text)
|
|
else: # 2 stacked values: +upper tolerance <above> -lower tolerance
|
|
# requires 2 text lines
|
|
self.text_height = self.char_height + (self.text_height * self.line_spacing)
|
|
self.text_width = self.get_text_width(self.text_upper, self.text_lower)
|
|
self.update_tolerance_text(self.maximum, self.minimum)
|
|
|
|
def update_tolerance_text(self, tol_upper: float, tol_lower: float):
|
|
if tol_upper == tol_lower:
|
|
self.text = PLUS_MINUS + self.format_text(abs(tol_upper))
|
|
else:
|
|
self.text_upper = sign_char(tol_upper) + self.format_text(abs(tol_upper))
|
|
self.text_lower = sign_char(tol_lower * -1) + self.format_text(
|
|
abs(tol_lower)
|
|
)
|
|
|
|
def init_limits(self):
|
|
# self.text is always an empty string (default value)
|
|
# Limit text are always 2 stacked numbers and requires the actual
|
|
# measurement!
|
|
self.text_height = self.char_height + (self.text_height * self.line_spacing)
|
|
|
|
def format_text(self, value: float) -> str:
|
|
"""Rounding and text formatting of tolerance `value`, removes leading
|
|
and trailing zeros if necessary.
|
|
|
|
"""
|
|
# dimpost is not applied to limits or tolerances!
|
|
return format_text(
|
|
value=value,
|
|
dimrnd=None,
|
|
dimdec=self.decimal_places,
|
|
dimzin=self.suppress_zeros,
|
|
dimdsep=self.text_decimal_separator,
|
|
)
|
|
|
|
def get_text_width(self, upr: str, lwr: str) -> float:
|
|
"""Returns the text width of the tolerance (upr/lwr) in drawing units."""
|
|
# todo: use matplotlib support
|
|
count = max(len(upr), len(lwr))
|
|
return self.text_height * self.text_width_factor * count
|
|
|
|
def compile_mtext(self, text: str) -> str:
|
|
if self.has_tolerance:
|
|
align = max(int(self.valign), 0)
|
|
align = min(align, 2)
|
|
if not self.text:
|
|
text = TOLERANCE_TEMPLATE2.format(
|
|
align=align,
|
|
txt=text,
|
|
fac=self.text_scale_factor,
|
|
upr=self.text_upper,
|
|
lwr=self.text_lower,
|
|
)
|
|
else:
|
|
text = TOLERANCE_TEMPLATE1.format(
|
|
align=align,
|
|
txt=text,
|
|
fac=self.text_scale_factor,
|
|
tol=self.text,
|
|
)
|
|
elif self.has_limits:
|
|
text = LIMITS_TEMPLATE.format(
|
|
upr=self.text_upper,
|
|
lwr=self.text_lower,
|
|
fac=self.text_scale_factor,
|
|
)
|
|
return text
|
|
|
|
def update_limits(self, measurement: float) -> None:
|
|
upper_limit = measurement + self.maximum
|
|
lower_limit = measurement - self.minimum
|
|
self.text_upper = self.format_text(upper_limit)
|
|
self.text_lower = self.format_text(lower_limit)
|
|
self.text_width = self.get_text_width(self.text_upper, self.text_lower)
|
|
|
|
|
|
class ExtensionLines:
|
|
default_lineweight: int = const.LINEWEIGHT_BYBLOCK
|
|
|
|
def __init__(self, dim_style: DimStyleOverride, default_color: int, scale: float):
|
|
get = dim_style.get
|
|
self.color: int = get("dimclre", default_color) # ACI
|
|
self.linetype1: str = get("dimltex1", "")
|
|
self.linetype2: str = get("dimltex2", "")
|
|
self.lineweight: int = get("dimlwe", self.default_lineweight)
|
|
self.suppress1: bool = bool(get("dimse1", 0))
|
|
self.suppress2: bool = bool(get("dimse2", 0))
|
|
|
|
# Extension of extension line above the dimension line, in extension
|
|
# line direction in most cases perpendicular to dimension line
|
|
# (oblique!)
|
|
self.extension_above: float = get("dimexe", 0.0) * scale
|
|
|
|
# Distance of extension line from the measurement point in extension
|
|
# line direction
|
|
self.offset: float = get("dimexo", 0.0) * scale
|
|
|
|
# Fixed length extension line, length above dimension line is still
|
|
# self.ext_line_extension
|
|
self.has_fixed_length: bool = bool(get("dimfxlon", 0))
|
|
|
|
# Length below the dimension line:
|
|
self.length_below: float = get("dimfxl", self.extension_above) * scale
|
|
|
|
def dxfattribs(self, num: int = 1) -> Any:
|
|
"""Returns default dimension line DXF attributes as dict."""
|
|
attribs: dict[str, Any] = {"color": self.color}
|
|
if num == 1:
|
|
linetype = self.linetype1
|
|
elif num == 2:
|
|
linetype = self.linetype2
|
|
else:
|
|
raise ValueError(f"invalid argument num:{num}")
|
|
|
|
if linetype:
|
|
attribs["linetype"] = linetype
|
|
if self.lineweight != self.default_lineweight:
|
|
attribs["lineweight"] = self.lineweight
|
|
return attribs
|
|
|
|
|
|
class DimensionLine:
|
|
default_lineweight: int = const.LINEWEIGHT_BYBLOCK
|
|
|
|
def __init__(self, dim_style: DimStyleOverride, default_color: int, scale: float):
|
|
get = dim_style.get
|
|
self.color: int = get("dimclrd", default_color) # ACI
|
|
|
|
# Dimension line extension, along the dimension line direction ('left'
|
|
# and 'right')
|
|
self.extension: float = get("dimdle", 0.0) * scale
|
|
self.linetype: str = get("dimltype", "")
|
|
self.lineweight: int = get("dimlwd", self.default_lineweight)
|
|
|
|
# Suppress first part of the dimension line
|
|
self.suppress1: bool = bool(get("dimsd1", 0))
|
|
|
|
# Suppress second part of the dimension line
|
|
self.suppress2: bool = bool(get("dimsd2", 0))
|
|
|
|
# Controls whether a dimension line is drawn between the extension lines
|
|
# even when the text is placed outside.
|
|
# For radius and diameter dimensions (when DIMTIX is off), draws a
|
|
# dimension line inside the circle or arc and places the text,
|
|
# arrowheads, and leader outside.
|
|
# 0 = no dimension line
|
|
# 1 = draw dimension line
|
|
# not supported yet - ezdxf behaves like option 1
|
|
self.has_dim_line_if_text_outside: bool = bool(get("dimtofl", 1))
|
|
|
|
def dxfattribs(self) -> Any:
|
|
"""Returns default dimension line DXF attributes as dict."""
|
|
attribs: dict[str, Any] = {"color": self.color}
|
|
if self.linetype:
|
|
attribs["linetype"] = self.linetype
|
|
if self.lineweight != self.default_lineweight:
|
|
attribs["lineweight"] = self.lineweight
|
|
return attribs
|
|
|
|
|
|
class Arrows:
|
|
def __init__(self, dim_style: DimStyleOverride, color: int, scale: float):
|
|
get = dim_style.get
|
|
self.color: int = get("dimclrd", color)
|
|
self.tick_size: float = get("dimtsz", 0.0) * scale
|
|
self.arrow1_name: str = "" # empty string is a closed filled arrow
|
|
self.arrow2_name: str = "" # empty string is a closed filled arrow
|
|
self.arrow_size: float = get("dimasz", 0.25) * scale
|
|
self.suppress1 = False # ezdxf only
|
|
self.suppress2 = False # ezdxf only
|
|
|
|
if self.tick_size > 0.0:
|
|
# Use oblique strokes as 'arrows', disables usual 'arrows' and user
|
|
# defined blocks tick size is per definition double the size of
|
|
# arrow size adjust arrow size to reuse the 'oblique' arrow block
|
|
self.arrow_size = self.tick_size * 2.0
|
|
else:
|
|
# Arrow name or block name if user defined arrow
|
|
(
|
|
self.arrow1_name,
|
|
self.arrow2_name,
|
|
) = dim_style.get_arrow_names()
|
|
|
|
@property
|
|
def has_ticks(self) -> bool:
|
|
return self.tick_size > 0.0
|
|
|
|
def dxfattribs(self) -> Any:
|
|
return {"color": self.color}
|
|
|
|
|
|
class Measurement:
|
|
def __init__(
|
|
self,
|
|
dim_style: DimStyleOverride,
|
|
color: int,
|
|
scale: float,
|
|
):
|
|
# update this values in method Measurement.update()
|
|
# raw measured value
|
|
self.raw_value: float = 0.0
|
|
# scaled measured value
|
|
self.value: float = 0.0
|
|
# Final formatted dimension text
|
|
self.text: str = ""
|
|
|
|
dimension = dim_style.dimension
|
|
doc = dimension.doc
|
|
assert doc is not None, "valid DXF document required"
|
|
|
|
# ezdxf specific attributes beyond DXF reference, therefore not stored
|
|
# in the DXF file (DSTYLE).
|
|
# Some of these are just an rendering effect, which will be ignored by
|
|
# CAD applications if they modify the DIMENSION entity
|
|
|
|
# User location override as UCS coordinates, stored as text_midpoint in
|
|
# the DIMENSION entity
|
|
self.user_location: Optional[Vec2] = OptionalVec2(
|
|
dim_style.pop("user_location", None)
|
|
)
|
|
|
|
# User location override relative to dimline center if True
|
|
self.relative_user_location: bool = dim_style.pop(
|
|
"relative_user_location", False
|
|
)
|
|
|
|
# Shift text away from default text location - implemented as user
|
|
# location override without leader
|
|
# Shift text along in text direction:
|
|
self.text_shift_h: float = dim_style.pop("text_shift_h", 0.0)
|
|
# Shift text perpendicular to text direction:
|
|
self.text_shift_v: float = dim_style.pop("text_shift_v", 0.0)
|
|
# End of ezdxf specific attributes
|
|
|
|
get = dim_style.get
|
|
# ezdxf locates attachment points always in the text center.
|
|
# Fixed predefined value for ezdxf rendering:
|
|
self.text_attachment_point: int = 5
|
|
|
|
# Ignored by ezdxf:
|
|
self.horizontal_direction: Optional[float] = dimension.get_dxf_attrib(
|
|
"horizontal_direction", None
|
|
)
|
|
|
|
# Dimension measurement factor:
|
|
self.measurement_factor: float = get("dimlfac", 1.0)
|
|
|
|
# Text style
|
|
style_name: str = get("dimtxsty", options.default_dimension_text_style)
|
|
if style_name not in doc.tables.styles:
|
|
style_name = "Standard"
|
|
self.text_style_name: str = style_name
|
|
text_style = get_text_style(doc, style_name)
|
|
self.text_height: float = get_char_height(dim_style, text_style) * scale
|
|
self.text_width_factor: float = text_style.get_dxf_attrib("width", 1.0)
|
|
self.stored_dim_text = dimension.dxf.text
|
|
|
|
# text_gap: gap between dimension line an dimension text
|
|
self.text_gap: float = get("dimgap", 0.625) * scale
|
|
|
|
# User defined text rotation - overrides everything:
|
|
self.user_text_rotation: float = dimension.get_dxf_attrib("text_rotation", None)
|
|
# calculated text rotation
|
|
self.text_rotation: float = self.user_text_rotation
|
|
self.text_color: int = get("dimclrt", color) # ACI
|
|
self.text_round: Optional[float] = get("dimrnd", None)
|
|
self.decimal_places: int = get("dimdec", 2)
|
|
self.angular_decimal_places: int = get("dimadec", 2)
|
|
|
|
# Controls the suppression of zeros in the primary unit value.
|
|
# Values 0-3 affect feet-and-inch dimensions only and are not supported
|
|
# 4 (Bit 3) = Suppresses leading zeros in decimal dimensions,
|
|
# e.g. 0.5000 becomes .5000
|
|
# 8 (Bit 4) = Suppresses trailing zeros in decimal dimensions,
|
|
# e.g. 12.5000 becomes 12.5
|
|
# 12 (Bit 3+4) = Suppresses both leading and trailing zeros,
|
|
# e.g. 0.5000 becomes .5)
|
|
self.suppress_zeros: int = get("dimzin", 8)
|
|
|
|
# special setting for angular dimensions (dimzin << 2) & 3
|
|
# 0 = Displays all leading and trailing zeros
|
|
# 1 = Suppresses leading zeros (for example, 0.5000 becomes .5000)
|
|
# 2 = Suppresses trailing zeros (for example, 12.5000 becomes 12.5)
|
|
# 3 = Suppresses leading and trailing zeros (for example, 0.5000 becomes .5)
|
|
self.angular_suppress_zeros: int = get("dimazin", 2)
|
|
|
|
# decimal separator char, default is ",":
|
|
self.decimal_separator: str = dim_style.get_decimal_separator()
|
|
|
|
self.text_post_process_format: str = get("dimpost", "")
|
|
# text_fill:
|
|
# 0 = None
|
|
# 1 = Background
|
|
# 2 = DIMTFILLCLR
|
|
self.text_fill: int = get("dimtfill", 0)
|
|
self.text_fill_color: int = get("dimtfillclr", 1) # ACI
|
|
self.text_box_fill_scale: float = 1.1
|
|
|
|
# text_halign:
|
|
# 0 = center
|
|
# 1 = left
|
|
# 2 = right
|
|
# 3 = above ext1
|
|
# 4 = above ext2
|
|
self.text_halign: int = get("dimjust", 0)
|
|
|
|
# text_valign:
|
|
# 0 = center
|
|
# 1 = above
|
|
# 2 = farthest away?
|
|
# 3 = JIS;
|
|
# 4 = below
|
|
# Options 2, 3 are ignored by ezdxf
|
|
self.text_valign: int = get("dimtad", 0)
|
|
|
|
# Controls the vertical position of dimension text above or below the
|
|
# dimension line, when DIMTAD = 0.
|
|
# The magnitude of the vertical offset of text is the product of the
|
|
# text height (+gap?) and DIMTVP.
|
|
# Setting DIMTVP to 1.0 is equivalent to setting DIMTAD = 1.
|
|
self.text_vertical_position: float = get("dimtvp", 0.0)
|
|
|
|
# Move text freely:
|
|
# 0 = Moves the dimension line with dimension text
|
|
# 1 = Adds a leader when dimension text is moved
|
|
# 2 = Allows text to be moved freely without a leader
|
|
self.text_movement_rule: int = get("dimtmove", 2)
|
|
|
|
self.has_leader: bool = (
|
|
self.user_location is not None and self.text_movement_rule == 1
|
|
)
|
|
|
|
# text_rotation is 0 if dimension text is 'inside', ezdxf defines
|
|
# 'inside' as at the default text location:
|
|
self.text_inside_horizontal: bool = get("dimtih", 0)
|
|
|
|
# text_rotation is 0 if dimension text is 'outside', ezdxf defines
|
|
# 'outside' as NOT at the default text location:
|
|
self.text_outside_horizontal: bool = get("dimtoh", 0)
|
|
|
|
# Force text location 'inside', even if the text should be moved
|
|
# 'outside':
|
|
self.force_text_inside: bool = bool(get("dimtix", 0))
|
|
|
|
# How dimension text and arrows are arranged when space is not
|
|
# sufficient to place both 'inside':
|
|
# 0 = Places both text and arrows outside extension lines
|
|
# 1 = Moves arrows first, then text
|
|
# 2 = Moves text first, then arrows
|
|
# 3 = Moves either text or arrows, whichever fits best
|
|
# not supported - ezdxf behaves like 2
|
|
self.text_fitting_rule: int = get("dimatfit", 2)
|
|
|
|
# Units for all dimension types except Angular.
|
|
# 1 = Scientific
|
|
# 2 = Decimal
|
|
# 3 = Engineering
|
|
# 4 = Architectural (always displayed stacked)
|
|
# 5 = Fractional (always displayed stacked)
|
|
# not supported - ezdxf behaves like 2
|
|
self.length_unit: int = get("dimlunit", 2)
|
|
|
|
# Fraction format when DIMLUNIT is set to 4 (Architectural) or
|
|
# 5 (Fractional).
|
|
# 0 = Horizontal stacking
|
|
# 1 = Diagonal stacking
|
|
# 2 = Not stacked (for example, 1/2)
|
|
self.fraction_format: int = get("dimfrac", 0) # not supported
|
|
|
|
# Units format for angular dimensions
|
|
# 0 = Decimal degrees
|
|
# 1 = Degrees/minutes/seconds
|
|
# 2 = Grad
|
|
# 3 = Radians
|
|
self.angle_units: int = get("dimaunit", 0)
|
|
|
|
self.has_arc_length_prefix: bool = False
|
|
if get("dimarcsym", 2) == 0:
|
|
self.has_arc_length_prefix = True
|
|
|
|
# Text_outside is only True if really placed outside of default text
|
|
# location
|
|
# remark: user defined text location is always outside per definition
|
|
# (not by real location)
|
|
self.text_is_outside: bool = False
|
|
|
|
# Final calculated or overridden dimension text location
|
|
self.text_location: Vec2 = Vec2(0, 0)
|
|
|
|
# True if dimension text doesn't fit between extension lines
|
|
self.is_wide_text: bool = False
|
|
|
|
# Text rotation was corrected to make upside down text better readable
|
|
self.has_upside_down_correction: bool = False
|
|
|
|
@property
|
|
def text_is_inside(self):
|
|
return not self.text_is_outside
|
|
|
|
@property
|
|
def has_relative_text_movement(self):
|
|
return bool(self.text_shift_h or self.text_shift_v)
|
|
|
|
def apply_text_shift(self, location: Vec2, text_rotation: float) -> Vec2:
|
|
"""Add `self.text_shift_h` and `sel.text_shift_v` to point `location`,
|
|
shifting along and perpendicular to text orientation defined by
|
|
`text_rotation`.
|
|
|
|
Args:
|
|
location: location point
|
|
text_rotation: text rotation in degrees
|
|
|
|
Returns: new location
|
|
|
|
"""
|
|
shift_vec = Vec2((self.text_shift_h, self.text_shift_v))
|
|
location += shift_vec.rotate_deg(text_rotation)
|
|
return location
|
|
|
|
@property
|
|
def vertical_placement(self) -> float:
|
|
"""Returns vertical placement of dimension text as 1 for above, 0 for
|
|
center and -1 for below dimension line.
|
|
|
|
"""
|
|
if self.text_valign == 0:
|
|
return 0
|
|
elif self.text_valign == 4:
|
|
return -1
|
|
else:
|
|
return 1
|
|
|
|
def text_vertical_distance(self) -> float:
|
|
"""Returns the vertical distance for dimension line to text midpoint.
|
|
Positive values are above the line, negative values are below the line.
|
|
|
|
"""
|
|
if self.text_valign == 0:
|
|
return self.text_height * self.text_vertical_position
|
|
else:
|
|
return (self.text_height / 2.0 + self.text_gap) * self.vertical_placement
|
|
|
|
def text_width(self, text: str) -> float:
|
|
"""
|
|
Return width of `text` in drawing units.
|
|
|
|
"""
|
|
# todo: use matplotlib support
|
|
char_width = self.text_height * self.text_width_factor
|
|
return len(text) * char_width
|
|
|
|
def text_override(self, measurement: float) -> str:
|
|
"""Create dimension text for `measurement` in drawing units and applies
|
|
text overriding properties.
|
|
|
|
"""
|
|
text = self.stored_dim_text
|
|
if text == " ": # suppresses text
|
|
return ""
|
|
elif text == "" or text == "<>": # measured distance
|
|
return self.format_text(measurement)
|
|
else: # user override
|
|
return text
|
|
|
|
def location_override(self, location: UVec, leader=False, relative=False) -> None:
|
|
"""Set user defined dimension text location. ezdxf defines a user
|
|
defined location per definition as 'outside'.
|
|
|
|
Args:
|
|
location: text midpoint
|
|
leader: use leader or not (movement rules)
|
|
relative: is location absolute (in UCS) or relative to dimension
|
|
line center.
|
|
|
|
"""
|
|
self.user_location = Vec2(location)
|
|
self.text_movement_rule = 1 if leader else 2
|
|
self.relative_user_location = relative
|
|
self.text_is_outside = True
|
|
|
|
def dxfattribs(self) -> Any:
|
|
return {"color": self.text_color}
|
|
|
|
@abc.abstractmethod
|
|
def update(self, raw_measurement_value: float) -> None:
|
|
"""Update raw measurement value, scaled measurement value and
|
|
dimension text.
|
|
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def format_text(self, value: float) -> str:
|
|
"""Rounding and text formatting of `value`, removes leading and
|
|
trailing zeros if necessary.
|
|
|
|
"""
|
|
|
|
|
|
class LengthMeasurement(Measurement):
|
|
def update(self, raw_measurement_value: float) -> None:
|
|
"""Update raw measurement value, scaled measurement value and
|
|
dimension text.
|
|
"""
|
|
self.raw_value = raw_measurement_value
|
|
self.value = raw_measurement_value * self.measurement_factor
|
|
self.text = self.text_override(self.value)
|
|
|
|
def format_text(self, value: float) -> str:
|
|
"""Rounding and text formatting of `value`, removes leading and
|
|
trailing zeros if necessary.
|
|
|
|
"""
|
|
text = format_text(
|
|
value,
|
|
self.text_round,
|
|
self.decimal_places,
|
|
self.suppress_zeros,
|
|
self.decimal_separator,
|
|
)
|
|
if self.text_post_process_format:
|
|
text = apply_dimpost(text, self.text_post_process_format)
|
|
return text
|
|
|
|
|
|
class Geometry:
|
|
"""
|
|
Geometry layout entities are located in the OCS defined by the extrusion
|
|
vector of the DIMENSION entity and the z-axis of the OCS
|
|
point 'text_midpoint' (group code 11).
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
dimension: Dimension,
|
|
ucs: UCS,
|
|
layout: "GenericLayoutType",
|
|
):
|
|
assert dimension.doc is not None, "valid DXF document required"
|
|
self.dimension: Dimension = dimension
|
|
self.doc: Drawing = dimension.doc
|
|
self.dxfversion: str = self.doc.dxfversion
|
|
self.supports_dxf_r2000: bool = self.dxfversion >= "AC1015"
|
|
self.supports_dxf_r2007: bool = self.dxfversion >= "AC1021"
|
|
self.ucs: UCS = ucs
|
|
self.extrusion: Vec3 = ucs.uz
|
|
self.requires_extrusion: bool = not self.extrusion.isclose(Z_AXIS)
|
|
self.layout: GenericLayoutType = layout
|
|
self._text_box: TextBox = TextBox()
|
|
|
|
@property
|
|
def has_text_box(self) -> bool:
|
|
return self._text_box.width > 0.0 and self._text_box.height > 0.0
|
|
|
|
def set_layout(self, layout: GenericLayoutType) -> None:
|
|
self.layout = layout
|
|
|
|
def set_text_box(self, text_box: TextBox) -> None:
|
|
self._text_box = text_box
|
|
|
|
def has_block(self, name: str) -> bool:
|
|
return name in self.doc.blocks
|
|
|
|
def add_arrow_blockref(
|
|
self,
|
|
name: str,
|
|
insert: Vec2,
|
|
size: float,
|
|
rotation: float,
|
|
dxfattribs,
|
|
) -> None:
|
|
# OCS of the arrow blocks is defined by the DIMENSION entity!
|
|
# Therefore remove OCS elevation, the elevation is defined by the
|
|
# DIMENSION 'text_midpoint' (group code 11) and do not set 'extrusion'
|
|
# either!
|
|
insert = self.ucs.to_ocs(Vec3(insert)).vec2
|
|
rotation = self.ucs.to_ocs_angle_deg(rotation)
|
|
self.layout.add_arrow_blockref(name, insert, size, rotation, dxfattribs)
|
|
|
|
def add_blockref(
|
|
self,
|
|
name: str,
|
|
insert: Vec2,
|
|
rotation: float,
|
|
dxfattribs,
|
|
) -> None:
|
|
# OCS of the arrow blocks is defined by the DIMENSION entity!
|
|
# Therefore remove OCS elevation, the elevation is defined by the
|
|
# DIMENSION 'text_midpoint' (group code 11) and do not set 'extrusion'
|
|
# either!
|
|
insert = self.ucs.to_ocs(Vec3(insert)).vec2
|
|
dxfattribs["rotation"] = self.ucs.to_ocs_angle_deg(rotation)
|
|
self.layout.add_blockref(name, insert, dxfattribs)
|
|
|
|
def add_text(self, text: str, pos: Vec2, rotation: float, dxfattribs) -> None:
|
|
dxfattribs["rotation"] = self.ucs.to_ocs_angle_deg(rotation)
|
|
entity = self.layout.add_text(text, dxfattribs=dxfattribs)
|
|
# OCS of the measurement text is defined by the DIMENSION entity!
|
|
# Therefore remove OCS elevation, the elevation is defined by the
|
|
# DIMENSION 'text_midpoint' (group code 11) and do not set 'extrusion'
|
|
# either!
|
|
entity.set_placement(
|
|
self.ucs.to_ocs(Vec3(pos)).vec2,
|
|
align=TextEntityAlignment.MIDDLE_CENTER,
|
|
)
|
|
|
|
def add_mtext(self, text: str, pos: Vec2, rotation: float, dxfattribs) -> None:
|
|
# OCS of the measurement text is defined by the DIMENSION entity!
|
|
# Therefore remove OCS elevation, the elevation is defined by the
|
|
# DIMENSION 'text_midpoint' (group code 11) and do not set 'extrusion'
|
|
# either!
|
|
dxfattribs["rotation"] = self.ucs.to_ocs_angle_deg(rotation)
|
|
dxfattribs["insert"] = self.ucs.to_ocs(Vec3(pos)).vec2
|
|
self.layout.add_mtext(text, dxfattribs)
|
|
|
|
def add_defpoints(self, points: Iterable[Vec2]) -> None:
|
|
attribs = {
|
|
"layer": "Defpoints",
|
|
}
|
|
for point in points:
|
|
# Despite the fact that the POINT entity has WCS coordinates,
|
|
# the coordinates of defpoints in DIMENSION entities have OCS
|
|
# coordinates.
|
|
location = self.ucs.to_ocs(Vec3(point)).replace(z=0.0)
|
|
self.layout.add_point(location, dxfattribs=attribs)
|
|
|
|
def add_line(
|
|
self,
|
|
start: Vec2,
|
|
end: Vec2,
|
|
dxfattribs,
|
|
remove_hidden_lines=False,
|
|
) -> None:
|
|
"""Add a LINE entity to the geometry layout. Removes parts of the line
|
|
hidden by dimension text if `remove_hidden_lines` is True.
|
|
|
|
Args:
|
|
start: start point of line
|
|
end: end point of line
|
|
dxfattribs: additional or overridden DXF attributes
|
|
remove_hidden_lines: removes parts of the line hidden by dimension
|
|
text if ``True``
|
|
|
|
"""
|
|
|
|
def add_line_to_block(start, end):
|
|
# LINE is handled like an OCS entity !?
|
|
self.layout.add_line(
|
|
to_ocs(Vec3(start)).vec2,
|
|
to_ocs(Vec3(end)).vec2,
|
|
dxfattribs=dxfattribs,
|
|
)
|
|
|
|
def order(a: Vec2, b: Vec2) -> tuple[Vec2, Vec2]:
|
|
if (start - a).magnitude < (start - b).magnitude:
|
|
return a, b
|
|
else:
|
|
return b, a
|
|
|
|
to_ocs = self.ucs.to_ocs
|
|
if remove_hidden_lines and self.has_text_box:
|
|
text_box = self._text_box
|
|
start_inside = int(text_box.is_inside(start))
|
|
end_inside = int(text_box.is_inside(end))
|
|
inside = start_inside + end_inside
|
|
if inside == 2: # start and end inside text_box
|
|
return # do not draw line
|
|
elif inside == 1: # one point inside text_box or on a border line
|
|
intersection_points = text_box.intersect(ConstructionLine(start, end))
|
|
if len(intersection_points) == 1:
|
|
# one point inside one point outside -> one intersection point
|
|
p1 = intersection_points[0]
|
|
else:
|
|
# second point on a text box border line
|
|
p1, _ = order(*intersection_points)
|
|
p2 = start if end_inside else end
|
|
add_line_to_block(p1, p2)
|
|
return
|
|
else:
|
|
intersection_points = text_box.intersect(ConstructionLine(start, end))
|
|
if len(intersection_points) == 2:
|
|
# sort intersection points by distance to start point
|
|
p1, p2 = order(intersection_points[0], intersection_points[1])
|
|
# line[start-p1] - gap - line[p2-end]
|
|
add_line_to_block(start, p1)
|
|
add_line_to_block(p2, end)
|
|
return
|
|
# else: fall through
|
|
add_line_to_block(start, end)
|
|
|
|
def add_arc(
|
|
self,
|
|
center: Vec2,
|
|
radius: float,
|
|
start_angle: float,
|
|
end_angle: float,
|
|
dxfattribs=None,
|
|
remove_hidden_lines=False,
|
|
) -> None:
|
|
"""Add a ARC entity to the geometry layout. Removes parts of the arc
|
|
hidden by dimension text if `remove_hidden_lines` is True.
|
|
|
|
Args:
|
|
center: center of arc
|
|
radius: radius of arc
|
|
start_angle: start angle in radians
|
|
end_angle: end angle in radians
|
|
dxfattribs: additional or overridden DXF attributes
|
|
remove_hidden_lines: removes parts of the arc hidden by dimension
|
|
text if ``True``
|
|
|
|
"""
|
|
|
|
def add_arc(s: float, e: float) -> None:
|
|
"""Add ARC entity to geometry block."""
|
|
self.layout.add_arc(
|
|
center=ocs_center,
|
|
radius=radius,
|
|
start_angle=math.degrees(ocs_angle(s)),
|
|
end_angle=math.degrees(ocs_angle(e)),
|
|
dxfattribs=dxfattribs,
|
|
)
|
|
|
|
# OCS of the ARC is defined by the DIMENSION entity!
|
|
# Therefore remove OCS elevation, the elevation is defined by the
|
|
# DIMENSION 'text_midpoint' (group code 11) and do not set 'extrusion'
|
|
# either!
|
|
ocs_center = self.ucs.to_ocs(Vec3(center)).vec2
|
|
ocs_angle = self.ucs.to_ocs_angle_rad
|
|
if remove_hidden_lines and self.has_text_box:
|
|
for start, end in visible_arcs(
|
|
center,
|
|
radius,
|
|
start_angle,
|
|
end_angle,
|
|
self._text_box,
|
|
):
|
|
add_arc(start, end)
|
|
else:
|
|
add_arc(start_angle, end_angle)
|
|
|
|
|
|
class BaseDimensionRenderer:
|
|
"""Base rendering class for DIMENSION entities."""
|
|
|
|
def __init__(
|
|
self,
|
|
dimension: Dimension,
|
|
ucs: Optional[UCS] = None,
|
|
override: Optional[DimStyleOverride] = None,
|
|
):
|
|
self.dimension: Dimension = dimension
|
|
self.geometry = self.init_geometry(dimension, ucs)
|
|
|
|
# DimStyleOverride object, manages dimension style overriding
|
|
self.dim_style: DimStyleOverride
|
|
if override:
|
|
self.dim_style = override
|
|
else:
|
|
self.dim_style = DimStyleOverride(dimension)
|
|
|
|
# ---------------------------------------------
|
|
# GENERAL PROPERTIES
|
|
# ---------------------------------------------
|
|
self.default_color: int = self.dimension.dxf.color # ACI
|
|
self.default_layer: str = self.dimension.dxf.layer
|
|
|
|
get = self.dim_style.get
|
|
# Overall scaling of DIMENSION entity:
|
|
self.dim_scale: float = get("dimscale", 1.0)
|
|
if abs(self.dim_scale) < 1e-9:
|
|
self.dim_scale = 1.0
|
|
|
|
# Controls drawing of circle or arc center marks and center lines, for
|
|
# DIMDIAMETER and DIMRADIUS, the center mark is drawn only if you place
|
|
# the dimension line outside the circle or arc.
|
|
# 0 = No center marks or lines are drawn
|
|
# <0 = Center lines are drawn
|
|
# >0 = Center marks are drawn
|
|
self.dim_center_marks: int = get("dimcen", 0)
|
|
|
|
self.measurement = self.init_measurement(self.default_color, self.dim_scale)
|
|
self.dimension_line: DimensionLine = self.init_dimension_line(
|
|
self.default_color, self.dim_scale
|
|
)
|
|
self.arrows: Arrows = self.init_arrows(self.default_color, self.dim_scale)
|
|
# Suppress arrow rendering - only rendering is suppressed (rendering
|
|
# effect).
|
|
# All placing related calculations are done without this settings.
|
|
# Used for multi point linear dimensions to avoid double rendering of
|
|
# non arrow ticks. These are ezdxf specific attributes!
|
|
self.arrows.suppress1 = self.dim_style.pop("suppress_arrow1", False)
|
|
self.arrows.suppress2 = self.dim_style.pop("suppress_arrow2", False)
|
|
|
|
self.extension_lines: ExtensionLines = self.init_extension_lines(
|
|
self.default_color, self.dim_scale
|
|
)
|
|
# tolerances have to be initialized after measurement:
|
|
self.tol: Tolerance = self.init_tolerance(self.dim_scale, self.measurement)
|
|
|
|
# Update text height
|
|
self.measurement.text_height = max(
|
|
self.measurement.text_height, self.tol.text_height
|
|
)
|
|
|
|
def init_geometry(self, dimension: Dimension, ucs: Optional[UCS] = None):
|
|
from ezdxf.layouts import VirtualLayout
|
|
|
|
return Geometry(dimension, ucs or PassTroughUCS(), VirtualLayout())
|
|
|
|
def init_tolerance(self, scale: float, measurement: Measurement) -> Tolerance:
|
|
return Tolerance(
|
|
self.dim_style,
|
|
cap_height=measurement.text_height,
|
|
width_factor=measurement.text_width_factor,
|
|
dim_scale=scale,
|
|
)
|
|
|
|
def init_extension_lines(self, color: int, scale: float) -> ExtensionLines:
|
|
return ExtensionLines(self.dim_style, color, scale)
|
|
|
|
def init_dimension_line(self, color: int, scale: float) -> DimensionLine:
|
|
return DimensionLine(self.dim_style, color, scale)
|
|
|
|
def init_arrows(self, color: int, scale: float) -> Arrows:
|
|
return Arrows(self.dim_style, color, scale)
|
|
|
|
def init_measurement(self, color: int, scale: float) -> Measurement:
|
|
return LengthMeasurement(self.dim_style, color, scale)
|
|
|
|
def init_text_box(self) -> TextBox:
|
|
measurement = self.measurement
|
|
return TextBox(
|
|
center=measurement.text_location,
|
|
width=self.total_text_width(),
|
|
height=measurement.text_height,
|
|
angle=measurement.text_rotation or 0.0,
|
|
# The currently used monospaced abstract font, returns a too large
|
|
# text width.
|
|
# Therefore, the horizontal text gap is ignored at all - yet!
|
|
hgap=0.0,
|
|
# Arbitrary choice to reduce the too large vertical gap!
|
|
vgap=measurement.text_gap * 0.75,
|
|
)
|
|
|
|
def get_required_defpoint(self, name: str) -> Vec2:
|
|
return get_required_defpoint(self.dimension, name)
|
|
|
|
def render(self, block: GenericLayoutType):
|
|
# Block entities are located in the OCS defined by the extrusion vector
|
|
# of the DIMENSION entity and the z-axis of the OCS point
|
|
# 'text_midpoint' (group code 11).
|
|
self.geometry.set_layout(block)
|
|
# Tolerance requires MTEXT support, switch off rendering of tolerances
|
|
# and limits
|
|
if not self.geometry.supports_dxf_r2000:
|
|
self.tol.disable()
|
|
|
|
def total_text_width(self) -> float:
|
|
width = 0.0
|
|
text = self.measurement.text
|
|
if text:
|
|
if self.tol.has_limits: # only limits are displayed
|
|
width = self.tol.text_width
|
|
else:
|
|
width = self.measurement.text_width(text)
|
|
if self.tol.has_tolerance:
|
|
width += self.tol.text_width
|
|
return width
|
|
|
|
def default_attributes(self) -> dict[str, Any]:
|
|
"""Returns default DXF attributes as dict."""
|
|
return {
|
|
"layer": self.default_layer,
|
|
"color": self.default_color,
|
|
}
|
|
|
|
def location_override(self, location: UVec, leader=False, relative=False) -> None:
|
|
"""Set user defined dimension text location. ezdxf defines a user
|
|
defined location per definition as 'outside'.
|
|
|
|
Args:
|
|
location: text midpoint
|
|
leader: use leader or not (movement rules)
|
|
relative: is location absolute (in UCS) or relative to dimension
|
|
line center.
|
|
|
|
"""
|
|
self.dim_style.set_location(location, leader, relative)
|
|
self.measurement.location_override(location, leader, relative)
|
|
|
|
def add_line(
|
|
self,
|
|
start: Vec2,
|
|
end: Vec2,
|
|
dxfattribs,
|
|
remove_hidden_lines=False,
|
|
) -> None:
|
|
"""Add a LINE entity to the dimension BLOCK. Remove parts of the line
|
|
hidden by dimension text if `remove_hidden_lines` is True.
|
|
|
|
Args:
|
|
start: start point of line
|
|
end: end point of line
|
|
dxfattribs: additional or overridden DXF attributes
|
|
remove_hidden_lines: removes parts of the line hidden by dimension
|
|
text if ``True``
|
|
|
|
"""
|
|
|
|
attribs = self.default_attributes()
|
|
if dxfattribs:
|
|
attribs.update(dxfattribs)
|
|
self.geometry.add_line(start, end, dxfattribs, remove_hidden_lines)
|
|
|
|
def add_arc(
|
|
self,
|
|
center: Vec2,
|
|
radius: float,
|
|
start_angle: float,
|
|
end_angle: float,
|
|
dxfattribs=None,
|
|
remove_hidden_lines=False,
|
|
) -> None:
|
|
"""Add a ARC entity to the geometry layout. Remove parts of the arc
|
|
hidden by dimension text if `remove_hidden_lines` is True.
|
|
|
|
Args:
|
|
center: center of arc
|
|
radius: radius of arc
|
|
start_angle: start angle in radians
|
|
end_angle: end angle in radians
|
|
dxfattribs: additional or overridden DXF attributes
|
|
remove_hidden_lines: removes parts of the arc hidden by dimension
|
|
text if ``True``
|
|
|
|
"""
|
|
attribs = self.default_attributes()
|
|
if dxfattribs:
|
|
attribs.update(dxfattribs)
|
|
self.geometry.add_arc(
|
|
center, radius, start_angle, end_angle, attribs, remove_hidden_lines
|
|
)
|
|
|
|
def add_blockref(
|
|
self,
|
|
name: str,
|
|
insert: Vec2,
|
|
rotation: float,
|
|
scale: float,
|
|
dxfattribs,
|
|
) -> None:
|
|
"""
|
|
Add block references and standard arrows to the dimension BLOCK.
|
|
|
|
Args:
|
|
name: block or arrow name
|
|
insert: insertion point in UCS
|
|
rotation: rotation angle in degrees in UCS (x-axis is 0 degrees)
|
|
scale: scaling factor for x- and y-direction
|
|
dxfattribs: additional or overridden DXF attributes
|
|
|
|
"""
|
|
attribs = self.default_attributes()
|
|
if dxfattribs:
|
|
attribs.update(dxfattribs)
|
|
|
|
if name in ARROWS:
|
|
# generates automatically BLOCK definitions for arrows if needed
|
|
self.geometry.add_arrow_blockref(name, insert, scale, rotation, attribs)
|
|
else:
|
|
if name is None or not self.geometry.has_block(name):
|
|
raise DXFUndefinedBlockError(f'Undefined block: "{name}"')
|
|
if scale != 1.0:
|
|
attribs["xscale"] = scale
|
|
attribs["yscale"] = scale
|
|
self.geometry.add_blockref(name, insert, rotation, attribs)
|
|
|
|
def add_text(self, text: str, pos: Vec2, rotation: float, dxfattribs) -> None:
|
|
"""
|
|
Add TEXT (DXF R12) or MTEXT (DXF R2000+) entity to the dimension BLOCK.
|
|
|
|
Args:
|
|
text: text as string
|
|
pos: insertion location in UCS
|
|
rotation: rotation angle in degrees in UCS (x-axis is 0 degrees)
|
|
dxfattribs: additional or overridden DXF attributes
|
|
|
|
"""
|
|
geometry = self.geometry
|
|
measurement = self.measurement
|
|
attribs = self.default_attributes()
|
|
attribs["style"] = measurement.text_style_name
|
|
attribs["color"] = measurement.text_color
|
|
|
|
if geometry.supports_dxf_r2000: # use MTEXT entity
|
|
attribs["char_height"] = measurement.text_height
|
|
attribs["attachment_point"] = measurement.text_attachment_point
|
|
if measurement.text_fill:
|
|
attribs["box_fill_scale"] = measurement.text_box_fill_scale
|
|
attribs["bg_fill_color"] = measurement.text_fill_color
|
|
attribs["bg_fill"] = 3 if measurement.text_fill == 1 else 1
|
|
|
|
if dxfattribs:
|
|
attribs.update(dxfattribs)
|
|
geometry.add_mtext(text, pos, rotation, dxfattribs=attribs)
|
|
else: # use TEXT entity
|
|
attribs["height"] = measurement.text_height
|
|
if dxfattribs:
|
|
attribs.update(dxfattribs)
|
|
geometry.add_text(text, pos, rotation, dxfattribs=attribs)
|
|
|
|
def add_leader(self, p1: Vec2, p2: Vec2, p3: Vec2):
|
|
"""
|
|
Add simple leader line from p1 to p2 to p3.
|
|
|
|
Args:
|
|
p1: target point
|
|
p2: first text point
|
|
p3: second text point
|
|
|
|
"""
|
|
# use only color and ignore linetype!
|
|
dxfattribs = {"color": self.dimension_line.color}
|
|
self.add_line(p1, p2, dxfattribs)
|
|
self.add_line(p2, p3, dxfattribs)
|
|
|
|
def transform_ucs_to_wcs(self) -> None:
|
|
"""Transforms dimension definition points into WCS or if required into
|
|
OCS.
|
|
|
|
Can not be called in __init__(), because inherited classes may be need
|
|
unmodified values.
|
|
|
|
"""
|
|
pass
|
|
|
|
def finalize(self) -> None:
|
|
self.transform_ucs_to_wcs()
|
|
if self.geometry.requires_extrusion:
|
|
self.dimension.dxf.extrusion = self.geometry.extrusion
|
|
|
|
|
|
def order_leader_points(p1: Vec2, p2: Vec2, p3: Vec2) -> tuple[Vec2, Vec2]:
|
|
if (p1 - p2).magnitude > (p1 - p3).magnitude:
|
|
return p3, p2
|
|
else:
|
|
return p2, p3
|
|
|
|
|
|
def get_center_leader_points(
|
|
target_point: Vec2, text_box: TextBox, leg_length: float
|
|
) -> tuple[Vec2, Vec2]:
|
|
"""Returns the leader points of the "leg" for a vertical centered leader."""
|
|
c0, c1, c2, c3 = text_box.corners
|
|
# c3-------c2
|
|
# left leg /---x text x---\ right leg
|
|
# / c0-------c1 \
|
|
left_center = c0.lerp(c3)
|
|
right_center = c1.lerp(c2)
|
|
connection_point = left_center
|
|
leg_vector = (c0 - c1).normalize(leg_length)
|
|
if target_point.distance(left_center) > target_point.distance(right_center):
|
|
connection_point = right_center
|
|
leg_vector = -leg_vector
|
|
# leader line: target_point -> leg_point -> connection_point
|
|
# The text gap between the text and the connection point is already included
|
|
# in the text_box corners!
|
|
# Do not order leader points!
|
|
return connection_point + leg_vector, connection_point
|
|
|
|
|
|
def get_required_defpoint(dim: Dimension, name: str) -> Vec2:
|
|
dxf = dim.dxf
|
|
if dxf.hasattr(name): # has to exist, ignore default value!
|
|
return Vec2(dxf.get(name))
|
|
raise const.DXFMissingDefinitionPoint(name)
|
|
|
|
|
|
def visible_arcs(
|
|
center: Vec2,
|
|
radius: float,
|
|
start_angle: float,
|
|
end_angle: float,
|
|
box: ConstructionBox,
|
|
) -> list[tuple[float, float]]:
|
|
"""Returns the visible parts of an arc intersecting with a construction box
|
|
as (start angle, end angle) tuples.
|
|
|
|
Args:
|
|
center: center of the arc
|
|
radius: radius of the arc
|
|
start_angle: start angle of arc in radians
|
|
end_angle: end angle of arc in radians
|
|
box: construction box which may intersect the arc
|
|
|
|
"""
|
|
|
|
intersection_angles: list[float] = [] # angles are in the range 0 to 2pi
|
|
start_angle %= math.tau
|
|
end_angle %= math.tau
|
|
arc = ConstructionArc(
|
|
center, radius, math.degrees(start_angle), math.degrees(end_angle)
|
|
)
|
|
for line in box.border_lines():
|
|
for intersection_point in arc.intersect_line(line):
|
|
angle = (intersection_point - center).angle % math.tau
|
|
if not intersection_angles:
|
|
intersection_angles.append(angle)
|
|
# new angle should be different than the last added angle:
|
|
elif not math.isclose(intersection_angles[-1], angle):
|
|
intersection_angles.append(angle)
|
|
# Arc has to intersect the box in exact two locations!
|
|
if len(intersection_angles) == 2:
|
|
if start_angle > end_angle: # arc passes 0 degrees
|
|
intersection_angles = [
|
|
(a if a >= start_angle else a + math.tau) for a in intersection_angles
|
|
]
|
|
intersection_angles.sort()
|
|
return [
|
|
(start_angle, intersection_angles[0]),
|
|
(intersection_angles[1], end_angle),
|
|
]
|
|
else:
|
|
# Ignore cases where the start- or the end point is inside the box.
|
|
# Ignore cases where the box touches the arc in one point.
|
|
return [(start_angle, end_angle)]
|
|
|
|
|
|
def get_text_style(doc: "Drawing", name: str) -> Textstyle:
|
|
assert doc is not None, "valid DXF document required"
|
|
get_style = doc.tables.styles.get
|
|
try:
|
|
style = get_style(name)
|
|
except const.DXFTableEntryError:
|
|
style = get_style("Standard")
|
|
return cast("Textstyle", style)
|
|
|
|
|
|
def get_char_height(dim_style: DimStyleOverride, text_style: Textstyle) -> float:
|
|
"""Unscaled character height defined by text style or DIMTXT."""
|
|
height: float = text_style.dxf.get("height", 0.0)
|
|
if height == 0.0: # variable text height (not fixed)
|
|
height = dim_style.get("dimtxt", 1.0)
|
|
return height
|
|
|
|
|
|
def compile_mtext(measurement: Measurement, tol: Tolerance) -> str:
|
|
text = measurement.text
|
|
if tol.enabled:
|
|
text = tol.compile_mtext(text)
|
|
return text
|