Skip to content

Commit 5fde00a

Browse files
committed
adding roundcorner
1 parent a88cbb3 commit 5fde00a

7 files changed

Lines changed: 166 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# Changelog
22

3-
## [v4.4] - forthcoming
3+
## [v4.4] - 2025-11-08
44

55
### Added
66

7+
- roundcorner()
8+
79
### Changed
810

11+
- compat entries
12+
913
### Removed
1014

1115
### Deprecated

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Aqua = "0.8"
3232
Base64 = "1.6"
3333
Cairo = "1.0, 1.1"
3434
Colors = "0.11, 0.12, 0.13"
35-
DataStructures = "0.18, 0.19"
35+
DataStructures = "0.19"
3636
Dates = "1.6"
3737
FFMPEG = "^0.4.1"
3838
FileIO = "1"

docs/src/howto/geometrytools.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,38 @@ nothing # hide
177177

178178
![angle three points](../assets/figures/anglethreepoints.png)
179179

180+
## Round corners
181+
182+
The [`roundcorner`](@ref) function takes three points that define a corner, plus a radius. The function returns three points and a Boolean flag: the first and third define the ends of the circular arc, the second defines the center of the circle with the given radius. The Boolean flag indicates whether the arc should be drawn clockwise.
183+
184+
```@example
185+
using Luxor # hide
186+
@drawsvg begin # hide
187+
background(0.8, 0.7, 0.7)
188+
fontsize(25)
189+
190+
p1, corner, p3 = ngon(O, 250, 3, vertices=true)
191+
192+
label.(("p1", "corner", "p3"), :NE, (p1, corner, p3))
193+
circle.((p1, corner, p3), 5, :fill)
194+
195+
p1c, cp, p2c, clockwise = roundcorner(p1, corner, p3, 80)
196+
197+
label.(("p1c", "cp", "p2c"), :NW, (p1c, cp, p2c))
198+
circle.((p1c, cp, p2c), 5, :fill)
199+
200+
@layer begin setdash("dot"); circle(cp, 80, :stroke) end
201+
202+
move(p1)
203+
line(p1c)
204+
arc2r(cp, p1c, p2c)
205+
line(p3)
206+
strokepath()
207+
end 800 600 # hide
208+
```
209+
210+
## Other functions
211+
180212
Other functions that help with geometry include:
181213

182214
- [`distance`](@ref) distance between two points

src/Luxor.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export Drawing,
112112
ellipseinquad, crescent, ngon, ngonside, star, pie, polycross,
113113
do_action, paint, paint_with_alpha, fillstroke, AbstractPoint, Point, O, randompoint, randompointarray, midpoint,
114114
between, slope, intersectionlines, pointlinedistance,
115-
getnearestpointonline, isinside,
115+
getnearestpointonline, isinside, roundcorner,
116116
rotatepoint, perpendicular, crossproduct,
117117
dotproduct, determinant3, distance, prettypoly, polysmooth, polysplit,
118118
poly, simplify, polycentroid, polysortbyangle, polyhull,

src/polygons.jl

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,92 @@ end
417417
polysmooth(points::Vector{Point}, radius; action = :none, debug = false, close = true) =
418418
polysmooth(points, radius, action; debug = debug, close = close)
419419

420+
"""
421+
roundcorner(p1::Point, cornerpoint::Point, p2::Point, radius)
422+
423+
Given a corner `cornerpoint` defined by the three points `p1`, `cornerpoint`, and `p2`,
424+
calculate the intersection points and centerpoint for a smooth rounded corner.
425+
426+
Returns
427+
428+
`p1_cross`, `circlepoint`, `p2_cross`, `clockwise`
429+
430+
The corner can be drawn as:
431+
432+
p1 -> p1_cross -> arc2r(circlepoint, p1_cross, p2_cross) -> p2_cross -> p2
433+
434+
`clockwise` is true if the arc is to drawn clockwise.
435+
"""
436+
function roundcorner(p1::Point, cornerpoint::Point, p2::Point, radius)
437+
if isapprox(radius, 0.0)
438+
throw(error("roundcorner: impossibly small radius $radius"))
439+
end
440+
dx1 = cornerpoint.x - p1.x # vector 1
441+
dy1 = cornerpoint.y - p1.y
442+
dx2 = cornerpoint.x - p2.x # vector 2
443+
dy2 = cornerpoint.y - p2.y
444+
445+
# Angle between vector 1 and vector 2 divided by 2
446+
angle2 = (atan(dy1, dx1) - atan(dy2, dx2)) / 2
447+
448+
# length of segment between corner point and the
449+
# points of intersection with the circle of a given radius
450+
t = abs(tan(angle2))
451+
segment = radius / t
452+
453+
# Check the segment
454+
length1 = hypot(dx1, dy1)
455+
length2 = hypot(dx2, dy2)
456+
seglength = min(length1, length2)
457+
if segment > seglength
458+
segment = seglength
459+
radius = seglength * t
460+
end
461+
462+
# points of intersection are calculated by the proportion between
463+
# the coordinates of the vector, length of vector and the length of the segment.
464+
p1_cross = Luxor.getproportionpoint(cornerpoint, segment, length1, dx1, dy1)
465+
p2_cross = Luxor.getproportionpoint(cornerpoint, segment, length2, dx2, dy2)
466+
467+
# calculation of the coordinates of the circle's center by the addition of angular vectors
468+
dx = cornerpoint.x * 2 - p1_cross.x - p2_cross.x
469+
dy = cornerpoint.y * 2 - p1_cross.y - p2_cross.y
470+
L = hypot(dx, dy)
471+
d = hypot(segment, radius)
472+
# this prevents impossible constructions; Cairo will crash if L is 0
473+
if isapprox(L, 0.0)
474+
L = 0.01
475+
end
476+
circlepoint = Luxor.getproportionpoint(cornerpoint, d, L, dx, dy)
477+
478+
# start angle and end engle of arc
479+
startangle = atan(p1_cross.y - circlepoint.y, p1_cross.x - circlepoint.x)
480+
endangle = atan(p2_cross.y - circlepoint.y, p2_cross.x - circlepoint.x)
481+
482+
if endangle < 0
483+
endangle = 2π + endangle
484+
end
485+
if startangle < 0
486+
startangle = 2π + startangle
487+
end
488+
sweepangle = endangle - startangle
489+
490+
if abs(sweepangle) > π
491+
if startangle < endangle
492+
clockwise = false
493+
else
494+
clockwise = true
495+
end
496+
else
497+
if startangle < endangle
498+
clockwise = true
499+
else
500+
clockwise = false
501+
end
502+
end
503+
return p1_cross, circlepoint, p2_cross, clockwise
504+
end
505+
420506
"""
421507
offsetpoly(plist::Vector{Point}, d::T) where T<:Number
422508

test/round-corner-test.jl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Luxor
2+
using Test
3+
using Random
4+
Random.seed!(42)
5+
6+
function test_roundcorner(fname)
7+
@draw begin
8+
p1, p2, p3 = ngon(O, 100, 3, vertices = true)
9+
s, cp, f = roundcorner(p1, p2, p3, 50)
10+
line(p1, s, :stroke)
11+
line(f, p3, :stroke)
12+
arc2r(cp, s, f, :stroke)
13+
14+
@test ispointonline(s, p1, p2) == true
15+
@test ispointonline(f, p2, p3) == true
16+
end
17+
18+
return Drawing(700, 700, fname)
19+
background("black")
20+
origin()
21+
setline(1)
22+
bx = ngon(O, 250, 4, vertices = true)
23+
for radius in 0.0000001:10:400
24+
for (n, pt) in enumerate(bx)
25+
randomhue()
26+
p1 = bx[mod1(n - 1, end)]
27+
p2 = bx[mod1(n, end)]
28+
p3 = bx[mod1(n + 1, end)]
29+
startc, centerp, endc, flag = roundcorner(
30+
p1, p2, p3, radius
31+
)
32+
arc2r(centerp, startc, endc, :stroke)
33+
end
34+
end
35+
@test finish() == true
36+
println("...finished test: output in $(fname)")
37+
end
38+
39+
test_roundcorner("round-corner-test-1.png")
40+
println("...finished roundcorner test")

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ function run_all_tests()
9696
include("arc-sagitta-test.jl")
9797
include("circlecircletangent-test.jl")
9898
include("crescent-test.jl")
99+
include("round-corner-test.jl")
99100
end
100101

101102
@testset "color" begin

0 commit comments

Comments
 (0)