diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index ab6c5a83942..4efceba8302 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -26,6 +26,7 @@ jobs: - uses: julia-actions/setup-julia@v1 with: version: '1' + include-all-prereleases: true arch: x64 - uses: julia-actions/cache@v1 - name: Benchmark diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 6a036a84727..af5f2f6d1c6 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -178,3 +178,7 @@ end function draw_atomic(::Scene, ::Screen, x) @warn "$(typeof(x)) is not supported by cairo right now" end + +function draw_atomic(::Scene, ::Screen, x::Makie.PlotList) + # Doesn't need drawing +end diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index 2de2e552910..a10df12fd81 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -248,7 +248,7 @@ function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig, scene: end function Screen(scene::Scene; screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(scene, config) end diff --git a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl index 48da57bfc8a..17b39705e4f 100644 --- a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl +++ b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl @@ -193,12 +193,11 @@ max_dim(t) = error("max_dim not implemented for: $(typeof(t)). This happen function (::Type{GPUArrayType})(data::Observable; kw...) where GPUArrayType <: GPUArray gpu_mem = GPUArrayType(data[]; kw...) # TODO merge these and handle update tracking during contruction - obs1 = on(_-> gpu_mem.requires_update[] = true, data) obs2 = on(new_data -> update!(gpu_mem, new_data), data) if GPUArrayType <: TextureBuffer - push!(gpu_mem.buffer.observers, obs1, obs2) + push!(gpu_mem.buffer.observers, obs2) else - push!(gpu_mem.observers, obs1, obs2) + push!(gpu_mem.observers, obs2) end return gpu_mem end diff --git a/GLMakie/src/GLAbstraction/GLBuffer.jl b/GLMakie/src/GLAbstraction/GLBuffer.jl index 6f123ade0e4..a19d789af23 100644 --- a/GLMakie/src/GLAbstraction/GLBuffer.jl +++ b/GLMakie/src/GLAbstraction/GLBuffer.jl @@ -5,7 +5,6 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} usage::GLenum context::GLContext # TODO maybe also delay upload to when render happens? - requires_update::Observable{Bool} observers::Vector{Observables.ObserverFunction} function GLBuffer{T}(ptr::Ptr{T}, buff_length::Int, buffertype::GLenum, usage::GLenum) where T @@ -18,8 +17,7 @@ mutable struct GLBuffer{T} <: GPUArray{T, 1} obj = new( id, (buff_length,), buffertype, usage, current_context(), - Observable(true), Observables.ObserverFunction[]) - + Observables.ObserverFunction[]) finalizer(free, obj) obj end @@ -68,7 +66,6 @@ function GLBuffer( au = ShaderAbstractions.updater(buffer) obsfunc = on(au.update) do (f, args) f(b, args...) # forward setindex! etc - b.requires_update[] = true return end push!(b.observers, obsfunc) diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index e48d3a11c3a..d6eb089f410 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -55,8 +55,6 @@ So rewriting this function could get us a lot of performance for scenes with a lot of objects. """ function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray) - renderobject.requires_update = false - if renderobject.visible renderobject.prerenderfunction() program = vertexarray.program diff --git a/GLMakie/src/GLAbstraction/GLShader.jl b/GLMakie/src/GLAbstraction/GLShader.jl index edbc7efff8c..bddc6e61302 100644 --- a/GLMakie/src/GLAbstraction/GLShader.jl +++ b/GLMakie/src/GLAbstraction/GLShader.jl @@ -249,7 +249,7 @@ function gl_convert(cache::ShaderCache, lazyshader::AbstractLazyShader, data) template_keys[i] = template replacements[i] = String[mustache2replacement(t, v, data) for t in template] end - program = get!(cache.program_cache, (paths, replacements)) do + return get!(cache.program_cache, (paths, replacements)) do # when we're here, this means there were uncached shaders, meaning we definitely have # to compile a new program shaders = Vector{Shader}(undef, length(paths)) diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 9f6cbd529d8..028c23db012 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -17,7 +17,6 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} parameters ::TextureParameters{NDIM} size ::NTuple{NDIM, Int} context ::GLContext - requires_update ::Observable{Bool} observers ::Vector{Observables.ObserverFunction} function Texture{T, NDIM}( id ::GLuint, @@ -37,7 +36,6 @@ mutable struct Texture{T <: GLArrayEltypes, NDIM} <: OpenglTexture{T, NDIM} parameters, size, current_context(), - Observable(true), Observables.ObserverFunction[] ) finalizer(free, tex) @@ -49,11 +47,8 @@ end mutable struct TextureBuffer{T <: GLArrayEltypes} <: OpenglTexture{T, 1} texture::Texture{T, 1} buffer::GLBuffer{T} - requires_update::Observable{Bool} - function TextureBuffer(texture::Texture{T, 1}, buffer::GLBuffer{T}) where T - x = map((_, _) -> true, buffer.requires_update, texture.requires_update) - new{T}(texture, buffer, x) + new{T}(texture, buffer) end end Base.size(t::TextureBuffer) = size(t.buffer) @@ -72,7 +67,6 @@ ShaderAbstractions.switch_context!(t::TextureBuffer) = switch_context!(t.texture function unsafe_free(tb::TextureBuffer) unsafe_free(tb.texture) unsafe_free(tb.buffer) - Observables.clear(tb.requires_update) end is_texturearray(t::Texture) = t.texturetype == GL_TEXTURE_2D_ARRAY @@ -148,8 +142,7 @@ function Texture(s::ShaderAbstractions.Sampler{T, N}; kwargs...) where {T, N} anisotropic = s.anisotropic; kwargs... ) obsfunc = ShaderAbstractions.connect!(s, tex) - obsfunc2 = on(x -> tex.requires_update[] = true, s.updates.update) - push!(tex.observers, obsfunc, obsfunc2) + push!(tex.observers, obsfunc) return tex end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 6e7a68f9c2a..19d7123e4fa 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -174,21 +174,8 @@ mutable struct GLVertexArray{T} buffers::Dict{String,GLBuffer} indices::T context::GLContext - requires_update::Observable{Bool} - function GLVertexArray{T}(program, id, bufferlength, buffers, indices) where T - va = new(program, id, bufferlength, buffers, indices, current_context(), true) - if indices isa GLBuffer - on(indices.requires_update) do _ # only triggers true anyway - va.requires_update[] = true - end - end - for (name, buffer) in buffers - on(buffer.requires_update) do _ # only triggers true anyway - va.requires_update[] = true - end - end - + va = new(program, id, bufferlength, buffers, indices, current_context()) return va end end @@ -318,7 +305,6 @@ mutable struct RenderObject{Pre} prerenderfunction::Pre postrenderfunction id::UInt32 - requires_update::Bool visible::Bool function RenderObject{Pre}( @@ -326,7 +312,7 @@ mutable struct RenderObject{Pre} uniforms::Dict{Symbol,Any}, observables::Vector{Observable}, vertexarray::GLVertexArray, prerenderfunctions, postrenderfunctions, - visible, track_updates = true + visible ) where Pre fxaa = Bool(to_value(get!(uniforms, :fxaa, true))) RENDER_OBJECT_ID_COUNTER[] += one(UInt32) @@ -340,57 +326,13 @@ mutable struct RenderObject{Pre} context, uniforms, observables, vertexarray, prerenderfunctions, postrenderfunctions, - id, true, visible[] + id, visible[] ) - - if track_updates - # visible changes should always trigger updates so that plots - # actually become invisible when visible is changed. - # Other uniforms and buffers don't need to trigger updates when - # visible = false - on(visible) do visible - robj.visible = visible - robj.requires_update = true - end - - function request_update(_::Any) - if robj.visible - robj.requires_update = true - end - return - end - - # gather update requests for polling in renderloop - for uniform in values(uniforms) - if uniform isa Observable - on(request_update, uniform) - elseif uniform isa GPUArray - on(request_update, uniform.requires_update) - end - end - on(request_update, vertexarray.requires_update) - else - on(visible) do visible - robj.visible = visible - end - - # remove tracking from GPUArrays - for uniform in values(uniforms) - if uniform isa GPUArray - foreach(off, uniform.requires_update.inputs) - empty!(uniform.requires_update.inputs) - end - end - for buffer in vertexarray.buffers - if buffer isa GPUArray - foreach(off, buffer.requires_update.inputs) - empty!(buffer.requires_update.inputs) - end - end - foreach(off, vertexarray.requires_update.inputs) - empty!(vertexarray.requires_update.inputs) + push!(observables, visible) + on(visible) do visible + robj.visible = visible + return end - return robj end end @@ -474,8 +416,7 @@ function RenderObject( vertexarray, pre, post, - visible, - track_updates + visible ) # automatically integrate object ID, will be discarded if shader doesn't use it @@ -502,7 +443,6 @@ function clean_up_observables(x::T) where T foreach(off, x.observers) empty!(x.observers) end - Observables.clear(x.requires_update) end # OpenGL has the annoying habit of reusing id's when creating a new context diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index a88ad9a9f97..ab6282c0ee4 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -261,16 +261,4 @@ function gl_convert(::Type{T}, a::Observable{<: AbstractArray{X, N}}; kw_args... T(s; kw_args...) end -lift_convert(a::AbstractArray, T, N) = lift(x -> convert(Array{T, N}, x), a) -function lift_convert(a::ShaderAbstractions.Sampler, T, N) - ShaderAbstractions.Sampler( - lift(x -> convert(Array{T, N}, x.data), a), - minfilter = a[].minfilter, magfilter = a[].magfilter, - x_repeat = a[].repeat[1], - y_repeat = a[].repeat[min(2, N)], - z_repeat = a[].repeat[min(3, N)], - anisotropic = a[].anisotropic, swizzle_mask = a[].swizzle_mask - ) -end - gl_convert(f::Function, a) = f(a) diff --git a/GLMakie/src/GLMakie.jl b/GLMakie/src/GLMakie.jl index 91a2b127204..bdc413795b8 100644 --- a/GLMakie/src/GLMakie.jl +++ b/GLMakie/src/GLMakie.jl @@ -43,7 +43,16 @@ export Sampler, Buffer const GL_ASSET_DIR = RelocatableFolders.@path joinpath(@__DIR__, "..", "assets") const SHADER_DIR = RelocatableFolders.@path joinpath(GL_ASSET_DIR, "shader") -loadshader(name) = joinpath(SHADER_DIR, name) +const LOADED_SHADERS = Dict{String, String}() + +function loadshader(name) + # Turns out, joinpath is so slow, that it actually makes sense + # To memoize it :-O + # when creating 1000 plots with the PlotSpec API, timing drop from 1.5s to 1s just from this change: + return get!(LOADED_SHADERS, name) do + return joinpath(SHADER_DIR, name) + end +end gl_texture_atlas() = Makie.get_texture_atlas(2048, 64) diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 540e1e7d62d..2b7170401b3 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -56,7 +56,7 @@ function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space] end get!(gl_attributes, :projection) do # return get!(cam.calculated_values, Symbol("projection_$(space[])")) do - return lift(cam.projection, cam.pixel_space, space) do _, _, space + return lift(plot, cam.projection, cam.pixel_space, space) do _, _, space return Makie.space_to_clip(cam, space, false) end # end @@ -80,9 +80,12 @@ function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space] return nothing end -function handle_intensities!(attributes, plot) +function handle_intensities!(screen, attributes, plot) color = plot.calculated_colors if color[] isa Makie.ColorMapping + onany(plot, color[].color_scaled, color[].colorrange_scaled, color[].colormap, color[].nan_color) do args... + screen.requires_update = true + end attributes[:intensity] = color[].color_scaled interp = color[].color_mapping_type[] === Makie.continuous ? :linear : :nearest attributes[:color_map] = Texture(color[].colormap; minfilter=interp) @@ -107,26 +110,40 @@ function get_space(x) return haskey(x, :markerspace) ? x.markerspace : x.space end -function cached_robj!(robj_func, screen, scene, x::AbstractPlot) - # poll inside functions to make wait on compile less prominent - pollevents(screen) - robj = get!(screen.cache, objectid(x)) do - filtered = filter(x.attributes) do (k, v) - !in(k, ( - :transformation, :tickranges, :ticklabels, :raw, :SSAO, +const EXCLUDE_KEYS = Set([:transformation, :tickranges, :ticklabels, :raw, :SSAO, :lightposition, :material, :axis_cycler, - :inspector_label, :inspector_hover, :inspector_clear, :inspectable, + :inspector_label, :inspector_hover, :inspector_clear, :inspectable, :colorrange, :colormap, :colorscale, :highclip, :lowclip, :nan_color, - :calculated_colors - )) - end + :calculated_colors, :space, :markerspace, :model]) + +function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) + # poll inside functions to make wait on compile less prominent + pollevents(screen) + robj = get!(screen.cache, objectid(plot)) do + + filtered = filter(plot.attributes) do (k, v) + return !in(k, EXCLUDE_KEYS) + end + track_updates = screen.config.render_on_demand + if track_updates + for arg in plot.args + on(plot, arg) do x + screen.requires_update = true + end + end + end gl_attributes = Dict{Symbol, Any}(map(filtered) do key_value key, value = key_value gl_key = to_glvisualize_key(key) - gl_value = lift_convert(key, value, x) + gl_value = lift_convert(key, value, plot, screen) gl_key => gl_value end) + gl_attributes[:model] = plot.model + if haskey(plot, :markerspace) + gl_attributes[:markerspace] = plot.markerspace + end + gl_attributes[:space] = plot.space pointlight = Makie.get_point_light(scene) if !isnothing(pointlight) @@ -137,18 +154,16 @@ function cached_robj!(robj_func, screen, scene, x::AbstractPlot) if !isnothing(ambientlight) gl_attributes[:ambient] = ambientlight.color end - gl_attributes[:track_updates] = screen.config.render_on_demand gl_attributes[:px_per_unit] = screen.px_per_unit - handle_intensities!(gl_attributes, x) - connect_camera!(x, gl_attributes, scene.camera, get_space(x)) + handle_intensities!(screen, gl_attributes, plot) + connect_camera!(plot, gl_attributes, scene.camera, get_space(plot)) robj = robj_func(gl_attributes) get!(gl_attributes, :ssao, Observable(false)) - screen.cache2plot[robj.id] = x - - robj + screen.cache2plot[robj.id] = plot + return robj end push!(screen, scene, robj) return robj @@ -156,7 +171,7 @@ end Base.insert!(::GLMakie.Screen, ::Scene, ::Makie.PlotList) = nothing -function Base.insert!(screen::Screen, scene::Scene, x::Combined) +function Base.insert!(screen::Screen, scene::Scene, @nospecialize(x::Combined)) ShaderAbstractions.switch_context!(screen.glscreen) # poll inside functions to make wait on compile less prominent pollevents(screen) @@ -171,12 +186,6 @@ function Base.insert!(screen::Screen, scene::Scene, x::Combined) end end -function remove_automatic!(attributes) - filter!(attributes) do (k, v) - to_value(v) != automatic - end -end - index1D(x::SubArray) = parentindices(x)[1] handle_view(array::AbstractVector, attributes) = array @@ -196,12 +205,13 @@ function handle_view(array::Observable{T}, attributes) where T <: SubArray return A end -function lift_convert(key, value, plot) - return lift_convert_inner(value, Key{key}(), Key{Makie.plotkey(plot)}(), plot) +function lift_convert(key, value, plot, screen) + return lift_convert_inner(value, Key{key}(), Key{Makie.plotkey(plot)}(), plot, screen) end -function lift_convert_inner(value, key, plot_key, plot) +function lift_convert_inner(value, key, plot_key, plot, screen) return lift(plot, value) do value + screen.requires_update = true return convert_attribute(value, key, plot_key) end end @@ -222,28 +232,29 @@ end pixel2world(scene, msize::AbstractVector) = pixel2world.(scene, msize) -function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Union{Scatter, MeshScatter})) - return cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Scatter, MeshScatter})) + return cached_robj!(screen, scene, plot) do gl_attributes # signals not supported for shading yet gl_attributes[:shading] = to_value(get(gl_attributes, :shading, true)) - marker = lift_convert(:marker, pop!(gl_attributes, :marker), x) + marker = pop!(gl_attributes, :marker) - space = x.space - positions = handle_view(x[1], gl_attributes) - positions = apply_transform(transform_func_obs(x), positions, space) + space = plot.space + positions = handle_view(plot[1], gl_attributes) + positions = lift(apply_transform, plot, transform_func_obs(plot), positions, space) - if x isa Scatter - mspace = x.markerspace + if plot isa Scatter + mspace = plot.markerspace cam = scene.camera - gl_attributes[:preprojection] = map(space, mspace, cam.projectionview, cam.resolution) do space, mspace, _, _ + gl_attributes[:preprojection] = lift(plot, space, mspace, cam.projectionview, + cam.resolution) do space, mspace, _, _ return Makie.clip_to_space(cam, mspace) * Makie.space_to_clip(cam, space) end # fast pixel does its own setup if !(marker[] isa FastPixel) - gl_attributes[:billboard] = map(rot-> isa(rot, Billboard), x.rotations) + gl_attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotations) atlas = gl_texture_atlas() isnothing(gl_attributes[:distancefield][]) && delete!(gl_attributes, :distancefield) - shape = lift(m-> Cint(Makie.marker_to_sdf_shape(m)), x, marker) + shape = lift(m -> Cint(Makie.marker_to_sdf_shape(m)), plot, marker) gl_attributes[:shape] = shape get!(gl_attributes, :distancefield) do if shape[] === Cint(DISTANCEFIELD) @@ -257,7 +268,8 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Union{Scatte get!(gl_attributes, :uv_offset_width) do return Makie.primitive_uv_offset_width(atlas, marker, font) end - scale, quad_offset = Makie.marker_attributes(atlas, marker, gl_attributes[:scale], font, gl_attributes[:quad_offset]) + scale, quad_offset = Makie.marker_attributes(atlas, marker, gl_attributes[:scale], font, + gl_attributes[:quad_offset], plot) gl_attributes[:scale] = scale gl_attributes[:quad_offset] = quad_offset end @@ -272,7 +284,7 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Union{Scatte end return draw_pixel_scatter(screen, positions, gl_attributes) else - if x isa MeshScatter + if plot isa MeshScatter if haskey(gl_attributes, :color) && to_value(gl_attributes[:color]) isa AbstractMatrix{<: Colorant} gl_attributes[:image] = gl_attributes[:color] end @@ -287,20 +299,20 @@ end _mean(xs) = sum(xs) / length(xs) # skip Statistics import -function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) - return cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Lines)) + return cached_robj!(screen, scene, plot) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) - positions = handle_view(x[1], data) + positions = handle_view(plot[1], data) - transform_func = transform_func_obs(x) + transform_func = transform_func_obs(plot) ls = to_value(linestyle) - space = x.space + space = plot.space if isnothing(ls) data[:pattern] = ls data[:fast] = true - positions = apply_transform(transform_func, positions, space) + positions = lift(apply_transform, plot, transform_func, positions, space) else linewidth = gl_attributes[:thickness] px_per_unit = data[:px_per_unit] @@ -309,8 +321,9 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) end data[:fast] = false - pvm = map(*, data[:projectionview], data[:model]) - positions = map(transform_func, positions, space, pvm, data[:resolution]) do f, ps, space, pvm, res + pvm = lift(*, plot, data[:projectionview], data[:model]) + positions = lift(plot, transform_func, positions, space, pvm, + data[:resolution]) do f, ps, space, pvm, res transformed = apply_transform(f, ps, space) output = Vector{Point3f}(undef, length(transformed)) scale = Vec3f(res[1], res[2], 1f0) @@ -325,8 +338,8 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) end end -function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::LineSegments)) - return cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::LineSegments)) + return cached_robj!(screen, scene, plot) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) px_per_unit = data[:px_per_unit] @@ -336,14 +349,14 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::LineSegments data[:fast] = true else linewidth = gl_attributes[:thickness] - data[:pattern] = map(linestyle, linewidth, px_per_unit) do ls, lw, ppu + data[:pattern] = lift(plot, linestyle, linewidth, px_per_unit) do ls, lw, ppu ppu * _mean(lw) .* ls end data[:fast] = false end - positions = handle_view(x.converted[1], data) + positions = handle_view(plot[1], data) - positions = apply_transform(transform_func_obs(x), positions, x.space) + positions = lift(apply_transform, plot, transform_func_obs(plot), positions, plot.space) if haskey(data, :intensity) data[:color] = pop!(data, :intensity) end @@ -353,25 +366,25 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::LineSegments end function draw_atomic(screen::Screen, scene::Scene, - x::Text{<:Tuple{<:Union{<:Makie.GlyphCollection, <:AbstractVector{<:Makie.GlyphCollection}}}}) - return cached_robj!(screen, scene, x) do gl_attributes - glyphcollection = x[1] + plot::Text{<:Tuple{<:Union{<:Makie.GlyphCollection, <:AbstractVector{<:Makie.GlyphCollection}}}}) + return cached_robj!(screen, scene, plot) do gl_attributes + glyphcollection = plot[1] - transfunc = Makie.transform_func_obs(x) + transfunc = Makie.transform_func_obs(plot) pos = gl_attributes[:position] - space = x.space - markerspace = x.markerspace + space = plot.space + markerspace = plot.markerspace offset = pop!(gl_attributes, :offset, Vec2f(0)) atlas = gl_texture_atlas() # calculate quad metrics - glyph_data = map(pos, glyphcollection, offset, transfunc, space) do pos, gc, offset, transfunc, space + glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space) do pos, gc, offset, transfunc, space Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) end # unpack values from the one signal: positions, char_offset, quad_offset, uv_offset_width, scale = map((1, 2, 3, 4, 5)) do i - lift(getindex, x, glyph_data, i) + lift(getindex, plot, glyph_data, i) end @@ -383,7 +396,7 @@ function draw_atomic(screen::Screen, scene::Scene, )) # space, end - gl_attributes[:color] = lift(x, glyphcollection) do gc + gl_attributes[:color] = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.colors, length(g.glyphs)) for g in gc), init = RGBAf[]) @@ -391,7 +404,7 @@ function draw_atomic(screen::Screen, scene::Scene, Makie.collect_vector(gc.colors, length(gc.glyphs)) end end - gl_attributes[:stroke_color] = lift(x, glyphcollection) do gc + gl_attributes[:stroke_color] = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.strokecolors, length(g.glyphs)) for g in gc), init = RGBAf[]) @@ -400,7 +413,7 @@ function draw_atomic(screen::Screen, scene::Scene, end end - gl_attributes[:rotation] = lift(x, glyphcollection) do gc + gl_attributes[:rotation] = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.rotations, length(g.glyphs)) for g in gc), init = Quaternionf[]) @@ -415,10 +428,10 @@ function draw_atomic(screen::Screen, scene::Scene, gl_attributes[:marker_offset] = char_offset gl_attributes[:uv_offset_width] = uv_offset_width gl_attributes[:distancefield] = get_texture!(atlas) - gl_attributes[:visible] = x.visible + gl_attributes[:visible] = plot.visible cam = scene.camera # gl_attributes[:preprojection] = Observable(Mat4f(I)) - gl_attributes[:preprojection] = map(space, markerspace, cam.projectionview, cam.resolution) do s, ms, pv, res + gl_attributes[:preprojection] = lift(plot, space, markerspace, cam.projectionview, cam.resolution) do s, ms, pv, res Makie.clip_to_space(cam, ms) * Makie.space_to_clip(cam, s) end @@ -432,12 +445,12 @@ xy_convert(x::AbstractArray{Float32}, n) = copy(x) xy_convert(x::AbstractArray, n) = el32convert(x) xy_convert(x, n) = Float32[LinRange(extrema(x)..., n + 1);] -function draw_atomic(screen::Screen, scene::Scene, heatmap::Heatmap) - return cached_robj!(screen, scene, heatmap) do gl_attributes - t = Makie.transform_func_obs(heatmap) - mat = heatmap[3] - space = heatmap.space # needs to happen before connect_camera! call - xypos = lift(t, heatmap[1], heatmap[2], space) do t, x, y, space +function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) + return cached_robj!(screen, scene, plot) do gl_attributes + t = Makie.transform_func_obs(plot) + mat = plot[3] + space = plot.space # needs to happen before connect_camera! call + xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space x1d = xy_convert(x, size(mat[], 1)) y1d = xy_convert(y, size(mat[], 2)) # Only if transform doesn't do anything, we can stay linear in 1/2D @@ -455,12 +468,12 @@ function draw_atomic(screen::Screen, scene::Scene, heatmap::Heatmap) return (x1d, y1d) end end - xpos = map(first, xypos) - ypos = map(last, xypos) + xpos = lift(first, plot, xypos) + ypos = lift(last, plot, xypos) gl_attributes[:position_x] = Texture(xpos, minfilter = :nearest) gl_attributes[:position_y] = Texture(ypos, minfilter = :nearest) # number of planes used to render the heatmap - gl_attributes[:instances] = map(xpos, ypos) do x, y + gl_attributes[:instances] = lift(plot, xpos, ypos) do x, y (length(x)-1) * (length(y)-1) end interp = to_value(pop!(gl_attributes, :interpolate)) @@ -476,15 +489,15 @@ function draw_atomic(screen::Screen, scene::Scene, heatmap::Heatmap) end end -function draw_atomic(screen::Screen, scene::Scene, x::Image) - return cached_robj!(screen, scene, x) do gl_attributes - position = lift(x, x[1], x[2]) do x, y +function draw_atomic(screen::Screen, scene::Scene, plot::Image) + return cached_robj!(screen, scene, plot) do gl_attributes + position = lift(plot, plot[1], plot[2]) do x, y xmin, xmax = extrema(x) ymin, ymax = extrema(y) rect = Rect2f(xmin, ymin, xmax - xmin, ymax - ymin) return decompose(Point2f, rect) end - gl_attributes[:vertices] = apply_transform(transform_func_obs(x), position, x.space) + gl_attributes[:vertices] = lift(apply_transform, plot, transform_func_obs(plot), position, plot.space) rect = Rect2f(0, 0, 1, 1) gl_attributes[:faces] = decompose(GLTriangleFace, rect) gl_attributes[:texturecoordinates] = map(decompose_uv(rect)) do uv @@ -502,7 +515,7 @@ function draw_atomic(screen::Screen, scene::Scene, x::Image) end end -function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, space=:data) +function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space=:data) # signals not supported for shading yet shading = to_value(pop!(gl_attributes, :shading)) gl_attributes[:shading] = shading @@ -514,18 +527,18 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, space=:data) delete!(gl_attributes, :color_map) delete!(gl_attributes, :color_norm) elseif to_value(color) isa Makie.AbstractPattern - img = lift(x -> el32convert(Makie.to_image(x)), color) + img = lift(x -> el32convert(Makie.to_image(x)), plot, color) gl_attributes[:image] = ShaderAbstractions.Sampler(img, x_repeat=:repeat, minfilter=:nearest) get!(gl_attributes, :fetch_pixel, true) elseif to_value(color) isa AbstractMatrix{<:Colorant} - gl_attributes[:image] = Texture(const_lift(el32convert, color), minfilter = interp) + gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) delete!(gl_attributes, :color_map) delete!(gl_attributes, :color_norm) elseif to_value(color) isa AbstractMatrix{<: Number} - gl_attributes[:image] = Texture(const_lift(el32convert, color), minfilter = interp) + gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) gl_attributes[:color] = nothing elseif to_value(color) isa AbstractVector{<: Union{Number, Colorant}} - gl_attributes[:vertex_color] = lift(el32convert, color) + gl_attributes[:vertex_color] = lift(el32convert, plot, color) else # error("Unsupported color type: $(typeof(to_value(color)))") end @@ -557,12 +570,12 @@ function draw_atomic(screen::Screen, scene::Scene, meshplot::Mesh) return cached_robj!(screen, scene, meshplot) do gl_attributes t = transform_func_obs(meshplot) space = meshplot.space # needs to happen before connect_camera! call - return mesh_inner(screen, meshplot[1], t, gl_attributes, space) + return mesh_inner(screen, meshplot[1], t, gl_attributes, meshplot, space) end end -function draw_atomic(screen::Screen, scene::Scene, x::Surface) - robj = cached_robj!(screen, scene, x) do gl_attributes +function draw_atomic(screen::Screen, scene::Scene, plot::Surface) + robj = cached_robj!(screen, scene, plot) do gl_attributes color = pop!(gl_attributes, :color) img = nothing # signals not supported for shading yet @@ -570,7 +583,7 @@ function draw_atomic(screen::Screen, scene::Scene, x::Surface) if haskey(gl_attributes, :intensity) img = pop!(gl_attributes, :intensity) elseif to_value(color) isa Makie.AbstractPattern - pattern_img = lift(x -> el32convert(Makie.to_image(x)), color) + pattern_img = lift(x -> el32convert(Makie.to_image(x)), plot, color) img = ShaderAbstractions.Sampler(pattern_img, x_repeat=:repeat, minfilter=:nearest) haskey(gl_attributes, :fetch_pixel) || (gl_attributes[:fetch_pixel] = true) gl_attributes[:color_map] = nothing @@ -583,18 +596,18 @@ function draw_atomic(screen::Screen, scene::Scene, x::Surface) gl_attributes[:color_norm] = nothing end - space = x.space + space = plot.space gl_attributes[:image] = img gl_attributes[:shading] = to_value(get(gl_attributes, :shading, true)) - @assert to_value(x[3]) isa AbstractMatrix - types = map(v -> typeof(to_value(v)), x[1:2]) + @assert to_value(plot[3]) isa AbstractMatrix + types = map(v -> typeof(to_value(v)), plot[1:2]) if all(T -> T <: Union{AbstractMatrix, AbstractVector}, types) - t = Makie.transform_func_obs(x) - mat = x[3] - xypos = map(t, x[1], x[2], space) do t, x, y, space + t = Makie.transform_func_obs(plot) + mat = plot[3] + xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space # Only if transform doesn't do anything, we can stay linear in 1/2D if Makie.is_identity_transform(t) return (x, y) @@ -609,18 +622,18 @@ function draw_atomic(screen::Screen, scene::Scene, x::Surface) return (first.(matrix), last.(matrix)) end end - xpos = map(first, xypos) - ypos = map(last, xypos) + xpos = lift(first, plot, xypos) + ypos = lift(last, plot, xypos) args = map((xpos, ypos, mat)) do arg - Texture(map(x-> convert(Array, el32convert(x)), arg); minfilter=:linear) + Texture(lift(x-> convert(Array, el32convert(x)), plot, arg); minfilter=:linear) end if isnothing(img) gl_attributes[:image] = args[3] end return draw_surface(screen, args, gl_attributes) else - gl_attributes[:ranges] = to_range.(to_value.(x[1:2])) - z_data = Texture(el32convert(x[3]); minfilter=:linear) + gl_attributes[:ranges] = to_range.(to_value.(plot[1:2])) + z_data = Texture(lift(el32convert, plot, plot[3]); minfilter=:linear) if isnothing(img) gl_attributes[:image] = z_data end @@ -630,11 +643,11 @@ function draw_atomic(screen::Screen, scene::Scene, x::Surface) return robj end -function draw_atomic(screen::Screen, scene::Scene, vol::Volume) - robj = cached_robj!(screen, scene, vol) do gl_attributes - model = vol[:model] - x, y, z = vol[1], vol[2], vol[3] - gl_attributes[:model] = lift(model, x, y, z) do m, xyz... +function draw_atomic(screen::Screen, scene::Scene, plot::Volume) + return cached_robj!(screen, scene, plot) do gl_attributes + model = plot.model + x, y, z = plot[1], plot[2], plot[3] + gl_attributes[:model] = lift(plot, model, x, y, z) do m, xyz... mi = minimum.(xyz) maxi = maximum.(xyz) w = maxi .- mi @@ -650,7 +663,7 @@ function draw_atomic(screen::Screen, scene::Scene, vol::Volume) intensity = pop!(gl_attributes, :intensity) return draw_volume(screen, intensity, gl_attributes) else - return draw_volume(screen, vol[4], gl_attributes) + return draw_volume(screen, plot[4], gl_attributes) end end end diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 70cd305ae7c..5010ba52746 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -208,7 +208,7 @@ function Makie.mouse_position(scene::Scene, screen::Screen) updater = MousePositionUpdater( screen, scene.events.mouseposition, scene.events.hasfocus ) - on(updater, screen.render_tick) + on(updater, scene, screen.render_tick) return end function Makie.disconnect!(screen::Screen, ::typeof(mouse_position)) diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index 3f028924d82..7901b34c074 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -129,7 +129,7 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) # To allow adding postprocessors in various combinations we need to keep # track of the buffer ids that are already in use. We may also want to reuse # buffers so we give them names for easy fetching. - buffer_ids = Dict( + buffer_ids = Dict{Symbol,GLuint}( :color => GL_COLOR_ATTACHMENT0, :objectid => GL_COLOR_ATTACHMENT1, :HDR_color => GL_COLOR_ATTACHMENT2, @@ -137,20 +137,20 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) :depth => GL_DEPTH_ATTACHMENT, :stencil => GL_STENCIL_ATTACHMENT, ) - buffers = Dict( - :color => color_buffer, + buffers = Dict{Symbol, Texture}( + :color => color_buffer, :objectid => objectid_buffer, :HDR_color => HDR_color_buffer, :OIT_weight => OIT_weight_buffer, - :depth => depth_buffer, - :stencil => depth_buffer + :depth => depth_buffer, + :stencil => depth_buffer ) return GLFramebuffer( fb_size_node, frambuffer_id, buffer_ids, buffers, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1] - ) + )::GLFramebuffer end function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index 6ddcc86e980..d2bd372aa14 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -10,14 +10,18 @@ macro compile(block) end end + + let @setup_workload begin x = rand(5) @compile_workload begin + GLMakie.activate!() screen = GLMakie.singleton_screen(false) close(screen) destroy!(screen) + base_path = normpath(joinpath(dirname(pathof(Makie)), "..", "precompile")) shared_precompile = joinpath(base_path, "shared-precompile.jl") include(shared_precompile) @@ -26,6 +30,22 @@ let catch end Makie.CURRENT_FIGURE[] = nothing + + screen = Screen(Scene()) + close(screen) + screen = empty_screen(false) + close(screen) + destroy!(screen) + + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}()) + screen = Screen(Scene(), config, nothing, MIME"image/png"(); visible=false, start_renderloop=false) + close(screen) + + + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol,Any}()) + screen = Screen(Scene(), config; visible=false, start_renderloop=false) + close(screen) + empty!(atlas_texture_cache) closeall() @assert isempty(SCREEN_REUSE_POOL) @@ -35,3 +55,16 @@ let end nothing end + +precompile(Screen, (Scene, ScreenConfig)) +precompile(GLFramebuffer, (NTuple{2,Int},)) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{Float32})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBAf})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBf})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBA{N0f8}})) +precompile(glTexImage, + (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{GLAbstraction.DepthStencil_24_8})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{Vec{2,GLuint}})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{RGBA{Float16}})) +precompile(glTexImage, (GLenum, Int, GLenum, Int, Int, Int, GLenum, GLenum, Ptr{N0f8})) +precompile(setindex!, (GLMakie.GLAbstraction.Texture{Float16,2}, Matrix{Float32}, Rect2{Int32})) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index e710bc6d444..64a2e1b2ffa 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -374,7 +374,7 @@ function Screen(; screen_config... ) # Screen config is managed by the current active theme, so managed by Makie - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) screen = screen_from_pool(config.debugging) apply_config!(screen, config; start_renderloop=start_renderloop) if !isnothing(resolution) @@ -400,7 +400,7 @@ function display_scene!(screen::Screen, scene::Scene) end function Screen(scene::Scene; start_renderloop=true, screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(scene, config; start_renderloop=start_renderloop) end @@ -452,10 +452,10 @@ function Makie.insertplots!(screen::Screen, scene::Scene) push!(screen.screens, (id, scene)) screen.requires_update = true onany( - (_, _, _, _, _, _) -> screen.requires_update = true, + (args...) -> screen.requires_update = true, scene, scene.visible, scene.backgroundcolor, scene.clear, - scene.ssao.bias, scene.ssao.blur, scene.ssao.radius + scene.ssao.bias, scene.ssao.blur, scene.ssao.radius, scene.camera.projectionview, scene.camera.resolution ) return id end @@ -912,9 +912,7 @@ function requires_update(screen::Screen) screen.requires_update = false return true end - for (_, _, robj) in screen.renderlist - robj.requires_update && return true - end + return false end diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 5a386d5d0e8..1f444752afa 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -81,7 +81,7 @@ end glFinish() end end - fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0) + fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0; color=:black) screen = display(GLMakie.Screen(;renderloop=(screen) -> nothing, start_renderloop=false), fig.scene) buff = RNG.rand(Point3f, 10^4) .* 20f0; update_loop(meshplot, buff, screen) @@ -97,9 +97,9 @@ end fig = Figure() left = LScene(fig[1, 1]) contour!(left, [sin(i+j) * sin(j+k) * sin(i+k) for i in 1:10, j in 1:10, k in 1:10], enable_depth = true) - mesh!(left, Sphere(Point3f(5), 6f0)) + mesh!(left, Sphere(Point3f(5), 6f0), color=:black) right = LScene(fig[1, 2]) volume!(right, [sin(2i) * sin(2j) * sin(2k) for i in 1:10, j in 1:10, k in 1:10], algorithm = :iso, enable_depth = true) - mesh!(right, Sphere(Point3f(5), 6f0)) + mesh!(right, Sphere(Point3f(5), 6.0f0); color=:black) fig end diff --git a/MakieCore/src/attributes.jl b/MakieCore/src/attributes.jl index 14018cbdf7d..989ec7cb6b6 100644 --- a/MakieCore/src/attributes.jl +++ b/MakieCore/src/attributes.jl @@ -59,7 +59,15 @@ function Base.deepcopy(attributes::Attributes) end Base.filter(f, x::Attributes) = Attributes(filter(f, attributes(x))) -Base.empty!(x::Attributes) = (empty!(attributes(x)); x) +function Base.empty!(x::Attributes) + attr = attributes(x) + for (key, obs) in attr + Observables.clear(obs) + end + empty!(attr) + return x +end + Base.length(x::Attributes) = length(attributes(x)) function Base.merge!(target::Attributes, args::Attributes...) diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index 2f3f564cfb9..70ac9e9f9b8 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -26,34 +26,17 @@ plotkey(any) = nothing argtypes(::T) where {T <: Tuple} = T -function create_figurelike end -function create_figurelike! end +function create_axis_like end +function create_axis_like! end function figurelike_return end function figurelike_return! end -function _create_plot(F, attributes::Dict, args...) - figlike, plot_kw, plot_args = create_figurelike(Combined{F}, attributes, args...) - plot = Combined{F}(plot_args, plot_kw) - plot!(figlike, plot) - return figurelike_return(figlike, plot) -end - -function _create_plot!(F, attributes::Dict, args...) - figlike, plot_kw, plot_args = create_figurelike!(Combined{F}, attributes, args...) - plot = Combined{F}(plot_args, plot_kw) - plot!(figlike, plot) - return figurelike_return!(figlike, plot) -end - -function _create_plot!(F, kw::Dict, scene::SceneLike, args...) - plot = Combined{F}(args, kw) - plot!(scene, plot) - return plot -end +function _create_plot end +function _create_plot! end -plot(args...; kw...) = _create_plot(plot, Dict{Symbol, Any}(kw), args...) -plot!(args...; kw...) = _create_plot!(plot, Dict{Symbol, Any}(kw), args...) +plot(args...; kw...) = _create_plot(plotfunc(plottype(map(to_value, args)...)), Dict{Symbol, Any}(kw), args...) +plot!(args...; kw...) = _create_plot!(plotfunc(plottype(map(to_value, args)...)), Dict{Symbol, Any}(kw), args...) """ Each argument can be named for a certain plot type `P`. Falls back to `arg1`, `arg2`, etc. @@ -230,4 +213,4 @@ e.g.: plottype(x::Array{<: AbstractFloat, 3}) = Volume ``` """ -plottype(plot_args...) = Combined{Any, Tuple{typeof.(to_value.(plot_args))...}} # default to dispatch to type recipes! +plottype(plot_args...) = Combined{plot, Tuple{map(typeof, plot_args)...}} # default to dispatch to type recipes! diff --git a/Project.toml b/Project.toml index 09fc29f3a74..fc1aed7a248 100644 --- a/Project.toml +++ b/Project.toml @@ -18,6 +18,7 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" FFMPEG_jll = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +FilePaths = "8fc22ac5-c921-52a6-82fd-178b2807b824" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index 07e2d491961..ea76382db46 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -184,12 +184,12 @@ function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig) end function Screen(fb_size::NTuple{2,<:Integer}; screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(fb_size, config) end function Screen(scene::Scene; screen_config...) - config = Makie.merge_screen_config(ScreenConfig, screen_config) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(scene, config) end diff --git a/ReferenceTests/src/database.jl b/ReferenceTests/src/database.jl index 081478b2311..0c008588532 100644 --- a/ReferenceTests/src/database.jl +++ b/ReferenceTests/src/database.jl @@ -29,33 +29,32 @@ macro reference_test(name, code) funcs = used_functions(code) skip = (title in SKIP_TITLES) || any(x-> x in funcs, SKIP_FUNCTIONS) return quote - t1 = time() @testset $(title) begin if $skip @test_broken false else + t1 = time() if $title in $REGISTERED_TESTS error("title must be unique. Duplicate title: $(title)") end println("running $(lpad(COUNTER[] += 1, 3)): $($title)") Makie.set_theme!(; resolution=(500, 500), - CairoMakie=(; px_per_unit=1), - GLMakie=(; scalefactor=1, px_per_unit=1), - WGLMakie=(; scalefactor=1, px_per_unit=1)) + CairoMakie=(; px_per_unit=1), + GLMakie=(; scalefactor=1, px_per_unit=1), + WGLMakie=(; scalefactor=1, px_per_unit=1)) ReferenceTests.RNG.seed_rng!() result = let $(esc(code)) end @test save_result(joinpath(RECORDING_DIR[], $title), result) push!($REGISTERED_TESTS, $title) + elapsed = round(time() - t1; digits=5) + total = Sys.total_memory() + mem = round((total - Sys.free_memory()) / 10^9; digits=3) + # TODO, write to file and create an overview in the end, similar to the benchmark results! + println("Used $(mem)gb of $(round(total / 10^9; digits=3))gb RAM, time: $(elapsed)s") end end - GC.gc(true) - elapsed = round(time() - t1; digits=5) - total = Sys.total_memory() - mem = round((total - Sys.free_memory()) / 10^9; digits=3) - # TODO, write to file and create an overview in the end, similar to the benchmark results! - println("Used $(mem)gb of $(round(total / 10^9; digits=3))gb RAM, time: $(elapsed)s") end end diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index a8ebbc782c5..fb10a5197f6 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -1069,7 +1069,7 @@ end s = Scene(camera = campixel!, resolution = (600, 600)) for (i, (offx, offy)) in enumerate(zip([0, 20, 50], [0, 10, 30])) for (j, rot) in enumerate([0, pi/4, pi/2]) - scatter!(s, 150i, 150j) + scatter!(s, 150i, 150j, color=:black) text!(s, 150i, 150j, text = L"\sqrt{x+y}", offset = (offx, offy), rotation = rot, fontsize = 30) end diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 40430f1540d..32f313657cb 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -59,7 +59,7 @@ end end @reference_test "Load Mesh" begin - mesh(loadasset("cat.obj")) + mesh(loadasset("cat.obj"); color=:black) end @reference_test "Colored Mesh" begin @@ -444,8 +444,8 @@ end @reference_test "Line GIF" begin us = range(0, stop=1, length=100) - f, ax, p = linesegments(Rect3f(Vec3f(0, -1, 0), Vec3f(1, 2, 2))) - p = lines!(ax, us, sin.(us), zeros(100), linewidth=3, transparency=true) + f, ax, p = linesegments(Rect3f(Vec3f(0, -1, 0), Vec3f(1, 2, 2)); color=:black) + p = lines!(ax, us, sin.(us), zeros(100), linewidth=3, transparency=true, color=:black) lineplots = [p] Makie.translate!(p, 0, 0, 0) colors = to_colormap(:RdYlBu) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 1f4ed54f998..a33927887eb 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -13,6 +13,7 @@ scalar .* (points .+ Point2f(linewidth*2, i * 3.25)), linewidth = linewidth, linestyle = linestyle, + color=:black ) end end @@ -49,6 +50,7 @@ end Point2f(i, j) .* 45, marker = m, markersize = ms, + color=:black ) end end @@ -71,6 +73,7 @@ end marker = m, markersize = 30, rotations = rot, + color=:black ) scatter!(s, p, color = :red, markersize = 6) end @@ -401,7 +404,7 @@ end function draw_marker_test!(scene, marker, center; markersize=300) # scatter!(scene, center, distancefield=matr, uv_offset_width=Vec4f(0, 0, 1, 1), markersize=600) - scatter!(scene, center, marker=marker, markersize=markersize, markerspace=:pixel) + scatter!(scene, center, color=:black, marker=marker, markersize=markersize, markerspace=:pixel) font = Makie.defaultfont() charextent = Makie.FreeTypeAbstraction.get_extent(font, marker) @@ -460,7 +463,7 @@ end f end - @reference_test "barplot with TeX-ed labels" begin +@reference_test "barplot with TeX-ed labels" begin fig = Figure(resolution = (800, 800)) lab1 = L"\int f(x) dx" lab2 = lab1 diff --git a/ReferenceTests/src/tests/refimages.jl b/ReferenceTests/src/tests/refimages.jl index 73ce26f4a42..f66d70b4e30 100644 --- a/ReferenceTests/src/tests/refimages.jl +++ b/ReferenceTests/src/tests/refimages.jl @@ -13,6 +13,9 @@ using ReferenceTests.Colors: RGB, N0f8 using ReferenceTests.DelaunayTriangulation using Makie: Record, volume +@testset "specapi" begin + include("specapi.jl") +end @testset "primitives" begin include("primitives.jl") end diff --git a/ReferenceTests/src/tests/specapi.jl b/ReferenceTests/src/tests/specapi.jl new file mode 100644 index 00000000000..5ddff48ed1c --- /dev/null +++ b/ReferenceTests/src/tests/specapi.jl @@ -0,0 +1,130 @@ +import Makie.SpecApi as S + +function synchronize() + # This is very unfortunate, but deletion and updates + # are async in WGLMakie and there is no way for use to synchronize on them YET + if nameof(Makie.CURRENT_BACKEND[]) == :WGLMakie + sleep(2) + end +end + +function sync_step!(stepper) + synchronize() + Makie.step!(stepper) +end + +@reference_test "FigureSpec" begin + f, _, pl = plot(S.Figure()) + st = Makie.Stepper(f) + sync_step!(st) + obs = pl[1] + obs[] = S.Figure(S.Axis(; plots=[S.lines(1:4; color=:black, linewidth=5), S.scatter(1:4; markersize=20)]), + S.Axis3(; plots=[S.scatter(Rect3f(Vec3f(0), Vec3f(1)); color=:red, markersize=50)])) + sync_step!(st) + obs[] = begin + f = S.Figure() + ax = S.Axis(f[1, 1]) + S.scatter!(ax, 1:4) + ax2 = S.Axis3(f[1, 2]; title="Title 0") + S.scatter!(ax2, 1:4; color=1:4, markersize=20) + S.Colorbar(f[1, 3]; limits=(0, 1), colormap=:heat) + f + end + sync_step!(st) + + obs[] = begin + f = S.Figure() + ax = S.Axis(f[1, 1]; title="Title 1") + S.scatter!(ax, 1:4; markersize=50) + ax2 = S.Axis3(f[1, 2]) + S.scatter!(ax2, 2:4; color=1:3, markersize=30) + S.Colorbar(f[1, 3]; limits=(2, 10), colormap=:viridis, width=50) + f + end + sync_step!(st) + + obs[] = S.Figure( + S.Axis(; plots=[S.scatter(1:4; markersize=20), S.lines(1:4; color=:darkred, linewidth=6)]), + S.Axis3(; plots=[S.scatter(Rect3f(Vec3f(0), Vec3f(1)); color=(:red, 0.5), markersize=30)])) + sync_step!(st) + + + elem_1 = [LineElement(; color=:red, linestyle=nothing), + MarkerElement(; color=:blue, marker='x', markersize=15, + strokecolor=:black)] + + elem_2 = [PolyElement(; color=:red, strokecolor=:blue, strokewidth=1), + LineElement(; color=:black, linestyle=:dash)] + + elem_3 = LineElement(; color=:green, linestyle=nothing, + points=Point2f[(0, 0), (0, 1), (1, 0), (1, 1)]) + + obs[] = begin + f = S.Figure() + S.Legend(f[1, 1], [elem_1, elem_2, elem_3], ["elem 1", "elem 2", "elem 3"], "Legend Title") + f + end + sync_step!(st) + + obs[] = begin + f = S.Figure() + S.Legend(f[1, 1], [elem_1, elem_2], ["elem 1", "elem 2"], "New Title") + f + end + sync_step!(st) + + obs[] = S.Figure() + sync_step!(st) + + st +end + +struct PlotGrid + nplots::Tuple{Int,Int} +end + +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::PlotGrid) + f = S.Figure(; fontsize=30) + for i in 1:obj.nplots[1] + for j in 1:obj.nplots[2] + ax = S.Axis(f[i, j]) + S.lines!(ax, 1:4; linewidth=5) + S.lines!(ax, 2:5; linewidth=7) + end + end + return f +end +struct LineScatter + show_lines::Bool + show_scatter::Bool +end +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::LineScatter, data...) + plots = PlotSpec[] + if obj.show_lines + push!(plots, S.lines(data...; linewidth=5)) + end + if obj.show_scatter + push!(plots, S.scatter(data...; markersize=20)) + end + return plots +end + +@reference_test "SpecApi in convert_arguments" begin + f = Figure() + p1 = plot(f[1, 1], PlotGrid((1, 1))) + ax, p2 = plot(f[1, 2], LineScatter(true, true), 1:4) + st = Makie.Stepper(f) + sync_step!(st) + p1[1] = PlotGrid((2, 2)) + p2[1] = LineScatter(false, true) + sync_step!(st) + + p1[1] = PlotGrid((3, 3)) + p2[1] = LineScatter(true, false) + sync_step!(st) + + p1[1] = PlotGrid((2, 1)) + p2[1] = LineScatter(true, true) + sync_step!(st) + st +end diff --git a/ReferenceTests/src/tests/text.jl b/ReferenceTests/src/tests/text.jl index 65be5393c1e..f5aa087d827 100644 --- a/ReferenceTests/src/tests/text.jl +++ b/ReferenceTests/src/tests/text.jl @@ -66,8 +66,7 @@ end for valign in (:top, :center, :bottom) for rotation in angles] - scatter!(scene, points, marker = :circle, markersize = 10px) - + scatter!(scene, points, marker = :circle, markersize = 10px, color=:black) text!(scene, points, text = strings, align = aligns, rotation = rotations, color = [(:black, alpha) for alpha in LinRange(0.3, 0.7, length(points))]) @@ -79,7 +78,7 @@ end scene = Scene(camera = campixel!, resolution = (800, 800)) points = [Point(x, y) .* 200 for x in 1:3 for y in 1:3] - scatter!(scene, points, marker = :circle, markersize = 10px) + scatter!(scene, points, marker = :circle, markersize = 10px, color=:black) symbols = (:left, :center, :right) @@ -284,7 +283,7 @@ end position = Point2f(50, 50), rotation = 0.0, markerspace = :data) - wireframe!(s, boundingbox(t)) + wireframe!(s, boundingbox(t), color=:black) s end diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index 4fd7f835587..a81beff2314 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -121,7 +121,9 @@ end obs = Observable(1:5) f, ax, pl = scatter(obs; markersize=150) s = display(f) - @test length(obs.listeners) == 1 + # So, for GLMakie it will be 2, since we register an additional listener for + # State changes for the on demand renderloop + @test length(obs.listeners) in (1, 2) delete!(ax, pl) @test length(obs.listeners) == 0 # ugh, hard to synchronize this with WGLMakie, so, we need to sleep for now to make sure the change makes it to the browser diff --git a/WGLMakie/src/Camera.js b/WGLMakie/src/Camera.js index cac43a96979..86de8616b19 100644 --- a/WGLMakie/src/Camera.js +++ b/WGLMakie/src/Camera.js @@ -46,17 +46,23 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { } const [w, h] = makie_camera.resolution.value; const camera = new THREE.PerspectiveCamera( - cam3d.fov, + cam3d.fov.value, w / h, - cam3d.near, - cam3d.far + cam3d.near.value, + cam3d.far.value ); - const center = new THREE.Vector3(...cam3d.lookat); - camera.up = new THREE.Vector3(...cam3d.upvector); - camera.position.set(...cam3d.eyeposition); + const center = new THREE.Vector3(...cam3d.lookat.value); + camera.up = new THREE.Vector3(...cam3d.upvector.value); + camera.position.set(...cam3d.eyeposition.value); camera.lookAt(center); + const use_julia_cam = () => + JSServe.can_send_to_julia && JSServe.can_send_to_julia(); + function update() { + if (use_julia_cam()) { + return; + } const view = camera.matrixWorldInverse; const projection = camera.projectionMatrix; const [width, height] = cam3d.resolution.value; @@ -72,13 +78,13 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { [x, y, z] ); } - cam3d.resolution.on(update); - function addMouseHandler(domObject, drag, zoomIn, zoomOut) { let startDragX = null; let startDragY = null; function mouseWheelHandler(e) { - e = window.event || e; + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } @@ -88,10 +94,12 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { } else if (delta == 1) { zoomIn(); } - e.preventDefault(); } function mouseDownHandler(e) { + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } @@ -101,6 +109,9 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { e.preventDefault(); } function mouseMoveHandler(e) { + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } @@ -113,6 +124,9 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { e.preventDefault(); } function mouseUpHandler(e) { + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } diff --git a/WGLMakie/src/Serialization.js b/WGLMakie/src/Serialization.js index 64bfa76268e..9b668190d1f 100644 --- a/WGLMakie/src/Serialization.js +++ b/WGLMakie/src/Serialization.js @@ -21,6 +21,7 @@ export function delete_scene(scene_id) { if (!scene) { return; } + delete_three_scene(scene); while (scene.children.length > 0) { scene.remove(scene.children[0]); } @@ -40,7 +41,10 @@ export function find_plots(plot_uuids) { export function delete_scenes(scene_uuids, plot_uuids) { plot_uuids.forEach((plot_id) => { - delete plot_cache[plot_id]; + const plot = plot_cache[plot_id]; + if (plot) { + delete_plot(plot); + } }); scene_uuids.forEach((scene_id) => { delete_scene(scene_id); @@ -54,14 +58,9 @@ export function insert_plot(scene_id, plot_data) { }); } -export function delete_plots(scene_id, plot_uuids) { - console.log(`deleting plots!: ${plot_uuids}`); - const scene = find_scene(scene_id); +export function delete_plots(plot_uuids) { const plots = find_plots(plot_uuids); - plots.forEach((p) => { - scene.remove(p); - delete plot_cache[p.plot_uuid]; - }); + plots.forEach(delete_plot); } function convert_texture(data) { @@ -171,6 +170,7 @@ export function deserialize_plot(data) { const ON_NEXT_INSERT = new Set(); + export function on_next_insert(f) { ON_NEXT_INSERT.add(f); } @@ -210,7 +210,7 @@ export function add_plot(scene, plot_data) { ); } const p = deserialize_plot(plot_data); - plot_cache[plot_data.uuid] = p; + plot_cache[p.plot_uuid] = p; scene.add(p); // execute all next insert callbacks const next_insert = new Set(ON_NEXT_INSERT); // copy @@ -547,16 +547,18 @@ export function deserialize_scene(data, screen) { update_cam(data.camera.value); if (data.cam3d_state) { + // add JS camera... This will only update the camera matrices via js if: + // JSServe.can_send_to_julia && can_send_to_julia() Camera.attach_3d_camera(canvas, camera, data.cam3d_state, scene); - } else { - data.camera.on(update_cam); } + data.camera.on(update_cam); data.plots.forEach((plot_data) => { add_plot(scene, plot_data); }); - scene.scene_children = data.children.map((child) => - deserialize_scene(child, screen) - ); + scene.scene_children = data.children.map((child) => { + const childscene = deserialize_scene(child, screen); + return childscene; + }); return scene; } diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index d0fc48f28d9..21551f7f7da 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -13,7 +13,7 @@ end function Base.size(screen::ThreeDisplay) # look at d.qs().clientWidth for displayed width js = js"[document.querySelector('canvas').width, document.querySelector('canvas').height]" - width, height = round.(Int, JSServe.evaljs_value(screen.session, js; time_out=100)) + width, height = round.(Int, JSServe.evaljs_value(screen.session, js; timeout=100)) return (width, height) end @@ -118,8 +118,6 @@ function mark_as_displayed!(screen::Screen, scene::Scene) return end - - for M in Makie.WEB_MIMES @eval begin function Makie.backend_show(screen::Screen, io::IO, m::$M, scene::Scene) @@ -192,7 +190,8 @@ end # TODO, create optimized screens, forward more options to JS/WebGL function Screen(scene::Scene; kw...) - return Screen(Channel{ThreeDisplay}(1), nothing, scene, Makie.merge_screen_config(ScreenConfig, kw)) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(kw)) + return Screen(Channel{ThreeDisplay}(1), nothing, scene, config) end Screen(scene::Scene, config::ScreenConfig) = Screen(Channel{ThreeDisplay}(1), nothing, scene, config) Screen(scene::Scene, config::ScreenConfig, ::IO, ::MIME) = Screen(scene, config) @@ -209,7 +208,7 @@ Makie.wait_for_display(screen::Screen) = get_three(screen) function Base.display(screen::Screen, scene::Scene; unused...) Makie.push_screen!(scene, screen) # Reference to three object which gets set once we serve this to a browser - app = App() do session, request + app = App() do session screen.session = session three, canvas, done_init = three_display(screen, session, scene) on(session, done_init) do _ @@ -254,10 +253,14 @@ function insert_scene!(disp, screen::Screen, scene::Scene) if js_uuid(scene) in screen.displayed_scenes return true else + if !(js_uuid(scene.parent) in screen.displayed_scenes) + # Parents serialize their child scenes, so we only need to + # serialize & update the parent scene + return insert_scene!(disp, screen, scene.parent) + end scene_ser = serialize_scene(scene) parent = scene.parent parent_uuid = js_uuid(parent) - insert_scene!(disp, screen, parent) # make sure parent is also already displayed err = "Cant find scene js_uuid(scene) == $(parent_uuid)" evaljs_value(disp.session, js""" $(WGL).then(WGL=> { @@ -274,14 +277,23 @@ function insert_scene!(disp, screen::Screen, scene::Scene) end end -function Base.insert!(screen::Screen, scene::Scene, plot::Combined) +function insert_plot!(disp::ThreeDisplay, scene::Scene, @nospecialize(plot::Combined)) + plot_data = serialize_plots(scene, [plot]) + plot_sub = Session(disp.session) + JSServe.init_session(plot_sub) + plot.__wgl_session = plot_sub + js = js""" + $(WGL).then(WGL=> { + WGL.insert_plot($(js_uuid(scene)), $plot_data); + })""" + JSServe.evaljs_value(plot_sub, js; timeout=50) + return +end + +function Base.insert!(screen::Screen, scene::Scene, @nospecialize(plot::Combined)) disp = get_three(screen; error="Plot needs to be displayed to insert additional plots") if js_uuid(scene) in screen.displayed_scenes - plot_data = serialize_plots(scene, [plot]) - JSServe.evaljs_value(disp.session, js""" - $(WGL).then(WGL=> { - WGL.insert_plot($(js_uuid(scene)), $plot_data); - })""") + insert_plot!(disp, scene, plot) else # Newly created scene gets inserted! # This must be a child plot of some parent, otherwise a plot wouldn't be inserted via `insert!(screen, ...)` @@ -299,25 +311,42 @@ function Base.insert!(screen::Screen, scene::Scene, plot::Combined) return end -function delete_js_objects!(screen::Screen, scene::String, uuids::Vector{String}) +function delete_js_objects!(screen::Screen, plot_uuids::Vector{String}, + session::Union{Nothing,Session}) three = get_three(screen) isnothing(three) && return # if no session we haven't displayed and dont need to delete isready(three.session) || return JSServe.evaljs(three.session, js""" $(WGL).then(WGL=> { - WGL.delete_plots($(scene), $(uuids)); + WGL.delete_plots($(plot_uuids)); })""") + !isnothing(session) && close(session) return end +function all_plots_scenes(scene::Scene; scene_uuids=String[], plots=Combined[]) + push!(scene_uuids, js_uuid(scene)) + append!(plots, scene.plots) + for child in scene.children + all_plots_scenes(child; plots=plots, scene_uuids=scene_uuids) + end + return scene_uuids, plots +end + function delete_js_objects!(screen::Screen, scene::Scene) three = get_three(screen) isnothing(three) && return # if no session we haven't displayed and dont need to delete isready(three.session) || return - scene_uuids, plot_uuids = all_plots_scenes(scene) + scene_uuids, plots = all_plots_scenes(scene) + for plot in plots + if haskey(plot, :__wgl_session) + session = plot.__wgl_session[] + close(session) + end + end JSServe.evaljs(three.session, js""" $(WGL).then(WGL=> { - WGL.delete_scenes($scene_uuids, $plot_uuids); + WGL.delete_scenes($scene_uuids, $(js_uuid.(plots))); })""") return end @@ -357,12 +386,14 @@ function run_jobs!(queue::LockfreeQueue) if !isnothing(q) while !isempty(q) item = pop!(q) - queue.execute_job(item...) + Base.invokelatest(queue.execute_job, item...) end end sleep(0.1) catch e - @warn "error while cleaning up JS objects" exception = (e, Base.catch_backtrace()) + if !(e isa EOFError) + @warn "error while running JS objects" exception = (e, Base.catch_backtrace()) + end end end end @@ -378,14 +409,15 @@ function Base.push!(queue::LockfreeQueue, item) end const DISABLE_JS_FINALZING = Base.RefValue(false) -const DELETE_QUEUE = LockfreeQueue{Tuple{Screen,String,Vector{String}}}(delete_js_objects!) +const DELETE_QUEUE = LockfreeQueue{Tuple{Screen, Vector{String}, Union{Session, Nothing}}}(delete_js_objects!) const SCENE_DELETE_QUEUE = LockfreeQueue{Tuple{Screen,Scene}}(delete_js_objects!) function Base.delete!(screen::Screen, scene::Scene, plot::Combined) - atomics = Makie.collect_atomic_plots(plot) # delete all atomics # only queue atomics to actually delete on js if !DISABLE_JS_FINALZING[] - push!(DELETE_QUEUE, (screen, js_uuid(scene), js_uuid.(atomics))) + plot_uuids = map(js_uuid, Makie.collect_atomic_plots(plot)) + session = to_value(get(plot, :__wgl_session, nothing)) + push!(DELETE_QUEUE, (screen, plot_uuids, session)) end return end @@ -394,4 +426,6 @@ function Base.delete!(screen::Screen, scene::Scene) if !DISABLE_JS_FINALZING[] push!(SCENE_DELETE_QUEUE, (screen, scene)) end + delete!(screen.displayed_scenes, js_uuid(scene)) + return end diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl index 6f936a247f7..d77184312b2 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -6,19 +6,16 @@ using Makie: el32convert, surface_normals, get_dim nothing_or_color(c) = to_color(c) nothing_or_color(c::Nothing) = RGBAf(0, 0, 0, 1) -lift_or(f, x) = f(x) -lift_or(f, x::Observable) = lift(f, x) - function create_shader(mscene::Scene, plot::Surface) # TODO OWN OPTIMIZED SHADER ... Or at least optimize this a bit more ... px, py, pz = plot[1], plot[2], plot[3] grid(x, y, z, trans, space) = Makie.matrix_grid(p-> apply_transform(trans, p, space), x, y, z) - positions = Buffer(lift(grid, px, py, pz, transform_func_obs(plot), get(plot, :space, :data))) + positions = Buffer(lift(grid, plot, px, py, pz, transform_func_obs(plot), get(plot, :space, :data))) rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), pz) - faces = Buffer(lift(r -> decompose(GLTriangleFace, r), rect)) - uv = Buffer(lift(decompose_uv, rect)) - normals = Buffer(lift(surface_normals, px, py, pz)) + faces = Buffer(lift(r -> decompose(GLTriangleFace, r), plot, rect)) + uv = Buffer(lift(decompose_uv, plot, rect)) + normals = Buffer(lift(surface_normals, plot, px, py, pz)) per_vertex = Dict(:positions => positions, :faces => faces, :uv => uv, :normals => normals) uniforms = Dict(:uniform_color => color, :color => false) @@ -26,9 +23,7 @@ function create_shader(mscene::Scene, plot::Surface) end function create_shader(mscene::Scene, plot::Union{Heatmap, Image}) - minfilter = to_value(get(plot, :interpolate, false)) ? :linear : :nearest mesh = limits_to_uvmesh(plot) - uniforms = Dict( :normals => Vec3f(0), :shading => false, @@ -45,7 +40,7 @@ function create_shader(mscene::Scene, plot::Volume) x, y, z, vol = plot[1], plot[2], plot[3], plot[4] box = GeometryBasics.mesh(Rect3f(Vec3f(0), Vec3f(1))) cam = cameracontrols(mscene) - model2 = lift(plot.model, x, y, z) do m, xyz... + model2 = lift(plot, plot.model, x, y, z) do m, xyz... mi = minimum.(xyz) maxi = maximum.(xyz) w = maxi .- mi @@ -53,20 +48,20 @@ function create_shader(mscene::Scene, plot::Volume) return convert(Mat4f, m) * m2 end - modelinv = lift(inv, model2) - algorithm = lift(x -> Cuint(convert_attribute(x, key"algorithm"())), plot.algorithm) + modelinv = lift(inv, plot, model2) + algorithm = lift(x -> Cuint(convert_attribute(x, key"algorithm"())), plot, plot.algorithm) - diffuse = lift(x -> convert_attribute(x, Key{:diffuse}()), plot.diffuse) - specular = lift(x -> convert_attribute(x, Key{:specular}()), plot.specular) - shininess = lift(x -> convert_attribute(x, Key{:shininess}()), plot.shininess) + diffuse = lift(x -> convert_attribute(x, Key{:diffuse}()), plot, plot.diffuse) + specular = lift(x -> convert_attribute(x, Key{:specular}()), plot, plot.specular) + shininess = lift(x -> convert_attribute(x, Key{:shininess}()), plot, plot.shininess) uniforms = Dict{Symbol, Any}( :modelinv => modelinv, - :isovalue => lift(Float32, plot.isovalue), - :isorange => lift(Float32, plot.isorange), - :absorption => lift(Float32, get(plot, :absorption, Observable(1.0f0))), + :isovalue => lift(Float32, plot, plot.isovalue), + :isorange => lift(Float32, plot, plot.isorange), + :absorption => lift(Float32, plot, get(plot, :absorption, Observable(1.0f0))), :algorithm => algorithm, :diffuse => diffuse, :specular => specular, @@ -128,24 +123,22 @@ function limits_to_uvmesh(plot) # TODO, this branch is only hit by Image, but not for Heatmap with stepranges # because convert_arguments converts x/y to Vector{Float32} if px[] isa StepRangeLen && py[] isa StepRangeLen && Makie.is_identity_transform(t) - rect = lift(px, py) do x, y + rect = lift(plot, px, py) do x, y xmin, xmax = extrema(x) ymin, ymax = extrema(y) return Rect2(xmin, ymin, xmax - xmin, ymax - ymin) end - positions = Buffer(lift(rect -> decompose(Point2f, rect), rect)) - faces = Buffer(lift(rect -> decompose(GLTriangleFace, rect), rect)) - uv = Buffer(lift(decompose_uv, rect)) + positions = Buffer(lift(rect -> decompose(Point2f, rect), plot, rect)) + faces = Buffer(lift(rect -> decompose(GLTriangleFace, rect), plot, rect)) + uv = Buffer(lift(decompose_uv, plot, rect)) else function grid(x, y, trans, space) return Makie.matrix_grid(p -> apply_transform(trans, p, space), x, y, zeros(length(x), length(y))) end - resolution = lift((x, y) -> (length(x), length(y)), px, py; ignore_equal_values=true) - positions = Buffer(lift(grid, px, py, t, get(plot, :space, :data))) - faces = Buffer(lift(fast_faces, resolution)) - uv = Buffer(lift(fast_uv, resolution)) + resolution = lift((x, y) -> (length(x), length(y)), plot, px, py; ignore_equal_values=true) + positions = Buffer(lift(grid, plot, px, py, t, get(plot, :space, :data))) + faces = Buffer(lift(fast_faces, plot, resolution)) + uv = Buffer(lift(fast_uv, plot, resolution)) end - vertices = GeometryBasics.meta(positions; uv=uv) - return Dict(:positions => positions, :faces => faces, :uv => uv) end diff --git a/WGLMakie/src/lines.jl b/WGLMakie/src/lines.jl index 19aff322e00..9140a167b71 100644 --- a/WGLMakie/src/lines.jl +++ b/WGLMakie/src/lines.jl @@ -19,15 +19,15 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) uniforms[name] = RGBAf(0, 0, 0, 0) end end - points_transformed = apply_transform(transform_func_obs(plot), plot[1], plot.space) - positions = lift(serialize_buffer_attribute, points_transformed) + points_transformed = lift(apply_transform, plot, transform_func_obs(plot), plot[1], plot.space) + positions = lift(serialize_buffer_attribute, plot, points_transformed) attributes = Dict{Symbol, Any}(:linepoint => positions) for (name, attr) in [:color => color, :linewidth => linewidth] if Makie.is_scalar_attribute(to_value(attr)) uniforms[Symbol("$(name)_start")] = attr uniforms[Symbol("$(name)_end")] = attr else - attributes[name] = lift(serialize_buffer_attribute, attr) + attributes[name] = lift(serialize_buffer_attribute, plot, attr) end end attr = Dict( @@ -37,7 +37,7 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) :plot_type => plot isa LineSegments ? "linesegments" : "lines", :cam_space => plot.space[], :uniforms => serialize_uniforms(uniforms), - :uniform_updater => uniform_updater(uniforms), + :uniform_updater => uniform_updater(plot, uniforms), :attributes => attributes ) return attr diff --git a/WGLMakie/src/meshes.jl b/WGLMakie/src/meshes.jl index 11922e71647..e1201f49e7f 100644 --- a/WGLMakie/src/meshes.jl +++ b/WGLMakie/src/meshes.jl @@ -3,8 +3,8 @@ function vertexbuffer(x, trans, space) return apply_transform(trans, pos, space) end -function vertexbuffer(x::Observable, p) - return Buffer(lift(vertexbuffer, x, transform_func_obs(p), get(p, :space, :data))) +function vertexbuffer(x::Observable, @nospecialize(p)) + return Buffer(lift(vertexbuffer, p, x, transform_func_obs(p), get(p, :space, :data))) end facebuffer(x) = faces(x) @@ -12,7 +12,7 @@ facebuffer(x::AbstractArray{<:GLTriangleFace}) = x facebuffer(x::Observable) = Buffer(lift(facebuffer, x)) function converted_attribute(plot::AbstractPlot, key::Symbol) - return lift(plot[key]) do value + return lift(plot, plot[key]) do value return convert_attribute(value, Key{key}(), Key{plotkey(plot)}()) end end @@ -21,7 +21,7 @@ function handle_color!(plot, uniforms, buffers, uniform_color_name = :uniform_co color = plot.calculated_colors minfilter = to_value(get(plot, :interpolate, true)) ? :linear : :nearest - convert_text(x) = permute_tex ? lift(permutedims, x) : x + convert_text(x) = permute_tex ? lift(permutedims, plot, x) : x if color[] isa Colorant uniforms[uniform_color_name] = color @@ -55,6 +55,9 @@ function handle_color!(plot, uniforms, buffers, uniform_color_name = :uniform_co return end +lift_or(f, p, x) = f(x) +lift_or(f, @nospecialize(p), x::Observable) = lift(f, p, x) + function draw_mesh(mscene::Scene, per_vertex, plot, uniforms; permute_tex=true) filter!(kv -> !(kv[2] isa Function), uniforms) handle_color!(plot, uniforms, per_vertex; permute_tex=permute_tex) @@ -76,7 +79,7 @@ function draw_mesh(mscene::Scene, per_vertex, plot, uniforms; permute_tex=true) for key in (:diffuse, :specular, :shininess, :backlight, :depth_shift) if !haskey(uniforms, key) - uniforms[key] = lift_or(x -> convert_attribute(x, Key{key}()), plot[key]) + uniforms[key] = lift_or(x -> convert_attribute(x, Key{key}()), plot, plot[key]) end end if haskey(uniforms, :color) && haskey(per_vertex, :color) @@ -98,7 +101,7 @@ function create_shader(scene::Scene, plot::Makie.Mesh) # Potentially per instance attributes mesh_signal = plot[1] mattributes = GeometryBasics.attributes - get_attribute(mesh, key) = lift(x -> getproperty(x, key), mesh) + get_attribute(mesh, key) = lift(x -> getproperty(x, key), plot, mesh) data = mattributes(mesh_signal[]) uniforms = Dict{Symbol,Any}() diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index 392e47ac164..7bdd7703b72 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -49,7 +49,7 @@ function create_shader(scene::Scene, plot::MeshScatter) return k in per_instance_keys && !(isscalar(v[])) end - per_instance[:offset] = apply_transform(transform_func_obs(plot), plot[1], plot.space) + per_instance[:offset] = lift(apply_transform, plot, transform_func_obs(plot), plot[1], plot.space) for (k, v) in per_instance per_instance[k] = Buffer(lift_convert(k, v, plot)) @@ -114,9 +114,6 @@ function serialize_three(fta::NoDataTextureAtlas) return tex end - - - function scatter_shader(scene::Scene, attributes, plot) # Potentially per instance attributes per_instance_keys = (:pos, :rotations, :markersize, :color, :intensity, @@ -127,19 +124,19 @@ function scatter_shader(scene::Scene, attributes, plot) atlas = wgl_texture_atlas() if haskey(attributes, :marker) font = get(attributes, :font, Observable(Makie.defaultfont())) - marker = lift(attributes[:marker]) do marker + marker = lift(plot, attributes[:marker]) do marker marker isa Makie.FastPixel && return Rect # FastPixel not supported, but same as Rect just slower return Makie.to_spritemarker(marker) end - markersize = lift(Makie.to_2d_scale, attributes[:markersize]) + markersize = lift(Makie.to_2d_scale, plot, attributes[:markersize]) - msize, offset = Makie.marker_attributes(atlas, marker, markersize, font, attributes[:quad_offset]) + msize, offset = Makie.marker_attributes(atlas, marker, markersize, font, attributes[:quad_offset], plot) attributes[:markersize] = msize attributes[:quad_offset] = offset attributes[:uv_offset_width] = Makie.primitive_uv_offset_width(atlas, marker, font) if to_value(marker) isa AbstractMatrix - uniform_dict[:image] = Sampler(lift(el32convert, marker)) + uniform_dict[:image] = Sampler(lift(el32convert, plot, marker)) end end @@ -166,7 +163,9 @@ function scatter_shader(scene::Scene, attributes, plot) if !isnothing(marker) get!(uniform_dict, :shape_type) do - return Makie.marker_to_sdf_shape(marker) + return lift(plot, marker; ignore_equal_values=true) do marker + return Cint(Makie.marker_to_sdf_shape(to_spritemarker(marker))) + end end end @@ -201,21 +200,15 @@ end function create_shader(scene::Scene, plot::Scatter) # Potentially per instance attributes - per_instance_keys = (:offset, :rotations, :markersize, :color, :intensity, - :quad_offset) - per_instance = filter(plot.attributes.attributes) do (k, v) - return k in per_instance_keys && !(isscalar(v[])) - end attributes = copy(plot.attributes.attributes) space = get(attributes, :space, :data) - cam = scene.camera attributes[:preprojection] = Mat4f(I) # calculate this in JS - attributes[:pos] = apply_transform(transform_func_obs(plot), plot[1], space) + attributes[:pos] = lift(apply_transform, plot, transform_func_obs(plot), plot[1], space) quad_offset = get(attributes, :marker_offset, Observable(Vec2f(0))) attributes[:marker_offset] = Vec3f(0) attributes[:quad_offset] = quad_offset - attributes[:billboard] = map(rot -> isa(rot, Billboard), plot.rotations) + attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotations) attributes[:model] = plot.model attributes[:depth_shift] = get(plot, :depth_shift, Observable(0f0)) @@ -237,16 +230,16 @@ function create_shader(scene::Scene, plot::Makie.Text{<:Tuple{<:Union{<:Makie.Gl offset = plot.offset atlas = wgl_texture_atlas() - glyph_data = map(pos, glyphcollection, offset, transfunc, space; ignore_equal_values=true) do pos, gc, offset, transfunc, space + glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space; ignore_equal_values=true) do pos, gc, offset, transfunc, space Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) end # unpack values from the one signal: positions, char_offset, quad_offset, uv_offset_width, scale = map((1, 2, 3, 4, 5)) do i - lift(getindex, glyph_data, i) + return lift(getindex, plot, glyph_data, i) end - uniform_color = lift(glyphcollection) do gc + uniform_color = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.colors, length(g.glyphs)) for g in gc), init = RGBAf[]) @@ -255,7 +248,7 @@ function create_shader(scene::Scene, plot::Makie.Text{<:Tuple{<:Union{<:Makie.Gl end end - uniform_rotation = lift(glyphcollection) do gc + uniform_rotation = lift(plot, glyphcollection) do gc if gc isa AbstractArray reduce(vcat, (Makie.collect_vector(g.rotations, length(g.glyphs)) for g in gc), init = Quaternionf[]) diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl index a113474df97..844e80147fe 100644 --- a/WGLMakie/src/serialization.jl +++ b/WGLMakie/src/serialization.jl @@ -192,10 +192,10 @@ function serialize_named_buffer(buffer) end) end -function register_geometry_updates(update_buffer::Observable, named_buffers) +function register_geometry_updates(@nospecialize(plot), update_buffer::Observable, named_buffers) for (name, buffer) in _pairs(named_buffers) if buffer isa Buffer - on(ShaderAbstractions.updater(buffer).update) do (f, args) + on(plot, ShaderAbstractions.updater(buffer).update) do (f, args) # update to replace the whole buffer! if f === ShaderAbstractions.update! new_array = args[1] @@ -209,19 +209,19 @@ function register_geometry_updates(update_buffer::Observable, named_buffers) return update_buffer end -function register_geometry_updates(update_buffer::Observable, program::Program) - return register_geometry_updates(update_buffer, program.vertexarray) +function register_geometry_updates(@nospecialize(plot), update_buffer::Observable, program::Program) + return register_geometry_updates(plot, update_buffer, program.vertexarray) end -function register_geometry_updates(update_buffer::Observable, program::InstancedProgram) - return register_geometry_updates(update_buffer, program.per_instance) +function register_geometry_updates(@nospecialize(plot), update_buffer::Observable, program::InstancedProgram) + return register_geometry_updates(plot, update_buffer, program.per_instance) end -function uniform_updater(uniforms::Dict) +function uniform_updater(@nospecialize(plot), uniforms::Dict) updater = Observable(Any[:none, []]) for (name, value) in uniforms if value isa Sampler - on(ShaderAbstractions.updater(value).update) do (f, args) + on(plot, ShaderAbstractions.updater(value).update) do (f, args) if f === ShaderAbstractions.update! updater[] = [name, [Int32[size(value.data)...], serialize_three(args[1])]] end @@ -229,7 +229,7 @@ function uniform_updater(uniforms::Dict) end else value isa Observable || continue - on(value) do value + on(plot, value) do value updater[] = [name, serialize_three(value)] return end @@ -238,53 +238,53 @@ function uniform_updater(uniforms::Dict) return updater end -function serialize_three(ip::InstancedProgram) - program = serialize_three(ip.program) +function serialize_three(@nospecialize(plot), ip::InstancedProgram) + program = serialize_three(plot, ip.program) program[:instance_attributes] = serialize_named_buffer(ip.per_instance) - register_geometry_updates(program[:attribute_updater], ip) + register_geometry_updates(plot, program[:attribute_updater], ip) return program end -reinterpret_faces(faces::AbstractVector) = collect(reinterpret(UInt32, decompose(GLTriangleFace, faces))) +reinterpret_faces(p, faces::AbstractVector) = collect(reinterpret(UInt32, decompose(GLTriangleFace, faces))) -function reinterpret_faces(faces::Buffer) - result = Observable(reinterpret_faces(ShaderAbstractions.data(faces))) - on(ShaderAbstractions.updater(faces).update) do (f, args) +function reinterpret_faces(@nospecialize(plot), faces::Buffer) + result = Observable(reinterpret_faces(plot, ShaderAbstractions.data(faces))) + on(plot, ShaderAbstractions.updater(faces).update) do (f, args) if f === ShaderAbstractions.update! - result[] = reinterpret_faces(args[1]) + result[] = reinterpret_faces(plot, args[1]) end end return result end -function serialize_three(program::Program) - facies = reinterpret_faces(_faces(program.vertexarray)) +function serialize_three(@nospecialize(plot), program::Program) + facies = reinterpret_faces(plot, _faces(program.vertexarray)) indices = convert(Observable, facies) uniforms = serialize_uniforms(program.uniforms) attribute_updater = Observable(["", [], 0]) - register_geometry_updates(attribute_updater, program) + register_geometry_updates(plot, attribute_updater, program) # TODO, make this configurable in ShaderAbstractions update_shader(x) = replace(x, "#version 300 es" => "") return Dict(:vertexarrays => serialize_named_buffer(program.vertexarray), :faces => indices, :uniforms => uniforms, :vertex_source => update_shader(program.vertex_source), :fragment_source => update_shader(program.fragment_source), - :uniform_updater => uniform_updater(program.uniforms), + :uniform_updater => uniform_updater(plot, program.uniforms), :attribute_updater => attribute_updater) end function serialize_scene(scene::Scene) hexcolor(c) = "#" * hex(Colors.color(to_color(c))) - pixel_area = lift(area -> Int32[minimum(area)..., widths(area)...], pixelarea(scene)) + pixel_area = lift(area -> Int32[minimum(area)..., widths(area)...], scene, pixelarea(scene)) cam_controls = cameracontrols(scene) cam3d_state = if cam_controls isa Camera3D fields = (:lookat, :upvector, :eyeposition, :fov, :near, :far) - dict = Dict((f => serialize_three(getfield(cam_controls, f)[]) for f in fields)) - dict[:resolution] = lift(res -> Int32[res...], scene.camera.resolution) + dict = Dict((f => lift(serialize_three, scene, getfield(cam_controls, f)) for f in fields)) + dict[:resolution] = lift(res -> Int32[res...], scene, scene.camera.resolution) dict else nothing @@ -293,7 +293,7 @@ function serialize_scene(scene::Scene) children = map(child-> serialize_scene(child), scene.children) serialized = Dict(:pixelarea => pixel_area, - :backgroundcolor => lift(hexcolor, scene.backgroundcolor), + :backgroundcolor => lift(hexcolor, scene, scene.backgroundcolor), :clearscene => scene.clear, :camera => serialize_camera(scene), :plots => serialize_plots(scene, scene.plots), @@ -304,7 +304,7 @@ function serialize_scene(scene::Scene) return serialized end -function serialize_plots(scene::Scene, plots::Vector{T}, result=[]) where {T<:AbstractPlot} +function serialize_plots(scene::Scene, @nospecialize(plots::Vector{T}), result=[]) where {T<:AbstractPlot} for plot in plots plot isa Makie.PlotList && continue # if no plots inserted, this truely is an atomic @@ -319,9 +319,9 @@ function serialize_plots(scene::Scene, plots::Vector{T}, result=[]) where {T<:Ab return result end -function serialize_three(scene::Scene, plot::AbstractPlot) +function serialize_three(scene::Scene, @nospecialize(plot::AbstractPlot)) program = create_shader(scene, plot) - mesh = serialize_three(program) + mesh = serialize_three(plot, program) mesh[:name] = string(Makie.plotkey(plot)) * "-" * string(objectid(plot)) mesh[:visible] = plot.visible mesh[:uuid] = js_uuid(plot) @@ -334,7 +334,7 @@ function serialize_three(scene::Scene, plot::AbstractPlot) pointlight = Makie.get_point_light(scene) if !isnothing(pointlight) uniforms[:lightposition] = serialize_three(pointlight.position[]) - on(pointlight.position) do value + on(plot, pointlight.position) do value updater[] = [:lightposition, serialize_three(value)] return end @@ -343,7 +343,7 @@ function serialize_three(scene::Scene, plot::AbstractPlot) ambientlight = Makie.get_ambient_light(scene) if !isnothing(ambientlight) uniforms[:ambient] = serialize_three(ambientlight.color[]) - on(ambientlight.color) do value + on(plot, ambientlight.color) do value updater[] = [:ambient, serialize_three(value)] return end diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl index 263e6854558..0c7eac08967 100644 --- a/WGLMakie/src/three_plot.jl +++ b/WGLMakie/src/three_plot.jl @@ -3,17 +3,6 @@ # We use objectid to find objects on the js side js_uuid(object) = string(objectid(object)) -function all_plots_scenes(scene::Scene; scene_uuids=String[], plot_uuids=String[]) - push!(scene_uuids, js_uuid(scene)) - for plot in scene.plots - append!(plot_uuids, (js_uuid(p) for p in Makie.collect_atomic_plots(plot))) - end - for child in scene.children - all_plots_scenes(child, plot_uuids=plot_uuids, scene_uuids=scene_uuids) - end - return scene_uuids, plot_uuids -end - function JSServe.print_js_code(io::IO, plot::AbstractPlot, context::JSServe.JSSourceContext) uuids = js_uuid.(Makie.collect_atomic_plots(plot)) # This is a bit more complicated then it has to be, since evaljs / on_document_load @@ -43,7 +32,7 @@ function three_display(screen::Screen, session::Session, scene::Scene) scene_serialized = serialize_scene(scene) window_open = scene.events.window_open width, height = size(scene) - canvas_width = lift(x -> [round.(Int, widths(x))...], pixelarea(scene)) + canvas_width = lift(x -> [round.(Int, widths(x))...], scene, pixelarea(scene)) canvas = DOM.m("canvas"; tabindex="0", style="display: block") wrapper = DOM.div(canvas; style="width: 100%; height: 100%") comm = Observable(Dict{String,Any}()) diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index cef88fb9eee..fc6e4daa184 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -20281,12 +20281,16 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { return; } const [w, h] = makie_camera.resolution.value; - const camera = new yt(cam3d.fov, w / h, cam3d.near, cam3d.far); - const center = new A(...cam3d.lookat); - camera.up = new A(...cam3d.upvector); - camera.position.set(...cam3d.eyeposition); + const camera = new yt(cam3d.fov.value, w / h, cam3d.near.value, cam3d.far.value); + const center = new A(...cam3d.lookat.value); + camera.up = new A(...cam3d.upvector.value); + camera.position.set(...cam3d.eyeposition.value); camera.lookAt(center); + const use_julia_cam = ()=>JSServe.can_send_to_julia && JSServe.can_send_to_julia(); function update() { + if (use_julia_cam()) { + return; + } const view = camera.matrixWorldInverse; const projection = camera.projectionMatrix; const [width, height] = cam3d.resolution.value; @@ -20303,12 +20307,13 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { z ]); } - cam3d.resolution.on(update); function addMouseHandler(domObject, drag, zoomIn, zoomOut) { let startDragX = null; let startDragY = null; function mouseWheelHandler(e) { - e = window.event || e; + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } @@ -20321,6 +20326,9 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { e.preventDefault(); } function mouseDownHandler(e) { + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } @@ -20329,6 +20337,9 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { e.preventDefault(); } function mouseMoveHandler(e) { + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } @@ -20339,6 +20350,9 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { e.preventDefault(); } function mouseUpHandler(e) { + if (use_julia_cam()) { + return; + } if (!in_scene(scene, e)) { return; } @@ -20555,6 +20569,7 @@ function delete_scene(scene_id) { if (!scene) { return; } + delete_three_scene(scene); while(scene.children.length > 0){ scene.remove(scene.children[0]); } @@ -20572,7 +20587,10 @@ function find_plots(plot_uuids) { } function delete_scenes(scene_uuids, plot_uuids) { plot_uuids.forEach((plot_id)=>{ - delete plot_cache[plot_id]; + const plot = plot_cache[plot_id]; + if (plot) { + delete_plot(plot); + } }); scene_uuids.forEach((scene_id)=>{ delete_scene(scene_id); @@ -20584,14 +20602,9 @@ function insert_plot(scene_id, plot_data) { add_plot(scene, plot); }); } -function delete_plots(scene_id, plot_uuids) { - console.log(`deleting plots!: ${plot_uuids}`); - const scene = find_scene(scene_id); +function delete_plots(plot_uuids) { const plots = find_plots(plot_uuids); - plots.forEach((p)=>{ - scene.remove(p); - delete plot_cache[p.plot_uuid]; - }); + plots.forEach(delete_plot); } function convert_texture(data) { const tex = create_texture(data); @@ -20931,7 +20944,7 @@ function add_plot(scene, plot_data) { plot_data.uniforms.preprojection = cam.preprojection_matrix(space.value, markerspace.value); } const p = deserialize_plot(plot_data); - plot_cache[plot_data.uuid] = p; + plot_cache[p.plot_uuid] = p; scene.add(p); const next_insert = new Set(ON_NEXT_INSERT); next_insert.forEach((f)=>f()); @@ -21209,13 +21222,15 @@ function deserialize_scene(data, screen) { update_cam(data.camera.value); if (data.cam3d_state) { attach_3d_camera(canvas, camera, data.cam3d_state, scene); - } else { - data.camera.on(update_cam); } + data.camera.on(update_cam); data.plots.forEach((plot_data)=>{ add_plot(scene, plot_data); }); - scene.scene_children = data.children.map((child)=>deserialize_scene(child, screen)); + scene.scene_children = data.children.map((child)=>{ + const childscene = deserialize_scene(child, screen); + return childscene; + }); return scene; } function delete_plot(plot) { @@ -21240,9 +21255,9 @@ function render_scene(scene, picking = false) { const canvas = renderer.domElement; if (!document.body.contains(canvas)) { console.log("EXITING WGL"); + delete_three_scene(scene); renderer.state.reset(); renderer.dispose(); - delete_three_scene(scene); return false; } if (!scene.visible.value) { diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index 8ba09b201f1..159f81e97f7 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -24,9 +24,9 @@ export function render_scene(scene, picking = false) { const canvas = renderer.domElement; if (!document.body.contains(canvas)) { console.log("EXITING WGL"); + delete_three_scene(scene); renderer.state.reset(); renderer.dispose(); - delete_three_scene(scene); return false; } // dont render invisible scenes diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 3983aa86e81..69cd79bde7a 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -65,13 +65,13 @@ edisplay = JSServe.use_electron_display(devtools=true) end @testset "memory leaks" begin - Makie._current_figure[] = nothing + Makie.CURRENT_FIGURE[] = nothing app = App(nothing) display(edisplay, app) GC.gc(true); # Somehow this may take a while to get emptied completely - JSServe.wait_for(() -> isempty(run(edisplay.window, "Object.keys(WGL.scene_cache)"));timeout=10) - wgl_plots = run(edisplay.window, "Object.keys(WGL.plot_cache)") + JSServe.wait_for(() -> (GC.gc(true);isempty(run(edisplay.window, "Object.keys(WGL.plot_cache)")));timeout=20) + wgl_plots = run(edisplay.window, "Object.keys(WGL.scene_cache)") @test isempty(wgl_plots) session = edisplay.browserdisplay.handler.session @@ -80,9 +80,11 @@ end @show session_size texture_atlas_size @test session_size / 10^6 < 6 @test texture_atlas_size < 6 + s_keys = "Object.keys(JSServe.Sessions.SESSIONS)" + JSServe.wait_for(() -> (GC.gc(true); 2 == length(run(edisplay.window, s_keys))); timeout=30) js_sessions = run(edisplay.window, "JSServe.Sessions.SESSIONS") js_objects = run(edisplay.window, "JSServe.Sessions.GLOBAL_OBJECT_CACHE") - @test Set([app.session[].id, app.session[].parent.id]) == keys(js_sessions) + # @test Set([app.session[].id, app.session[].parent.id]) == keys(js_sessions) # we used Retain for global_obs, so it should stay as long as root session is open @test keys(js_objects) == Set([WGLMakie.TEXTURE_ATLAS.id]) end diff --git a/docs/reference/blocks/legend.md b/docs/reference/blocks/legend.md index d84b033f66b..ac606452b54 100644 --- a/docs/reference/blocks/legend.md +++ b/docs/reference/blocks/legend.md @@ -321,4 +321,4 @@ f ## Attributes -\attrdocs{Legend} \ No newline at end of file +\attrdocs{Legend} diff --git a/docs/reference/plots/specapi.md b/docs/reference/plots/specapi.md new file mode 100644 index 00000000000..0924eb1db25 --- /dev/null +++ b/docs/reference/plots/specapi.md @@ -0,0 +1,211 @@ +# SpecApi + +!!! warning + The SpecApi is still under active development and might introduce breaking changes quickly in the future. + It's also slower for animations then using the normal Makie API, since it needs to re-create plots often and needs to go over the whole plot tree to find different values. + While the performance will always be slower then directly using Observables to update attributes, it's still not much optimized so we expect to improve it in the future. + You should also expect bugs, since the API is still very new while offering lots of new and complex functionality. + Don't hesitate to open issues if you run into unexpected behaviour. + PRs are also more then welcome, the code isn't actually that complex and should be easy to dive into (src/basic_recipes/specapi.jl). + +The `SpecApi` is a convenient scope for creating PlotSpec objects. +PlotSpecs are a simple way to create plots in a declarative way, which can then get converted to Makie plots. +You can use `Observable{SpecApi.PlotSpec}`, or `Observable{SpecApi.Figure}` to create complete figures that can be updated dynamically. + +The API is supposed mirror the normal Makie API 1:1, just prefixed by `SpecApi`: +```julia +import Makie.SpecApi as S # For convenience import it as a shorter name +S.scatter(1:4) # create a single PlotSpec object + +# Create a complete figure +f = S.Figure() # +ax = S.Axis(f[1, 1]) +S.scatter!(ax, 1:4) +fig_observable = Observable(f) +plot(fig_observable) # Plot the whole figure +# Efficiently update the complete figure with a new FigureSpec +fig_observable[] = S.Figure(S.Axis(; title="lines", plots=[S.lines(1:4)])) +``` + +You can also drop to the lower level constructors: + +```julia +s = Makie.PlotSpec(:scatter, 1:4; color=:red) +axis = Makie.BlockSpec(:Axis; position=(1, 1), title="Axis at layout position (1, 1)") +``` + +Or use the Declarative API: +```julia +f = S.Figure([ + S.Axis( + plots = [ + S.scatter(1:4) + ] + ) +]) +``` +For the declaritive API, `S.Figure` accepts a vector of blockspecs or matrix of blockspecs, which places the Blocks at the indices of those arrays: +\begin{examplefigure}{} +```julia +using GLMakie, DelimitedFiles, FileIO +import Makie.SpecApi as S +GLMakie.activate!() # hide +volcano = readdlm(Makie.assetpath("volcano.csv"), ',', Float64) +brain = load(assetpath("brain.stl")) +r = LinRange(-1, 1, 100) +cube = [(x .^ 2 + y .^ 2 + z .^ 2) for x = r, y = r, z = r] + +ax1 = S.Axis(; title="Axis 1", plots=map(x -> S.density(x * randn(200) .+ 3x, color=:y), 1:5)) +ax2 = S.Axis(; title="Axis 2", plots=[S.contourf(volcano; colormap=:inferno)]) +ax3 = S.Axis3(; title="Axis3", plots=[S.mesh(brain, colormap=:Spectral, color=[tri[1][2] for tri in brain for i in 1:3])]) +ax4 = S.Axis3(; plots=[S.contour(cube, alpha=0.5)]) + + +spec_array = S.Figure([ax1, ax2]); +spec_matrix = S.Figure([ax1 ax2; ax3 ax4]); +f = Figure(; resolution=(1000, 500)) +plot(f[1, 1], spec_array) +plot(f[1, 2], spec_matrix) +f +``` +\end{examplefigure} + +# Usage in convert_arguments + +!!! warning + It's not decided yet how to forward keyword arguments from `plots(...; kw...)` to `convert_arguments` for the SpecApi in a more convenient and performant way. Until then, one needs to use the regular mechanism via `Makie.used_attributes`, which completely redraws the entire Spec on change of any attribute. + +You can overload `convert_arguments` and return an array of `PlotSpecs` or a `FigureSpec`. +The main difference between those is, that returning an array of `PlotSpecs` can be plotted like any recipe into axes etc, while overloads returning a whole Figure spec can only be plotted to whole layout position (e.g. `figure[1, 1]`). + +## convert_arguments for FigureSpec + +Simple example to create a dynamic grid of axes: + +\begin{examplefigure}{} +```julia +using CairoMakie +import Makie.SpecApi as S +struct PlotGrid + nplots::Tuple{Int,Int} +end + +Makie.used_attributes(::Type{<:AbstractPlot}, ::PlotGrid) = (:color,) +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::PlotGrid; color=:black) + f = S.Figure(; fontsize=30) + for i in 1:obj.nplots[1] + for j in 1:obj.nplots[2] + ax = S.Axis(f[i, j]) + S.lines!(ax, cumsum(randn(1000)); color=color) + end + end + return f +end + +f = Figure() +plot(f[1, 1], PlotGrid((1, 1)); color=Cycled(1)) +plot(f[1, 2], PlotGrid((2, 2)); color=Cycled(2)) +f +``` +\end{examplefigure} + +## convert_arguments for PlotSpec + +With this we can dynamically create plots in convert_arguments. +Note, that this still doesn't allow to easily forward keyword arguments from the plot command to `convert_arguments`, so we put the plot arguments into `LineScatter` in this example: + +\begin{examplefigure}{} +```julia +using CairoMakie +import Makie.SpecApi as S +struct LineScatter + show_lines::Bool + show_scatter::Bool + kw::Dict{Symbol,Any} +end +LineScatter(lines, scatter; kw...) = LineScatter(lines, scatter, Dict{Symbol,Any}(kw)) + +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::LineScatter, data...) + plots = PlotSpec[] + if obj.show_lines + push!(plots, S.lines(data...; obj.kw...)) + end + if obj.show_scatter + push!(plots, S.scatter(data...; obj.kw...)) + end + return plots +end + +f = Figure() +ax = Axis(f[1, 1]) +# Can be plotted into Axis, since it doesn't create its own axes like FigureSpec +plot!(ax, LineScatter(true, true; markersize=20, color=1:4), 1:4) +plot!(ax, LineScatter(true, false; color=:darkcyan, linewidth=3), 2:4) +f +``` +\end{examplefigure} + + +# Interactivity + +The SpecApi is geared towards dashboards and interactively creating complex plots. +Here is a simple example using Slider and Menu, to visualize a fake simulation: + +~~~ + +~~~ +```julia:simulation +struct MySimulation + plottype::Symbol + arguments::AbstractVector +end + +function Makie.convert_arguments(::Type{<:AbstractPlot}, sim::MySimulation) + return map(enumerate(sim.arguments)) do (i, data) + return PlotSpec(sim.plottype, data) + end +end +f = Figure() +s = Slider(f[1, 1], range=1:10) +m = Menu(f[1, 2], options=[:scatter, :lines, :barplot]) +sim = lift(s.value, m.selection) do n_plots, p + args = [cumsum(randn(100)) for i in 1:n_plots] + return MySimulation(p, args) +end +ax, pl = plot(f[2, :], sim) +tight_ticklabel_spacing!(ax) +# lower priority to make sure the call back is always called last +on(sim; priority=-1) do x + autolimits!(ax) +end +record(f, "interactive_specapi.mp4", framerate=1) do io + pause = 0.1 + m.i_selected[] = 1 + for i in 1:4 + set_close_to!(s, i) + sleep(pause) + recordframe!(io) + end + m.i_selected[] = 2 + sleep(pause) + recordframe!(io) + for i in 5:7 + set_close_to!(s, i) + sleep(pause) + recordframe!(io) + end + m.i_selected[] = 3 + sleep(pause) + recordframe!(io) + for i in 7:10 + set_close_to!(s, i) + sleep(pause) + recordframe!(io) + end +end +``` +~~~ + +~~~ + +\video{interactive_specapi, autoplay = true} diff --git a/docs/utils.jl b/docs/utils.jl index 50b3d35f39b..b8523df4141 100644 --- a/docs/utils.jl +++ b/docs/utils.jl @@ -20,6 +20,7 @@ end using Makie function html_docstring(fname) + fname == :SpecApi && return "" doc = Base.doc(getfield(Makie, Symbol(fname))) body = Markdown.html(doc) diff --git a/metrics/ttfp/run-benchmark.jl b/metrics/ttfp/run-benchmark.jl index 46c11b19319..b33fd95dc01 100644 --- a/metrics/ttfp/run-benchmark.jl +++ b/metrics/ttfp/run-benchmark.jl @@ -153,6 +153,9 @@ function update_comment(old_comment, package_name, (pr_bench, master_bench, eval for (i, value) in enumerate(evaluation) rows[idx + 2][i + 1] = [value] end + open("benchmark.md", "w") do io + return show(io, md) + end return sprint(show, md) end diff --git a/specplottest.jl b/specplottest.jl new file mode 100644 index 00000000000..7a276119d22 --- /dev/null +++ b/specplottest.jl @@ -0,0 +1,333 @@ +using DataFrames +import Makie.SpecApi as S +using Random +using WGLMakie +function gen_data(N=1000) + return DataFrame( + :continuous2 => cumsum(randn(N)), + :continuous3 => cumsum(randn(N)), + :continuous4 => cumsum(randn(N)), + :continuous5 => cumsum(randn(N)), + + :condition2 => rand(["string1", "string2"], N), + :condition3 => rand(["cat", "dog", "fox"], N), + :condition4 => rand(["eagle", "nashorn"], N), + :condition5 => rand(["bug", "honey", "riddle", "carriage"], N), + + :data_condition2 => cumsum(randn(N)), + :data_condition3 => cumsum(randn(N)), + :data_condition4 => cumsum(randn(N)), + :data_condition5 => cumsum(randn(N)), + ) +end + + +function plot_data(data, categorical_vars, continuous_vars) + fig = S.Figure() + mpalette = [:circle, :star4, :xcross, :diamond] + cpalette = Makie.wong_colors() + cat_styles = [:color => cpalette, :marker => mpalette, :markersize => [5, 10, 20, 30], :marker => ['c', 'x', 'y', 'm']] + cat_values = [unique(data[!, cat]) for cat in categorical_vars] + scatter_styles = Dict([cat => (style[1] => Dict(zip(vals, style[2]))) for (style, vals, cat) in zip(cat_styles, cat_values, categorical_vars)]) + + continous_styles = [:viridis, :heat, :rainbow, :turku50] + continuous_values = [extrema(data[!, con]) for con in continuous_vars] + line_styles = Dict([cat => (; colormap=style, colorrange=limits) for (style, limits, cat) in zip(continous_styles, continuous_values, continuous_vars)]) + ax = S.Axis(fig[1, 1]) + for var in categorical_vars + values = data[!, var] + kw, vals = scatter_styles[var] + args = [kw => map(x-> vals[x], values)] + d = data[!, Symbol("data_$var")] + S.scatter!(ax, d; args...) + end + for var in continuous_vars + points = data[!, var] + S.lines!(ax, points; line_styles[var]..., color=points) + end + fig +end + + +using WGLMakie, JSServe +App() do + data = gen_data(1000) + continous_vars = Observable(["continuous2", "continuous3"]) + categorical_vars = Observable(["condition2", "condition4"]) + s = JSServe.Slider(1:10) + + obs = lift(continous_vars, categorical_vars) do con_vars, cat_vars + plot_data(data, cat_vars, con_vars) + end + all_vars = ["continuous$i" for i in 2:5] + all_cond_vars = ["condition$i" for i in 2:5] + Makie.on_latest(s.value) do va + continous_vars[] = shuffle!(all_vars[unique(rand(1:4, rand(1:4)))]) + categorical_vars[] = shuffle!(all_cond_vars[unique(rand(1:4, rand(1:4)))]) + end + fig = plot(obs) + DOM.div(s, fig) +end + +for i in 1:1000 + all_vars = ["continuous$i" for i in 2:5] + all_cond_vars = ["condition$i" for i in 2:5] + + continous_vars[] = shuffle!(all_vars[unique(rand(1:4, rand(1:4)))]) + categorical_vars[] = shuffle!(all_cond_vars[unique(rand(1:4, rand(1:4)))]) + yield() +end +end_size = Base.summarysize(fig) / 10^6 + +obs[] = S.Figure() +obs[] = S.Figure(S.Axis((1, 1), plots=[S.scatter(1:4), S.lines(1:4; color=:red)]), + S.Axis3((1, 2), plots=[S.scatter(rand(Point3f, 10); color=:red)])) + + +using Makie +import Makie.SpecApi as S +using GLMakie +GLMakie.activate!(; float=true) + +function test(f_obs) + f_obs[] = begin + f = S.Figure() + ax = S.Axis(f[1, 1]) + S.scatter!(ax, 1:4) + ax2 = S.Axis3(f[1, 2]) + S.scatter!(ax2, rand(Point3f, 10); color=1:10, markersize=20) + S.Colorbar(f[1, 3]; limits=(0, 1), colormap=:heat) + f + end + yield() + f_obs[] = begin + S.Figure(S.Axis((1, 1), + S.scatter(1:4), + S.lines(1:4; color=:red)), + S.Axis3((1, 2), S.scatter(rand(Point3f, 10); color=:red))) + end + return yield() +end + +begin + f = S.Figure() + f_obs = Observable(f) + fig = Makie.update_fig(Figure(), f_obs) +end +f_obs[] = begin + f = S.Figure() + ax = S.Axis(f[1, 1]) + S.scatter(ax, 0:0.01:1, 0:0.01:1) + S.scatter(ax, rand(Point2f, 10); color=:green, markersize=20) + S.scatter(ax, rand(Point2f, 10); color=:red, markersize=20) + f +end; + +for i in 1:20 + f_obs[] = begin + f = S.Figure() + ax = S.Axis(f[1, 1]) + S.scatter!(ax, 1:4) + ax2 = S.Axis3(f[1, 2]) + S.scatter!(ax2, rand(Point3f, 10); color=1:10, markersize=20) + S.scatter!(ax2, rand(Point3f, 10); color=1:10, markersize=20) + f + end + yield() + f_obs[] = begin + S.Figure(S.Axis((1, 1), + S.scatter(1:4), + S.lines(1:4; color=:red)), + S.Axis3((1, 2), S.scatter(rand(Point3f, 10); color=:red))) + end + yield() +end +[GC.gc(true) for i in 1:5] + +using JSServe, WGLMakie +rm(JSServe.bundle_path(WGLMakie.WGL)) +rm(JSServe.bundle_path(JSServe.JSServeLib)) +WGLMakie.activate!() +fig = Figure() +ax = LScene(fig[1, 1]); +ax = Axis3(fig[1, 2]); +scatter(1:4) + +using SnoopCompileCore, Makie + +macro ctime(x) + return quote + tstart = time_ns() + $(esc(x)) + ts = Float64(time_ns() - tstart) / 10^9 + println("time: $(round(ts, digits=5))s") + end +end + +tinf = @snoopi_deep @ctime scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true); +# tinf = @snoopi_deep(@ctime(colorbuffer(fig))); +using SnoopCompile, ProfileView; ProfileView.view(flamegraph(tinf)) + + +using GLMakie + +struct MySimulation + plottype::Symbol + arguments::AbstractVector +end + +function Makie.convert_arguments(::Type{<:AbstractPlot}, sim::MySimulation) + return map(enumerate(sim.arguments)) do (i, data) + return PlotSpec(sim.plottype, data) + end +end +f = Figure() +s = Slider(f[1, 1], range = 1:10) +m = Menu(f[1, 2], options = [:scatter, :lines, :barplot]) +sim = lift(s.value, m.selection) do n_plots, p + args = [rand(Point2f, 10) for i in 1:n_plots] + return MySimulation(p, args) +end +ax, pl = plot(f[2, :], sim) +display(f) + +resample_cmap(:viridis, 2) + +using GLMakie +import Makie.SpecApi as S +plot(Observable( + [S.scatter(1:4), S.scatter(2:5)] +)) + +function Makie.convert_arguments(T::Type{<:AbstractPlot}, data::Matrix) + return map(1:size(data, 2)) do i + return PlotSpec(plotkey(T), data[:, i]; color=Parent()) + end +end + +scatter(rand(10, 4); color=:red) + +using GLMakie +struct MySpec3 + type::Any + args::Any + kws::Any +end +MySpec3(type, args...; kws...) = MySpec3(type, args, kws) + +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::MySpec3) + f = S.Figure() + Makie.BlockSpec(obj.type, f[1, 1], obj.args...; obj.kws...) + return f +end +GLMakie.activate!(; float=true) +obs = Observable(MySpec3(:Axis; title="test")) +f = plot(obs) +elem_1 = [LineElement(; color=:red, linestyle=nothing), + MarkerElement(; color=:blue, marker='x', markersize=15, + strokecolor=:black)] + +elem_2 = [PolyElement(; color=:red, strokecolor=:blue, strokewidth=1), + LineElement(; color=:black, linestyle=:dash)] + +elem_3 = LineElement(; color=:green, linestyle=nothing, + points=Point2f[(0, 0), (0, 1), (1, 0), (1, 1)]) + +elem_4 = MarkerElement(; color=:blue, marker='π', markersize=15, + points=Point2f[(0.2, 0.2), (0.5, 0.8), (0.8, 0.2)]) + +elem_5 = PolyElement(; color=:green, strokecolor=:black, strokewidth=2, + points=Point2f[(0, 0), (1, 0), (0, 1)]) +obs[] = MySpec3(:Slider; range=1:10); + +using GLMakie + +import Makie.SpecApi as S + +struct PlotGrid + nplots::Tuple{Int,Int} +end + +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::PlotGrid) + f = S.Figure(; fontsize=30) + for i in 1:obj.nplots[1] + for j in 1:obj.nplots[2] + ax = S.Axis(f[i, j]) + S.lines!(ax,cumsum(randn(1000))) + end + end + return f +end + + +f = Figure() +s1 = Slider(f[1, 1]; range=1:4) +s2 = Slider(f[1, 2]; range=1:4) +obs = lift(s1.value, s2.value) do i, j + PlotGrid((i, j)) +end + +plot(f[2, :], obs) +f + + +f = S.Figure(; fontsize=30) +for i in 1:2 + for j in 1:2 + ax = S.Axis(f[i, j]) + S.lines!(ax, cumsum(randn(1000))) + end +end + +f = Figure() +fs = f[1, :] +ax1, pl = scatter(fs[1, 1], 1:4) +ax2, pl = scatter(fs[1, 2], 1:4) +f + + +struct PlotGrid + nplots::Tuple{Int,Int} +end + +Makie.used_attributes(::Type{<:AbstractPlot}, ::PlotGrid) = (:color,) +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::PlotGrid; color=:black) + f = S.Figure(; fontsize=30) + for i in 1:obj.nplots[1] + for j in 1:obj.nplots[2] + ax = S.Axis(f[i, j]) + S.lines!(ax, cumsum(randn(1000)); color=color) + end + end + return f +end + +f = Figure() +plot(f[1, 1], PlotGrid((1, 1)); color=:red) +plot(f[1, 2], PlotGrid((2, 2)); color=:black) +f + + +struct LineScatter + show_lines::Bool + show_scatter::Bool +end + +function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::LineScatter, data...) + plots = PlotSpec[] + if obj.show_lines + push!(plots, S.lines(data...)) + end + if obj.show_scatter + push!(plots, S.scatter(data...)) + end + return plots +end + +f = Figure() +ax = Axis(f[1, 1]) +# Can be plotted into Axis, since it doesn't create its own axes like FigureSpec +plot!(ax, LineScatter(true, true), 1:4) +plot!(ax, LineScatter(true, false), 2:4) +f +``` diff --git a/src/Makie.jl b/src/Makie.jl index 78a9987bf47..4ae4cb9f260 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -12,6 +12,12 @@ using .ContoursHygiene const Contours = ContoursHygiene.Contour using Base64 +# Import FilePaths for invalidations +# When loading Electron for WGLMakie, which depends on FilePaths +# It invalidates half of Makie. Simplest fix is to load it early on in Makie +# So that the bulk of Makie gets compiled after FilePaths invalidadet Base code +# +import FilePaths using LaTeXStrings using MathTeXEngine using Random @@ -80,7 +86,7 @@ using MakieCore: Pixel, px, Unit, Billboard using MakieCore: not_implemented_for import MakieCore: plot, plot!, theme, plotfunc, plottype, merge_attributes!, calculated_attributes!, get_attribute, plotsym, plotkey, attributes, used_attributes -import MakieCore: create_figurelike, create_figurelike!, figurelike_return, figurelike_return! +import MakieCore: create_axis_like, create_axis_like!, figurelike_return, figurelike_return! import MakieCore: arrows, heatmap, image, lines, linesegments, mesh, meshscatter, poly, scatter, surface, text, volume import MakieCore: arrows!, heatmap!, image!, lines!, linesegments!, mesh!, meshscatter!, poly!, scatter!, surface!, text!, volume! import MakieCore: convert_arguments, convert_attribute, default_theme, conversion_trait @@ -131,7 +137,7 @@ include("camera/camera3d.jl") include("camera/old_camera3d.jl") # basic recipes -include("basic_recipes/plotspec.jl") +include("basic_recipes/specapi.jl") include("basic_recipes/convenience_functions.jl") include("basic_recipes/ablines.jl") include("basic_recipes/annotations.jl") diff --git a/src/basic_recipes/plotspec.jl b/src/basic_recipes/plotspec.jl deleted file mode 100644 index e0f978edb62..00000000000 --- a/src/basic_recipes/plotspec.jl +++ /dev/null @@ -1,226 +0,0 @@ -# Ideally we re-use Makie.PlotSpec, but right now we need a bit of special behaviour to make this work nicely. -# If the implementation stabilizes, we should think about refactoring PlotSpec to work for both use cases, and then just have one PlotSpec type. -@nospecialize - -""" - PlotSpec{P<:AbstractPlot}(args...; kwargs...) - -Object encoding positional arguments (`args`), a `NamedTuple` of attributes (`kwargs`) -as well as plot type `P` of a basic plot. -""" -struct PlotSpec{P<:AbstractPlot} - args::Vector{Any} - kwargs::Dict{Symbol, Any} - function PlotSpec{P}(args...; kwargs...) where {P<:AbstractPlot} - kw = Dict{Symbol,Any}() - for (k, v) in kwargs - # convert eagerly, so that we have stable types for matching later - # E.g. so that PlotSpec(; color = :red) has the same type as PlotSpec(; color = RGBA(1, 0, 0, 1)) - kw[k] = convert_attribute(v, Key{k}(), Key{plotkey(P)}()) - end - return new{P}(Any[args...], kw) - end - PlotSpec(args...; kwargs...) = new{Combined{plot}}(args...; kwargs...) -end -@specialize - -Base.getindex(p::PlotSpec, i::Int) = getindex(p.args, i) -Base.getindex(p::PlotSpec, i::Symbol) = getproperty(p.kwargs, i) - -to_plotspec(::Type{P}, args; kwargs...) where {P} = PlotSpec{P}(args...; kwargs...) - -function to_plotspec(::Type{P}, p::PlotSpec{S}; kwargs...) where {P,S} - return PlotSpec{plottype(P, S)}(p.args...; p.kwargs..., kwargs...) -end - -plottype(::PlotSpec{P}) where {P} = P - -""" -apply for return type PlotSpec -""" -function apply_convert!(P, attributes::Attributes, x::PlotSpec{S}) where {S} - args, kwargs = x.args, x.kwargs - # Note that kw_args in the plot spec that are not part of the target plot type - # will end in the "global plot" kw_args (rest) - for (k, v) in pairs(kwargs) - attributes[k] = v - end - return (plottype(S, P), (args...,)) -end - -function apply_convert!(P, ::Attributes, x::AbstractVector{<:PlotSpec}) - return (PlotList, (x,)) -end - -""" -apply for return type - (args...,) -""" -apply_convert!(P, ::Attributes, x::Tuple) = (P, x) - -function MakieCore.argtypes(plot::PlotSpec{P}) where {P} - args_converted = convert_arguments(P, plot.args...) - return MakieCore.argtypes(args_converted) -end - -struct SpecApi end -function Base.getproperty(::SpecApi, field::Symbol) - P = Combined{getfield(Makie, field)} - return PlotSpec{P} -end - -const PlotspecApi = SpecApi() - -# comparison based entirely of types inside args + kwargs -compare_specs(a::PlotSpec{A}, b::PlotSpec{B}) where {A, B} = false - -function compare_specs(a::PlotSpec{T}, b::PlotSpec{T}) where {T} - length(a.args) == length(b.args) || return false - all(i-> typeof(a.args[i]) == typeof(b.args[i]), 1:length(a.args)) || return false - - length(a.kwargs) == length(b.kwargs) || return false - ka = keys(a.kwargs) - kb = keys(b.kwargs) - ka == kb || return false - all(k -> typeof(a.kwargs[k]) == typeof(b.kwargs[k]), ka) || return false - return true -end - -function update_plot!(plot::AbstractPlot, spec::PlotSpec) - # Update args in plot `input_args` list - for i in eachindex(spec.args) - # we should only call update_plot!, if compare_spec(spec_plot_got_created_from, spec) == true, - # Which should guarantee, that args + kwargs have the same length and types! - arg_obs = plot.args[i] - if to_value(arg_obs) != spec.args[i] # only update if different - @debug("updating arg $i") - arg_obs[] = spec.args[i] - end - end - # Update attributes - for (attribute, new_value) in spec.kwargs - if plot[attribute][] != new_value # only update if different - @debug("updating kw $attribute") - plot[attribute] = new_value - end - end -end - -""" - plotlist!( - [ - PlotSpec{SomePlotType}(args...; kwargs...), - PlotSpec{SomeOtherPlotType}(args...; kwargs...), - ] - ) - -Plots a list of PlotSpec's, which can be an observable, making it possible to create efficiently animated plots with the following API: - -## Example -```julia -using GLMakie -import Makie.PlotspecApi as P - -fig = Figure() -ax = Axis(fig[1, 1]) -plots = Observable([P.heatmap(0 .. 1, 0 .. 1, Makie.peaks()), P.lines(0 .. 1, sin.(0:0.01:1); color=:blue)]) -pl = plot!(ax, plots) -display(fig) - -# Updating the plot dynamically -plots[] = [P.heatmap(0 .. 1, 0 .. 1, Makie.peaks()), P.lines(0 .. 1, sin.(0:0.01:1); color=:red)] -plots[] = [ - P.image(0 .. 1, 0 .. 1, Makie.peaks()), - P.poly(Rect2f(0.45, 0.45, 0.1, 0.1)), - P.lines(0 .. 1, sin.(0:0.01:1); linewidth=10, color=Makie.resample_cmap(:viridis, 101)), -] - -plots[] = [ - P.surface(0..1, 0..1, Makie.peaks(); colormap = :viridis, translation = Vec3f(0, 0, -1)), -] -``` -""" -@recipe(PlotList, plotspecs) do scene - Attributes() -end - -convert_arguments(::Type{<:AbstractPlot}, args::AbstractArray{<:PlotSpec}) = (args,) -plottype(::AbstractVector{<:PlotSpec}) = PlotList - -# Since we directly plot into the parent scene (hacky), we need to overload these -Base.insert!(::MakieScreen, ::Scene, ::PlotList) = nothing - -# TODO, make this work with Cycling and also with convert_arguments returning -# Vector{PlotSpec} so that one can write recipes like this: -quote - Makie.convert_arguments(obj::MyType) = [ - obj.lineplot ? P.lines(obj.args...; obj.kwargs...) : P.scatter(obj.args...; obj.kw...) - ] -end - -function Base.show(io::IO, ::MIME"text/plain", spec::PlotSpec{P}) where {P} - args = join(map(x -> string("::", typeof(x)), spec.args), ", ") - kws = join([string(k, " = ", typeof(v)) for (k, v) in spec.kwargs], ", ") - println(io, "P.", plotfunc(P), "($args; $kws)") -end - -function Base.show(io::IO, spec::PlotSpec{P}) where {P} - args = join(map(x -> string("::", typeof(x)), spec.args), ", ") - kws = join([string(k, " = ", typeof(v)) for (k, v) in spec.kwargs], ", ") - return println(io, "P.", plotfunc(P), "($args; $kws)") -end - -function to_combined(ps::PlotSpec{P}) where {P} - return P((ps.args...,), copy(ps.kwargs)) -end - -function Makie.plot!(p::PlotList{<: Tuple{<: AbstractArray{<: PlotSpec}}}) - # Cache plots here so that we aren't re-creating plots every time; - # if a plot still exists from last time, update it accordingly. - # If the plot is removed from `plotspecs`, we'll delete it from here - # and re-create it if it ever returns. - cached_plots = Pair{PlotSpec, Combined}[] - scene = Makie.parent_scene(p) - on(p.plotspecs; update=true) do plotspecs - used_plots = Set{Int}() - for plotspec in plotspecs - # we need to compare by types with compare_specs, since we can only update plots if the types of all attributes match - idx = findfirst(x-> compare_specs(x[1], plotspec), cached_plots) - if isnothing(idx) - @debug("Creating new plot for spec") - # Create new plot, store it into our `cached_plots` dictionary - plot = plot!(scene, to_combined(plotspec)) - push!(p.plots, plot) - push!(cached_plots, plotspec => plot) - push!(used_plots, length(cached_plots)) - else - @debug("updating old plot with spec") - push!(used_plots, idx) - plot = cached_plots[idx][2] - update_plot!(plot, plotspec) - cached_plots[idx] = plotspec => plot - end - end - unused_plots = setdiff(1:length(cached_plots), used_plots) - # Next, delete all plots that we haven't used - # TODO, we could just hide them, until we reach some max_plots_to_be_cached, so that we re-create less plots. - for idx in unused_plots - _, plot = cached_plots[idx] - delete!(scene, plot) - filter!(x -> x !== plot, p.plots) - end - splice!(cached_plots, sort!(collect(unused_plots))) - end -end - -# Prototype for Pluto + Ijulia integration with Observable(ListOfPlots) -function Base.showable(::Union{MIME"juliavscode/html",MIME"text/html"}, ::Observable{<: AbstractVector{<:PlotSpec}}) - return true -end - -function Base.show(io::IO, m::Union{MIME"juliavscode/html",MIME"text/html"}, - plotspec::Observable{<:AbstractVector{<:PlotSpec}}) - f = plot(plotspec) - show(io, m, f) - return -end diff --git a/src/basic_recipes/specapi.jl b/src/basic_recipes/specapi.jl new file mode 100644 index 00000000000..9ca23ec5e94 --- /dev/null +++ b/src/basic_recipes/specapi.jl @@ -0,0 +1,481 @@ +# Ideally we re-use Makie.PlotSpec, but right now we need a bit of special behaviour to make this work nicely. +# If the implementation stabilizes, we should think about refactoring PlotSpec to work for both use cases, and then just have one PlotSpec type. +@nospecialize +""" + PlotSpec(plottype, args...; kwargs...) + +Object encoding positional arguments (`args`), a `NamedTuple` of attributes (`kwargs`) +as well as plot type `P` of a basic plot. +""" +struct PlotSpec + type::Symbol + args::Vector{Any} + kwargs::Dict{Symbol, Any} + function PlotSpec(type::Symbol, args...; kwargs...) + if string(type)[end] == '!' + error("PlotSpec objects are supposed to be used without !, unless when using `S.$(type)(axis::P.Axis, args...; kwargs...)`") + end + kw = Dict{Symbol,Any}() + for (k, v) in kwargs + # convert eagerly, so that we have stable types for matching later + # E.g. so that PlotSpec(; color = :red) has the same type as PlotSpec(; color = RGBA(1, 0, 0, 1)) + if v isa Cycled # special case for conversions needing a scene + kw[k] = v + elseif v isa Observable + error("PlotSpec are supposed to be used without Observables") + else + try + # Really unfortunate! + # Recipes don't have convert_attribute + # (e.g. band(...; color=:y)) + # So on error we don't convert for now via try catch + # Since we also dont have an API to figure out if a convert is defined correctly + # TODO, I think we can do this more elegantly but will need a bit of a convert_attribute refactor + kw[k] = convert_attribute(v, Key{k}(), Key{type}()) + catch e + kw[k] = v + end + end + end + return new(type, Any[args...], kw) + end + PlotSpec(args...; kwargs...) = new(:plot, args...; kwargs...) +end +@specialize + +Base.getindex(p::PlotSpec, i::Int) = getindex(p.args, i) +Base.getindex(p::PlotSpec, i::Symbol) = getproperty(p.kwargs, i) + +to_plotspec(::Type{P}, args; kwargs...) where {P} = PlotSpec(plotkey(P), args...; kwargs...) + +function to_plotspec(::Type{P}, p::PlotSpec; kwargs...) where {P} + S = plottype(p) + return PlotSpec(plotkey(plottype(P, S)), p.args...; p.kwargs..., kwargs...) +end + +plottype(p::PlotSpec) = Combined{getfield(Makie, p.type)} + +mutable struct BlockSpec + type::Symbol + position::Union{Nothing, Tuple{Any,Any}} + kwargs::Dict{Symbol,Any} + plots::Vector{PlotSpec} +end + +function BlockSpec(typ::Symbol, args...; position=nothing, plots::Vector{PlotSpec}=PlotSpec[], kw...) + attr = Dict{Symbol,Any}(kw) + if typ == :Legend + # TODO, this is hacky and works around the fact, + # that legend gets its legend elements from the positional arguments + # But we can only update them via legend.entrygroups + defaults = block_defaults(:Legend, attr, nothing) + entrygroups = to_entry_group(Attributes(defaults), args...) + attr[:entrygroups] = entrygroups + return BlockSpec(typ, position, attr, plots) + else + if !isempty(args) + error("BlockSpecs, with an exception for Legend, don't support positional arguments yet.") + end + return BlockSpec(typ, position, attr, plots) + end +end + +struct FigureSpec + blocks::Vector{BlockSpec} + kw::Dict{Symbol, Any} + function FigureSpec(blocks::Array{BlockSpec, N}, kw::Dict{Symbol, Any}) where N + if !(N in (1, 2)) + error("Blocks need to be matrix or vector of BlockSpecs") + end + for ij in CartesianIndices(blocks) + block = blocks[ij] + if isnothing(block.position) + if N === 1 + block.position = (1, ij[1]) + else + block.position = Tuple(ij) + end + end + end + return new(vec(blocks), kw) + end + +end + +FigureSpec(blocks::BlockSpec...; kw...) = FigureSpec(BlockSpec[blocks...], Dict{Symbol,Any}(kw)) +FigureSpec(blocks::Array{BlockSpec, N}; kw...) where N = FigureSpec(blocks, Dict{Symbol,Any}(kw)) + +struct FigurePosition + f::FigureSpec + position::Tuple{Any,Any} +end + +function Base.getindex(f::FigureSpec, arg1, arg2) + return FigurePosition(f, (arg1, arg2)) +end + +function BlockSpec(typ::Symbol, pos::FigurePosition, args...; plots::Vector{PlotSpec}=PlotSpec[], kw...) + block = BlockSpec(typ, args...; position=pos.position, plots=plots, kw...) + push!(pos.f.blocks, block) + return block +end + +function PlotSpec(type::Symbol, ax::BlockSpec, args...; kwargs...) + tstring = string(type) + if !endswith(tstring, "!") + error("Need to call $(type)! to create a plot in an axis") + end + type = Symbol(tstring[1:end-1]) + plot = PlotSpec(type, args...; kwargs...) + push!(ax.plots, plot) + return plot +end + +""" +apply for return type PlotSpec +""" +function apply_convert!(P, attributes::Attributes, x::PlotSpec) + args, kwargs = x.args, x.kwargs + # Note that kw_args in the plot spec that are not part of the target plot type + # will end in the "global plot" kw_args (rest) + for (k, v) in pairs(kwargs) + attributes[k] = v + end + return (plottype(plottype(x), P), (args...,)) +end + +function apply_convert!(P, ::Attributes, x::AbstractVector{PlotSpec}) + return (PlotList, (x,)) +end + +""" +apply for return type + (args...,) +""" +apply_convert!(P, ::Attributes, x::Tuple) = (P, x) + +function MakieCore.argtypes(plot::PlotSpec) + args_converted = convert_arguments(plottype(plot), plot.args...) + return MakieCore.argtypes(args_converted) +end + +""" +See documentation for specapi. +""" +struct _SpecApi end +const SpecApi = _SpecApi() + +function Base.getproperty(::_SpecApi, field::Symbol) + field === :Figure && return FigureSpec + # TODO, we wanted to track all recipe names in a set + # in MakieCore via the recipe macro, but due to precompilation & caching + # It seems impossible to merge the recipes from all modules + # Since precompilation will cache only MakieCore's state + # And once everything is compiled, and MakieCore is loaded into a package + # The names are loaded from cache and dont contain anything after MakieCore. + fname = Symbol(replace(string(field), "!" => "")) + func = getfield(Makie, fname) + if func isa Function + return (args...; kw...) -> PlotSpec(field, args...; kw...) + elseif func <: Block + return (args...; kw...) -> BlockSpec(field, args...; kw...) + else + # TODO better error! + error("$(field) not a valid Block or Plot function") + end +end + + +# comparison based entirely of types inside args + kwargs +function compare_specs(a::PlotSpec, b::PlotSpec) + a.type === b.type || return false + length(a.args) == length(b.args) || return false + all(i-> typeof(a.args[i]) == typeof(b.args[i]), 1:length(a.args)) || return false + + length(a.kwargs) == length(b.kwargs) || return false + ka = keys(a.kwargs) + kb = keys(b.kwargs) + ka == kb || return false + all(k -> typeof(a.kwargs[k]) == typeof(b.kwargs[k]), ka) || return false + return true +end + +@inline function is_different(a, b) + # First check if they are the same object + # This disallows mutating PlotSpec arguments in place + a === b && return false + # If they're not the same objcets, we see if they contain the same values + a == b && return false + return true +end + +function update_plot!(plot::AbstractPlot, spec::PlotSpec) + # Update args in plot `input_args` list + any_different = false + for i in eachindex(spec.args) + # we should only call update_plot!, if compare_spec(spec_plot_got_created_from, spec) == true, + # Which should guarantee, that args + kwargs have the same length and types! + arg_obs = plot.args[i] + if is_different(to_value(arg_obs), spec.args[i]) # only update if different + any_different = true + arg_obs.val = spec.args[i] + end + end + + # Update attributes + to_notify = Symbol[] + for (attribute, new_value) in spec.kwargs + old_attr = plot[attribute] + # only update if different + if is_different(old_attr[], new_value) + @debug("updating kw $attribute") + old_attr.val = new_value + push!(to_notify, attribute) + end + end + # We first update obs.val only to prevent dimension missmatch problems + # We shouldn't have many since we only update if the types match, but I already run into a few regardless + # TODO, have update!(plot, new_attributes), which doesn't run into this problem and + # is also more efficient e.g. for WGLMakie, where every update sends a separate message via the websocket + if any_different + # It should be enough to notify first arg, since `convert_arguments` depends on all args + notify(plot.args[1]) + end + for attribute in to_notify + notify(plot[attribute]) + end +end + +""" + plotlist!( + [ + PlotSpec(:scatter, args...; kwargs...), + PlotSpec(:lines, args...; kwargs...), + ] + ) + +Plots a list of PlotSpec's, which can be an observable, making it possible to create efficiently animated plots with the following API: + +## Example +```julia +using GLMakie +import Makie.SpecApi as S + +fig = Figure() +ax = Axis(fig[1, 1]) +plots = Observable([S.heatmap(0 .. 1, 0 .. 1, Makie.peaks()), S.lines(0 .. 1, sin.(0:0.01:1); color=:blue)]) +pl = plot!(ax, plots) +display(fig) + +# Updating the plot dynamically +plots[] = [S.heatmap(0 .. 1, 0 .. 1, Makie.peaks()), S.lines(0 .. 1, sin.(0:0.01:1); color=:red)] +plots[] = [ + S.image(0 .. 1, 0 .. 1, Makie.peaks()), + S.poly(Rect2f(0.45, 0.45, 0.1, 0.1)), + S.lines(0 .. 1, sin.(0:0.01:1); linewidth=10, color=Makie.resample_cmap(:viridis, 101)), +] + +plots[] = [ + S.surface(0..1, 0..1, Makie.peaks(); colormap = :viridis, translation = Vec3f(0, 0, -1)), +] +``` +""" +@recipe(PlotList, plotspecs) do scene + Attributes() +end + +convert_arguments(::Type{<:AbstractPlot}, args::AbstractArray{<:PlotSpec}) = (args,) +plottype(::AbstractVector{PlotSpec}) = PlotList + +# Since we directly plot into the parent scene (hacky), we need to overload these +Base.insert!(::MakieScreen, ::Scene, ::PlotList) = nothing + +function Base.show(io::IO, ::MIME"text/plain", spec::PlotSpec) + args = join(map(x -> string("::", typeof(x)), spec.args), ", ") + kws = join([string(k, " = ", typeof(v)) for (k, v) in spec.kwargs], ", ") + println(io, "S.", spec.type, "($args; $kws)") + return +end + +function Base.show(io::IO, spec::PlotSpec) + args = join(map(x -> string("::", typeof(x)), spec.args), ", ") + kws = join([string(k, " = ", typeof(v)) for (k, v) in spec.kwargs], ", ") + println(io, "S.", spec.type, "($args; $kws)") + return +end + +function to_combined(ps::PlotSpec) + P = plottype(ps) + return P((ps.args...,), copy(ps.kwargs)) +end + +function update_plotspecs!(scene::Scene, list_of_plotspecs::Observable, plotlist::Union{Nothing, PlotList}=nothing) + # Cache plots here so that we aren't re-creating plots every time; + # if a plot still exists from last time, update it accordingly. + # If the plot is removed from `plotspecs`, we'll delete it from here + # and re-create it if it ever returns. + l = Base.ReentrantLock() + cached_plots = IdDict{PlotSpec,Combined}() + on(scene, list_of_plotspecs; update=true) do plotspecs + lock(l) do + old_plots = copy(cached_plots) # needed for set diff + previoues_plots = copy(cached_plots) # needed to be mutated + empty!(cached_plots) + for plotspec in plotspecs + # we need to compare by types with compare_specs, since we can only update plots if the types of all attributes match + reused_plot = nothing + for (spec, plot) in previoues_plots + if compare_specs(spec, plotspec) + reused_plot = plot + delete!(previoues_plots, spec) + break + end + end + if isnothing(reused_plot) + @debug("Creating new plot for spec") + # Create new plot, store it into our `cached_plots` dictionary + plot = plot!(scene, to_combined(plotspec)) + if !isnothing(plotlist) + push!(plotlist.plots, plot) + end + cached_plots[plotspec] = plot + else + @debug("updating old plot with spec") + update_plot!(reused_plot, plotspec) + cached_plots[plotspec] = reused_plot + end + end + unused_plots = setdiff(values(old_plots), values(cached_plots)) + # Next, delete all plots that we haven't used + # TODO, we could just hide them, until we reach some max_plots_to_be_cached, so that we re-create less plots. + for plot in unused_plots + if !isnothing(plotlist) + filter!(x -> x !== plot, plotlist.plots) + end + delete!(scene, plot) + end + return + end + end +end + +function Makie.plot!(p::PlotList{<: Tuple{<: AbstractArray{PlotSpec}}}) + scene = Makie.parent_scene(p) + update_plotspecs!(scene, p[1], p) + return +end + +## BlockSpec + +function compare_block(a::BlockSpec, b::BlockSpec) + a.type === b.type || return false + a.position === b.position || return false + return true +end + +function to_block(fig, spec::BlockSpec) + BType = getfield(Makie, spec.type) + return BType(fig[spec.position...]; spec.kwargs...) +end + +function update_block!(block::T, plot_obs, old_spec::BlockSpec, spec::BlockSpec) where T <: Block + old_attr = keys(old_spec.kwargs) + new_attr = keys(spec.kwargs) + # attributes that have been set previously and need to get unset now + reset_to_defaults = setdiff(old_attr, new_attr) + if !isempty(reset_to_defaults) + default_attrs = default_attribute_values(T, block.blockscene) + for attr in reset_to_defaults + setproperty!(block, attr, default_attrs[attr]) + end + end + # Attributes needing an update + to_update = setdiff(new_attr, reset_to_defaults) + for key in to_update + val = spec.kwargs[key] + prev_val = to_value(getproperty(block, key)) + if val !== prev_val || val != prev_val + setproperty!(block, key, val) + end + end + # Reset the cycler + if hasproperty(block, :scene) + empty!(block.scene.cycler.counters) + end + plot_obs[] = spec.plots + return +end + +function update_fig!(fig, figure_obs) + cached_blocks = Pair{BlockSpec,Tuple{Block,Observable}}[] + l = Base.ReentrantLock() + pfig = fig isa Figure ? fig : get_top_parent(fig) + on(pfig.scene, figure_obs; update=true) do figure + lock(l) do + used_specs = Set{Int}() + for spec in figure.blocks + # we need to compare by types with compare_specs, since we can only update plots if the types of all attributes match + idx = findfirst(x -> compare_block(x[1], spec), cached_blocks) + if isnothing(idx) + @debug("Creating new block for spec") + # Create new plot, store it into our `cached_blocks` dictionary + block = to_block(fig, spec) + if block isa AbstractAxis + obs = Observable(spec.plots) + scene = get_scene(block) + update_plotspecs!(scene, obs) + else + obs = Observable([]) + end + push!(cached_blocks, spec => (block, obs)) + push!(used_specs, length(cached_blocks)) + else + @debug("updating old block with spec") + push!(used_specs, idx) + old_spec, (block, plot_obs) = cached_blocks[idx] + update_block!(block, plot_obs, old_spec, spec) + cached_blocks[idx] = spec => (block, plot_obs) + end + update_state_before_display!(block) + end + unused_plots = setdiff(1:length(cached_blocks), used_specs) + # Next, delete all plots that we haven't used + # TODO, we could just hide them, until we reach some max_plots_to_be_cached, so that we re-create less plots. + layouts_to_trim = Set{GridLayout}() + for idx in unused_plots + _, (block, obs) = cached_blocks[idx] + gc = GridLayoutBase.gridcontent(block) + push!(layouts_to_trim, gc.parent) + delete!(block) + Makie.Observables.clear(obs) + end + splice!(cached_blocks, sort!(collect(unused_plots))) + foreach(trim!, layouts_to_trim) + return + end + end + return fig +end + +function plot(figure_obs::Observable{FigureSpec}; figure=(;)) + fig = Figure(; figure...) + update_fig!(fig, figure_obs) + return fig +end + +args_preferred_axis(::FigureSpec) = FigureOnly + +plot!(plot::Combined{MakieCore.plot,Tuple{Makie.FigureSpec}}) = plot + +function plot!(fig::Union{Figure, GridLayoutBase.GridPosition}, plot::Combined{MakieCore.plot,Tuple{Makie.FigureSpec}}) + figure = fig isa Figure ? fig : get_top_parent(fig) + connect_plot!(figure.scene, plot) + update_fig!(fig, plot[1]) + return fig +end + +function apply_convert!(P, attributes::Attributes, x::FigureSpec) + return (Combined{plot}, (x,)) +end + +MakieCore.argtypes(::FigureSpec) = Tuple{Nothing} diff --git a/src/basic_recipes/text.jl b/src/basic_recipes/text.jl index 01625e0cda5..209ed0f76e1 100644 --- a/src/basic_recipes/text.jl +++ b/src/basic_recipes/text.jl @@ -8,13 +8,13 @@ function plot!(plot::Text) check_textsize_deprecation(plot) positions = plot[1] # attach a function to any text that calculates the glyph layout and stores it - glyphcollections = Observable(GlyphCollection[]) - linesegs = Observable(Point2f[]) - linewidths = Observable(Float32[]) - linecolors = Observable(RGBAf[]) + glyphcollections = Observable(GlyphCollection[]; ignore_equal_values=true) + linesegs = Observable(Point2f[]; ignore_equal_values=true) + linewidths = Observable(Float32[]; ignore_equal_values=true) + linecolors = Observable(RGBAf[]; ignore_equal_values=true) lineindices = Ref(Int[]) - onany(plot.text, plot.fontsize, plot.font, plot.fonts, plot.align, + onany(plot, plot.text, plot.fontsize, plot.font, plot.fonts, plot.align, plot.rotation, plot.justification, plot.lineheight, plot.calculated_colors, plot.strokecolor, plot.strokewidth, plot.word_wrap_width, plot.offset) do str, ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs @@ -30,7 +30,8 @@ function plot!(plot::Text) lwidths = Float32[] lcolors = RGBAf[] lindices = Int[] - function push_args((gc, ls, lw, lc, lindex)) + function push_args(args...) + gc, ls, lw, lc, lindex = _get_glyphcollection_and_linesegments(args...) push!(gcs, gc) append!(lsegs, ls) append!(lwidths, lw) @@ -38,18 +39,15 @@ function plot!(plot::Text) append!(lindices, lindex) return end - func = push_args ∘ _get_glyphcollection_and_linesegments if str isa Vector # If we have a Vector of strings, Vector arguments are interpreted # as per string. - broadcast_foreach( - func, - str, 1:attr_broadcast_length(str), ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs + broadcast_foreach(push_args, str, 1:attr_broadcast_length(str), ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs ) else # Otherwise Vector arguments are interpreted by layout_text/ # glyph_collection as per character. - func(str, 1, ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs) + push_args(str, 1, ts, f, fs, al, rot, jus, lh, col, scol, swi, www, offs) end glyphcollections[] = gcs linewidths[] = lwidths @@ -58,11 +56,11 @@ function plot!(plot::Text) linesegs[] = lsegs end - linesegs_shifted = Observable(Point2f[]) + linesegs_shifted = Observable(Point2f[]; ignore_equal_values=true) sc = parent_scene(plot) - onany(linesegs, positions, sc.camera.projectionview, sc.px_area, + onany(plot, linesegs, positions, sc.camera.projectionview, sc.px_area, transform_func_obs(sc), get(plot, :space, :data)) do segs, pos, _, _, transf, space pos_transf = plot_to_screen(plot, pos) linesegs_shifted[] = map(segs, lineindices[]) do seg, index @@ -164,7 +162,7 @@ function plot!(plot::Text{<:Tuple{<:AbstractArray{<:Tuple{<:Any, <:Point}}}}) text!(plot, positions; text = strings, attrs...) # update both text and positions together - on(strings_and_positions) do str_pos + on(plot, strings_and_positions) do str_pos strs = first.(str_pos) poss = to_ndim.(Ref(Point3f), last.(str_pos), 0) diff --git a/src/bezier.jl b/src/bezier.jl index 81f18f506cc..1ad450c8351 100644 --- a/src/bezier.jl +++ b/src/bezier.jl @@ -1,29 +1,31 @@ using StableHashTraits +const Point2d = Point2{Float64} + struct MoveTo - p::Point2{Float64} + p::Point2d end -MoveTo(x, y) = MoveTo(Point(x, y)) +MoveTo(x, y) = MoveTo(Point2d(x, y)) struct LineTo - p::Point2{Float64} + p::Point2d end -LineTo(x, y) = LineTo(Point(x, y)) +LineTo(x, y) = LineTo(Point2d(x, y)) struct CurveTo - c1::Point2{Float64} - c2::Point2{Float64} - p::Point2{Float64} + c1::Point2d + c2::Point2d + p::Point2d end CurveTo(cx1, cy1, cx2, cy2, p1, p2) = CurveTo( - Point(cx1, cy1), Point(cx2, cy2), Point(p1, p2) + Point2d(cx1, cy1), Point2d(cx2, cy2), Point2d(p1, p2) ) struct EllipticalArc - c::Point2{Float64} + c::Point2d r1::Float64 r2::Float64 angle::Float64 @@ -31,16 +33,88 @@ struct EllipticalArc a2::Float64 end -EllipticalArc(cx, cy, r1, r2, angle, a1, a2) = EllipticalArc(Point(cx, cy), +EllipticalArc(cx, cy, r1, r2, angle, a1, a2) = EllipticalArc(Point2d(cx, cy), r1, r2, angle, a1, a2) struct ClosePath end - const PathCommand = Union{MoveTo, LineTo, CurveTo, EllipticalArc, ClosePath} +function bbox(commands::Vector{PathCommand}) + prev = commands[1] + bb = nothing + for comm in @view(commands[2:end]) + if comm isa MoveTo || comm isa ClosePath + continue + else + endp = endpoint(prev) + _bb = cleanup_bbox(bbox(endp, comm)) + bb = bb === nothing ? _bb : union(bb, _bb) + end + prev = comm + end + return bb +end + +function elliptical_arc_to_beziers(arc::EllipticalArc) + delta_a = abs(arc.a2 - arc.a1) + n_beziers = ceil(Int, delta_a / 0.5pi) + angles = range(arc.a1, arc.a2; length=n_beziers + 1) + + startpoint = Point2f(cos(arc.a1), sin(arc.a1)) + curves = map(angles[1:(end - 1)], angles[2:end]) do start, stop + theta = stop - start + kappa = 4 / 3 * tan(theta / 4) + c1 = Point2f(cos(start) - kappa * sin(start), sin(start) + kappa * cos(start)) + c2 = Point2f(cos(stop) + kappa * sin(stop), sin(stop) - kappa * cos(stop)) + b = Point2f(cos(stop), sin(stop)) + return CurveTo(c1, c2, b) + end + + path = BezierPath([LineTo(startpoint), curves...]) + path = scale(path, Vec2{Float64}(arc.r1, arc.r2)) + path = rotate(path, arc.angle) + return translate(path, arc.c) +end + +bbox(p, x::Union{LineTo,CurveTo}) = bbox(segment(p, x)) +function bbox(p, e::EllipticalArc) + return bbox(elliptical_arc_to_beziers(e)) +end + +endpoint(m::MoveTo) = m.p +endpoint(l::LineTo) = l.p +endpoint(c::CurveTo) = c.p +function endpoint(e::EllipticalArc) + return point_at_angle(e, e.a2) +end + +function point_at_angle(e::EllipticalArc, theta) + M = abs(e.r1) * cos(theta) + N = abs(e.r2) * sin(theta) + return Point2f(e.c[1] + cos(e.angle) * M - sin(e.angle) * N, + e.c[2] + sin(e.angle) * M + cos(e.angle) * N) +end + +function cleanup_bbox(bb::Rect2f) + if any(x -> x < 0, bb.widths) + p = bb.origin .+ (bb.widths .< 0) .* bb.widths + return Rect2f(p, abs.(bb.widths)) + end + return bb +end + struct BezierPath commands::Vector{PathCommand} + boundingbox::Rect2f + hash::UInt32 + function BezierPath(commands::Vector) + c = convert(Vector{PathCommand}, commands) + return new(c, bbox(c), StableHashTraits.stable_hash(c; alg=crc32c, version=2)) + end end +bbox(x::BezierPath) = x.boundingbox +fast_stable_hash(x::BezierPath) = x.hash + # so that the same bezierpath with a different instance of a vector hashes the same # and we don't create the same texture atlas entry twice @@ -52,7 +126,7 @@ function Base.:+(pc::P, p::Point2) where P <: PathCommand return P(map(f -> getfield(pc, f) + p, fnames)...) end -scale(bp::BezierPath, s::Real) = BezierPath([scale(x, Vec(s, s)) for x in bp.commands]) +scale(bp::BezierPath, s::Real) = BezierPath([scale(x, Vec2{Float64}(s, s)) for x in bp.commands]) scale(bp::BezierPath, v::VecTypes{2}) = BezierPath([scale(x, v) for x in bp.commands]) translate(bp::BezierPath, v::VecTypes{2}) = BezierPath([translate(x, v) for x in bp.commands]) @@ -114,7 +188,7 @@ function fit_to_bbox(b::BezierPath, bb_target::Rect2; keep_aspect = true) scale_factor end - bb_t = translate(scale(translate(b, -center_path), scale_factor_aspect), center_target) + return translate(scale(translate(b, -center_path), scale_factor_aspect), center_target) end function fit_to_unit_square(b::BezierPath, keep_aspect = true) @@ -127,74 +201,13 @@ Base.:+(bp::BezierPath, p::Point2) = BezierPath(bp.commands .+ Ref(p)) # markers that fit into a square with sidelength 1 centered on (0, 0) -const BezierCircle = let - r = 0.47 # sqrt(1/pi) - BezierPath([ - MoveTo(Point(r, 0.0)), - EllipticalArc(Point(0.0, 0), r, r, 0.0, 0.0, 2pi), - ClosePath(), - ]) -end - -const BezierUTriangle = let - aspect = 1 - h = 0.97 # sqrt(aspect) * sqrt(2) - w = 0.97 # 1/sqrt(aspect) * sqrt(2) - # r = Float32(sqrt(1 / (3 * sqrt(3) / 4))) - p1 = Point(0, h/2) - p2 = Point2(-w/2, -h/2) - p3 = Point2(w/2, -h/2) - centroid = (p1 + p2 + p3) / 3 - bp = BezierPath([ - MoveTo(p1 - centroid), - LineTo(p2 - centroid), - LineTo(p3 - centroid), - ClosePath() - ]) -end - -const BezierLTriangle = rotate(BezierUTriangle, pi/2) -const BezierDTriangle = rotate(BezierUTriangle, pi) -const BezierRTriangle = rotate(BezierUTriangle, 3pi/2) - - -const BezierSquare = let - r = 0.95 * sqrt(pi)/2/2 # this gives a little less area as the r=0.5 circle - BezierPath([ - MoveTo(Point2(r, -r)), - LineTo(Point2(r, r)), - LineTo(Point2(-r, r)), - LineTo(Point2(-r, -r)), - ClosePath() - ]) -end - -const BezierCross = let - cutfraction = 2/3 - r = 0.5 # 1/(2 * sqrt(1 - cutfraction^2)) - ri = 0.166 #r * (1 - cutfraction) - - first_three = Point2[(r, ri), (ri, ri), (ri, r)] - all = map(0:pi/2:3pi/2) do a - m = Mat2f(sin(a), cos(a), cos(a), -sin(a)) - Ref(m) .* first_three - end |> x -> reduce(vcat, x) - - BezierPath([ - MoveTo(all[1]), - LineTo.(all[2:end])..., - ClosePath() - ]) -end - -const BezierX = rotate(BezierCross, pi/4) function bezier_ngon(n, radius, angle) points = [radius * Point2f(cos(a + angle), sin(a + angle)) for a in range(0, 2pi, length = n+1)[1:end-1]] BezierPath([ MoveTo(points[1]); - LineTo.(points[2:end]); + LineTo.(@view points[2:end]); ClosePath() ]) end @@ -239,10 +252,10 @@ function BezierPath(svg::AbstractString; fit = false, bbox = nothing, flipy = fa commands = parse_bezier_commands(svg) p = BezierPath(commands) if flipy - p = scale(p, Vec(1, -1)) + p = scale(p, Vec2{Float64}(1, -1)) end if flipx - p = scale(p, Vec(-1, 1)) + p = scale(p, Vec2{Float64}(-1, 1)) end if fit if bbox === nothing @@ -266,14 +279,14 @@ function parse_bezier_commands(svg) function lastp() c = commands[end] if isnothing(lastcomm) - Point(0, 0) + return Point2d(0, 0) elseif c isa ClosePath r = reverse(commands) backto = findlast(x -> !(x isa ClosePath), r) if isnothing(backto) error("No point to go back to") end - r[backto].p + return r[backto].p elseif c isa EllipticalArc let ϕ = c.angle @@ -281,10 +294,10 @@ function parse_bezier_commands(svg) rx = c.r1 ry = c.r2 m = Mat2(cos(ϕ), sin(ϕ), -sin(ϕ), cos(ϕ)) - m * Point(rx * cos(a2), ry * sin(a2)) + c.c + return m * Point2d(rx * cos(a2), ry * sin(a2)) + c.c end else - c.p + return c.p end end @@ -300,27 +313,27 @@ function parse_bezier_commands(svg) if comm == "M" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, MoveTo(Point2(x, y))) + push!(commands, MoveTo(Point2d(x, y))) i += 3 elseif comm == "m" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, MoveTo(Point2(x, y) + lastp())) + push!(commands, MoveTo(Point2d(x, y) + lastp())) i += 3 elseif comm == "L" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, LineTo(Point2(x, y))) + push!(commands, LineTo(Point2d(x, y))) i += 3 elseif comm == "l" x, y = parse.(Float64, args[i+1:i+2]) - push!(commands, LineTo(Point2(x, y) + lastp())) + push!(commands, LineTo(Point2d(x, y) + lastp())) i += 3 elseif comm == "H" x = parse(Float64, args[i+1]) - push!(commands, LineTo(Point2(x, lastp()[2]))) + push!(commands, LineTo(Point2d(x, lastp()[2]))) i += 2 elseif comm == "h" x = parse(Float64, args[i+1]) - push!(commands, LineTo(X(x) + lastp())) + push!(commands, LineTo(Point2d(x, 0) + lastp())) i += 2 elseif comm == "Z" push!(commands, ClosePath()) @@ -330,25 +343,25 @@ function parse_bezier_commands(svg) i += 1 elseif comm == "C" x1, y1, x2, y2, x3, y3 = parse.(Float64, args[i+1:i+6]) - push!(commands, CurveTo(Point2(x1, y1), Point2(x2, y2), Point2(x3, y3))) + push!(commands, CurveTo(Point2d(x1, y1), Point2d(x2, y2), Point2d(x3, y3))) i += 7 elseif comm == "c" x1, y1, x2, y2, x3, y3 = parse.(Float64, args[i+1:i+6]) l = lastp() - push!(commands, CurveTo(Point2(x1, y1) + l, Point2(x2, y2) + l, Point2(x3, y3) + l)) + push!(commands, CurveTo(Point2d(x1, y1) + l, Point2d(x2, y2) + l, Point2d(x3, y3) + l)) i += 7 elseif comm == "S" x1, y1, x2, y2 = parse.(Float64, args[i+1:i+4]) prev = commands[end] reflected = prev.p + (prev.p - prev.c2) - push!(commands, CurveTo(reflected, Point2(x1, y1), Point2(x2, y2))) + push!(commands, CurveTo(reflected, Point2d(x1, y1), Point2d(x2, y2))) i += 5 elseif comm == "s" x1, y1, x2, y2 = parse.(Float64, args[i+1:i+4]) prev = commands[end] reflected = prev.p + (prev.p - prev.c2) l = lastp() - push!(commands, CurveTo(reflected, Point2(x1, y1) + l, Point2(x2, y2) + l)) + push!(commands, CurveTo(reflected, Point2d(x1, y1) + l, Point2d(x2, y2) + l)) i += 5 elseif comm == "A" args[i+1:i+7] @@ -374,12 +387,12 @@ function parse_bezier_commands(svg) elseif comm == "v" dy = parse(Float64, args[i+1]) l = lastp() - push!(commands, LineTo(Point2(l[1], l[2] + dy))) + push!(commands, LineTo(Point2d(l[1], l[2] + dy))) i += 2 elseif comm == "V" y = parse(Float64, args[i+1]) l = lastp() - push!(commands, LineTo(Point2(l[1], y))) + push!(commands, LineTo(Point2d(l[1], y))) i += 2 else for c in commands @@ -408,7 +421,7 @@ function EllipticalArc(x1, y1, x2, y2, rx, ry, ϕ, largearc::Bool, sweepflag::Bo (rx^2 * y1′^2 + ry^2 * x1′^2) c′ = (largearc == sweepflag ? -1 : 1) * - sqrt(tempsqrt) * Point(rx * y1′ / ry, -ry * x1′ / rx) + sqrt(tempsqrt) * Point2d(rx * y1′ / ry, -ry * x1′ / rx) c = Mat2(cos(ϕ), sin(ϕ), -sin(ϕ), cos(ϕ)) * c′ + 0.5 * (p1 + p2) @@ -440,7 +453,6 @@ function make_outline(path) points = FT_Vector[] tags = Int8[] contours = Int16[] - flags = Int32(0) for command in path.commands new_contour, n_newpoints, newpoints, newtags = convert_command(command) if new_contour @@ -489,13 +501,13 @@ function render_path(path, bitmap_size_px = 256) scale_factor = bitmap_size_px * 64 # We transform the path into a rectangle of size (aspect, 1) or (1, aspect) - # such that aspect ≤ 1. We then scale that rectangle up to a size of 4096 by + # such that aspect ≤ 1. We then scale that rectangle up to a size of 4096 by # 4096 * aspect, which results in at most a 64px by 64px bitmap # freetype has no ClosePath and EllipticalArc, so those need to be replaced path_replaced = replace_nonfreetype_commands(path) - # Minimal size that becomes integer when mutliplying by 64 (target size for + # Minimal size that becomes integer when mutliplying by 64 (target size for # atlas). This adds padding to avoid blurring/scaling factors from rounding # during sdf generation path_size = widths(bbox(path)) / maximum(widths(bbox(path))) @@ -512,7 +524,7 @@ function render_path(path, bitmap_size_px = 256) # Adjust bitmap size to match path size w = ceil(Int, bitmap_size_px * path_size[1]) h = ceil(Int, bitmap_size_px * path_size[2]) - + pitch = w * 1 # 8 bit gray pixelbuffer = zeros(UInt8, h * pitch) bitmap_ref = Ref{FT_Bitmap}() @@ -579,60 +591,12 @@ struct LineSegment to::Point2f end -function bbox(b::BezierPath) - prev = b.commands[1] - bb = nothing - for comm in b.commands[2:end] - if comm isa MoveTo || comm isa ClosePath - continue - else - endp = endpoint(prev) - _bb = cleanup_bbox(bbox(endp, comm)) - bb = bb === nothing ? _bb : union(bb, _bb) - end - prev = comm - end - bb -end - -segment(p, l::LineTo) = LineSegment(p, l.p) -segment(p, c::CurveTo) = BezierSegment(p, c.c1, c.c2, c.p) - -endpoint(m::MoveTo) = m.p -endpoint(l::LineTo) = l.p -endpoint(c::CurveTo) = c.p -function endpoint(e::EllipticalArc) - point_at_angle(e, e.a2) -end - -function point_at_angle(e::EllipticalArc, theta) - M = abs(e.r1) * cos(theta) - N = abs(e.r2) * sin(theta) - Point2f( - e.c[1] + cos(e.angle) * M - sin(e.angle) * N, - e.c[2] + sin(e.angle) * M + cos(e.angle) * N - ) -end - -function cleanup_bbox(bb::Rect2f) - if any(x -> x < 0, bb.widths) - p = bb.origin .+ (bb.widths .< 0) .* bb.widths - return Rect2f(p, abs.(bb.widths)) - end - return bb -end - -bbox(p, x::Union{LineTo, CurveTo}) = bbox(segment(p, x)) -function bbox(p, e::EllipticalArc) - bbox(elliptical_arc_to_beziers(e)) -end function bbox(ls::LineSegment) - Rect2f(ls.from, ls.to - ls.from) + return Rect2f(ls.from, ls.to - ls.from) end function bbox(b::BezierSegment) - p0 = b.from p1 = b.c1 p2 = b.c2 @@ -642,68 +606,103 @@ function bbox(b::BezierSegment) ma = [max.(p0, p3)...] c = -p0 + p1 - b = p0 - 2p1 + p2 + b = p0 - 2p1 + p2 a = -p0 + 3p1 - 3p2 + 1p3 - h = [(b.*b - a.*c)...] + h = [(b .* b - a .* c)...] if h[1] > 0 h[1] = sqrt(h[1]) t = (-b[1] - h[1]) / a[1] if t > 0 && t < 1 - s = 1.0-t - q = s*s*s*p0[1] + 3.0*s*s*t*p1[1] + 3.0*s*t*t*p2[1] + t*t*t*p3[1] - mi[1] = min(mi[1],q) - ma[1] = max(ma[1],q) + s = 1.0 - t + q = s * s * s * p0[1] + 3.0 * s * s * t * p1[1] + 3.0 * s * t * t * p2[1] + t * t * t * p3[1] + mi[1] = min(mi[1], q) + ma[1] = max(ma[1], q) end - t = (-b[1] + h[1])/a[1] - if t>0 && t<1 - s = 1.0-t - q = s*s*s*p0[1] + 3.0*s*s*t*p1[1] + 3.0*s*t*t*p2[1] + t*t*t*p3[1] - mi[1] = min(mi[1],q) - ma[1] = max(ma[1],q) + t = (-b[1] + h[1]) / a[1] + if t > 0 && t < 1 + s = 1.0 - t + q = s * s * s * p0[1] + 3.0 * s * s * t * p1[1] + 3.0 * s * t * t * p2[1] + t * t * t * p3[1] + mi[1] = min(mi[1], q) + ma[1] = max(ma[1], q) end end - if h[2]>0.0 + if h[2] > 0.0 h[2] = sqrt(h[2]) - t = (-b[2] - h[2])/a[2] - if t>0.0 && t<1.0 - s = 1.0-t - q = s*s*s*p0[2] + 3.0*s*s*t*p1[2] + 3.0*s*t*t*p2[2] + t*t*t*p3[2] - mi[2] = min(mi[2],q) - ma[2] = max(ma[2],q) + t = (-b[2] - h[2]) / a[2] + if t > 0.0 && t < 1.0 + s = 1.0 - t + q = s * s * s * p0[2] + 3.0 * s * s * t * p1[2] + 3.0 * s * t * t * p2[2] + t * t * t * p3[2] + mi[2] = min(mi[2], q) + ma[2] = max(ma[2], q) end - t = (-b[2] + h[2])/a[2] - if t>0.0 && t<1.0 - s = 1.0-t - q = s*s*s*p0[2] + 3.0*s*s*t*p1[2] + 3.0*s*t*t*p2[2] + t*t*t*p3[2] - mi[2] = min(mi[2],q) - ma[2] = max(ma[2],q) + t = (-b[2] + h[2]) / a[2] + if t > 0.0 && t < 1.0 + s = 1.0 - t + q = s * s * s * p0[2] + 3.0 * s * s * t * p1[2] + 3.0 * s * t * t * p2[2] + t * t * t * p3[2] + mi[2] = min(mi[2], q) + ma[2] = max(ma[2], q) end end - Rect2f(Point(mi...), Point(ma...) - Point(mi...)) + return Rect2f(Point(mi...), Point(ma...) - Point(mi...)) end +segment(p, l::LineTo) = LineSegment(p, l.p) +segment(p, c::CurveTo) = BezierSegment(p, c.c1, c.c2, c.p) -function elliptical_arc_to_beziers(arc::EllipticalArc) - delta_a = abs(arc.a2 - arc.a1) - n_beziers = ceil(Int, delta_a / 0.5pi) - angles = range(arc.a1, arc.a2, length = n_beziers + 1) - startpoint = Point2f(cos(arc.a1), sin(arc.a1)) - curves = map(angles[1:end-1], angles[2:end]) do start, stop - theta = stop - start - kappa = 4/3 * tan(theta/4) - c1 = Point2f(cos(start) - kappa * sin(start), sin(start) + kappa * cos(start)) - c2 = Point2f(cos(stop) + kappa * sin(stop), sin(stop) - kappa * cos(stop)) - b = Point2f(cos(stop), sin(stop)) - CurveTo(c1, c2, b) - end +const BezierCircle = let + r = 0.47 # sqrt(1/pi) + BezierPath([MoveTo(Point(r, 0.0)), + EllipticalArc(Point(0.0, 0), r, r, 0.0, 0.0, 2pi), + ClosePath()]) +end - path = BezierPath([LineTo(startpoint), curves...]) - path = scale(path, Vec(arc.r1, arc.r2)) - path = rotate(path, arc.angle) - path = translate(path, arc.c) +const BezierUTriangle = let + aspect = 1 + h = 0.97 # sqrt(aspect) * sqrt(2) + w = 0.97 # 1/sqrt(aspect) * sqrt(2) + # r = Float32(sqrt(1 / (3 * sqrt(3) / 4))) + p1 = Point(0, h / 2) + p2 = Point2d(-w / 2, -h / 2) + p3 = Point2d(w / 2, -h / 2) + centroid = (p1 + p2 + p3) / 3 + bp = BezierPath([MoveTo(p1 - centroid), + LineTo(p2 - centroid), + LineTo(p3 - centroid), + ClosePath()]) +end + +const BezierLTriangle = rotate(BezierUTriangle, pi / 2) +const BezierDTriangle = rotate(BezierUTriangle, pi) +const BezierRTriangle = rotate(BezierUTriangle, 3pi / 2) + +const BezierSquare = let + r = 0.95 * sqrt(pi) / 2 / 2 # this gives a little less area as the r=0.5 circle + BezierPath([MoveTo(Point2d(r, -r)), + LineTo(Point2d(r, r)), + LineTo(Point2d(-r, r)), + LineTo(Point2d(-r, -r)), + ClosePath()]) +end + +const BezierCross = let + cutfraction = 2 / 3 + r = 0.5 # 1/(2 * sqrt(1 - cutfraction^2)) + ri = 0.166 #r * (1 - cutfraction) + + first_three = Point2d[(r, ri), (ri, ri), (ri, r)] + all = (x -> reduce(vcat, x))(map(0:(pi / 2):(3pi / 2)) do a + m = Mat2f(sin(a), cos(a), cos(a), -sin(a)) + return Ref(m) .* first_three + end) + + BezierPath([MoveTo(all[1]), + LineTo.(all[2:end])..., + ClosePath()]) end + +const BezierX = rotate(BezierCross, pi / 4) diff --git a/src/colorsampler.jl b/src/colorsampler.jl index 99deb026186..a8800b46b69 100644 --- a/src/colorsampler.jl +++ b/src/colorsampler.jl @@ -244,13 +244,19 @@ colormapping_type(@nospecialize(colormap)) = continuous colormapping_type(::PlotUtils.CategoricalColorGradient) = banded colormapping_type(::Categorical) = categorical -function ColorMapping( - color::AbstractArray{<:Number, N}, colors_obs, colormap, colorrange, - colorscale, alpha, lowclip, highclip, nan_color, - color_mapping_type=lift(colormapping_type, colormap; ignore_equal_values=true)) where {N} - T = _array_value_type(color) - color_tight = convert(Observable{T}, colors_obs) +function _colormapping( + color_tight::Observable{V}, + @nospecialize(colors_obs), + @nospecialize(colormap), + @nospecialize(colorrange), + @nospecialize(colorscale), + @nospecialize(alpha), + @nospecialize(lowclip), + @nospecialize(highclip), + @nospecialize(nan_color), + color_mapping_type) where {V <: AbstractArray{T, N}} where {N, T} + map_colors = Observable(RGBAf[]; ignore_equal_values=true) raw_colormap = Observable(RGBAf[]; ignore_equal_values=true) mapping = Observable{Union{Nothing,Vector{Float64}}}(nothing; ignore_equal_values=true) @@ -276,7 +282,7 @@ function ColorMapping( _lowclip = Observable{Union{Automatic,RGBAf}}(automatic; ignore_equal_values=true) on(lowclip; update=true) do lc - _lowclip[] = lc isa Union{Nothing, Automatic} ? automatic : to_color(lc) + _lowclip[] = lc isa Union{Nothing,Automatic} ? automatic : to_color(lc) return end _highclip = Observable{Union{Automatic,RGBAf}}(automatic; ignore_equal_values=true) @@ -296,21 +302,38 @@ function ColorMapping( color_scaled = lift(color_tight, colorscale) do color, scale return el32convert(apply_scale(scale, color)) end - CT = ColorMapping{N,T,typeof(color_scaled[])} - - return CT( - color_tight, - map_colors, - raw_colormap, - colorscale, - mapping, - colorrange, - _lowclip, - _highclip, - lift(to_color, nan_color), - color_mapping_type, - colorrange_scaled, - color_scaled) + CT = ColorMapping{N,V,typeof(color_scaled[])} + + return CT(color_tight, + map_colors, + raw_colormap, + colorscale, + mapping, + colorrange, + _lowclip, + _highclip, + lift(to_color, nan_color), + color_mapping_type, + colorrange_scaled, + color_scaled) +end + +function ColorMapping( + color::AbstractArray{<:Number, N}, + @nospecialize(colors_obs), + @nospecialize(colormap), + @nospecialize(colorrange), + @nospecialize(colorscale), + @nospecialize(alpha), + @nospecialize(lowclip), + @nospecialize(highclip), + @nospecialize(nan_color), + color_mapping_type=lift(colormapping_type, colormap; ignore_equal_values=true)) where {N} + + T = _array_value_type(color) + color_tight = convert(Observable{T}, colors_obs)::Observable{T} + _colormapping(color_tight, colors_obs, colormap, colorrange, + colorscale, alpha, lowclip, highclip, nan_color, color_mapping_type) end function assemble_colors(c::AbstractArray{<:Number}, @nospecialize(color), @nospecialize(plot)) diff --git a/src/conversions.jl b/src/conversions.jl index 26436a1a5d9..f79e008e05e 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -1419,6 +1419,7 @@ to_spritemarker(x::Rect) = x to_spritemarker(b::BezierPath) = b to_spritemarker(b::Polygon) = BezierPath(b) to_spritemarker(b) = error("Not a valid scatter marker: $(typeof(b))") +to_spritemarker(x::Shape) = x function to_spritemarker(str::String) error("Using strings for multiple char markers is deprecated. Use `collect(string)` or `['x', 'o', ...]` instead. Found: $(str)") diff --git a/src/display.jl b/src/display.jl index a5b4440a322..2409cc546db 100644 --- a/src/display.jl +++ b/src/display.jl @@ -66,14 +66,13 @@ function set_screen_config!(backend::Module, new_values) return backend_defaults end -function merge_screen_config(::Type{Config}, screen_config_kw) where Config +function merge_screen_config(::Type{Config}, config::Dict) where Config backend = parentmodule(Config) key = nameof(backend) backend_defaults = CURRENT_DEFAULT_THEME[key] - kw_nt = values(screen_config_kw) arguments = map(fieldnames(Config)) do name - if haskey(kw_nt, name) - return getfield(kw_nt, name) + if haskey(config, name) + return config[name] else return to_value(backend_defaults[name]) end @@ -110,7 +109,7 @@ end can_show_inline(::Missing) = false # no backend function can_show_inline(Backend) - for mime in [MIME"juliavscode/html"(), MIME"text/html"(), MIME"image/png"(), MIME"image/svg+xml"()] + for mime in (MIME"juliavscode/html"(), MIME"text/html"(), MIME"image/png"(), MIME"image/svg+xml"()) if backend_showable(Backend.Screen, mime) return has_mime_display(mime) end @@ -130,6 +129,7 @@ see `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options. """ function Base.display(figlike::FigureLike; backend=current_backend(), inline=ALWAYS_INLINE_PLOTS[], update = true, screen_config...) + config = Dict{Symbol, Any}(screen_config) if ismissing(backend) error(""" No backend available! @@ -145,7 +145,7 @@ function Base.display(figlike::FigureLike; backend=current_backend(), if (inline === true || inline === automatic) && can_show_inline(backend) # We can't forward the screenconfig to show, but show uses the current screen if there is any # We use that, to create a screen before show and rely on show picking up that screen - screen = getscreen(backend, scene; screen_config...) + screen = getscreen(backend, scene, config) push_screen!(scene, screen) Core.invoke(display, Tuple{Any}, figlike) # In WGLMakie, we need to wait for the display being done @@ -162,7 +162,7 @@ function Base.display(figlike::FigureLike; backend=current_backend(), """ end update && update_state_before_display!(figlike) - screen = getscreen(backend, scene; screen_config...) + screen = getscreen(backend, scene, config) display(screen, scene) return screen end @@ -255,7 +255,7 @@ function Base.show(io::IO, m::MIME, figlike::FigureLike) backend = current_backend() # get current screen the scene is already displayed on, or create a new screen update_state_before_display!(figlike) - screen = getscreen(backend, scene, io, m; visible=false) + screen = getscreen(backend, scene, Dict(:visible=>false), io, m) backend_show(screen, io, m, scene) return screen end @@ -326,13 +326,16 @@ function FileIO.save( # query the filetype only from the file extension F = filetype(file) mime = format2mime(F) + try return open(filename, "w") do io # If the scene already got displayed, we get the current screen its displayed on # Else, we create a new scene and update the state of the fig update && update_state_before_display!(fig) visible = !isnothing(getscreen(scene)) # if already has a screen, don't hide it! - screen = getscreen(backend, scene, io, mime; visible=visible, screen_config...) + config = Dict{Symbol, Any}(screen_config) + get!(config, :visible, visible) + screen = getscreen(backend, scene, config, io, mime) backend_show(screen, io, mime, scene) end catch e @@ -399,9 +402,9 @@ end getscreen(scene::SceneLike, backend=current_backend()) = getscreen(get_scene(scene), backend) -function getscreen(backend::Union{Missing, Module}, scene::Scene, args...; screen_config...) +function getscreen(backend::Union{Missing, Module}, scene::Scene, _config::Dict, args...) screen = getscreen(scene, backend) - config = Makie.merge_screen_config(backend.ScreenConfig, screen_config) + config = merge_screen_config(backend.ScreenConfig, _config) if !isnothing(screen) && parentmodule(typeof(screen)) == backend new_screen = apply_screen_config!(screen, config, scene, args...) if new_screen !== screen @@ -446,7 +449,10 @@ function colorbuffer(fig::FigureLike, format::ImageStorageFormat = JuliaNative; scene = get_scene(fig) update && update_state_before_display!(fig) visible = !isnothing(getscreen(scene)) # if already has a screen, don't hide it! - screen = getscreen(backend, scene; start_renderloop=false, visible=visible, screen_config...) + config = Dict{Symbol,Any}(screen_config) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, scene, config) img = colorbuffer(screen, format) if !isroot(scene) return get_sub_picture(img, format, pixelarea(scene)[]) diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl index 2ae838e6d6d..a3f5eed6fc1 100644 --- a/src/ffmpeg-util.jl +++ b/src/ffmpeg-util.jl @@ -221,7 +221,10 @@ function VideoStream(fig::FigureLike; path = joinpath(dir, "$(gensym(:video)).$(format)") scene = get_scene(fig) update_state_before_display!(fig) - screen = getscreen(backend, scene, GLNative; visible=visible, start_renderloop=false, screen_config...) + config = Dict{Symbol,Any}(screen_config) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, scene, config, GLNative) _xdim, _ydim = size(screen) xdim = iseven(_xdim) ? _xdim : _xdim + 1 ydim = iseven(_ydim) ? _ydim : _ydim + 1 diff --git a/src/figureplotting.jl b/src/figureplotting.jl index 532ab56d38c..e876e7f2252 100644 --- a/src/figureplotting.jl +++ b/src/figureplotting.jl @@ -32,24 +32,18 @@ function _disallow_keyword(kw, attributes) end end -plot_preferred_axis(@nospecialize(x)) = nothing # nothing == I dont know -plot_preferred_axis(p::PlotFunc) = plot_preferred_axis(Makie.conversion_trait(p)) -plot_preferred_axis(::Type{<:Volume}) = LScene -plot_preferred_axis(::VolumeLike) = LScene -plot_preferred_axis(::Type{<:Image}) = Axis -plot_preferred_axis(::Type{<:Heatmap}) = Axis - -function args_preferred_axis(P::Type, args...) - result = plot_preferred_axis(P) - isnothing(result) || return result - return args_preferred_axis(args...) -end +# For plots that dont require an axis, +# E.g. BlockSpec +struct FigureOnly end + function args_preferred_axis(::Type{<:Union{Wireframe,Surface,Contour3d}}, x::AbstractArray, y::AbstractArray, z::AbstractArray) return all(x -> z[1] ≈ x, z) ? Axis : LScene end +args_preferred_axis(x) = nothing + function args_preferred_axis(@nospecialize(args...)) # Fallback: check each single arg if they have a favorite axis type for arg in args @@ -59,37 +53,31 @@ function args_preferred_axis(@nospecialize(args...)) return nothing end -args_preferred_axis(x) = nothing - -args_preferred_axis(x::AbstractVector, y::AbstractVector, z::AbstractVector, f::Function) = LScene -args_preferred_axis(m::AbstractArray{T,3}) where {T} = LScene +args_preferred_axis(::AbstractVector, ::AbstractVector, ::AbstractVector, ::Function) = LScene +args_preferred_axis(::AbstractArray{T,3}) where {T} = LScene -function args_preferred_axis(m::AbstractVector{<:Union{AbstractGeometry{DIM},GeometryBasics.Mesh{DIM}}}) where {DIM} +function args_preferred_axis(::AbstractVector{<:Union{AbstractGeometry{DIM},GeometryBasics.Mesh{DIM}}}) where {DIM} return DIM === 2 ? Axis : LScene end -function args_preferred_axis(m::Union{AbstractGeometry{DIM},GeometryBasics.Mesh{DIM}}) where {DIM} + +function args_preferred_axis(::Union{AbstractGeometry{DIM},GeometryBasics.Mesh{DIM}}) where {DIM} return DIM === 2 ? Axis : LScene end args_preferred_axis(::AbstractVector{<:Point3}) = LScene args_preferred_axis(::AbstractVector{<:Point2}) = Axis -function preferred_axis_type(@nospecialize(p::PlotFunc), @nospecialize(args...)) - # First check if the Plot type "knows" whether it's always 3D - result = plot_preferred_axis(p) - isnothing(result) || return result +preferred_axis_type(::Volume) = LScene +preferred_axis_type(::Union{Image,Heatmap}) = Axis + +function preferred_axis_type(p::Combined{F}) where F # Otherwise, we check the arguments - non_obs = map(to_value, args) - RealP = plottype(p, non_obs...) - result = plot_preferred_axis(RealP) + input_args = map(to_value, p.args) + result = args_preferred_axis(Combined{F}, input_args...) isnothing(result) || return result - - pre_conversion_result = args_preferred_axis(RealP, non_obs...) - isnothing(pre_conversion_result) || return pre_conversion_result - conv = convert_arguments(RealP, non_obs...) - FinalP, args_conv = apply_convert!(RealP, Attributes(), conv) - result = args_preferred_axis(FinalP, args_conv...) + conv_args = map(to_value, p.converted) + result = args_preferred_axis(Combined{F}, conv_args...) isnothing(result) && return Axis # Fallback to Axis if nothing found return result end @@ -104,36 +92,46 @@ function extract_attributes(dict, key) return to_dict(attributes) end -function create_axis_from_kw(PlotType, figlike, attributes::Dict, args...) +function create_axis_for_plot(figure::Figure, plot::AbstractPlot, attributes::Dict) axis_kw = extract_attributes(attributes, :axis) AxType = if haskey(axis_kw, :type) pop!(axis_kw, :type) else - preferred_axis_type(PlotType, args...) + preferred_axis_type(plot) + end + if AxType == FigureOnly # For FigureSpec, which creates Axes dynamically + return nothing end bbox = pop!(axis_kw, :bbox, nothing) - return _block(AxType, figlike, [], axis_kw, bbox) + return _block(AxType, figure, [], axis_kw, bbox) end -function create_figurelike(PlotType, attributes::Dict, args...) +function create_axis_like(plot::AbstractPlot, attributes::Dict, ::Nothing) figure_kw = extract_attributes(attributes, :figure) figure = Figure(; figure_kw...) - ax = create_axis_from_kw(PlotType, figure, attributes, args...) - figure[1, 1] = ax - return FigureAxis(figure, ax), attributes, args + ax = create_axis_for_plot(figure, plot, attributes) + if isnothing(ax) # For FigureSpec + return figure + else + figure[1, 1] = ax + return FigureAxis(figure, ax) + end end -function create_figurelike!(@nospecialize(PlotType), attributes::Dict, @nospecialize(args...)) +MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, s::Union{Combined, Scene}) = s + +function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, ::Nothing) figure = current_figure() isnothing(figure) && error("There is no current figure to plot into.") _disallow_keyword(:figure, attributes) ax = current_axis(figure) isnothing(ax) && error("There is no current axis to plot into.") _disallow_keyword(:axis, attributes) - return ax, attributes, args + return ax end -function create_figurelike!(PlotType, attributes::Dict, gp::GridPosition, args...) + +function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, gp::GridPosition) _disallow_keyword(:figure, attributes) c = contents(gp; exact=true) if !(length(c) == 1 && can_be_current_axis(c[1])) @@ -141,12 +139,12 @@ function create_figurelike!(PlotType, attributes::Dict, gp::GridPosition, args.. end ax = first(c) _disallow_keyword(:axis, attributes) - return ax, attributes, args + return ax end -function create_figurelike(PlotType, attributes::Dict, gp::GridPosition, args...) +function create_axis_like(plot::AbstractPlot, attributes::Dict, gp::GridPosition) _disallow_keyword(:figure, attributes) - f = get_top_parent(gp) + figure = get_top_parent(gp) c = contents(gp; exact=true) if !isempty(c) error(""" @@ -156,12 +154,16 @@ function create_figurelike(PlotType, attributes::Dict, gp::GridPosition, args... If you really want to place an axis on top of other blocks, make your intention clear and create it manually. """) end - ax = create_axis_from_kw(PlotType, f, attributes, args...) - gp[] = ax - return ax, attributes, args + ax = create_axis_for_plot(figure, plot, attributes) + if isnothing(ax) # For FigureSpec + return gp + else + gp[] = ax + return ax + end end -function create_figurelike!(PlotType, attributes::Dict, gsp::GridSubposition, args...) +function MakieCore.create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, gsp::GridSubposition) _disallow_keyword(:figure, attributes) layout = GridLayoutBase.get_layout_at!(gsp.parent; createmissing=false) gp = layout[gsp.rows, gsp.cols, gsp.side] @@ -169,13 +171,13 @@ function create_figurelike!(PlotType, attributes::Dict, gsp::GridSubposition, ar if !(length(c) == 1 && can_be_current_axis(c[1])) error("There is not just one axis at $(gp).") end - ax = first(c) - return ax, attributes, args + _disallow_keyword(:axis, attributes) + return first(c) end -function create_figurelike(PlotType, attributes::Dict, gsp::GridSubposition, args...) +function create_axis_like(plot::AbstractPlot, attributes::Dict, gsp::GridSubposition) _disallow_keyword(:figure, attributes) - layout = GridLayoutBase.get_layout_at!(gsp.parent; createmissing=true) + GridLayoutBase.get_layout_at!(gsp.parent; createmissing=true) c = contents(gsp; exact=true) if !isempty(c) error(""" @@ -188,35 +190,30 @@ function create_figurelike(PlotType, attributes::Dict, gsp::GridSubposition, arg """) end - fig = get_top_parent(gsp) - - ax = create_axis_from_kw(PlotType, fig, attributes, args...) + figure = get_top_parent(gsp) + ax = create_axis_for_plot(figure, plot, attributes) gsp.parent[gsp.rows, gsp.cols, gsp.side] = ax - return ax, attributes, args + return ax end -function create_figurelike!(PlotType, attributes::Dict, ax::AbstractAxis, args...) +function create_axis_like!(@nospecialize(::AbstractPlot), attributes::Dict, ax::AbstractAxis) _disallow_keyword(:axis, attributes) - return ax, attributes, args + return ax end -function create_figurelike(PlotType, attributes::Dict, ::Union{Scene,AbstractAxis}, args...) +function create_axis_like(@nospecialize(::AbstractPlot), ::Dict, ::Union{Scene,AbstractAxis}) return error("Plotting into an axis without !") end figurelike_return(fa::FigureAxis, plot) = FigureAxisPlot(fa.figure, fa.axis, plot) figurelike_return(ax::AbstractAxis, plot) = AxisPlot(ax, plot) -figurelike_return!(ax::AbstractAxis, plot) = plot +figurelike_return!(::AbstractAxis, plot) = plot +figurelike_return!(::Union{Combined, Scene}, plot) = plot plot!(fa::FigureAxis, plot) = plot!(fa.axis, plot) function plot!(ax::AbstractAxis, plot::P) where {P <: AbstractPlot} - if hasproperty(ax, :cycler) && hasproperty(ax, :palette) - plot.axis_cycler = (ax.cycler, ax.palette) - end - plot!(ax.scene, plot) - # some area-like plots basically always look better if they cover the whole plot area. # adjust the limit margins in those cases automatically. needs_tight_limits(plot) && tightlimits!(ax) @@ -242,3 +239,52 @@ function update_state_before_display!(ax::AbstractAxis) reset_limits!(ax) return end + + +@inline plot_args(args...) = (nothing, args) +@inline function plot_args(a::Union{Figure,AbstractAxis,Scene,Combined,GridSubposition,GridPosition}, + args...) + return (a, args) +end +function fig_keywords!(kws) + figkws = Dict{Symbol,Any}() + if haskey(kws, :axis) + figkws[:axis] = pop!(kws, :axis) + end + if haskey(kws, :figure) + figkws[:figure] = pop!(kws, :figure) + end + return figkws +end + +# Don't inline these, since they will get called from `scatter!(args...; kw...)` which gets specialized to all kw args +@noinline function MakieCore._create_plot(F, attributes::Dict, args...) + figarg, pargs = plot_args(args...) + figkws = fig_keywords!(attributes) + plot = Combined{F}(pargs, attributes) + ax = create_axis_like(plot, figkws, figarg) + plot!(ax, plot) + return figurelike_return(ax, plot) +end + +@noinline function MakieCore._create_plot!(F, attributes::Dict, args...) + figarg, pargs = plot_args(args...) + figkws = fig_keywords!(attributes) + plot = Combined{F}(pargs, attributes) + ax = create_axis_like!(plot, figkws, figarg) + plot!(ax, plot) + return figurelike_return!(ax, plot) +end + +@noinline function MakieCore._create_plot!(F, attributes::Dict, scene::SceneLike, args...) + plot = Combined{F}(args, attributes) + plot!(scene, plot) + return plot +end + +# This enables convert_arguments(::Type{<:AbstractPlot}, ::X) -> FigureSpec +# Which skips axis creation +# TODO, what to return for the dynamically created axes? +figurelike_return(f::GridPosition, p::Combined) = p +figurelike_return(f::Figure, p::Combined) = FigureAxisPlot(f, nothing, p) +MakieCore.create_axis_like!(::AbstractPlot, attributes::Dict, fig::Figure) = fig diff --git a/src/interaction/events.jl b/src/interaction/events.jl index 7d599680730..0c82d73055c 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -13,7 +13,7 @@ entered_window(scene, native_window) = not_implemented_for(native_window) function connect_screen(scene::Scene, screen) - on(screen.window_open) do open + on(scene, screen.window_open) do open events(scene).window_open[] = open end @@ -249,9 +249,9 @@ Furthermore you can also make any button, button collection or boolean expression exclusive by wrapping it in `Exclusively(...)`. With that `ispressed` will only return true if the currently pressed buttons match the request exactly. -For cases where you want to react to a release event you can optionally add +For cases where you want to react to a release event you can optionally add a key or mousebutton `waspressed` which is then assumed to be pressed regardless -of it's current state. For example, when reacting to a mousebutton event, you can +of it's current state. For example, when reacting to a mousebutton event, you can pass `event.button` so that a key combination including that button still evaluates as true. diff --git a/src/interfaces.jl b/src/interfaces.jl index 2bab5185fd8..34bfcdccf32 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -1,6 +1,8 @@ function color_and_colormap!(plot, colors = plot.color) - if haskey(plot, :cycle) && haskey(plot, :axis_cycler) - (cycler, palette) = plot.axis_cycler[] + scene = parent_scene(plot) + if !isnothing(scene) && haskey(plot, :cycle) + cycler = scene.cycler + palette = scene.theme.palette cycle = get_cycle_for_plottype(to_value(plot.cycle)) add_cycle_attributes!(plot, cycle, cycler, palette) end @@ -8,15 +10,17 @@ function color_and_colormap!(plot, colors = plot.color) attributes(plot.attributes)[:calculated_colors] = colors end -function calculated_attributes!(T::Type{<: AbstractPlot}, plot) - if haskey(plot, :cycle) && haskey(plot, :axis_cycler) - (cycler, palette) = plot.axis_cycler[] +function calculated_attributes!(::Type{<: AbstractPlot}, plot) + scene = parent_scene(plot) + if !isnothing(scene) && haskey(plot, :cycle) + cycler = scene.cycler + palette = scene.theme.palette cycle = get_cycle_for_plottype(to_value(plot.cycle)) add_cycle_attributes!(plot, cycle, cycler, palette) end end -function calculated_attributes!(T::Type{<: Mesh}, plot) +function calculated_attributes!(::Type{<: Mesh}, plot) mesha = lift(GeometryBasics.attributes, plot, plot.mesh) color = haskey(mesha[], :color) ? lift(x-> x[:color], plot, mesha) : plot.color color_and_colormap!(plot, color) @@ -123,14 +127,18 @@ function convert_arguments!(plot::Combined{F}) where {F} end function Combined{Func}(args::Tuple, plot_attributes::Dict) where {Func} - if first(args) isa Attributes + if !isempty(args) && first(args) isa Attributes merge!(plot_attributes, attributes(first(args))) return Combined{Func}(Base.tail(args), plot_attributes) end P = Combined{Func} used_attrs = used_attributes(P, to_value.(args)...) - kw = [Pair(k, to_value(v)) for (k, v) in plot_attributes if k in used_attrs] - args_converted = convert_arguments(P, map(to_value, args)...; kw...) + if used_attrs === () + args_converted = convert_arguments(P, map(to_value, args)...) + else + kw = [Pair(k, to_value(v)) for (k, v) in plot_attributes if k in used_attrs] + args_converted = convert_arguments(P, map(to_value, args)...; kw...) + end PNew, converted = apply_convert!(P, Attributes(), args_converted) obs_args = Any[convert(Observable, x) for x in args] @@ -228,7 +236,8 @@ function connect_plot!(scene::SceneLike, plot::Combined{F}) where {F} transform!(t, t_user) plot.transformation = t end - connect!(transformation(scene), transformation(plot)) + obsfunc = connect!(transformation(scene), transformation(plot)) + append!(plot.deregister_callbacks, obsfunc) end plot.model = transformationmatrix(plot) convert_arguments!(plot) diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index 9f296523465..840740f8a80 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -1,17 +1,20 @@ Base.parent(t::Transformation) = isassigned(t.parent) ? t.parent[] : nothing function Observables.connect!(parent::Transformation, child::Transformation; connect_func=true) - on(parent.model; update=true) do m + tfuncs = [] + obsfunc = on(parent.model; update=true) do m return child.parent_model[] = m end + push!(tfuncs, obsfunc) if connect_func - on(parent.transform_func; update=true) do f + t2 = on(parent.transform_func; update=true) do f child.transform_func[] = f return end + push!(tfuncs, t2) end child.parent[] = parent - return + return tfuncs end function free(transformation::Transformation) diff --git a/src/makielayout/blocks.jl b/src/makielayout/blocks.jl index 18cfc4dde6b..e0cd7cea2ed 100644 --- a/src/makielayout/blocks.jl +++ b/src/makielayout/blocks.jl @@ -1,5 +1,4 @@ -abstract type Block end -abstract type AbstractAxis <: Block end + function is_attribute end function default_attribute_values end @@ -7,14 +6,12 @@ function attribute_default_expressions end function _attribute_docs end function has_forwarded_layout end - macro Block(_name::Union{Expr, Symbol}, body::Expr = Expr(:block)) body.head === :block || error("A Block needs to be defined within a `begin end` block") type_expr = _name isa Expr ? _name : :($_name <: Makie.Block) name = _name isa Symbol ? _name : _name.args[1] - structdef = quote mutable struct $(type_expr) parent::Union{Figure, Scene, Nothing} @@ -278,7 +275,32 @@ function _block(T::Type{<:Block}, fig_or_scene::Union{Figure, Scene}, args...; b return _block(T, fig_or_scene, Any[args...], Dict{Symbol,Any}(kwargs), bbox) end -function _block(T::Type{<:Block}, fig_or_scene::Union{Figure,Scene}, args, kwdict::Dict, bbox) +function block_defaults(blockname::Symbol, attribute_kwargs::Dict, scene::Union{Nothing, Scene}) + default_attrs = default_attribute_values(getfield(Makie, blockname), scene) + typekey_scene_attrs = get(theme(scene), blockname, Attributes()) + typekey_attrs = theme(blockname; default=Attributes())::Attributes + attributes = Dict{Symbol,Any}() + # make a final attribute dictionary using different priorities + # for the different themes + for (key, val) in default_attrs + # give kwargs priority + if haskey(attribute_kwargs, key) + attributes[key] = attribute_kwargs[key] + # otherwise scene theme + elseif haskey(typekey_scene_attrs, key) + attributes[key] = typekey_scene_attrs[key] + # otherwise global theme + elseif haskey(typekey_attrs, key) + attributes[key] = typekey_attrs[key] + # otherwise its the value from the type default theme + else + attributes[key] = val + end + end + return attributes +end + +function _block(T::Type{<:Block}, fig_or_scene::Union{Figure,Scene}, args, kwdict::Dict, bbox; kwdict_complete=false) # first sort out all user kwargs that correspond to block attributes check_textsize_deprecation(kwdict) @@ -295,28 +317,11 @@ function _block(T::Type{<:Block}, fig_or_scene::Union{Figure,Scene}, args, kwdic topscene = get_topscene(fig_or_scene) # retrieve the default attributes for this block given the scene theme # and also the `Block = (...` style attributes from scene and global theme - default_attrs = default_attribute_values(T, topscene) - typekey_scene_attrs = get(theme(topscene), nameof(T), Attributes())::Attributes - typekey_attrs = theme(nameof(T); default=Attributes())::Attributes - # make a final attribute dictionary using different priorities - # for the different themes - attributes = Dict{Symbol, Any}() - for (key, val) in default_attrs - # give kwargs priority - if haskey(attribute_kwargs, key) - attributes[key] = attribute_kwargs[key] - # otherwise scene theme - elseif haskey(typekey_scene_attrs, key) - attributes[key] = typekey_scene_attrs[key] - # otherwise global theme - elseif haskey(typekey_attrs, key) - attributes[key] = typekey_attrs[key] - # otherwise its the value from the type default theme - else - attributes[key] = val - end + if kwdict_complete + attributes = attribute_kwargs + else + attributes = block_defaults(nameof(T), attribute_kwargs, topscene) end - # create basic layout observables and connect attribute observables further down # after creating the block with its observable fields @@ -408,6 +413,7 @@ end """ Get the scene which blocks need from their parent to plot stuff into """ +get_topscene(f::Union{GridPosition, GridSubposition}) = get_topscene(get_top_parent(f)) get_topscene(f::Figure) = f.scene function get_topscene(s::Scene) if !(Makie.cameracontrols(s) isa Makie.PixelCamera) @@ -513,22 +519,22 @@ end # if a non-observable is passed, its value is converted and placed into an observable of # the correct type which is then used as the block field -function init_observable!(@nospecialize(x), key, @nospecialize(OT), @nospecialize(value)) +function init_observable!(@nospecialize(block), key::Symbol, @nospecialize(OT), @nospecialize(value)) o = convert_for_attribute(observable_type(OT), value) - setfield!(x, key, OT(o)) - return x + setfield!(block, key, OT(o)) + return block end # if an observable is passed, a converted type is lifted off of it, so it is # not used directly as a block field -function init_observable!(@nospecialize(x), key, @nospecialize(OT), @nospecialize(value::Observable)) +function init_observable!(@nospecialize(block), key::Symbol, @nospecialize(OT), @nospecialize(value::Observable)) obstype = observable_type(OT) o = Observable{obstype}() map!(o, value) do v convert_for_attribute(obstype, v) end - setfield!(x, key, o) - return x + setfield!(block, key, o) + return block end observable_type(x::Type{Observable{T}}) where T = T diff --git a/src/makielayout/blocks/axis.jl b/src/makielayout/blocks/axis.jl index 923f2888696..40e0bcfec4c 100644 --- a/src/makielayout/blocks/axis.jl +++ b/src/makielayout/blocks/axis.jl @@ -163,11 +163,6 @@ function initialize_block!(ax::Axis; palette = nothing) elements = Dict{Symbol, Any}() ax.elements = elements - if palette === nothing - palette = fast_deepcopy(get(blockscene.theme, :palette, Makie.DEFAULT_PALETTES)) - end - ax.palette = palette isa Attributes ? palette : Attributes(palette) - # initialize either with user limits, or pick defaults based on scales # so that we don't immediately error targetlimits = Observable{Rect2f}(defaultlimits(ax.limits[], ax.xscale[], ax.yscale[])) @@ -175,8 +170,6 @@ function initialize_block!(ax::Axis; palette = nothing) setfield!(ax, :targetlimits, targetlimits) setfield!(ax, :finallimits, finallimits) - ax.cycler = Cycler() - on(blockscene, targetlimits) do lims # this should validate the targetlimits before anything else happens with them # so there should be nothing before this lifting `targetlimits` @@ -192,6 +185,12 @@ function initialize_block!(ax::Axis; palette = nothing) scene = Scene(blockscene, px_area=scenearea) ax.scene = scene + if !isnothing(palette) + # Backwards compatibility for when palette was part of axis! + palette_attr = palette isa Attributes ? palette : Attributes(palette) + ax.scene.theme.palette = palette_attr + end + # TODO: replace with mesh, however, CairoMakie needs a poly path for this signature # so it doesn't rasterize the scene background = poly!(blockscene, scenearea; color=ax.backgroundcolor, inspectable=false, shading=false, strokecolor=:transparent) diff --git a/src/makielayout/blocks/axis3d.jl b/src/makielayout/blocks/axis3d.jl index f46a82c3565..484300f60f0 100644 --- a/src/makielayout/blocks/axis3d.jl +++ b/src/makielayout/blocks/axis3d.jl @@ -105,9 +105,6 @@ function initialize_block!(ax::Axis3) markerspace = :data, inspectable = false) - ax.cycler = Cycler() - ax.palette = Makie.DEFAULT_PALETTES - ax.mouseeventhandle = addmouseevents!(scene) scrollevents = Observable(ScrollEvent(0, 0)) setfield!(ax, :scrollevents, scrollevents) diff --git a/src/makielayout/blocks/colorbar.jl b/src/makielayout/blocks/colorbar.jl index ca2fb97da30..1856ad4df73 100644 --- a/src/makielayout/blocks/colorbar.jl +++ b/src/makielayout/blocks/colorbar.jl @@ -253,7 +253,6 @@ function initialize_block!(cb::Colorbar) show_cats[] = true end end - heatmap!(blockscene, xrange, yrange, continous_pixels; colormap=colormap, @@ -411,11 +410,13 @@ function initialize_block!(cb::Colorbar) # trigger protrusions with one of the attributes notify(cb.vertical) # We set everything via the ColorMapping now. To be backwards compatible, we always set those fields: - setfield!(cb, :limits, convert(Observable{Any}, limits)) - setfield!(cb, :colormap, convert(Observable{Any}, cmap.colormap)) - setfield!(cb, :highclip, convert(Observable{Any}, cmap.highclip)) - setfield!(cb, :lowclip, convert(Observable{Any}, cmap.lowclip)) - setfield!(cb, :scale, convert(Observable{Any}, cmap.scale)) + if (cb.colormap[] isa ColorMapping) + setfield!(cb, :limits, convert(Observable{Any}, limits)) + setfield!(cb, :colormap, convert(Observable{Any}, cmap.colormap)) + setfield!(cb, :highclip, convert(Observable{Any}, cmap.highclip)) + setfield!(cb, :lowclip, convert(Observable{Any}, cmap.lowclip)) + setfield!(cb, :scale, convert(Observable{Any}, cmap.scale)) + end # trigger bbox notify(cb.layoutobservables.suggestedbbox) notify(barbox) diff --git a/src/makielayout/blocks/label.jl b/src/makielayout/blocks/label.jl index a755cc54e05..adc7de7fbc7 100644 --- a/src/makielayout/blocks/label.jl +++ b/src/makielayout/blocks/label.jl @@ -11,12 +11,13 @@ function initialize_block!(l::Label) t = text!( topscene, textpos, text = l.text, fontsize = l.fontsize, font = l.font, color = l.color, visible = l.visible, align = (:center, :center), rotation = l.rotation, markerspace = :data, - justification = l.justification, lineheight = l.lineheight, word_wrap_width = word_wrap_width, + justification = l.justification, lineheight = l.lineheight, word_wrap_width = word_wrap_width, inspectable = false) textbb = Ref(BBox(0, 1, 0, 1)) - onany(l.text, l.fontsize, l.font, l.rotation, word_wrap_width, l.padding) do _, _, _, _, _, padding + onany(topscene, l.text, l.fontsize, l.font, l.rotation, word_wrap_width, + l.padding) do _, _, _, _, _, padding textbb[] = Rect2f(boundingbox(t)) autowidth = width(textbb[]) + padding[1] + padding[2] autoheight = height(textbb[]) + padding[3] + padding[4] @@ -28,7 +29,7 @@ function initialize_block!(l::Label) return end - onany(layoutobservables.computedbbox, l.padding) do bbox, padding + onany(topscene, layoutobservables.computedbbox, l.padding) do bbox, padding if l.word_wrap[] tw = width(bbox) - padding[1] - padding[2] else diff --git a/src/makielayout/blocks/legend.jl b/src/makielayout/blocks/legend.jl index 227101f18e8..eed52447cd4 100644 --- a/src/makielayout/blocks/legend.jl +++ b/src/makielayout/blocks/legend.jl @@ -1,6 +1,5 @@ -function initialize_block!(leg::Legend, - entry_groups::Observable{Vector{Tuple{Any, Vector{LegendEntry}}}}) - +function initialize_block!(leg::Legend; entrygroups) + entry_groups = convert(Observable{Vector{Tuple{Any,Vector{LegendEntry}}}}, entrygroups) blockscene = leg.blockscene # by default, `tellwidth = true` and `tellheight = false` for vertical legends @@ -468,7 +467,24 @@ function Base.propertynames(legendelement::T) where T <: LegendElement [fieldnames(T)..., keys(legendelement.attributes)...] end +function to_entry_group(legend_defaults, contents::AbstractVector, labels::AbstractVector, title=nothing) + if length(contents) != length(labels) + error("Number of elements not equal: $(length(contents)) content elements and $(length(labels)) labels.") + end + entries = [LegendEntry(label, content, legend_defaults) for (content, label) in zip(contents, labels)] + return [(title, entries)] +end +function to_entry_group( + legend_defaults, contentgroups::AbstractVector{<:AbstractVector}, + labelgroups::AbstractVector{<:AbstractVector}, titles::AbstractVector) + if !(length(titles) == length(contentgroups) == length(labelgroups)) + error("Number of elements not equal: $(length(titles)) titles, $(length(contentgroups)) content groups and $(length(labelgroups)) label groups.") + end + entries = [[LegendEntry(l, pg, legend_defaults) for (l, pg) in zip(labelgroup, contentgroup)] + for (labelgroup, contentgroup) in zip(labelgroups, contentgroups)] + return [(t, en) for (t, en) in zip(titles, entries)] +end """ Legend( @@ -487,17 +503,15 @@ function Legend(fig_or_scene, contents::AbstractVector, labels::AbstractVector, title = nothing; - kwargs...) + bbox=nothing, kwargs...) - if length(contents) != length(labels) - error("Number of elements not equal: $(length(contents)) content elements and $(length(labels)) labels.") - end - - entrygroups = Observable{Vector{EntryGroup}}([]) - legend = Legend(fig_or_scene, entrygroups; kwargs...) - entries = [LegendEntry(label, content, legend) for (content, label) in zip(contents, labels)] - entrygroups[] = [(title, entries)] - legend + scene = get_topscene(fig_or_scene) + legend_defaults = block_defaults(:Legend, Dict{Symbol, Any}(kwargs), scene) + entry_groups = to_entry_group(Attributes(legend_defaults), contents, labels, title) + entrygroups = Observable(entry_groups) + legend_defaults[:entrygroups] = entrygroups + # Use low-level constructor to not calculate legend_defaults a second time + return _block(Legend, fig_or_scene, (), legend_defaults, bbox; kwdict_complete=true) end @@ -522,19 +536,14 @@ function Legend(fig_or_scene, contentgroups::AbstractVector{<:AbstractVector}, labelgroups::AbstractVector{<:AbstractVector}, titles::AbstractVector; - kwargs...) - - if !(length(titles) == length(contentgroups) == length(labelgroups)) - error("Number of elements not equal: $(length(titles)) titles, $(length(contentgroups)) content groups and $(length(labelgroups)) label groups.") - end - - - entrygroups = Observable{Vector{EntryGroup}}([]) - legend = Legend(fig_or_scene, entrygroups; kwargs...) - entries = [[LegendEntry(l, pg, legend) for (l, pg) in zip(labelgroup, contentgroup)] - for (labelgroup, contentgroup) in zip(labelgroups, contentgroups)] - entrygroups[] = [(t, en) for (t, en) in zip(titles, entries)] - legend + bbox=nothing, kwargs...) + + scene = get_scene(fig_or_scene) + legend_defaults = block_defaults(:Legend, Dict{Symbol,Any}(kwargs), scene) + entry_groups = to_entry_group(legend_defaults, contentgroups, labelgroups, titles) + entrygroups = Observable(entry_groups) + legend_defaults[:entrygroups] = entrygroups + return _block(Legend, fig_or_scene, (), legend_defaults, bbox; kwdict_complete=true) end diff --git a/src/makielayout/blocks/polaraxis.jl b/src/makielayout/blocks/polaraxis.jl index 4a5ef1a93b9..be7bcef0d9f 100644 --- a/src/makielayout/blocks/polaraxis.jl +++ b/src/makielayout/blocks/polaraxis.jl @@ -19,14 +19,11 @@ function initialize_block!(po::PolarAxis; palette=nothing) transformation = Transformation(po.scene, transform_func = identity) ) - - # Setup Cycler - po.cycler = Cycler() - if palette === nothing - palette = fast_deepcopy(get(po.blockscene.theme, :palette, DEFAULT_PALETTES)) + if !isnothing(palette) + # Backwards compatibility for when palette was part of axis! + palette_attr = palette isa Attributes ? palette : Attributes(palette) + po.scene.theme.palette = palette_attr end - po.palette = palette isa Attributes ? palette : Attributes(palette) - # Setup camera/limits and Polar transform usable_fraction, radius_at_origin = setup_camera_matrices!(po) @@ -924,4 +921,4 @@ Sets the angular limits of a given `PolarAxis`. function thetalims!(po::PolarAxis, thetamin::Union{Nothing, Real}, thetamax::Union{Nothing, Real}) po.thetalimits[] = (thetamin, thetamax) return -end \ No newline at end of file +end diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index aef1b24b73e..c8ed4abe2c9 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -8,14 +8,6 @@ end struct DataAspect end - -struct Cycler - counters::IdDict{Type, Int} -end - -Cycler() = Cycler(IdDict{Type, Int}()) - - struct Cycle cycle::Vector{Pair{Vector{Symbol}, Symbol}} covary::Bool @@ -211,8 +203,6 @@ end yaxislinks::Vector{Axis} targetlimits::Observable{Rect2f} finallimits::Observable{Rect2f} - cycler::Cycler - palette::Attributes block_limit_linking::Observable{Bool} mouseeventhandle::MouseEventHandle scrollevents::Observable{ScrollEvent} @@ -1358,8 +1348,6 @@ end scrollevents::Observable{ScrollEvent} keysevents::Observable{KeysEvent} interactions::Dict{Symbol, Tuple{Bool, Any}} - cycler::Cycler - palette::Attributes @attributes begin "The height setting of the scene." height = nothing @@ -1646,8 +1634,6 @@ end target_rlims::Observable{Tuple{Float64, Float64}} target_thetalims::Observable{Tuple{Float64, Float64}} target_theta_0::Observable{Float32} - cycler::Cycler - palette::Attributes @attributes begin # Generic diff --git a/src/precompiles.jl b/src/precompiles.jl index 7b71d31148d..1ed6f3e0054 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -44,3 +44,11 @@ for T in (DragPan, RectangleZoom, LimitReset) end precompile(process_axis_event, (Axis, MouseEvent)) precompile(process_interaction, (ScrollZoom, ScrollEvent, Axis)) +precompile(el32convert, (Vector{Int64},)) +precompile(translate, (MoveTo, Vec2{Float64})) +precompile(scale, (MoveTo, Vec{2,Float32})) +precompile(append!, (Vector{FreeType.FT_Vector_}, Vector{FreeType.FT_Vector_})) +precompile(convert_command, (MoveTo,)) +precompile(plot!, (MakieCore.Text{Tuple{Vector{Point{2, Float32}}}},)) +precompile(Vec2{Float64}, (Tuple{Int64,Int64},)) +precompile(MakieCore._create_plot, (typeof(scatter), Dict{Symbol,Any}, UnitRange{Int64})) diff --git a/src/recording.jl b/src/recording.jl index 18cc72b0d6f..a06999a8251 100644 --- a/src/recording.jl +++ b/src/recording.jl @@ -27,13 +27,19 @@ mutable struct RamStepper end function Stepper(figlike::FigureLike; backend=current_backend(), format=:png, visible=false, connect=false, screen_kw...) - screen = getscreen(backend, get_scene(figlike), JuliaNative; visible=visible, start_renderloop=false, screen_kw...) + config = Dict{Symbol,Any}(screen_kw) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, get_scene(figlike), config, JuliaNative) display(screen, figlike; connect=connect) return RamStepper(figlike, screen, Matrix{RGBf}[], format) end function Stepper(figlike::FigureLike, path::String, step::Int; format=:png, backend=current_backend(), visible=false, connect=false, screen_kw...) - screen = getscreen(backend, get_scene(figlike), JuliaNative; visible=visible, start_renderloop=false, screen_kw...) + config = Dict{Symbol,Any}(screen_kw) + get!(config, :visible, visible) + get!(config, :start_renderloop, false) + screen = getscreen(backend, get_scene(figlike), config, JuliaNative) display(screen, figlike; connect=connect) return FolderStepper(figlike, screen, path, format, step) end diff --git a/src/scenes.jl b/src/scenes.jl index 2c7994c9b98..b8e7d9e5865 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -114,6 +114,7 @@ mutable struct Scene <: AbstractScene ssao::SSAO lights::Vector{AbstractLight} deregister_callbacks::Vector{Observables.ObserverFunction} + cycler::Cycler function Scene( parent::Union{Nothing, Scene}, @@ -148,7 +149,8 @@ mutable struct Scene <: AbstractScene visible, ssao, lights, - Observables.ObserverFunction[] + Observables.ObserverFunction[], + Cycler() ) finalizer(free, scene) return scene @@ -156,19 +158,19 @@ mutable struct Scene <: AbstractScene end # on & map versions that deregister when scene closes! -function Observables.on(f, scene::Union{Combined,Scene}, observable::Observable; update=false, priority=0) - to_deregister = on(f, observable; update=update, priority=priority) - push!(scene.deregister_callbacks, to_deregister) +function Observables.on(@nospecialize(f), @nospecialize(scene::Union{Combined,Scene}), @nospecialize(observable::Observable); update=false, priority=0) + to_deregister = on(f, observable; update=update, priority=priority)::Observables.ObserverFunction + push!(scene.deregister_callbacks::Vector{Observables.ObserverFunction}, to_deregister) return to_deregister end -function Observables.onany(f, scene::Union{Combined,Scene}, observables...; priority=0) +function Observables.onany(@nospecialize(f), @nospecialize(scene::Union{Combined,Scene}), @nospecialize(observables...); priority=0) to_deregister = onany(f, observables...; priority=priority) - append!(scene.deregister_callbacks, to_deregister) + append!(scene.deregister_callbacks::Vector{Observables.ObserverFunction}, to_deregister) return to_deregister end -@inline function Base.map!(@nospecialize(f), scene::Union{Combined,Scene}, result::AbstractObservable, os...; +@inline function Base.map!(f, @nospecialize(scene::Union{Combined,Scene}), result::AbstractObservable, os...; update::Bool=true, priority = 0) # note: the @inline prevents de-specialization due to the splatting callback = Observables.MapCallback(f, result, os) @@ -179,7 +181,7 @@ end return result end -@inline function Base.map(f::F, scene::Union{Combined,Scene}, arg1::AbstractObservable, args...; +@inline function Base.map(f::F, @nospecialize(scene::Union{Combined,Scene}), arg1::AbstractObservable, args...; ignore_equal_values=false, priority = 0) where {F} # note: the @inline prevents de-specialization due to the splatting obs = Observable(f(arg1[], map(Observables.to_value, args)...); ignore_equal_values=ignore_equal_values) @@ -465,16 +467,15 @@ function Base.empty!(scene::Scene; free=false) return nothing end - function Base.push!(plot::Combined, subplot) subplot.parent = plot push!(plot.plots, subplot) end -function Base.push!(scene::Scene, plot::AbstractPlot) +function Base.push!(scene::Scene, @nospecialize(plot::AbstractPlot)) push!(scene.plots, plot) for screen in scene.current_screens - insert!(screen, scene, plot) + Base.invokelatest(insert!, screen, scene, plot) end end @@ -491,6 +492,9 @@ function free(plot::AbstractPlot) for f in plot.deregister_callbacks Observables.off(f) end + for arg in plot.args + Observables.clear(arg) + end foreach(free, plot.plots) empty!(plot.plots) empty!(plot.deregister_callbacks) @@ -549,7 +553,8 @@ function plots_from_camera(scene::Scene, camera::Camera, list=AbstractPlot[]) list end -function insertplots!(screen::AbstractDisplay, scene::Scene) + +function insertplots!(@nospecialize(screen::AbstractDisplay), scene::Scene) for elem in scene.plots insert!(screen, scene, elem) end diff --git a/src/stats/distributions.jl b/src/stats/distributions.jl index 0e219fb0bd9..f12c022c995 100644 --- a/src/stats/distributions.jl +++ b/src/stats/distributions.jl @@ -113,7 +113,7 @@ maybefit(x, _) = x function convert_arguments(::Type{<:QQPlot}, x′, y; qqline = :none) x = maybefit(x′, y) points, line = fit_qqplot(x, y; qqline = qqline) - return PlotSpec{QQPlot}(points, line) + return PlotSpec(:qqplot, points, line) end convert_arguments(::Type{<:QQNorm}, y; qqline = :none) = diff --git a/src/theming.jl b/src/theming.jl index 47450cdfa0c..d4d673f80fc 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -224,6 +224,7 @@ function with_theme(f, theme = Theme(); kwargs...) end theme(::Nothing, key::Symbol; default=nothing) = theme(key; default) +theme(::Nothing) = CURRENT_DEFAULT_THEME function theme(key::Symbol; default=nothing) if haskey(CURRENT_DEFAULT_THEME, key) val = to_value(CURRENT_DEFAULT_THEME[key]) diff --git a/src/types.jl b/src/types.jl index 601d8ef9c5a..8e1096d4953 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,4 +1,6 @@ abstract type AbstractCamera end +abstract type Block end +abstract type AbstractAxis <: Block end # placeholder if no camera is present struct EmptyCamera <: AbstractCamera end @@ -447,3 +449,10 @@ end (s::ReversibleScale)(args...) = s.forward(args...) # functor Base.show(io::IO, s::ReversibleScale) = print(io, "ReversibleScale($(s.name))") Base.show(io::IO, ::MIME"text/plain", s::ReversibleScale) = print(io, "ReversibleScale($(s.name))") + + +struct Cycler + counters::IdDict{Type,Int} +end + +Cycler() = Cycler(IdDict{Type,Int}()) diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index 352353c5f50..358e45e12a4 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -1,4 +1,4 @@ -const SERIALIZATION_FORMAT_VERSION = "v5" +const SERIALIZATION_FORMAT_VERSION = "v6" struct TextureAtlas rectangle_packer::RectanglePacker{Int32} @@ -157,7 +157,7 @@ function get_texture_atlas(resolution::Int = 2048, pix_per_glyph::Int = 64) end end -const CACHE_DOWNLOAD_URL = "https://github.com/MakieOrg/Makie.jl/releases/download/v0.19.0/" +const CACHE_DOWNLOAD_URL = "https://github.com/MakieOrg/Makie.jl/releases/download/v0.20.0/" function cached_load(resolution::Int, pix_per_glyph::Int) path = get_cache_path(resolution, pix_per_glyph) @@ -291,19 +291,26 @@ function glyph_uv_width!(atlas::TextureAtlas, b::BezierPath) return atlas.uv_rectangles[glyph_index!(atlas, b)] end + +# Seems like StableHashTraits is so slow, that it's worthwhile to memoize the hashes +const MEMOIZED_HASHES = Dict{Any, UInt32}() + +function fast_stable_hash(x) + return get!(MEMOIZED_HASHES, x) do + return StableHashTraits.stable_hash(x; alg=crc32c, version=2) + end +end + function insert_glyph!(atlas::TextureAtlas, glyph, font::NativeFont) glyphindex = FreeTypeAbstraction.glyph_index(font, glyph) - hash = StableHashTraits.stable_hash((glyphindex, FreeTypeAbstraction.fontname(font)); - alg=crc32c, version=2) + hash = fast_stable_hash((glyphindex, FreeTypeAbstraction.fontname(font))) return insert_glyph!(atlas, hash, (glyphindex, font)) end function insert_glyph!(atlas::TextureAtlas, path::BezierPath) - return insert_glyph!(atlas, StableHashTraits.stable_hash(path; alg=crc32c, version=2), - path) + return insert_glyph!(atlas, fast_stable_hash(path), path) end - function insert_glyph!(atlas::TextureAtlas, hash::UInt32, path_or_glyp::Union{BezierPath, Tuple{UInt64, NativeFont}}) return get!(atlas.mapping, hash) do uv_pixel = render(atlas, path_or_glyp) @@ -434,6 +441,7 @@ function marker_to_sdf_shape(arr::AbstractVector) shape1 = marker_to_sdf_shape(first(arr)) for elem in arr shape2 = marker_to_sdf_shape(elem) + shape2 isa Shape && shape1 isa Shape && continue shape1 !== shape2 && error("Can't use an array of markers that require different primitive_shapes $(typeof.(arr)).") end return shape1 @@ -552,10 +560,11 @@ end offset_marker(atlas, marker, font, markersize, markeroffset) = markeroffset -function marker_attributes(atlas::TextureAtlas, marker, markersize, font, marker_offset) +function marker_attributes(atlas::TextureAtlas, marker, markersize, font, marker_offset, plot_object) atlas_obs = Observable(atlas) # for map to work - scale = map(rescale_marker, atlas_obs, marker, font, markersize; ignore_equal_values=true) - quad_offset = map(offset_marker, atlas_obs, marker, font, markersize, marker_offset; ignore_equal_values=true) + scale = map(rescale_marker, plot_object, atlas_obs, marker, font, markersize; ignore_equal_values=true) + quad_offset = map(offset_marker, plot_object, atlas_obs, marker, font, markersize, marker_offset; + ignore_equal_values=true) return scale, quad_offset end diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index fd2d653eb56..3fc94c42901 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -196,25 +196,20 @@ An example would be a collection of scatter markers that have different sizes bu The length of an attribute is determined with `attr_broadcast_length` and elements are accessed with `attr_broadcast_getindex`. """ -@generated function broadcast_foreach(f, args...) - N = length(args) - quote - lengths = Base.Cartesian.@ntuple $N i -> attr_broadcast_length(args[i]) - maxlen = maximum(lengths) - any_wrong_length = Base.Cartesian.@nany $N i -> lengths[i] ∉ (0, 1, maxlen) - if any_wrong_length - error("All non scalars need same length, Found lengths for each argument: $lengths, $(map(typeof, args))") - end - # skip if there's a zero length element (like an empty annotations collection, etc) - # this differs from standard broadcasting logic in which all non-scalar shapes have to match - 0 in lengths && return - - for i in 1:maxlen - Base.Cartesian.@ncall $N f (j -> attr_broadcast_getindex(args[j], i)) - end - - return +function broadcast_foreach(f, args...) + lengths = map(attr_broadcast_length, args) + maxlen = maximum(lengths) + any_wrong_length = any(len-> !(len in (0, 1, maxlen)), lengths) + if any_wrong_length + error("All non scalars need same length, Found lengths for each argument: $lengths, $(map(typeof, args))") + end + # skip if there's a zero length element (like an empty annotations collection, etc) + # this differs from standard broadcasting logic in which all non-scalar shapes have to match + 0 in lengths && return + for i in 1:maxlen + f(attr_broadcast_getindex.(args, i)...) end + return end diff --git a/test/pipeline.jl b/test/pipeline.jl index 6ab9a701327..e54a1a52055 100644 --- a/test/pipeline.jl +++ b/test/pipeline.jl @@ -112,7 +112,7 @@ end @testset "Cycled" begin # Test for https://github.com/MakieOrg/Makie.jl/issues/3266 f, ax, pl = lines(1:4; color=Cycled(2)) - cpalette = ax.palette[:color][] + cpalette = ax.scene.theme.palette[:color][] @test pl.calculated_colors[] == cpalette[2] pl2 = lines!(ax, 1:4; color=Cycled(1)) @test pl2.calculated_colors[] == cpalette[1]