# Copyright (c) 2021-2024, Manfred Moitzi # License: MIT License from __future__ import annotations import math from ezdxf.math import ( cubic_bezier_arc_parameters, Matrix44, UVec, basic_transformation, Vec3, ) from ezdxf.render import forms from .path import Path from . import converter __all__ = [ "unit_circle", "elliptic_transformation", "rect", "ngon", "wedge", "star", "gear", "helix", ] def unit_circle( start_angle: float = 0, end_angle: float = math.tau, segments: int = 1, transform: Matrix44 | None = None, ) -> Path: """Returns a unit circle as a :class:`Path` object, with the center at (0, 0, 0) and the radius of 1 drawing unit. The arc spans from the start- to the end angle in counter-clockwise orientation. The end angle has to be greater than the start angle and the angle span has to be greater than 0. Args: start_angle: start angle in radians end_angle: end angle in radians (end_angle > start_angle!) segments: count of Bèzier-curve segments, default is one segment for each arc quarter (π/2) transform: transformation Matrix applied to the unit circle """ path = Path() start_flag = True for start, ctrl1, ctrl2, end in cubic_bezier_arc_parameters( start_angle, end_angle, segments ): if start_flag: path.start = start start_flag = False path.curve4_to(end, ctrl1, ctrl2) if transform is None: return path else: return path.transform(transform) def wedge( start_angle: float, end_angle: float, segments: int = 1, transform: Matrix44 | None = None, ) -> Path: """Returns a wedge as a :class:`Path` object, with the center at (0, 0, 0) and the radius of 1 drawing unit. The arc spans from the start- to the end angle in counter-clockwise orientation. The end angle has to be greater than the start angle and the angle span has to be greater than 0. Args: start_angle: start angle in radians end_angle: end angle in radians (end_angle > start_angle!) segments: count of Bèzier-curve segments, default is one segment for each arc quarter (π/2) transform: transformation Matrix applied to the wedge """ path = Path() start_flag = True for start, ctrl1, ctrl2, end in cubic_bezier_arc_parameters( start_angle, end_angle, segments ): if start_flag: path.line_to(start) start_flag = False path.curve4_to(end, ctrl1, ctrl2) path.line_to((0, 0, 0)) if transform is None: return path else: return path.transform(transform) def elliptic_transformation( center: UVec = (0, 0, 0), radius: float = 1, ratio: float = 1, rotation: float = 0, ) -> Matrix44: """Returns the transformation matrix to transform a unit circle into an arbitrary circular- or elliptic arc. Example how to create an ellipse with a major axis length of 3, a minor axis length 1.5 and rotated about 90°:: m = elliptic_transformation(radius=3, ratio=0.5, rotation=math.pi / 2) ellipse = shapes.unit_circle(transform=m) Args: center: curve center in WCS radius: radius of the major axis in drawing units ratio: ratio of minor axis to major axis rotation: rotation angle about the z-axis in radians """ if radius < 1e-6: raise ValueError(f"invalid radius: {radius}") if ratio < 1e-6: raise ValueError(f"invalid ratio: {ratio}") scale_x = radius scale_y = radius * ratio return basic_transformation(center, (scale_x, scale_y, 1), rotation) def rect( width: float = 1, height: float = 1, transform: Matrix44 | None = None ) -> Path: """Returns a closed rectangle as a :class:`Path` object, with the center at (0, 0, 0) and the given `width` and `height` in drawing units. Args: width: width of the rectangle in drawing units, width > 0 height: height of the rectangle in drawing units, height > 0 transform: transformation Matrix applied to the rectangle """ if width < 1e-9: raise ValueError(f"invalid width: {width}") if height < 1e-9: raise ValueError(f"invalid height: {height}") w2 = float(width) / 2.0 h2 = float(height) / 2.0 path = converter.from_vertices( [(w2, h2), (-w2, h2), (-w2, -h2), (w2, -h2)], close=True ) if transform is None: return path else: return path.transform(transform) def ngon( count: int, length: float | None = None, radius: float = 1.0, transform: Matrix44 | None = None, ) -> Path: """Returns a `regular polygon `_ a :class:`Path` object, with the center at (0, 0, 0). The polygon size is determined by the edge `length` or the circum `radius` argument. If both are given `length` has higher priority. Default size is a `radius` of 1. The ngon starts with the first vertex is on the x-axis! The base geometry is created by function :func:`ezdxf.render.forms.ngon`. Args: count: count of polygon corners >= 3 length: length of polygon side radius: circum radius, default is 1 transform: transformation Matrix applied to the ngon """ vertices = forms.ngon(count, length=length, radius=radius) if transform is not None: vertices = transform.transform_vertices(vertices) return converter.from_vertices(vertices, close=True) def star(count: int, r1: float, r2: float, transform: Matrix44 | None = None) -> Path: """Returns a `star shape `_ as a :class:`Path` object, with the center at (0, 0, 0). Argument `count` defines the count of star spikes, `r1` defines the radius of the "outer" vertices and `r2` defines the radius of the "inner" vertices, but this does not mean that `r1` has to be greater than `r2`. The star shape starts with the first vertex is on the x-axis! The base geometry is created by function :func:`ezdxf.render.forms.star`. Args: count: spike count >= 3 r1: radius 1 r2: radius 2 transform: transformation Matrix applied to the star """ vertices = forms.star(count, r1=r1, r2=r2) if transform is not None: vertices = transform.transform_vertices(vertices) return converter.from_vertices(vertices, close=True) def gear( count: int, top_width: float, bottom_width: float, height: float, outside_radius: float, transform: Matrix44 | None = None, ) -> Path: """ Returns a `gear `_ (cogwheel) shape as a :class:`Path` object, with the center at (0, 0, 0). The base geometry is created by function :func:`ezdxf.render.forms.gear`. .. warning:: This function does not create correct gears for mechanical engineering! Args: count: teeth count >= 3 top_width: teeth width at outside radius bottom_width: teeth width at base radius height: teeth height; base radius = outside radius - height outside_radius: outside radius transform: transformation Matrix applied to the gear shape """ vertices = forms.gear(count, top_width, bottom_width, height, outside_radius) if transform is not None: vertices = transform.transform_vertices(vertices) return converter.from_vertices(vertices, close=True) def helix( radius: float, pitch: float, turns: float, ccw=True, segments: int = 4, ) -> Path: """ Returns a `helix `_ as a :class:`Path` object. The center of the helix is always (0, 0, 0), a positive `pitch` value creates a helix along the +z-axis, a negative value along the -z-axis. Args: radius: helix radius pitch: the height of one complete helix turn turns: count of turns ccw: creates a counter-clockwise turning (right-handed) helix if ``True`` segments: cubic Bezier segments per turn """ # Source of algorithm: https://www.arc.id.au/HelixDrawing.html def bezier_ctrl_points(b, angle, segments): zz = 0.0 z_step = angle / segments * p z_step_2 = z_step * 0.5 for _, v1, v2, v3 in cubic_bezier_arc_parameters(0, angle, segments): yield ( Vec3(v1.x * rx, v1.y * ry, zz + z_step_2 - b), Vec3(v2.x * rx, v2.y * ry, zz + z_step_2 + b), Vec3(v3.x * rx, v3.y * ry, zz + z_step), ) zz += z_step def param_b(alpha: float) -> float: cos_a = math.cos(alpha) b_1 = (1.0 - cos_a) * (3.0 - cos_a) * alpha * p b_2 = math.sin(alpha) * (4.0 - cos_a) * math.tan(alpha) return b_1 / b_2 rx = radius ry = radius if not ccw: ry = -ry path = Path(start=(radius, 0, 0)) p = pitch / math.tau b = param_b(math.pi / segments) full_turns = int(math.floor(turns)) if full_turns > 0: curve_params = list(bezier_ctrl_points(b, math.tau, segments)) for _ in range(full_turns): z = Vec3(0, 0, path.end.z) for v1, v2, v3 in curve_params: path.curve4_to(z + v3, z + v1, z + v2) reminder = turns - full_turns if reminder > 1e-6: segments = math.ceil(reminder * 4) b = param_b(reminder * math.pi / segments) z = Vec3(0, 0, path.end.z) for v1, v2, v3 in bezier_ctrl_points(b, math.tau * reminder, segments): path.curve4_to(z + v3, z + v1, z + v2) return path