123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- # Copyright (c) 2018 Manfred Moitzi
- # License: MIT License
- from typing import TYPE_CHECKING, Tuple
- from .vector import Vec2
- from .bbox import BoundingBox2d
- from .construct2d import ConstructionTool, enclosing_angles
- from .circle import ConstructionCircle
- from .ucs import OCS, UCS
- import math
- if TYPE_CHECKING:
- from ezdxf.eztypes import Vertex, GenericLayoutType
- from ezdxf.eztypes import Arc as DXFArc
- QUARTER_ANGLES = [0, math.pi * .5, math.pi, math.pi * 1.5]
- class ConstructionArc(ConstructionTool):
- def __init__(self,
- center: 'Vertex' = (0, 0),
- radius: float = 1,
- start_angle: float = 0,
- end_angle: float = 360,
- is_counter_clockwise: bool = True):
- self.center = Vec2(center)
- self.radius = radius
- if is_counter_clockwise:
- self.start_angle = start_angle
- self.end_angle = end_angle
- else:
- self.start_angle = end_angle
- self.end_angle = start_angle
- @property
- def start_point(self):
- return self.center + Vec2.from_deg_angle(self.start_angle, self.radius)
- @property
- def end_point(self):
- return self.center + Vec2.from_deg_angle(self.end_angle, self.radius)
- @property
- def bounding_box(self) -> 'BoundingBox2d':
- bbox = BoundingBox2d((self.start_point, self.end_point))
- bbox.extend(self.main_axis_points())
- return bbox
- def main_axis_points(self):
- center = self.center
- radius = self.radius
- start = math.radians(self.start_angle)
- end = math.radians(self.end_angle)
- for angle in QUARTER_ANGLES:
- if enclosing_angles(angle, start, end):
- yield center + Vec2.from_angle(angle, radius)
- def move(self, dx: float, dy: float) -> None:
- self.center += Vec2((dx, dy))
- @property
- def start_angle_rad(self) -> float:
- return math.radians(self.start_angle)
- @property
- def end_angle_rad(self) -> float:
- return math.radians(self.end_angle)
- @staticmethod
- def validate_start_and_end_point(start_point: 'Vertex', end_point: 'Vertex') -> Tuple[Vec2, Vec2]:
- start_point = Vec2(start_point)
- end_point = Vec2(end_point)
- if start_point == end_point:
- raise ValueError("start- and end point has to be different points.")
- return start_point, end_point
- @classmethod
- def from_2p_angle(cls, start_point: 'Vertex', end_point: 'Vertex', angle: float,
- ccw: bool = True) -> 'ConstructionArc':
- """
- Create arc from two points and enclosing angle. Additional precondition: arc goes by default in counter
- clockwise orientation from start_point to end_point, can be changed by ccw=False.
- Z-axis of start_point and end_point has to be 0 if given.
- Args:
- start_point: start point (x, y) as args accepted by Vec2()
- end_point: end point (x, y) as args accepted by Vec2()
- angle: enclosing angle in degrees
- ccw: counter clockwise direction True/False
- """
- start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
- angle = math.radians(angle)
- if angle == 0:
- raise ValueError("angle can not be 0.")
- if ccw is False:
- start_point, end_point = end_point, start_point
- alpha2 = angle / 2.
- distance = end_point.distance(start_point)
- distance2 = distance / 2.
- radius = distance2 / math.sin(alpha2)
- height = distance2 / math.tan(alpha2)
- mid_point = end_point.lerp(start_point, factor=.5)
- distance_vector = end_point - start_point
- height_vector = distance_vector.orthogonal().normalize(height)
- center = mid_point + height_vector
- return ConstructionArc(
- center=center,
- radius=radius,
- start_angle=(start_point - center).angle_deg,
- end_angle=(end_point - center).angle_deg,
- is_counter_clockwise=True,
- )
- @classmethod
- def from_2p_radius(cls, start_point: 'Vertex', end_point: 'Vertex', radius: float, ccw: bool = True,
- center_is_left: bool = True) -> 'ConstructionArc':
- """
- Create arc from two points and arc radius. Additional precondition: arc goes by default in counter clockwise
- orientation from start_point to end_point can be changed by ccw=False.
- Z-axis of start_point and end_point has to be 0 if given.
- The parameter *center_is_left* defines if the center of the arc is left or right of the line *start point* ->
- *end point*. Parameter ccw=False swaps start- and end point, which inverts the meaning of center_is_left.
- Args:
- start_point: start point (x, y) as args accepted by Vec2()
- end_point: end point (x, y) as args accepted by Vec2()
- radius: arc radius
- ccw: counter clockwise direction True/False
- center_is_left: center point of arc is left of line SP->EP if True, else on the right side of this line
- """
- start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
- radius = float(radius)
- if radius <= 0:
- raise ValueError("radius has to be > 0.")
- if ccw is False:
- start_point, end_point = end_point, start_point
- mid_point = end_point.lerp(start_point, factor=.5)
- distance = end_point.distance(start_point)
- distance2 = distance / 2.
- height = math.sqrt(radius ** 2 - distance2 ** 2)
- center = mid_point + (end_point - start_point).orthogonal(ccw=center_is_left).normalize(height)
- return ConstructionArc(
- center=center,
- radius=radius,
- start_angle=(start_point - center).angle_deg,
- end_angle=(end_point - center).angle_deg,
- is_counter_clockwise=True,
- )
- @classmethod
- def from_3p(cls, start_point: 'Vertex', end_point: 'Vertex', def_point: 'Vertex',
- ccw: bool = True) -> 'ConstructionArc':
- """
- Create arc from three points. Additional precondition: arc goes in counter clockwise
- orientation from start_point to end_point. Z-axis of start_point, end_point and def_point has to be 0 if given.
- Args:
- start_point: start point (x, y) as args accepted by Vec2()
- end_point: end point (x, y) as args accepted by Vec2()
- def_point: additional definition point as (x, y) as args accepted by Vec2()
- ccw: counter clockwise direction True/False
- """
- start_point, end_point = cls.validate_start_and_end_point(start_point, end_point)
- def_point = Vec2(def_point)
- if def_point == start_point or def_point == end_point:
- raise ValueError("def point has to be different to start- and end point")
- circle = ConstructionCircle.from_3p(start_point, end_point, def_point)
- center = Vec2(circle.center)
- return ConstructionArc(
- center=center,
- radius=circle.radius,
- start_angle=(start_point - center).angle_deg,
- end_angle=(end_point - center).angle_deg,
- is_counter_clockwise=ccw,
- )
- def add_to_layout(self, layout: 'GenericLayoutType', ucs: UCS = None, dxfattribs: dict = None) -> 'DXFArc':
- """
- Add arc as DXF entity to a layout.
- Supports 3D arcs by using an UCS. An ConstructionArc is always defined in the xy-plane, but by using an arbitrary UCS, the
- arc can be placed in 3D space, automatically OCS transformation included.
- Args:
- layout: destination layout (model space, paper space or block)
- ucs: arc properties transformation from ucs to ocs
- dxfattribs: usual DXF attributes supported by ARC
- Returns: DXF ConstructionArc() object
- """
- if ucs is not None:
- if dxfattribs is None:
- dxfattribs = {}
- dxfattribs['extrusion'] = ucs.uz
- ocs = OCS(ucs.uz)
- wcs_center = ucs.to_wcs(self.center)
- ocs_center = ocs.from_wcs(wcs_center)
- arc = self.__class__(radius=self.radius)
- arc.center = ocs_center
- arc.start_angle = ucs.to_ocs_angle_deg(self.start_angle)
- arc.end_angle = ucs.to_ocs_angle_deg(self.end_angle)
- else:
- arc = self
- return layout.add_arc(
- center=arc.center,
- radius=arc.radius,
- start_angle=arc.start_angle,
- end_angle=arc.end_angle,
- dxfattribs=dxfattribs,
- )
|