JuliaGraphics / Luxor.jl

Simple drawings using vector graphics; Cairo "for tourists!"
http://juliagraphics.github.io/Luxor.jl/
Other
588 stars 71 forks source link

A way to extend the functions `strokepath` and `fillpath` #231

Closed ArbitRandomUser closed 2 years ago

ArbitRandomUser commented 2 years ago

The PR

This PR essentially implements a way for Javis to have its own code run when any of these 4 functions i.e strokepath,strokepreserve,fillpath,fillpreserve are called. This is needed by Javis to implement some features.

This PR does not change the behaviour of Luxor from the defaultunless a global variable Luxor.DISPATCHER is changed

https://github.com/ArbitRandomUser/Luxor.jl/tree/javis_branch3

Why ?

Objects in Javis have an Object.func which the user defines , typically by making calls to Luxor functions to draw what the object is. What we require is a way to intercept all calls that paint the current path onto the canvas. Before the canvas is painted we would like to store the current Path and other current drawing information like line width , color alpha etc into Javis' own struct called JPath. This JPath would then allow us to do animations like partially drawing the Object or morphing it into other Objects or other shapes defined by a function.

Although morphs have a current implementation , the way to use it is a does not feel natural , one has to define a function that never actually draws anything to the canvas . This along with other constraints on using morphs makes their use in in Javis very awkward.

Take strokepath for example . The crux of our problem is changing the behaviour of all calls to strokepath wherever they are called, in Javis or through actions passed in Luxor functions (ex line(O,O+10,:stroke)). This cannot be achieved by implementing Javis's own strokepath. Doing so would require users to only call Luxor functions with action=:path and then calling strokepath in Javis. If implemented this allows users to use any of Luxors functions freely without these constraints.

The first approach we tried was using Cassette.jl. But the approach seemed too hacky . We also did try to "monkey patch" the luxor code in Javis' source but the approach gives warnings about incremental compilation being broken. And in general this would be type piracy.

Essentially we want any valid Luxor code that draws something to be usable inside the Object's function.

See this topic on discourse

Discourse link

How (it works) ?

The 4 functions in question in the source of Luxor have a general structure of

funcname() = -do_some_cairo_things-; 

What we change it to is

funcname() = funcname(DISPATCHER[1])

DISPATCHER is a global variable in luxor. It is a one element array ( hence mutable and can be changed from other modules). In luxor we define

abstract type LDispatcher end
struct DefaultLuxor <: LDispatcher end
const DISPATCHER = Array{LDispatcher}([DefaultLuxor()])

Luxors value for DISPATCHER is DefualtLuxor .

The usual method definition instead of dispatching on empty args funcname() now dispatch on funcname(::DefaultLuxor)

funcname(::DefaultLuxor) = -do_some_cairo_things-;

And as mentioned before the default argumentless dispatch is changed to

funcname() = funcname(DISPATCHER[1])

Other modules using Luxor can change this to an instance of their own type (subtyped from LDispatcher) to change the behavior of these functions. And define methods which dispatch on these types to have custom behavior

For example in Javis we do

struct JavisLuxorDispatcher <: Luxor.LDispatcher end

function Luxor.$funcname(::JavisLuxorDispatcher)
        # some custom  behaviour based on Javis globals
            if CURRENT_FETCHPATH_STATE
                occursin("stroke",string($funcname)) ? update_currentjpath(:stroke) : update_currentjpath(:fill)
            end
            if !DISABLE_LUXOR_DRAW
                $funcname(Luxor.DefaultLuxor())
            elseif !occursin("preserve",string($funcname))
                newpath()
            end
end

at render time we set

Luxor.DISPATCHER[1] = JavisLuxorDispatcher()

This way all calls to strokepath() eventually end up calling strokepath(JavisLuxorDispatcher()) defined in Javis

Javis PR

In Conclusion

Requesting code to be changed to accomodate the needs of Javis is a big favour to ask for... But as of the moment we do not see a cleaner way to implement this and hope you'd give it a consideration. As stated before , this change in no way affects regular usage of Luxor without Javis . We have also ensured that this ability to extend Luxor is not Javis specific and any other package that wants to extend these four functions can do so. All theLuxor.jl tests pass without problems and we have added a test for this dispatch functionality too .

ArbitRandomUser commented 2 years ago

@gpucce @Wikunia @Sov-trotter

codecov[bot] commented 2 years ago

Codecov Report

Merging #231 (40ed0c0) into master (b623c8c) will increase coverage by 0.06%. The diff coverage is 100.00%.

@@            Coverage Diff             @@
##           master     #231      +/-   ##
==========================================
+ Coverage   73.48%   73.54%   +0.06%     
==========================================
  Files          32       32              
  Lines        6158     6173      +15     
==========================================
+ Hits         4525     4540      +15     
  Misses       1633     1633              
Impacted Files Coverage Δ
src/basics.jl 92.30% <100.00%> (+0.49%) :arrow_up:

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update b623c8c...40ed0c0. Read the comment docs.

cormullion commented 2 years ago

Looks cool!

You could usefully add a small description of the mechanism to the docs - perhaps after the Luxor/Cairo - section.

Screenshot 2022-07-16 at 15 26 44

If all the tests still pass, we should be good to go!

ArbitRandomUser commented 2 years ago

@cormullion Added a section in the docs like you asked for . Let me know if it needs more elaboration (and where)

cormullion commented 2 years ago

Thanks!

ArbitRandomUser commented 2 years ago

we should be thanking you ... ! :)