diff --git a/assets/reftests.jl-13.png b/assets/reftests.jl-13.png new file mode 100644 index 00000000..a54ae1e6 Binary files /dev/null and b/assets/reftests.jl-13.png differ diff --git a/docs/examples/reftests.jl b/docs/examples/reftests.jl index 8064d7e1..0d5195a2 100644 --- a/docs/examples/reftests.jl +++ b/docs/examples/reftests.jl @@ -126,3 +126,9 @@ graphplot(fig[2,1], edge_plottype = :beziersegments, ) @save_reference fig + +# ##self loop with waypoints +g1 = SimpleDiGraph(1) +add_edge!(g1, 1, 1) #add self loop +fig, ax, p = graphplot(g1, layout = _ -> [(0,0)], waypoints = [[(1,-1),(1,1),(-1,1),(-1,-1)]]) +@save_reference fig diff --git a/src/beziercurves.jl b/src/beziercurves.jl index 631e4f69..2a3cbf5b 100644 --- a/src/beziercurves.jl +++ b/src/beziercurves.jl @@ -197,7 +197,7 @@ function Path(P::Vararg{PT, N}; tangents=nothing, tfactor=.5) where {PT<:Abstrac # first command, recalculate WP if tangent is given first_wp = WP[1] if tangents !== nothing - p1, p2, t = P[1], P[2], normalize(tangents[1]) + p1, p2, t = P[1], P[2], normalize(Pointf(tangents[1])) dir = p2 - p1 d = tfactor * norm(dir ⋅ t) first_wp = PT(p1+d*t) @@ -214,7 +214,7 @@ function Path(P::Vararg{PT, N}; tangents=nothing, tfactor=.5) where {PT<:Abstrac # last command, recalculate last WP if tangent is given last_wp = (P[N] + WP[N-1])/2 if tangents !== nothing - p1, p2, t = P[N-1], P[N], normalize(tangents[2]) + p1, p2, t = P[N-1], P[N], normalize(Pointf(tangents[2])) dir = p2 - p1 d = tfactor * norm(dir ⋅ t) last_wp = PT(p2-d*t) diff --git a/src/recipes.jl b/src/recipes.jl index 09f35988..29a54dce 100644 --- a/src/recipes.jl +++ b/src/recipes.jl @@ -72,11 +72,12 @@ the edge. - `edge_plottype=Makie.automatic()`: Either `automatic`, `:linesegments` or `:beziersegments`. `:beziersegments` are much slower for big graphs! -Self edges / loops: +Self edges / loops: -- `selfedge_size=Makie.automatic()`: Size of self-edge-loop (dict/vector possible). -- `selfedge_direction=Makie.automatic()`: Direction of self-edge-loop as `Point2` (dict/vector possible). +- `selfedge_size=Makie.automatic()`: Size of selfloop (dict/vector possible). +- `selfedge_direction=Makie.automatic()`: Direction of center of the selfloop as `Point2` (dict/vector possible). - `selfedge_width=Makie.automatic()`: Opening of selfloop in rad (dict/vector possible). +- Note: If valid waypoints are provided for selfloops, the selfedge attributes above will be ignored. High level interface for curvy edges: @@ -104,6 +105,8 @@ Tangents interface for curvy edges: Higher factor means bigger radius. Can be tuple per edge to specify different factor for src and dst. +- Note: Tangents are ignored on selfloops if no waypoints are provided. + Waypoints along edges: - `waypoints=nothing` @@ -111,7 +114,12 @@ Waypoints along edges: dict. Waypoints will be crossed using natural cubic splines. The waypoints may or may not include the src/dst positions. -- `waypoint_radius=nothing`: If number (dict/vector possible) bent lines within radius of waypoints. +- `waypoint_radius=nothing` + + If the attribute `waypoint_radius` is `nothing` or `:spline` the waypoints will + be crossed using natural cubic spline interpolation. If number (dict/vector + possible), the waypoints won't be reached, instead they will be connected with + straight lines which bend in the given radius around the waypoints. """ @recipe(GraphPlot, graph) do scene # TODO: figure out this whole theme business @@ -356,61 +364,48 @@ function find_edge_paths(g, attr, pos::AbstractVector{PT}) where {PT} paths = Vector{AbstractPath{PT}}(undef, ne(g)) for (i, e) in enumerate(edges(g)) - if src(e) == dst(e) # selfedge - size = getattr(attr.selfedge_size, i) - direction = getattr(attr.selfedge_direction, i) - width = getattr(attr.selfedge_width, i) - paths[i] = selfedge_path(g, pos, src(e), size, direction, width) - else # no selfedge - p1, p2 = pos[src(e)], pos[dst(e)] - tangents = getattr(attr.tangents, i) - tfactor = getattr(attr.tfactor, i) - waypoints::Vector{PT} = getattr(attr.waypoints, i, PT[]) - - cdu = getattr(attr.curve_distance_usage, i) - if cdu === true + p1, p2 = pos[src(e)], pos[dst(e)] + tangents = getattr(attr.tangents, i) + tfactor = getattr(attr.tfactor, i) + waypoints::Vector{PT} = getattr(attr.waypoints, i, PT[]) + if !isnothing(waypoints) && !isempty(waypoints) #remove p1 and p2 from waypoints if these are given + waypoints[begin] == p1 && popfirst!(waypoints) + waypoints[end] == p2 && pop!(waypoints) + end + + cdu = getattr(attr.curve_distance_usage, i) + if cdu === true + curve_distance = getattr(attr.curve_distance, i, 0.0) + elseif cdu === false + curve_distance = 0.0 + elseif cdu === automatic + if is_directed(g) && has_edge(g, dst(e), src(e)) curve_distance = getattr(attr.curve_distance, i, 0.0) - elseif cdu === false + else curve_distance = 0.0 - elseif cdu === automatic - if is_directed(g) && has_edge(g, dst(e), src(e)) - curve_distance = getattr(attr.curve_distance, i, 0.0) - else - curve_distance = 0.0 - end end + end - if !isnothing(waypoints) && !isempty(waypoints) #there are waypoints - # the waypoints may already include the endpoints - waypoints[begin] == p1 && popfirst!(waypoints) - waypoints[end] == p2 && pop!(waypoints) - - radius = getattr(attr.waypoint_radius, i, nothing) - - if isempty(waypoints) || radius === nothing || radius === :spline - paths[i] = Path(p1, waypoints..., p2; tangents, tfactor) - elseif radius isa Real - paths[i] = Path(radius, p1, waypoints..., p2) - else - throw(ArgumentError("Invalid radius $radius for edge $i!")) - end - elseif !isnothing(tangents) - paths[i] = Path(p1, p2; tangents, tfactor) - elseif PT<:Point2 && !iszero(curve_distance) - d = curve_distance - s = norm(p2 - p1) - γ = 2*atan(2 * d/s) - a = (p2 - p1)/s * (4*d^2 + s^2)/(3s) - - m = @SMatrix[cos(γ) -sin(γ); sin(γ) cos(γ)] - c1 = PT(p1 + m*a) - c2 = PT(p2 - transpose(m)*a) - - commands = [MoveTo(p1), CurveTo(c1, c2, p2)] - paths[i] = BezierPath(commands) - else # straight line - paths[i] = Path(p1, p2) + if !isnothing(waypoints) && !isempty(waypoints) #there are waypoints + radius = getattr(attr.waypoint_radius, i, nothing) + if radius === nothing || radius === :spline + paths[i] = Path(p1, waypoints..., p2; tangents, tfactor) + elseif radius isa Real + paths[i] = Path(radius, p1, waypoints..., p2) + else + throw(ArgumentError("Invalid radius $radius for edge $i!")) end + elseif src(e) == dst(e) # selfedge + size = getattr(attr.selfedge_size, i) + direction = getattr(attr.selfedge_direction, i) + width = getattr(attr.selfedge_width, i) + paths[i] = selfedge_path(g, pos, src(e), size, direction, width) + elseif !isnothing(tangents) + paths[i] = Path(p1, p2; tangents, tfactor) + elseif PT<:Point2 && !iszero(curve_distance) + paths[i] = curved_path(p1, p2, curve_distance) + else # straight line + paths[i] = Path(p1, p2) end end @@ -462,7 +457,7 @@ end """ selfedge_path(g, pos, v, size, direction, width) -Return a Path for the +Return a BezierPath for a selfedge. """ function selfedge_path(g, pos::AbstractVector{<:Point2}, v, size, direction, width) vp = pos[v] @@ -519,6 +514,24 @@ function selfedge_path(g, pos::AbstractVector{<:Point3}, v, size, direction, wid error("Self edges in 3D not yet supported") end +""" + curved_path(p1, p2, curve_distance) + +Return a BezierPath for a curved edge (not selfedge). +""" +function curved_path(p1::PT, p2::PT, curve_distance) where {PT} + d = curve_distance + s = norm(p2 - p1) + γ = 2*atan(2 * d/s) + a = (p2 - p1)/s * (4*d^2 + s^2)/(3s) + + m = @SMatrix[cos(γ) -sin(γ); sin(γ) cos(γ)] + c1 = PT(p1 + m*a) + c2 = PT(p2 - transpose(m)*a) + + return BezierPath([MoveTo(p1), CurveTo(c1, c2, p2)]) +end + """ edgeplot(paths::Vector{AbstractPath}) edgeplot!(sc, paths::Vector{AbstractPath})