978 lines
34 KiB
Python
978 lines
34 KiB
Python
# Copyright (c) 2021-2022, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import TYPE_CHECKING, Optional
|
|
from abc import abstractmethod
|
|
import logging
|
|
import math
|
|
|
|
from ezdxf.math import (
|
|
Vec2,
|
|
Vec3,
|
|
NULLVEC,
|
|
UCS,
|
|
decdeg2dms,
|
|
arc_angle_span_rad,
|
|
xround,
|
|
)
|
|
from ezdxf.entities import DimStyleOverride, Dimension, DXFEntity
|
|
from .dim_base import (
|
|
BaseDimensionRenderer,
|
|
get_required_defpoint,
|
|
format_text,
|
|
apply_dimpost,
|
|
Tolerance,
|
|
Measurement,
|
|
LengthMeasurement,
|
|
compile_mtext,
|
|
order_leader_points,
|
|
get_center_leader_points,
|
|
)
|
|
from ezdxf.render.arrows import ARROWS, arrow_length
|
|
from ezdxf.tools.text import is_upside_down_text_angle
|
|
from ezdxf.math import intersection_line_line_2d
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.eztypes import GenericLayoutType
|
|
|
|
__all__ = ["AngularDimension", "Angular3PDimension", "ArcLengthDimension"]
|
|
logger = logging.getLogger("ezdxf")
|
|
|
|
ARC_PREFIX = "( "
|
|
|
|
|
|
def has_required_attributes(entity: DXFEntity, attrib_names: list[str]):
|
|
has = entity.dxf.hasattr
|
|
return all(has(attrib_name) for attrib_name in attrib_names)
|
|
|
|
|
|
GRAD = 200.0 / math.pi
|
|
DEG = 180.0 / math.pi
|
|
|
|
|
|
def format_angular_text(
|
|
value: float,
|
|
angle_units: int,
|
|
dimrnd: Optional[float],
|
|
dimdec: int,
|
|
dimzin: int,
|
|
dimdsep: str,
|
|
) -> str:
|
|
def decimal_format(_value: float) -> str:
|
|
return format_text(
|
|
_value,
|
|
dimrnd=dimrnd,
|
|
dimdec=dimdec,
|
|
dimzin=dimzin,
|
|
dimdsep=dimdsep,
|
|
)
|
|
|
|
def dms_format(_value: float) -> str:
|
|
if dimrnd is not None:
|
|
_value = xround(_value, dimrnd)
|
|
d, m, s = decdeg2dms(_value)
|
|
if dimdec > 4:
|
|
places = dimdec - 5
|
|
s = round(s, places)
|
|
return f"{d:.0f}°{m:.0f}'{decimal_format(s)}\""
|
|
if dimdec > 2:
|
|
return f"{d:.0f}°{m:.0f}'{s:.0f}\""
|
|
if dimdec > 0:
|
|
return f"{d:.0f}°{m:.0f}'"
|
|
return f"{d:.0f}°"
|
|
|
|
# angular_unit:
|
|
# 0 = Decimal degrees
|
|
# 1 = Degrees/minutes/seconds
|
|
# 2 = Grad
|
|
# 3 = Radians
|
|
text = ""
|
|
if angle_units == 0:
|
|
text = decimal_format(value * DEG) + "°"
|
|
elif angle_units == 1:
|
|
text = dms_format(value * DEG)
|
|
elif angle_units == 2:
|
|
text = decimal_format(value * GRAD) + "g"
|
|
elif angle_units == 3:
|
|
text = decimal_format(value) + "r"
|
|
return text
|
|
|
|
|
|
_ANGLE_UNITS = [
|
|
DEG,
|
|
DEG,
|
|
GRAD,
|
|
1.0,
|
|
]
|
|
|
|
|
|
def to_radians(value: float, dimaunit: int) -> float:
|
|
try:
|
|
return value / _ANGLE_UNITS[dimaunit]
|
|
except IndexError:
|
|
return value / DEG
|
|
|
|
|
|
class AngularTolerance(Tolerance):
|
|
def __init__(
|
|
self,
|
|
dim_style: DimStyleOverride,
|
|
cap_height: float = 1.0,
|
|
width_factor: float = 1.0,
|
|
dim_scale: float = 1.0,
|
|
angle_units: int = 0,
|
|
):
|
|
self.angular_units = angle_units
|
|
super().__init__(dim_style, cap_height, width_factor, dim_scale)
|
|
# Tolerance values are interpreted in dimaunit:
|
|
# dimtp 1 means 1 degree for dimaunit = 0 or 1, but 1 radians for
|
|
# dimaunit = 3
|
|
# format_text() requires radians as input:
|
|
self.update_tolerance_text(
|
|
to_radians(self.maximum, angle_units),
|
|
to_radians(self.minimum, angle_units),
|
|
)
|
|
|
|
def format_text(self, value: float) -> str:
|
|
"""Rounding and text formatting of tolerance `value`, removes leading
|
|
and trailing zeros if necessary.
|
|
|
|
"""
|
|
return format_angular_text(
|
|
value=value,
|
|
angle_units=self.angular_units,
|
|
dimrnd=None,
|
|
dimdec=self.decimal_places,
|
|
dimzin=self.suppress_zeros,
|
|
dimdsep=self.text_decimal_separator,
|
|
)
|
|
|
|
def update_limits(self, measurement: float) -> None:
|
|
# measurement is in radians, tolerance values are interpreted in
|
|
# dimaunit: dimtp 1 means 1 degree for dimaunit = 0 or 1,
|
|
# but 1 radians for dimaunit = 3
|
|
# format_text() requires radians as input:
|
|
upper_limit = measurement + to_radians(self.maximum, self.angular_units)
|
|
lower_limit = measurement - to_radians(self.minimum, self.angular_units)
|
|
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 AngleMeasurement(Measurement):
|
|
def update(self, raw_measurement_value: float) -> None:
|
|
self.raw_value = raw_measurement_value
|
|
self.value = raw_measurement_value
|
|
self.text = self.text_override(raw_measurement_value)
|
|
|
|
def format_text(self, value: float) -> str:
|
|
text = format_angular_text(
|
|
value=value,
|
|
angle_units=self.angle_units,
|
|
dimrnd=None,
|
|
dimdec=self.angular_decimal_places,
|
|
dimzin=self.angular_suppress_zeros << 2, # convert to dimzin value
|
|
dimdsep=self.decimal_separator,
|
|
)
|
|
if self.text_post_process_format:
|
|
text = apply_dimpost(text, self.text_post_process_format)
|
|
return text
|
|
|
|
|
|
def fits_into_arc_span(length: float, radius: float, arc_span: float) -> bool:
|
|
required_arc_span: float = length / radius
|
|
return arc_span > required_arc_span
|
|
|
|
|
|
class _CurvedDimensionLine(BaseDimensionRenderer):
|
|
def __init__(
|
|
self,
|
|
dimension: Dimension,
|
|
ucs: Optional[UCS] = None,
|
|
override: Optional[DimStyleOverride] = None,
|
|
):
|
|
super().__init__(dimension, ucs, override)
|
|
# Common parameters for all sub-classes:
|
|
# Use hidden line detection for dimension line:
|
|
# Disable expensive hidden line calculation if possible!
|
|
self.remove_hidden_lines_of_dimline = True
|
|
self.center_of_arc: Vec2 = self.get_center_of_arc()
|
|
self.dim_line_radius: float = self.get_dim_line_radius()
|
|
self.ext1_dir: Vec2 = self.get_ext1_dir()
|
|
self.start_angle_rad: float = self.ext1_dir.angle
|
|
self.ext2_dir: Vec2 = self.get_ext2_dir()
|
|
self.end_angle_rad: float = self.ext2_dir.angle
|
|
|
|
# Angle between extension lines for all curved dimensions:
|
|
# equal to the angle measurement of angular dimensions
|
|
self.arc_angle_span_rad: float = arc_angle_span_rad(
|
|
self.start_angle_rad, self.end_angle_rad
|
|
)
|
|
self.center_angle_rad = (
|
|
self.start_angle_rad + self.arc_angle_span_rad / 2.0
|
|
)
|
|
|
|
# Additional required parameters but calculated later by sub-classes:
|
|
self.ext1_start = Vec2() # start of 1st extension line
|
|
self.ext2_start = Vec2() # start of 2nd extension line
|
|
|
|
# Class specific setup:
|
|
self.update_measurement()
|
|
if self.tol.has_limits:
|
|
self.tol.update_limits(self.measurement.value)
|
|
|
|
# Text width and -height is required first, text location and -rotation
|
|
# are not valid yet:
|
|
self.text_box = self.init_text_box()
|
|
|
|
# Place arrows outside?
|
|
self.arrows_outside = False
|
|
|
|
self.setup_text_and_arrow_fitting()
|
|
self.setup_text_location()
|
|
|
|
# update text box location and -rotation:
|
|
self.text_box.center = self.measurement.text_location
|
|
self.text_box.angle = self.measurement.text_rotation
|
|
self.geometry.set_text_box(self.text_box)
|
|
|
|
# Update final text location in the DIMENSION entity:
|
|
self.dimension.dxf.text_midpoint = self.measurement.text_location
|
|
|
|
@property
|
|
def ocs_center_of_arc(self) -> Vec3:
|
|
return self.geometry.ucs.to_ocs(Vec3(self.center_of_arc))
|
|
|
|
@property
|
|
def dim_midpoint(self) -> Vec2:
|
|
"""Return the midpoint of the dimension line."""
|
|
return self.center_of_arc + Vec2.from_angle(
|
|
self.center_angle_rad, self.dim_line_radius
|
|
)
|
|
|
|
@abstractmethod
|
|
def update_measurement(self) -> None:
|
|
"""Setup measurement text."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def get_ext1_dir(self) -> Vec2:
|
|
"""Return the direction of the 1st extension line == start angle."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def get_ext2_dir(self) -> Vec2:
|
|
"""Return the direction of the 2nd extension line == end angle."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def get_center_of_arc(self) -> Vec2:
|
|
"""Return the center of the arc."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def get_dim_line_radius(self) -> float:
|
|
"""Return the distance from the center of the arc to the dimension line
|
|
location
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
def get_defpoints(self) -> list[Vec2]:
|
|
...
|
|
|
|
def transform_ucs_to_wcs(self) -> None:
|
|
"""Transforms dimension definition points into WCS or if required into
|
|
OCS.
|
|
"""
|
|
|
|
def from_ucs(attr, func):
|
|
if dxf.is_supported(attr):
|
|
point = dxf.get(attr, NULLVEC)
|
|
dxf.set(attr, func(point))
|
|
|
|
dxf = self.dimension.dxf
|
|
ucs = self.geometry.ucs
|
|
from_ucs("defpoint", ucs.to_wcs)
|
|
from_ucs("defpoint2", ucs.to_wcs)
|
|
from_ucs("defpoint3", ucs.to_wcs)
|
|
from_ucs("defpoint4", ucs.to_wcs)
|
|
from_ucs("defpoint5", ucs.to_wcs)
|
|
from_ucs("text_midpoint", ucs.to_ocs)
|
|
|
|
def default_location(self, shift: float = 0.0) -> Vec2:
|
|
radius = (
|
|
self.dim_line_radius
|
|
+ self.measurement.text_vertical_distance()
|
|
+ shift
|
|
)
|
|
text_radial_dir = Vec2.from_angle(self.center_angle_rad)
|
|
return self.center_of_arc + text_radial_dir * radius
|
|
|
|
def setup_text_and_arrow_fitting(self) -> None:
|
|
# self.text_box.width includes the gaps between text and dimension line
|
|
# Is the measurement text without the arrows too wide to fit between the
|
|
# extension lines?
|
|
self.measurement.is_wide_text = not fits_into_arc_span(
|
|
self.text_box.width, self.dim_line_radius, self.arc_angle_span_rad
|
|
)
|
|
|
|
required_text_and_arrows_space: float = (
|
|
# The suppression of the arrows is not taken into account:
|
|
self.text_box.width
|
|
+ 2.0 * self.arrows.arrow_size
|
|
)
|
|
|
|
# dimatfit: measurement text fitting rule is ignored!
|
|
# Place arrows outside?
|
|
self.arrows_outside = not fits_into_arc_span(
|
|
required_text_and_arrows_space,
|
|
self.dim_line_radius,
|
|
self.arc_angle_span_rad,
|
|
)
|
|
# Place measurement text outside?
|
|
self.measurement.text_is_outside = not fits_into_arc_span(
|
|
required_text_and_arrows_space * 1.1, # add some extra space
|
|
self.dim_line_radius,
|
|
self.arc_angle_span_rad,
|
|
)
|
|
|
|
if (
|
|
self.measurement.text_is_outside
|
|
and self.measurement.user_text_rotation is None
|
|
):
|
|
# Intersection of the measurement text with the dimension line is
|
|
# not possible:
|
|
self.remove_hidden_lines_of_dimline = False
|
|
|
|
def setup_text_location(self) -> None:
|
|
"""Setup geometric text properties (location, rotation) and the TextBox
|
|
object.
|
|
"""
|
|
# dimtix: measurement.force_text_inside is ignored
|
|
# dimtih: measurement.text_inside_horizontal is ignored
|
|
# dimtoh: measurement.text_outside_horizontal is ignored
|
|
|
|
# text radial direction = center -> text
|
|
text_radial_dir: Vec2 # text "vertical" direction
|
|
measurement = self.measurement
|
|
|
|
# determine text location:
|
|
at_default_location: bool = measurement.user_location is None
|
|
has_text_shifting: bool = bool(
|
|
measurement.text_shift_h or measurement.text_shift_v
|
|
)
|
|
if at_default_location:
|
|
# place text in the "horizontal" center of the dimension line at the
|
|
# default location defined by measurement.text_valign (dimtad):
|
|
text_radial_dir = Vec2.from_angle(self.center_angle_rad)
|
|
shift_text_upwards: float = 0.0
|
|
if measurement.text_is_outside:
|
|
# reset vertical alignment to "above"
|
|
measurement.text_valign = 1
|
|
if measurement.is_wide_text:
|
|
# move measurement text "above" the extension line endings:
|
|
shift_text_upwards = self.extension_lines.extension_above
|
|
measurement.text_location = self.default_location(
|
|
shift=shift_text_upwards
|
|
)
|
|
if (
|
|
measurement.text_valign > 0 and not has_text_shifting
|
|
): # not in the center and no text shifting is applied
|
|
# disable expensive hidden line calculation
|
|
self.remove_hidden_lines_of_dimline = False
|
|
else:
|
|
# apply dimtmove: measurement.text_movement_rule
|
|
user_location = measurement.user_location
|
|
assert isinstance(user_location, Vec2)
|
|
if measurement.relative_user_location:
|
|
user_location += self.dim_midpoint
|
|
measurement.text_location = user_location
|
|
if measurement.text_movement_rule == 0:
|
|
# Moves the dimension line with dimension text and
|
|
# aligns the text direction perpendicular to the connection
|
|
# line from the arc center to the text center:
|
|
self.dim_line_radius = (
|
|
self.center_of_arc - user_location
|
|
).magnitude
|
|
# Attributes about the text and arrow fitting have to be
|
|
# updated now:
|
|
self.setup_text_and_arrow_fitting()
|
|
elif measurement.text_movement_rule == 1:
|
|
# Adds a leader when dimension text, text direction is
|
|
# "horizontal" or user text rotation if given.
|
|
# Leader location is defined by dimtad (text_valign):
|
|
# "center" - connects to the left or right center of the text
|
|
# "below" - add a line below the text
|
|
if measurement.user_text_rotation is None:
|
|
# override text rotation
|
|
measurement.user_text_rotation = 0.0
|
|
measurement.text_is_outside = True # by definition
|
|
elif measurement.text_movement_rule == 2:
|
|
# Allows text to be moved freely without a leader and
|
|
# aligns the text direction perpendicular to the connection
|
|
# line from the arc center to the text center:
|
|
measurement.text_is_outside = True # by definition
|
|
text_radial_dir = (
|
|
measurement.text_location - self.center_of_arc
|
|
).normalize()
|
|
|
|
# set text "horizontal":
|
|
text_tangential_dir = text_radial_dir.orthogonal(ccw=False)
|
|
|
|
if at_default_location and has_text_shifting:
|
|
# Apply text relative shift (ezdxf only feature)
|
|
if measurement.text_shift_h:
|
|
measurement.text_location += (
|
|
text_tangential_dir * measurement.text_shift_h
|
|
)
|
|
if measurement.text_shift_v:
|
|
measurement.text_location += (
|
|
text_radial_dir * measurement.text_shift_v
|
|
)
|
|
|
|
# apply user text rotation; rotation in degrees:
|
|
if measurement.user_text_rotation is None:
|
|
rotation = text_tangential_dir.angle_deg
|
|
else:
|
|
rotation = measurement.user_text_rotation
|
|
|
|
if not self.geometry.requires_extrusion:
|
|
# todo: extrusion vector (0, 0, -1)?
|
|
# Practically all DIMENSION entities are 2D entities,
|
|
# where OCS == WCS, check WCS text orientation:
|
|
wcs_angle = self.geometry.ucs.to_ocs_angle_deg(rotation)
|
|
if is_upside_down_text_angle(wcs_angle):
|
|
measurement.has_upside_down_correction = True
|
|
rotation += 180.0 # apply to UCS rotation!
|
|
measurement.text_rotation = rotation
|
|
|
|
def get_leader_points(self) -> tuple[Vec2, Vec2]:
|
|
# Leader location is defined by dimtad (text_valign):
|
|
# "center":
|
|
# - connects to the left or right vertical center of the text
|
|
# - distance between text and leader line is measurement.text_gap (dimgap)
|
|
# and is already included in the text_box corner points
|
|
# - length of "leg": arrows.arrow_size
|
|
# "below" - add a line below the text
|
|
if self.measurement.text_valign == 0: # "center"
|
|
return get_center_leader_points(
|
|
self.dim_midpoint, self.text_box, self.arrows.arrow_size
|
|
)
|
|
else: # "below"
|
|
c0, c1, c2, c3 = self.text_box.corners
|
|
if self.measurement.has_upside_down_correction:
|
|
p1, p2 = c2, c3
|
|
else:
|
|
p1, p2 = c0, c1
|
|
return order_leader_points(self.dim_midpoint, p1, p2)
|
|
|
|
def render(self, block: GenericLayoutType) -> None:
|
|
"""Main method to create dimension geometry of basic DXF entities in the
|
|
associated BLOCK layout.
|
|
|
|
Args:
|
|
block: target BLOCK for rendering
|
|
|
|
"""
|
|
super().render(block)
|
|
self.add_extension_lines()
|
|
adjust_start_angle, adjust_end_angle = self.add_arrows()
|
|
|
|
measurement = self.measurement
|
|
if measurement.text:
|
|
if self.geometry.supports_dxf_r2000:
|
|
text = compile_mtext(measurement, self.tol)
|
|
else:
|
|
text = measurement.text
|
|
self.add_measurement_text(
|
|
text, measurement.text_location, measurement.text_rotation
|
|
)
|
|
if measurement.has_leader:
|
|
p1, p2 = self.get_leader_points()
|
|
self.add_leader(self.dim_midpoint, p1, p2)
|
|
self.add_dimension_line(adjust_start_angle, adjust_end_angle)
|
|
self.geometry.add_defpoints(self.get_defpoints())
|
|
|
|
def add_extension_lines(self) -> None:
|
|
ext_lines = self.extension_lines
|
|
if not ext_lines.suppress1:
|
|
self._add_ext_line(
|
|
self.ext1_start, self.ext1_dir, ext_lines.dxfattribs(1)
|
|
)
|
|
if not ext_lines.suppress2:
|
|
self._add_ext_line(
|
|
self.ext2_start, self.ext2_dir, ext_lines.dxfattribs(2)
|
|
)
|
|
|
|
def _add_ext_line(self, start: Vec2, direction: Vec2, dxfattribs) -> None:
|
|
ext_lines = self.extension_lines
|
|
center = self.center_of_arc
|
|
radius = self.dim_line_radius
|
|
ext_above = ext_lines.extension_above
|
|
is_inside = (start - center).magnitude > radius
|
|
|
|
if ext_lines.has_fixed_length:
|
|
ext_below = ext_lines.length_below
|
|
if is_inside:
|
|
ext_below, ext_above = ext_above, ext_below
|
|
start = center + direction * (radius - ext_below)
|
|
else:
|
|
offset = ext_lines.offset
|
|
if is_inside:
|
|
ext_above = -ext_above
|
|
offset = -offset
|
|
start += direction * offset
|
|
end = center + direction * (radius + ext_above)
|
|
self.add_line(start, end, dxfattribs=dxfattribs)
|
|
|
|
def add_arrows(self) -> tuple[float, float]:
|
|
"""Add arrows or ticks to dimension.
|
|
|
|
Returns: dimension start- and end angle offsets to adjust the
|
|
dimension line
|
|
|
|
"""
|
|
arrows = self.arrows
|
|
attribs = arrows.dxfattribs()
|
|
radius = self.dim_line_radius
|
|
if abs(radius) < 1e-12:
|
|
return 0.0, 0.0
|
|
|
|
start = self.center_of_arc + self.ext1_dir * radius
|
|
end = self.center_of_arc + self.ext2_dir * radius
|
|
angle1 = self.ext1_dir.orthogonal().angle_deg
|
|
angle2 = self.ext2_dir.orthogonal().angle_deg
|
|
outside = self.arrows_outside
|
|
arrow1 = not arrows.suppress1
|
|
arrow2 = not arrows.suppress2
|
|
start_angle_offset = 0.0
|
|
end_angle_offset = 0.0
|
|
if arrows.tick_size > 0.0: # oblique stroke, but double the size
|
|
if arrow1:
|
|
self.add_blockref(
|
|
ARROWS.oblique,
|
|
insert=start,
|
|
rotation=angle1,
|
|
scale=arrows.tick_size * 2.0,
|
|
dxfattribs=attribs,
|
|
)
|
|
if arrow2:
|
|
self.add_blockref(
|
|
ARROWS.oblique,
|
|
insert=end,
|
|
rotation=angle2,
|
|
scale=arrows.tick_size * 2.0,
|
|
dxfattribs=attribs,
|
|
)
|
|
else:
|
|
arrow_size = arrows.arrow_size
|
|
# Note: The arrow blocks are correct as they are!
|
|
# The arrow head is tilted to match the connection point of the
|
|
# dimension line (even for datum arrows).
|
|
# tilting angle = 1/2 of the arc angle defined by the arrow length
|
|
arrow_tilt: float = arrow_size / radius * 0.5 * DEG
|
|
start_angle = angle1 + 180.0
|
|
end_angle = angle2
|
|
if outside:
|
|
start_angle += 180.0
|
|
end_angle += 180.0
|
|
arrow_tilt = -arrow_tilt
|
|
scale = arrow_size
|
|
if arrow1:
|
|
self.add_blockref(
|
|
arrows.arrow1_name,
|
|
insert=start,
|
|
scale=scale,
|
|
rotation=start_angle + arrow_tilt,
|
|
dxfattribs=attribs,
|
|
) # reverse
|
|
if arrow2:
|
|
self.add_blockref(
|
|
arrows.arrow2_name,
|
|
insert=end,
|
|
scale=scale,
|
|
rotation=end_angle - arrow_tilt,
|
|
dxfattribs=attribs,
|
|
)
|
|
if not outside:
|
|
# arrows inside extension lines:
|
|
# adjust angles for the remaining dimension line
|
|
if arrow1:
|
|
start_angle_offset = (
|
|
arrow_length(arrows.arrow1_name, arrow_size) / radius
|
|
)
|
|
if arrow2:
|
|
end_angle_offset = (
|
|
arrow_length(arrows.arrow2_name, arrow_size) / radius
|
|
)
|
|
return start_angle_offset, end_angle_offset
|
|
|
|
def add_dimension_line(
|
|
self,
|
|
start_offset: float,
|
|
end_offset: float,
|
|
) -> None:
|
|
# Start- and end angle adjustments have to be limited between the
|
|
# extension lines.
|
|
# Negative offset extends the dimension line outside!
|
|
start_angle: float = self.start_angle_rad
|
|
end_angle: float = self.end_angle_rad
|
|
arrows = self.arrows
|
|
size = arrows.arrow_size
|
|
radius = self.dim_line_radius
|
|
max_adjustment: float = abs(self.arc_angle_span_rad) / 2.0
|
|
|
|
if start_offset > max_adjustment:
|
|
start_offset = 0.0
|
|
if end_offset > max_adjustment:
|
|
end_offset = 0.0
|
|
|
|
self.add_arc(
|
|
self.center_of_arc,
|
|
radius,
|
|
start_angle + start_offset,
|
|
end_angle - end_offset,
|
|
dxfattribs=self.dimension_line.dxfattribs(),
|
|
# hidden line detection if text is not placed outside:
|
|
remove_hidden_lines=self.remove_hidden_lines_of_dimline,
|
|
)
|
|
if self.arrows_outside and not arrows.has_ticks:
|
|
# add arrow extension lines
|
|
start_offset, end_offset = arrow_offset_angles(
|
|
arrows.arrow1_name, size, radius
|
|
)
|
|
self.add_arrow_extension_line(
|
|
start_angle - end_offset,
|
|
start_angle - start_offset,
|
|
)
|
|
start_offset, end_offset = arrow_offset_angles(
|
|
arrows.arrow1_name, size, radius
|
|
)
|
|
self.add_arrow_extension_line(
|
|
end_angle + start_offset,
|
|
end_angle + end_offset,
|
|
)
|
|
|
|
def add_arrow_extension_line(self, start_angle: float, end_angle: float):
|
|
self.add_arc(
|
|
self.center_of_arc,
|
|
self.dim_line_radius,
|
|
start_angle=start_angle,
|
|
end_angle=end_angle,
|
|
dxfattribs=self.dimension_line.dxfattribs(),
|
|
)
|
|
|
|
def add_measurement_text(
|
|
self, dim_text: str, pos: Vec2, rotation: float
|
|
) -> None:
|
|
"""Add measurement text to dimension BLOCK.
|
|
|
|
Args:
|
|
dim_text: dimension text
|
|
pos: text location
|
|
rotation: text rotation in degrees
|
|
|
|
"""
|
|
attribs = self.measurement.dxfattribs()
|
|
self.add_text(dim_text, pos=pos, rotation=rotation, dxfattribs=attribs)
|
|
|
|
|
|
class _AngularCommonBase(_CurvedDimensionLine):
|
|
def init_tolerance(
|
|
self, scale: float, measurement: Measurement
|
|
) -> Tolerance:
|
|
return AngularTolerance(
|
|
self.dim_style,
|
|
cap_height=measurement.text_height,
|
|
width_factor=measurement.text_width_factor,
|
|
dim_scale=scale,
|
|
angle_units=measurement.angle_units,
|
|
)
|
|
|
|
def init_measurement(self, color: int, scale: float) -> Measurement:
|
|
return AngleMeasurement(
|
|
self.dim_style, self.default_color, self.dim_scale
|
|
)
|
|
|
|
def update_measurement(self) -> None:
|
|
self.measurement.update(self.arc_angle_span_rad)
|
|
|
|
|
|
class AngularDimension(_AngularCommonBase):
|
|
"""
|
|
Angular dimension line renderer. The dimension line is defined by two lines.
|
|
|
|
Supported render types:
|
|
|
|
- default location above
|
|
- default location center
|
|
- user defined location, text aligned with dimension line
|
|
- user defined location horizontal text
|
|
|
|
Args:
|
|
dimension: DIMENSION entity
|
|
ucs: user defined coordinate system
|
|
override: dimension style override management object
|
|
|
|
"""
|
|
|
|
# Required defpoints:
|
|
# defpoint = start point of 1st leg (group code 10)
|
|
# defpoint4 = end point of 1st leg (group code 15)
|
|
# defpoint3 = start point of 2nd leg (group code 14)
|
|
# defpoint2 = end point of 2nd leg (group code 13)
|
|
# defpoint5 = location of dimension line (group code 16)
|
|
|
|
# unsupported or ignored features (at least by BricsCAD):
|
|
# dimtih: text inside horizontal
|
|
# dimtoh: text outside horizontal
|
|
# dimjust: text position horizontal
|
|
# dimdle: dimline extension
|
|
|
|
def __init__(
|
|
self,
|
|
dimension: Dimension,
|
|
ucs: Optional[UCS] = None,
|
|
override: Optional[DimStyleOverride] = None,
|
|
):
|
|
self.leg1_start = get_required_defpoint(dimension, "defpoint")
|
|
self.leg1_end = get_required_defpoint(dimension, "defpoint4")
|
|
self.leg2_start = get_required_defpoint(dimension, "defpoint3")
|
|
self.leg2_end = get_required_defpoint(dimension, "defpoint2")
|
|
self.dim_line_location = get_required_defpoint(dimension, "defpoint5")
|
|
super().__init__(dimension, ucs, override)
|
|
# The extension line parameters depending on the location of the
|
|
# dimension line related to the definition point.
|
|
# Detect the extension start point.
|
|
# Which definition point is closer to the dimension line:
|
|
self.ext1_start = detect_closer_defpoint(
|
|
direction=self.ext1_dir,
|
|
base=self.dim_line_location,
|
|
p1=self.leg1_start,
|
|
p2=self.leg1_end,
|
|
)
|
|
self.ext2_start = detect_closer_defpoint(
|
|
direction=self.ext2_dir,
|
|
base=self.dim_line_location,
|
|
p1=self.leg2_start,
|
|
p2=self.leg2_end,
|
|
)
|
|
|
|
def get_defpoints(self) -> list[Vec2]:
|
|
return [
|
|
self.leg1_start,
|
|
self.leg1_end,
|
|
self.leg2_start,
|
|
self.leg2_end,
|
|
self.dim_line_location,
|
|
]
|
|
|
|
def get_center_of_arc(self) -> Vec2:
|
|
center = intersection_line_line_2d(
|
|
(self.leg1_start, self.leg1_end),
|
|
(self.leg2_start, self.leg2_end),
|
|
)
|
|
if center is None:
|
|
logger.warning(
|
|
f"Invalid colinear or parallel angle legs found in {self.dimension})"
|
|
)
|
|
# This case can not be created by the GUI in BricsCAD, but DXF
|
|
# files can contain any shit!
|
|
# The interpolation of the end-points is an arbitrary choice and
|
|
# maybe not the best choice!
|
|
center = self.leg1_end.lerp(self.leg2_end)
|
|
return center
|
|
|
|
def get_dim_line_radius(self) -> float:
|
|
return (self.dim_line_location - self.center_of_arc).magnitude
|
|
|
|
def get_ext1_dir(self) -> Vec2:
|
|
center = self.center_of_arc
|
|
start = (
|
|
self.leg1_end
|
|
if self.leg1_start.isclose(center)
|
|
else self.leg1_start
|
|
)
|
|
return (start - center).normalize()
|
|
|
|
def get_ext2_dir(self) -> Vec2:
|
|
center = self.center_of_arc
|
|
start = (
|
|
self.leg2_end
|
|
if self.leg2_start.isclose(center)
|
|
else self.leg2_start
|
|
)
|
|
return (start - center).normalize()
|
|
|
|
|
|
class Angular3PDimension(_AngularCommonBase):
|
|
"""
|
|
Angular dimension line renderer. The dimension line is defined by three
|
|
points.
|
|
|
|
Supported render types:
|
|
|
|
- default location above
|
|
- default location center
|
|
- user defined location, text aligned with dimension line
|
|
- user defined location horizontal text
|
|
|
|
Args:
|
|
dimension: DIMENSION entity
|
|
ucs: user defined coordinate system
|
|
override: dimension style override management object
|
|
|
|
"""
|
|
|
|
# Required defpoints:
|
|
# defpoint = location of dimension line (group code 10)
|
|
# defpoint2 = 1st leg (group code 13)
|
|
# defpoint3 = 2nd leg (group code 14)
|
|
# defpoint4 = center of angle (group code 15)
|
|
|
|
def __init__(
|
|
self,
|
|
dimension: Dimension,
|
|
ucs: Optional[UCS] = None,
|
|
override: Optional[DimStyleOverride] = None,
|
|
):
|
|
self.dim_line_location = get_required_defpoint(dimension, "defpoint")
|
|
self.leg1_start = get_required_defpoint(dimension, "defpoint2")
|
|
self.leg2_start = get_required_defpoint(dimension, "defpoint3")
|
|
self.center_of_arc = get_required_defpoint(dimension, "defpoint4")
|
|
super().__init__(dimension, ucs, override)
|
|
self.ext1_start = self.leg1_start
|
|
self.ext2_start = self.leg2_start
|
|
|
|
def get_defpoints(self) -> list[Vec2]:
|
|
return [
|
|
self.dim_line_location,
|
|
self.leg1_start,
|
|
self.leg2_start,
|
|
self.center_of_arc,
|
|
]
|
|
|
|
def get_center_of_arc(self) -> Vec2:
|
|
return self.center_of_arc
|
|
|
|
def get_dim_line_radius(self) -> float:
|
|
return (self.dim_line_location - self.center_of_arc).magnitude
|
|
|
|
def get_ext1_dir(self) -> Vec2:
|
|
return (self.leg1_start - self.center_of_arc).normalize()
|
|
|
|
def get_ext2_dir(self) -> Vec2:
|
|
return (self.leg2_start - self.center_of_arc).normalize()
|
|
|
|
|
|
class ArcLengthMeasurement(LengthMeasurement):
|
|
def format_text(self, value: float) -> str:
|
|
text = format_text(
|
|
value=value,
|
|
dimrnd=self.text_round,
|
|
dimdec=self.decimal_places,
|
|
dimzin=self.suppress_zeros,
|
|
dimdsep=self.decimal_separator,
|
|
)
|
|
if self.has_arc_length_prefix:
|
|
text = ARC_PREFIX + text
|
|
if self.text_post_process_format:
|
|
text = apply_dimpost(text, self.text_post_process_format)
|
|
return text
|
|
|
|
|
|
class ArcLengthDimension(_CurvedDimensionLine):
|
|
"""Arc length dimension line renderer.
|
|
Requires DXF R2004.
|
|
|
|
Supported render types:
|
|
|
|
- default location above
|
|
- default location center
|
|
- user defined location, text aligned with dimension line
|
|
- user defined location horizontal text
|
|
|
|
Args:
|
|
dimension: DXF entity DIMENSION
|
|
ucs: user defined coordinate system
|
|
override: dimension style override management object
|
|
|
|
"""
|
|
|
|
# Required defpoints:
|
|
# defpoint = location of dimension line (group code 10)
|
|
# defpoint2 = 1st arc point (group code 13)
|
|
# defpoint3 = 2nd arc point (group code 14)
|
|
# defpoint4 = center of arc (group code 15)
|
|
|
|
def __init__(
|
|
self,
|
|
dimension: Dimension,
|
|
ucs: Optional[UCS] = None,
|
|
override: Optional[DimStyleOverride] = None,
|
|
):
|
|
self.dim_line_location = get_required_defpoint(dimension, "defpoint")
|
|
self.leg1_start = get_required_defpoint(dimension, "defpoint2")
|
|
self.leg2_start = get_required_defpoint(dimension, "defpoint3")
|
|
self.center_of_arc = get_required_defpoint(dimension, "defpoint4")
|
|
self.arc_radius = (self.leg1_start - self.center_of_arc).magnitude
|
|
super().__init__(dimension, ucs, override)
|
|
self.ext1_start = self.leg1_start
|
|
self.ext2_start = self.leg2_start
|
|
|
|
def get_defpoints(self) -> list[Vec2]:
|
|
return [
|
|
self.dim_line_location,
|
|
self.leg1_start,
|
|
self.leg2_start,
|
|
self.center_of_arc,
|
|
]
|
|
|
|
def init_measurement(self, color: int, scale: float) -> Measurement:
|
|
return ArcLengthMeasurement(
|
|
self.dim_style, self.default_color, self.dim_scale
|
|
)
|
|
|
|
def get_center_of_arc(self) -> Vec2:
|
|
return self.center_of_arc
|
|
|
|
def get_dim_line_radius(self) -> float:
|
|
return (self.dim_line_location - self.center_of_arc).magnitude
|
|
|
|
def get_ext1_dir(self) -> Vec2:
|
|
return (self.leg1_start - self.center_of_arc).normalize()
|
|
|
|
def get_ext2_dir(self) -> Vec2:
|
|
return (self.leg2_start - self.center_of_arc).normalize()
|
|
|
|
def update_measurement(self) -> None:
|
|
angle = arc_angle_span_rad(self.start_angle_rad, self.end_angle_rad)
|
|
arc_length = angle * self.arc_radius
|
|
self.measurement.update(arc_length)
|
|
|
|
|
|
def detect_closer_defpoint(
|
|
direction: Vec2, base: Vec2, p1: Vec2, p2: Vec2
|
|
) -> Vec2:
|
|
# Calculate the projected distance onto the (normalized) direction vector:
|
|
d0 = direction.dot(base)
|
|
d1 = direction.dot(p1)
|
|
d2 = direction.dot(p2)
|
|
# Which defpoint is closer to the base point (d0)?
|
|
if abs(d1 - d0) <= abs(d2 - d0):
|
|
return p1
|
|
return p2
|
|
|
|
|
|
def arrow_offset_angles(
|
|
arrow_name: str, size: float, radius: float
|
|
) -> tuple[float, float]:
|
|
start_offset: float = 0.0
|
|
end_offset: float = size / radius
|
|
length = arrow_length(arrow_name, size)
|
|
if length > 0.0:
|
|
start_offset = length / radius
|
|
end_offset *= 2.0
|
|
return start_offset, end_offset
|