from math import atan2, ceil, cos, degrees, floor, hypot, pi, radians, sin
from .backend import fill_default_args, svg_command, svg_path, tikz_command
from .pobject import PObject, POProperty, RenderingContext
from .style import CustomStyle, color
from .style.draw import Fill, Stroke
from .utils import cartesian_to_canvas
__all__ = ["Angle", "RAngle"]
[docs]
class Angle(PObject):
"""
An angle described by three points and its radius, following the right-hand rule.
Attributes:
p1: Starting point.
p2: Corner of the angle.
p3: End point.
radius: Radius of the arc.
"""
p1: tuple[float, float]
p2: tuple[float, float]
p3: tuple[float, float]
radius: float
def __init__(
self,
p1: tuple[float, float],
p2: tuple[float, float],
p3: tuple[float, float],
radius: float = 1,
zord: int = 0,
) -> None:
self.p1 = p1
self.p2 = p2
self.p3 = p3
self.radius = radius
self._zord = zord
[docs]
def extrema(self) -> list[tuple[float, float]]:
v1x, v1y = self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]
v2x, v2y = self.p3[0] - self.p2[0], self.p3[1] - self.p2[1]
mag1 = hypot(v1x, v1y)
mag2 = hypot(v2x, v2y)
if mag1 == 0 or mag2 == 0:
return [self.p2]
extrema = [
self.p2,
(
self.p2[0] + self.radius * (v1x / mag1),
self.p2[1] + self.radius * (v1y / mag1),
),
(
self.p2[0] + self.radius * (v2x / mag2),
self.p2[1] + self.radius * (v2y / mag2),
),
]
theta1 = atan2(v1y, v1x)
theta2 = atan2(v2y, v2x)
if theta2 < theta1:
theta2 += 2 * pi
for k in range(ceil(theta1 / (pi / 2)), floor(theta2 / (pi / 2)) + 1):
angle = k * (pi / 2)
cx = self.p2[0] + self.radius * cos(angle)
cy = self.p2[1] + self.radius * sin(angle)
extrema.append((cx, cy))
return extrema
[docs]
def tikz(self, ctx: RenderingContext, *args: POProperty) -> str:
v1x, v1y = self.p1[0] - self.p2[0], self.p1[1] - self.p2[1]
v2x, v2y = self.p3[0] - self.p2[0], self.p3[1] - self.p2[1]
deg1 = degrees(atan2(v1y, v1x))
deg2 = degrees(atan2(v2y, v2x))
deg1 += 360 if deg1 < 0 else 0
deg2 += 360 if deg2 < 0 else 0
if deg2 < deg1:
deg2 += 360
start_x = self.p2[0] + self.radius * cos(radians(deg1))
start_y = self.p2[1] + self.radius * sin(radians(deg1))
return tikz_command(
"draw", f"({start_x}, {start_y}) arc ({deg1}:{deg2}:{self.radius})", *args
)
[docs]
def svg(self, ctx: RenderingContext, *args: POProperty) -> str:
ax_c, ay_c = cartesian_to_canvas(self.p1, ctx)
bx_c, by_c = cartesian_to_canvas(self.p2, ctx)
cx_c, cy_c = cartesian_to_canvas(self.p3, ctx)
v1 = (ax_c - bx_c, ay_c - by_c)
v2 = (cx_c - bx_c, cy_c - by_c)
mag1 = hypot(v1[0], v1[1])
mag2 = hypot(v2[0], v2[1])
if mag1 == 0 or mag2 == 0:
raise ValueError("Invalid angle.")
start_x = bx_c + self.radius * ctx.scale * (v1[0] / mag1)
start_y = by_c + self.radius * ctx.scale * (v1[1] / mag1)
end_x = bx_c + self.radius * ctx.scale * (v2[0] / mag2)
end_y = by_c + self.radius * ctx.scale * (v2[1] / mag2)
sweep_angle = (
atan2(self.p3[1] - self.p2[1], self.p3[0] - self.p2[0])
- atan2(self.p1[1] - self.p2[1], self.p1[0] - self.p2[0])
) % (2 * pi)
large_arc_flag = 1 if sweep_angle > pi else 0
sweep_flag = 0
d = f"M {start_x:.4f} {start_y:.4f} A {self.radius * ctx.scale:.4f} {self.radius * ctx.scale:.4f} 0 {large_arc_flag} {sweep_flag} {end_x:.4f} {end_y:.4f}"
return svg_command(
"path",
CustomStyle("d", d),
*fill_default_args(args, (Fill, Fill(None)), (Stroke, Stroke(color.BLACK))),
)
[docs]
class RAngle(Angle):
"""
A special type of :class:`Angle` instance, where the shape is drawn with straight lines instead of an arc.
This results in a square whenever the angle measures 90 degrees, and a rhombus otherwise.
"""
[docs]
def extrema(self) -> list[tuple[float, float]]:
rn = self.radius / (2 ** (1 / 2))
v1 = (self.p1[0] - self.p2[0], self.p1[1] - self.p2[1])
n1 = hypot(*v1)
u1 = (v1[0] / n1, v1[1] / n1)
v2 = (self.p3[0] - self.p2[0], self.p3[1] - self.p2[1])
n2 = hypot(*v2)
u2 = (v2[0] / n2, v2[1] / n2)
return [
self.p2,
(self.p2[0] + u1[0] * rn, self.p2[1] + u1[1] * rn),
(self.p2[0] + (u1[0] + u2[0]) * rn, self.p2[1] + (u1[1] + u2[1]) * rn),
(self.p2[0] + u2[0] * rn, self.p2[1] + u2[1] * rn),
]
[docs]
def tikz(self, ctx: RenderingContext, *args: POProperty) -> str:
points = self.extrema()
points.append(points[0])
return tikz_command(
"draw", " -- ".join(f"({p[0]}, {p[1]})" for p in points), *args
)
[docs]
def svg(self, ctx: RenderingContext, *args: POProperty) -> str:
points = self.extrema()
points.append(points[0])
return svg_path(
(cartesian_to_canvas(p, ctx) for p in points),
*fill_default_args(args, (Fill, Fill(None)), (Stroke, Stroke(color.BLACK))),
)