# Copyright (c) 2018-2022, Manfred Moitzi # License: MIT License from __future__ import annotations from typing import TYPE_CHECKING, Optional from ezdxf.math import Vec2, UCS, UVec from ezdxf.tools import normalize_text_angle from ezdxf.render.arrows import ARROWS, connection_point from ezdxf.entities.dimstyleoverride import DimStyleOverride from ezdxf.lldxf.const import DXFInternalEzdxfError from ezdxf.render.dim_base import ( BaseDimensionRenderer, Measurement, LengthMeasurement, compile_mtext, ) if TYPE_CHECKING: from ezdxf.entities import Dimension from ezdxf.eztypes import GenericLayoutType class RadiusMeasurement(LengthMeasurement): def __init__( self, dim_style: DimStyleOverride, color: int, scale: float, prefix: str ): super().__init__(dim_style, color, scale) self.text_prefix = prefix def text_override(self, measurement: float) -> str: text = super().text_override(measurement) if text and text[0] != self.text_prefix: text = self.text_prefix + text return text class RadiusDimension(BaseDimensionRenderer): """ Radial dimension line renderer. Supported render types: - default location inside, text aligned with radial dimension line - default location inside horizontal text - default location outside, text aligned with radial dimension line - default location outside horizontal text - user defined location, text aligned with radial dimension line - user defined location horizontal text Args: dimension: DXF entity DIMENSION ucs: user defined coordinate system override: dimension style override management object """ # Super class of DiameterDimension def _center(self): return Vec2(self.dimension.dxf.defpoint) def __init__( self, dimension: Dimension, ucs: Optional[UCS] = None, override: Optional[DimStyleOverride] = None, ): super().__init__(dimension, ucs, override) dimtype = self.dimension.dimtype measurement = self.measurement if dimtype == 3: self.is_diameter_dim = True elif dimtype == 4: self.is_radius_dim = True else: raise DXFInternalEzdxfError(f"Invalid dimension type {dimtype}") self.center: Vec2 = self._center() # override in diameter dimension self.point_on_circle: Vec2 = Vec2(self.dimension.dxf.defpoint4) # modify parameters for special scenarios if measurement.user_location is None: if ( measurement.text_is_inside and measurement.text_inside_horizontal and measurement.text_movement_rule == 1 ): # move text, add leader # use algorithm for user define dimension line location measurement.user_location = self.center.lerp( self.point_on_circle ) measurement.text_valign = 0 # text vertical centered direction = self.point_on_circle - self.center self.dim_line_vec = direction.normalize() self.dim_line_angle = self.dim_line_vec.angle_deg self.radius = direction.magnitude # get_measurement() works for radius and diameter dimension measurement.update(self.dimension.get_measurement()) self.outside_default_distance = self.radius + 2 * self.arrows.arrow_size self.outside_default_defpoint = self.center + ( self.dim_line_vec * self.outside_default_distance ) self.outside_text_force_dimline = bool(self.dim_style.get("dimtofl", 1)) # final dimension text (without limits or tolerance) # default location is outside, if not forced to be inside measurement.text_is_outside = not measurement.force_text_inside # text_outside: user defined location, overrides default location if measurement.user_location is not None: measurement.text_is_outside = self.is_location_outside( measurement.user_location ) self._total_text_width: float = 0.0 if measurement.text: # text width and required space self._total_text_width = self.total_text_width() if self.tol.has_limits: # limits show the upper and lower limit of the measurement as # stacked values and with the size of tolerances self.tol.update_limits(measurement.value) # default rotation is angle of dimension line, from center to point on circle. rotation = self.dim_line_angle if measurement.text_is_outside and measurement.text_outside_horizontal: rotation = 0.0 elif measurement.text_is_inside and measurement.text_inside_horizontal: rotation = 0.0 # final absolute text rotation (x-axis=0) measurement.text_rotation = normalize_text_angle( rotation, fix_upside_down=True ) # final text location measurement.text_location = self.get_text_location() self.geometry.set_text_box(self.init_text_box()) # write final text location into DIMENSION entity if measurement.user_location: self.dimension.dxf.text_midpoint = measurement.user_location # default locations elif ( measurement.text_is_outside and measurement.text_outside_horizontal ): self.dimension.dxf.text_midpoint = self.outside_default_defpoint else: self.dimension.dxf.text_midpoint = measurement.text_location def init_measurement(self, color: int, scale: float) -> Measurement: return RadiusMeasurement(self.dim_style, color, scale, "R") def get_text_location(self) -> Vec2: """Returns text midpoint from user defined location or default text location. """ if self.measurement.user_location is not None: return self.get_user_defined_text_location() else: return self.get_default_text_location() def get_default_text_location(self) -> Vec2: """Returns default text midpoint based on `text_valign` and `text_outside` """ measurement = self.measurement if measurement.text_is_outside and measurement.text_outside_horizontal: hdist = self._total_text_width / 2.0 if ( measurement.vertical_placement == 0 ): # shift text horizontal if vertical centered hdist += self.arrows.arrow_size angle = self.dim_line_angle % 360.0 # normalize 0 .. 360 if 90.0 < angle <= 270.0: hdist = -hdist return self.outside_default_defpoint + Vec2( (hdist, measurement.text_vertical_distance()) ) text_direction = Vec2.from_deg_angle(measurement.text_rotation) vertical_direction = text_direction.orthogonal(ccw=True) vertical_distance = measurement.text_vertical_distance() if measurement.text_is_inside: hdist = (self.radius - self.arrows.arrow_size) / 2.0 text_midpoint = self.center + (self.dim_line_vec * hdist) else: hdist = ( self._total_text_width / 2.0 + self.arrows.arrow_size + measurement.text_gap ) text_midpoint = self.point_on_circle + (self.dim_line_vec * hdist) return text_midpoint + (vertical_direction * vertical_distance) def get_user_defined_text_location(self) -> Vec2: """Returns text midpoint for user defined dimension location.""" measurement = self.measurement assert isinstance(measurement.user_location, Vec2) text_outside_horiz = ( measurement.text_is_outside and measurement.text_outside_horizontal ) text_inside_horiz = ( measurement.text_is_inside and measurement.text_inside_horizontal ) if text_outside_horiz or text_inside_horiz: hdist = self._total_text_width / 2.0 if ( measurement.vertical_placement == 0 ): # shift text horizontal if vertical centered hdist += self.arrows.arrow_size if measurement.user_location.x <= self.point_on_circle.x: hdist = -hdist vdist = measurement.text_vertical_distance() return measurement.user_location + Vec2((hdist, vdist)) else: text_normal_vec = Vec2.from_deg_angle( measurement.text_rotation ).orthogonal() return ( measurement.user_location + text_normal_vec * measurement.text_vertical_distance() ) def is_location_outside(self, location: Vec2) -> bool: radius = (location - self.center).magnitude return radius > self.radius def render(self, block: GenericLayoutType) -> None: """Create dimension geometry of basic DXF entities in the associated BLOCK layout. """ # call required to setup some requirements super().render(block) measurement = self.measurement if not self.dimension_line.suppress1: if measurement.user_location is not None: self.render_user_location() else: self.render_default_location() # add measurement text as last entity to see text fill properly if measurement.text: if self.geometry.supports_dxf_r2000: text = compile_mtext(self.measurement, self.tol) else: text = measurement.text self.add_measurement_text( text, measurement.text_location, measurement.text_rotation ) # add POINT entities at definition points self.geometry.add_defpoints([self.center, self.point_on_circle]) def render_default_location(self) -> None: """Create dimension geometry at the default dimension line locations.""" measurement = self.measurement if not self.arrows.suppress1: arrow_connection_point = self.add_arrow( self.point_on_circle, rotate=measurement.text_is_outside ) else: arrow_connection_point = self.point_on_circle if measurement.text_is_outside: if self.outside_text_force_dimline: self.add_radial_dim_line(self.point_on_circle) else: add_center_mark(self) if measurement.text_outside_horizontal: self.add_horiz_ext_line_default(arrow_connection_point) else: self.add_radial_ext_line_default(arrow_connection_point) else: if measurement.text_movement_rule == 1: # move text, add leader -> dimline from text to point on circle self.add_radial_dim_line_from_text( self.center.lerp(self.point_on_circle), arrow_connection_point, ) add_center_mark(self) else: # dimline from center to point on circle self.add_radial_dim_line(arrow_connection_point) def render_user_location(self) -> None: """Create dimension geometry at user defined dimension locations.""" measurement = self.measurement preserve_outside = measurement.text_is_outside leader = measurement.text_movement_rule != 2 if not leader: measurement.text_is_outside = ( False # render dimension line like text inside ) # add arrow symbol (block references) if not self.arrows.suppress1: arrow_connection_point = self.add_arrow( self.point_on_circle, rotate=measurement.text_is_outside ) else: arrow_connection_point = self.point_on_circle if measurement.text_is_outside: if self.outside_text_force_dimline: self.add_radial_dim_line(self.point_on_circle) else: add_center_mark(self) if measurement.text_outside_horizontal: self.add_horiz_ext_line_user(arrow_connection_point) else: self.add_radial_ext_line_user(arrow_connection_point) else: if measurement.text_inside_horizontal: self.add_horiz_ext_line_user(arrow_connection_point) else: if measurement.text_movement_rule == 2: # move text, no leader! # dimline from center to point on circle self.add_radial_dim_line(arrow_connection_point) else: # move text, add leader -> dimline from text to point on circle self.add_radial_dim_line_from_text( measurement.user_location, arrow_connection_point ) add_center_mark(self) measurement.text_is_outside = preserve_outside def add_arrow(self, location, rotate: bool) -> Vec2: """Add arrow or tick to dimension line, returns dimension line connection point.""" arrows = self.arrows attribs = arrows.dxfattribs() arrow_name = arrows.arrow1_name if arrows.tick_size > 0.0: # oblique stroke, but double the size self.add_blockref( ARROWS.oblique, insert=location, rotation=self.dim_line_angle, scale=arrows.tick_size * 2.0, dxfattribs=attribs, ) else: scale = arrows.arrow_size angle = self.dim_line_angle if rotate: angle += 180.0 self.add_blockref( arrow_name, insert=location, scale=scale, rotation=angle, dxfattribs=attribs, ) location = connection_point(arrow_name, location, scale, angle) return location def add_radial_dim_line(self, end: UVec) -> None: """Add radial dimension line.""" attribs = self.dimension_line.dxfattribs() self.add_line( self.center, end, dxfattribs=attribs, remove_hidden_lines=True ) def add_radial_dim_line_from_text(self, start, end: UVec) -> None: """Add radial dimension line, starting point at the measurement text.""" attribs = self.dimension_line.dxfattribs() hshift = self._total_text_width / 2 if self.measurement.vertical_placement != 0: # not center hshift = -hshift self.add_line( start + self.dim_line_vec * hshift, end, dxfattribs=attribs, remove_hidden_lines=False, ) def add_horiz_ext_line_default(self, start: UVec) -> None: """Add horizontal outside extension line from start for default locations. """ attribs = self.dimension_line.dxfattribs() self.add_line(start, self.outside_default_defpoint, dxfattribs=attribs) if self.measurement.vertical_placement == 0: hdist = self.arrows.arrow_size else: hdist = self._total_text_width angle = self.dim_line_angle % 360.0 # normalize 0 .. 360 if 90 < angle <= 270: hdist = -hdist end = self.outside_default_defpoint + Vec2((hdist, 0)) self.add_line(self.outside_default_defpoint, end, dxfattribs=attribs) def add_horiz_ext_line_user(self, start: UVec) -> None: """Add horizontal extension line from start for user defined locations.""" measurement = self.measurement assert isinstance(measurement.user_location, Vec2) attribs = self.dimension_line.dxfattribs() self.add_line(start, measurement.user_location, dxfattribs=attribs) if measurement.vertical_placement == 0: hdist = self.arrows.arrow_size else: hdist = self._total_text_width if measurement.user_location.x <= self.point_on_circle.x: hdist = -hdist end = measurement.user_location + Vec2((hdist, 0)) self.add_line(measurement.user_location, end, dxfattribs=attribs) def add_radial_ext_line_default(self, start: UVec) -> None: """Add radial outside extension line from start for default locations.""" attribs = self.dimension_line.dxfattribs() length = self.measurement.text_gap + self._total_text_width end = start + self.dim_line_vec * length self.add_line(start, end, dxfattribs=attribs, remove_hidden_lines=True) def add_radial_ext_line_user(self, start: UVec) -> None: """Add radial outside extension line from start for user defined location.""" attribs = self.dimension_line.dxfattribs() length = self._total_text_width / 2.0 if self.measurement.vertical_placement == 0: length = -length end = self.measurement.user_location + self.dim_line_vec * length self.add_line(start, end, dxfattribs=attribs) def add_measurement_text( self, dim_text: str, pos: Vec2, rotation: float ) -> None: """Add measurement text to dimension BLOCK.""" attribs = self.measurement.dxfattribs() self.add_text(dim_text, pos=pos, rotation=rotation, dxfattribs=attribs) 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. """ def from_ucs(attr, func): point = self.dimension.get_dxf_attrib(attr) self.dimension.set_dxf_attrib(attr, func(point)) ucs = self.geometry.ucs from_ucs("defpoint", ucs.to_wcs) from_ucs("defpoint4", ucs.to_wcs) from_ucs("text_midpoint", ucs.to_ocs) def add_center_mark(dim: RadiusDimension) -> None: """Add center mark/lines to radius and diameter dimensions. Args: dim: RadiusDimension or DiameterDimension renderer """ dim_type = dim.dimension.dimtype if dim_type == 4: # Radius Dimension radius = dim.measurement.raw_value elif dim_type == 3: # Diameter Dimension radius = dim.measurement.raw_value / 2.0 else: raise TypeError(f"Invalid dimension type: {dim_type}") mark_size = dim.dim_style.get("dimcen", 0) if mark_size == 0: return center_lines = False if mark_size < 0: mark_size = abs(mark_size) center_lines = True center = Vec2(dim.center) # draw center mark mark_x_vec = Vec2((mark_size, 0)) mark_y_vec = Vec2((0, mark_size)) # use only color and ignore linetype! dxfattribs = {"color": dim.dimension_line.color} dim.add_line(center - mark_x_vec, center + mark_x_vec, dxfattribs) dim.add_line(center - mark_y_vec, center + mark_y_vec, dxfattribs) if center_lines: size = mark_size + radius if size < 2 * mark_size: return # not enough space for center lines start_x_vec = mark_x_vec * 2 start_y_vec = mark_y_vec * 2 end_x_vec = Vec2((size, 0)) end_y_vec = Vec2((0, size)) dim.add_line(center + start_x_vec, center + end_x_vec, dxfattribs) dim.add_line(center - start_x_vec, center - end_x_vec, dxfattribs) dim.add_line(center + start_y_vec, center + end_y_vec, dxfattribs) dim.add_line(center - start_y_vec, center - end_y_vec, dxfattribs)