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]