diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d4b3ea..8bc8cd84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog +## upcoming release + +### Added + +- circle/tangent functions + +### Changed + +- Travis documentation stage + +### Removed + +### Deprecated + ## [v0.12.0] - 2018-08-17 - new version number for better compatibility with v1.0 diff --git a/docs/src/simplegraphics.md b/docs/src/simplegraphics.md index 26cb7801..dcb03788 100644 --- a/docs/src/simplegraphics.md +++ b/docs/src/simplegraphics.md @@ -223,6 +223,101 @@ nothing # hide circlepath ``` +### Circles and tangents + +Functions to find circles that are tangential to other circles include: + +- `circletangent2circles()` finds circles of a particular radius tangential to two circles +- `circlepointtangent()` finds circles of a particular radius passing through a point and tangential to another circle + +These functions can return 0, 1, or 2 points (since there are often two solutions to a specific geometric layout). + +`circletangent2circles()` takes the required radius and two existing circles: + +```@example +using Luxor # hide +Drawing(600, 250, "assets/figures/circle-tangents.png") # hide +origin() # hide +background("white") # hide +sethue("black") # hide +setline(1) # hide + +circle1 = (Point(-100, 0), 90) +circle(circle1..., :stroke) +circle2 = (Point(100, 0), 90) +circle(circle2..., :stroke) + +requiredradius = 25 +ncandidates, p1, p2 = circletangent2circles(requiredradius, circle1..., circle2...) + +if ncandidates==2 + sethue("orange") + circle(p1, requiredradius, :fill) + sethue("green") + circle(p2, requiredradius, :fill) + sethue("purple") + circle(p1, requiredradius, :stroke) + circle(p2, requiredradius, :stroke) +end + +# the circles are 10 apart, so there should be just one circle +# that fits there + +requiredradius = 10 +ncandidates, p1, p2 = circletangent2circles(requiredradius, circle1..., circle2...) + +if ncandidates==1 + sethue("blue") + circle(p1, requiredradius, :fill) + sethue("cyan") + circle(p1, requiredradius, :stroke) +end + +finish() # hide +nothing # hide +``` + +![circle tangents](assets/figures/circle-tangents.png) + +`circlepointtangent()` looks for circles of a specified radius that pass through a point and are tangential to a circle. There are usually two candidates. + +```@example +using Luxor # hide +Drawing(600, 250, "assets/figures/circle-point-tangent.png") # hide +origin() # hide +background("white") # hide +sethue("black") # hide +setline(1) # hide + +circle1 = (Point(-100, 0), 90) +circle(circle1..., :stroke) + +requiredradius = 50 +requiredpassthrough = O + (80, 0) +ncandidates, p1, p2 = circlepointtangent(requiredpassthrough, requiredradius, circle1...) + +if ncandidates==2 + sethue("orange") + circle(p1, requiredradius, :stroke) + sethue("green") + circle(p2, requiredradius, :stroke) +end + +sethue("black") +circle(requiredpassthrough, 4, :fill) + +finish() # hide +nothing # hide +``` + +![circle tangents 2](assets/figures/circle-point-tangent.png) + + +```@docs +circletangent2circles +circlepointtangent +``` + ## More curved shapes: sectors, spirals, and squircles A sector (technically an "annular sector") has an inner and outer radius, as well as start and end angles. diff --git a/src/Luxor.jl b/src/Luxor.jl index d3721ae6..d6f033dc 100644 --- a/src/Luxor.jl +++ b/src/Luxor.jl @@ -82,7 +82,7 @@ export Drawing, currentdrawing, circle, circlepath, ellipse, hypotrochoid, epitrochoid, squircle, center3pts, curve, arc, carc, arc2r, carc2r, spiral, sector, intersection2circles, intersection_line_circle, intersectionlinecircle, intersectioncirclecircle, ispointonline, - intersectlinepoly, polyintersections, + intersectlinepoly, polyintersections, circlepointtangent, circletangent2circles, intersectboundingboxes, boundingboxesintersect, diff --git a/src/curves.jl b/src/curves.jl index 4286bc4d..69b39ad2 100644 --- a/src/curves.jl +++ b/src/curves.jl @@ -701,4 +701,80 @@ function intersectioncirclecircle(cp1, r1, cp2, r2) return (true, p3, p4) end +""" + circlepointtangent(through::Point, radius, targetcenter::Point, targetradius) + +Find the centers of up to two circles of radius `radius` that pass through point +`through` and are tangential to a circle that has radius `targetradius` and +center `targetcenter`. + +This function returns a tuple: + +* (0, O, O) - no circles exist + +* (1, pt1, O) - 1 circle exists, centered at pt1 + +* (2, pt1, pt2) - 2 circles exist, with centers at pt1 and pt2 + +(The O are just dummy points so that three values are always returned.) +""" +function circlepointtangent(through::Point, radius, targetcenter::Point, targetradius) + distx = targetcenter.x - through.x + disty = targetcenter.y - through.y + dsq = distance(through, targetcenter)^2 + if isless(dsq, 10e-6) # coincident + return (0, O, O) + else + sqinv=0.5/dsq + s = dsq - ((2radius + targetradius) * targetradius) + root = 4(radius^2) * dsq - s^2 + s *= sqinv + if isless(dsq, 0.0) # no center possible + return (0, O, O) + else + if isless(root, 10e-6) # only one circle possible + x = through.x + distx * s + y = through.y + disty * s + if isless(abs(distance(through, Point(x, y)) - radius), 10e-6) + return (1, Point(x, y), O) + else + return (0, O, O) + end + else # two circles are possible + root = sqrt(root) * sqinv + xconst = through.x + distx * s + yconst = through.y + disty * s + xvar = disty * root + yvar = distx * root + return (2, Point(xconst - xvar, yconst + yvar), Point(xconst + xvar, yconst - yvar)) + end + end + end +end + +""" + circletangent2circles(radius, circle1center::Point, circle1radius, circle2center::Point, circle2radius) + +Find the centers of up to two circles of radius `radius` that are tangent to the +two circles defined by `circle1...` and `circle2...`. These two circles can +overlap, but one can't be inside the other. + +* (0, O, O) - no such circles exist + +* (1, pt1, O) - 1 circle exists, centered at pt1 + +* (2, pt1, pt2) - 2 circles exist, with centers at pt1 and pt2 + +(The O are just dummy points so that three values are always returned.) +""" +function circletangent2circles(radius, circle1center::Point, circle1radius, circle2center::Point, circle2radius) + modradius1 = radius + circle1radius + modradius2 = circle2radius - circle1radius + return circlepointtangent(circle1center, modradius1, circle2center, modradius2) +end + +function randpoint() + return Point(rand(-200:200), rand(-200:200)) +end + # eof diff --git a/test/circletangent-test.jl b/test/circletangent-test.jl new file mode 100644 index 00000000..7de25bcf --- /dev/null +++ b/test/circletangent-test.jl @@ -0,0 +1,54 @@ +#!/usr/bin/env julia + +using Luxor, Test, Random + +Random.seed!(7) + +function findanddrawsometangentialcircles(c1center, c1radius, c2center, c2radius) + for requiredradius in 1:150 + res = circletangent2circles(requiredradius, c1center, c1radius, c2center, c2radius) + setopacity(0.5) + if first(res) == 1 + circle(res[2], requiredradius, :stroke) + elseif first(res) == 2 + circle(res[2], requiredradius, :stroke) + circle(res[3], requiredradius, :stroke) + elseif first(res) == 0 + sethue("red") + circle(c1center, 5, :fill) + circle(c2center, 5, :fill) + end + end +end + +function test_circletangents(fname) + pagewidth, pageheight = 1200, 1400 + Drawing(pagewidth, pageheight, fname) + origin() # move 0/0 to center + background("ivory") + setline(0.5) + tiles = Tiler(1200, 1400, 5, 5) + for (pos, n) in tiles + @layer begin + translate(pos) + circle1 = (O + Point(rand(-20:20), rand(-20:20)), rand(20:40)) + circle2 = (O + Point(rand(-20:20), rand(-20:20)), rand(20:40)) + sethue("black") + setopacity(0.5) + circle(circle1..., :fill) + circle(circle2..., :fill) + setopacity(1.0) + circle(circle1..., :stroke) + circle(circle2..., :stroke) + sethue("purple") + findanddrawsometangentialcircles(circle1[1], circle1[2], circle2[1], circle2[2]) + end + end + + @test finish() == true + println("...finished circle tangent test, saved in $(fname)") +end + +fname = "circletangent-test.pdf" + +test_circletangents(fname) diff --git a/test/runtests.jl b/test/runtests.jl index 010e1b13..a225c0c0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -55,6 +55,7 @@ function run_all_tests() include("bezierpath.jl") include("bezierpathtopoly.jl") include("bezierstroke-test.jl") + include("circletangent-test.jl") end @testset "color" begin