From 37707e4ba785beb7d5de317a655df178097e514e Mon Sep 17 00:00:00 2001 From: cormullion Date: Mon, 1 Jul 2024 11:07:47 +0100 Subject: [PATCH] rule two points --- CHANGELOG.md | 2 ++ docs/src/howto/simplegraphics.md | 4 ++- docs/src/howto/tables-grids.md | 2 +- src/Luxor.jl | 2 +- src/basics.jl | 42 ++++++++++++++++++++++--- src/curves.jl | 54 ++++++++++++++++++++++++++++++-- src/tiles-grids.jl | 25 +++++++++++---- test/rules2.jl | 33 +++++++++++++++++++ test/runtests.jl | 1 + 9 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 test/rules2.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a460a52..a4db18cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Added - triangular grids +- `squirclepath()` +- `rule(pt1, pt2)` ### Changed diff --git a/docs/src/howto/simplegraphics.md b/docs/src/howto/simplegraphics.md index 8a9f03dd..d6283b37 100644 --- a/docs/src/howto/simplegraphics.md +++ b/docs/src/howto/simplegraphics.md @@ -605,6 +605,8 @@ Use [`line`](@ref) and [`rline`](@ref) to draw straight lines. `line(pt1, pt2, a You can use [`rule`](@ref) to draw a horizontal line through a point. Supply an angle for lines at an angle to the current x-axis. +Another method of `rule()` draws a line through two points. + ```@example using Luxor # hide Drawing(800, 200, "../assets/figures/rule.png") # hide @@ -625,7 +627,7 @@ nothing # hide ![arc](../assets/figures/rule.png) -Use the `boundingbox` keyword argument to crop the ruled lines with a BoundingBox. +Use the `boundingbox` keyword argument to crop the ruled lines where they cross the boundaries of a BoundingBox. ```@example using Luxor # hide diff --git a/docs/src/howto/tables-grids.md b/docs/src/howto/tables-grids.md index c4f59c85..5a77473f 100644 --- a/docs/src/howto/tables-grids.md +++ b/docs/src/howto/tables-grids.md @@ -514,7 +514,7 @@ using Luxor, Colors vertices, trianglenumber = e sethue(HSB(rescale(trianglenumber, 0, nrows * ncols, 0, 360), 0.7, 0.8)) - poly(vertices, :fillpreserve) + poly(vertices, :fillpreserve, close=true) sethue("black") strokepath() text(string(trianglenumber), halign=:center, polycentroid(vertices)) diff --git a/src/Luxor.jl b/src/Luxor.jl index af358622..79e31973 100644 --- a/src/Luxor.jl +++ b/src/Luxor.jl @@ -100,7 +100,7 @@ export Drawing, boxmiddlecenter, boxmiddleright, boxbottomleft, boxbottomcenter, boxbottomright, intersectboundingboxes, boundingboxesintersect, pointcrossesboundingbox, BoxmapTile, boxmap, circle, circlepath, circlering, ellipse, hypotrochoid, epitrochoid, - squircle, polysuper, center3pts, curve, arc, carc, arc2r, carc2r, + squircle, squirclepath, polysuper, center3pts, curve, arc, carc, arc2r, carc2r, isarcclockwise, arc2sagitta, carc2sagitta, spiral, sector, intersection2circles, intersection_line_circle, intersectionlinecircle, intersectioncirclecircle, diff --git a/src/basics.jl b/src/basics.jl index 3f8a82ef..315cd728 100644 --- a/src/basics.jl +++ b/src/basics.jl @@ -491,9 +491,11 @@ The function: rule(O, pi/2, boundingbox=BoundingBox()/2) ``` -draws a line that spans a bounding box half the width and height of the drawing, -and returns a Set of end points. If you just want the vertices and don't want to -draw anything, use `vertices=true`. +draws a line that spans a bounding box half the width and height of the drawing. + +Returns the two end points in a Vector. + +Use `vertices=true` to just return the end points, without drawing a line. """ function rule(pos, theta = 0.0; boundingbox = BoundingBox(), @@ -507,8 +509,8 @@ function rule(pos, theta = 0.0; bottomside = vcat(bbox[4], bbox[1]) leftside = bbox[1:2] - # ruled line could be as long as the diagonal so add a bit extra - r = boxdiagonal(boundingbox) + 10 + # ruled line could be as long as the diagonal so make it longer + r = boxdiagonal(boundingbox) * 2 rcosa = r * cos(theta) rsina = r * sin(theta) ruledline = (pos - (rcosa, rsina), pos + (rcosa, rsina)) @@ -550,6 +552,9 @@ function rule(pos, theta = 0.0; # eliminate duplicates due to rounding errors ruledline = unique(interpoints) + if length(ruledline) != 2 + @debug "bounding box doesn't contain this line" + end # finally draw the line if we have two points if vertices == false && length(ruledline) == 2 line(ruledline..., :stroke) @@ -557,6 +562,33 @@ function rule(pos, theta = 0.0; return ruledline end +""" + + rule1(point1::Point, point2::Point; + boundingbox=BoundingBox(), + vertices=false) + +Draw a straight line passing through points `point1` and `point2`. + +```julia +rule(Point(10, 10), Point(30, -30)) +``` + +Supply a BoundingBox to restrict the ruled lines to a rectangular area. + +Returns the two end points in a Vector. + +Use `vertices=true` to just return the end points, without drawing a line. +""" +function rule(point1::Point, point2::Point; + boundingbox=BoundingBox(), + vertices=false) + theta = slope(point1, point2) + rule(point1, theta; + boundingbox=boundingbox, + vertices=vertices) +end + # we need a thread safe way to store the color palette as a stack: # access to the stack is only possible using the global function: # - predefine all needed Dict entries in a thread safe way diff --git a/src/curves.jl b/src/curves.jl index 8b2a6979..508da04d 100644 --- a/src/curves.jl +++ b/src/curves.jl @@ -212,8 +212,8 @@ ellipse(c::Point, w::Real, h::Real, action::Symbol) = ellipse(c, w, h, action = squircle(center::Point, hradius, vradius, action; rt = 0.5, stepby = pi/40, vertices=false) -Make a squircle or superellipse (basically a rectangle with rounded corners), -and add it to the current path. Specify the center position, horizontal radius +Make a squircle or superellipse (basically a rectangle with rounded corners). +Specify the center position, horizontal radius (distance from center to a side), and vertical radius (distance from center to top or bottom): @@ -221,7 +221,9 @@ The root (`rt`) option defaults to 0.5, and gives an intermediate shape. Values less than 0.5 make the shape more rectangular. Values above make the shape more round. The horizontal and vertical radii can be different. -See also: `polysuper()`. +This function generates a series of points and makes a polygon or returns an array of Points. + +See also: `polysuper()` and `squirclepath()`. """ function squircle(center::Point, hradius::Real, vradius::Real; action = :none, @@ -254,6 +256,52 @@ squircle(center::Point, hradius::Real, vradius::Real, action::Symbol; stepby = pi / 40, reversepath = false) +""" + squirclepath(cpos, w, h; + action = :none, + kappa = 0.75, + reversepath = false) + +Make a squircle or superellipse (basically a rectangle with rounded corners). +Specify the center position, width, and height. + +`kappa` is a value (usually between 0 and 1) that determines the circularity of +the shape. If `kappa` is `4.0 * (sqrt(2.0) - 1.0) / 3.0` (ie `0.5522847498307936`), +the shape is a resonable approximation to a circle. + +Use the `reversepath` option to construct the path in the opposite direction. + +The difference betweeen `squircle()` and `squirclepath()` is that the former +builds polygons from points, the latter uses Bezier curves to build paths. + +See also `circlepath()`. +""" +function squirclepath(cpos, w, h; + action = :none, + kappa = 0.75, + reversepath = false) + @layer begin + translate(cpos) + if reversepath + bottom_left, top_left, top_right, bottom_right = reverse(box(O, 2w, 2h)) + else + bottom_left, top_left, top_right, bottom_right = box(O, 2w, 2h) + end + top_center = midpoint(top_left, top_right) + right_center = midpoint(top_right, bottom_right) + bottom_center = midpoint(bottom_left, bottom_right) + left_center = midpoint(top_left, bottom_left) + move(left_center) + curve(between(left_center, top_left, kappa), between(top_center, top_left, kappa), top_center) + curve(between(top_center, top_right, kappa), between(right_center, top_right, kappa), right_center) + curve(between(right_center, bottom_right, kappa), between(bottom_center, bottom_right, kappa), bottom_center) + curve(between(bottom_center, bottom_left, kappa), between(left_center, bottom_left, kappa), left_center) + closepath() + result = storepath() + do_action(action) + end + return result +end function arc(xc, yc, radius, angle1, angle2; action = :none) Cairo.arc(_get_current_cr(), xc, yc, radius, angle1, angle2) diff --git a/src/tiles-grids.jl b/src/tiles-grids.jl index e546d399..10b67690 100644 --- a/src/tiles-grids.jl +++ b/src/tiles-grids.jl @@ -566,9 +566,13 @@ An EquilateralTriangleGrid is an iterator that makes a grid of equilateral triangles with side length `side` positioned on a rectangular grid with `nrows` and `ncols`. +```julia +EquilateralTriangleGrid(startpoint::Point, side, nrows, ncols; up = true) +``` + The first triangle is centered at `startpoint` and points up if `up` is true. -Example +###Example ```julia nrows = 5 @@ -576,6 +580,11 @@ ncols = 8 side = 150 eqtg = EquilateralTriangleGrid(O, side, nrows, ncols) +@show first(eqtg) +# (Point[Point(-75.0, 64.9519052838329), + Point(0.0, -64.9519052838329), + Point(75.0, 64.9519052838329)], 1) + # now you can use the iterator to generate (and draw) triangles: for tri in eqtg vertices, trianglenumber = tri @@ -629,13 +638,15 @@ function Base.iterate(eqt::EquilateralTriangleGrid) eqt.currentrow, eqt.currentcol = 1, 1 cellnumber = (eqt.currentrow - 1) * eqt.nrows + eqt.currentcol h = (sqrt(3) * eqt.side) / 2 + r = (eqt.side * sqrt(3)) / 3 # circumradius pointup = (isodd(eqt.currentrow) && isodd(eqt.currentcol)) || (iseven(eqt.currentrow) && iseven(eqt.currentcol)) ? eqt.up : !eqt.up # the first triangle - cpt_c = Point(eqt.startpoint.x - eqt.side/2 + (eqt.side / 2 * eqt.currentcol), - eqt.startpoint.y -h + (h * eqt.currentrow)) + cpt_c = Point(eqt.startpoint.x + ((eqt.side / 2) * eqt.currentcol) - eqt.side / 2, + eqt.startpoint.y + (h * eqt.currentrow) - h) pts_c = _equilateral_triangle(cpt_c, eqt.side, pointup ? :up : :down) # next one - cpt_n = Point(eqt.startpoint.x + (eqt.side / 2 * eqt.currentcol), eqt.startpoint.y + (h * eqt.currentrow)) + cpt_n = Point(eqt.startpoint.x + (eqt.side / 2 * eqt.currentcol), + eqt.startpoint.y + (h * eqt.currentrow)) pts_n = _equilateral_triangle(cpt_n, eqt.side, pointup ? :down : :up) return ((pts_c, cellnumber), (pts_n, cellnumber + 1)) end @@ -651,9 +662,11 @@ function Base.iterate(eqt::EquilateralTriangleGrid, state) eqt.currentrow += 1 end h = (sqrt(3) * eqt.side) / 2 + r = (eqt.side * sqrt(3)) / 3 pointup = (isodd(eqt.currentrow) && isodd(eqt.currentcol)) || (iseven(eqt.currentrow) && iseven(eqt.currentcol)) ? eqt.up : !eqt.up - cpt_c = Point(eqt.startpoint.x - eqt.side / 2 + (eqt.side / 2 * eqt.currentcol), - eqt.startpoint.y - h + (h * eqt.currentrow)) + + cpt_c = Point(eqt.startpoint.x + ((eqt.side / 2) * eqt.currentcol) - eqt.side / 2, + eqt.startpoint.y + (h * eqt.currentrow) - h) pts_c = _equilateral_triangle(cpt_c, eqt.side, pointup ? :up : :down) cpt_n = Point(eqt.startpoint.x + (eqt.side / 2 * eqt.currentcol), eqt.startpoint.y + (h * eqt.currentrow)) pts_n = _equilateral_triangle(cpt_n, eqt.side, pointup ? :down : :up) diff --git a/test/rules2.jl b/test/rules2.jl new file mode 100644 index 00000000..42bfdfd7 --- /dev/null +++ b/test/rules2.jl @@ -0,0 +1,33 @@ +using Luxor +using Test + +function test_rule2(fname) + Drawing(500, 500, fname) + background("white") + origin() + setline(0.7) + + L = 110 + + toppts = between.(Point(-250, -250), Point(250, -250), range(0, 1, length = L)) + bottompts = between.(Point(-250, 250), Point(250, 250), range(0, 1, length = L)) + + circle.(toppts, 3, :fill) + circle.(bottompts, 3, :fill) + + bbox = BoundingBox(box(O, 495, 495)) + rule.(toppts, bottompts, boundingbox = bbox) + rule.(toppts, reverse(bottompts), boundingbox = bbox) + + leftpts = between.(Point(-250, -250), Point(-250, 250), range(0, 1, length = L)) + rightpts = between.(Point(250, -250), Point(250, 250), range(0, 1, length = L)) + + rule.(leftpts, rightpts) + rule.(leftpts, reverse(rightpts)) + + @test finish() == true + println("...finished test: output in $(fname)") +end + +test_rule2("rule2.png") +println("...finished rule2 test") \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index a5c43d62..05aa9725 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -176,6 +176,7 @@ function run_all_tests() @testset "misc" begin include("luxor-test1.jl") include("rules.jl") + include("rules2.jl") include("barstest.jl") include("unit-conversions.jl") include("grid-test.jl")