From 0f4b02d1fc9febf6ce0333d81cf5358046a860e0 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 4 Nov 2024 15:06:58 +0100 Subject: [PATCH] Allow plots to move between scenes in SpecApi (#4478) * refactor JS Plot object to be movable * allow to move plots between scenes * correctly close channel * re-use plots, that got moved between axes * remove lock * fix specapi * fix makie tests * fix CairoMakie * Update CHANGELOG.md * try showing more infos * implement moveto! correctly for GLMakie and test for all backends * test & fix move_to! * add another test * make move_to optional * revert GLMakie changes * revert more changes * fix WGLMakie move_to * rename test * improve tests * clean up test --- CHANGELOG.md | 1 + MakieCore/src/recipes.jl | 4 +- ReferenceTests/src/tests/examples2d.jl | 42 +- ReferenceTests/src/tests/specapi.jl | 35 +- ReferenceTests/src/tests/updating.jl | 70 ++- ReferenceUpdater/src/local_server.jl | 10 +- WGLMakie/src/Serialization.js | 297 +++++++----- WGLMakie/src/display.jl | 7 +- WGLMakie/src/picking.jl | 6 +- WGLMakie/src/serialization.jl | 26 +- WGLMakie/src/three_plot.jl | 17 + WGLMakie/src/wglmakie.bundled.js | 598 ++++++++++++++----------- WGLMakie/test/runtests.jl | 8 +- src/interaction/inspector.jl | 63 +-- src/layouting/transformation.jl | 2 + src/scenes.jl | 44 +- src/specapi.jl | 102 +++-- test/specapi.jl | 6 +- 18 files changed, 844 insertions(+), 494 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af1bc5031e..74cebd149f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Allow plots to move between scenes in SpecApi [#4132](https://github.com/MakieOrg/Makie.jl/pull/4132). - Added empty constructor to all backends for `Screen` allowing `display(Makie.current_backend().Screen(), fig)` [#4561](https://github.com/MakieOrg/Makie.jl/pull/4561). - Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489). - Added the `jitter_width` and `side_nudge` attributes to the `raincloud` plot definition, so that they can be used as kwargs [#4517]https://github.com/MakieOrg/Makie.jl/pull/4517) diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index 6e8da443ba6..6d87c95a565 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -435,7 +435,7 @@ function default_theme(scene, T::Type{<: Plot}) end return attr end - + function extract_docstring(str) if VERSION >= v"1.11" && str isa Base.Docs.DocStr return only(str.text::Core.SimpleVector) @@ -502,7 +502,7 @@ function create_recipe_expr(Tsym, args, attrblock) end function ($funcname!)(args...; kw...) kwdict = Dict{Symbol, Any}(kw) - _create_plot!($funcname, kwdict, args...) + _create_plot!($funcname, kwdict, args...) end $(arg_type_func) diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 94414126528..89ef03ce26a 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -1498,14 +1498,14 @@ end @reference_test "Violin" begin fig = Figure() - + categories = vcat(fill(1, 300), fill(2, 300), fill(3, 300)) values = vcat(RNG.randn(300), (1.5 .* RNG.rand(300)).^2, -(1.5 .* RNG.rand(300)).^2) violin(fig[1, 1], categories, values) dodge = RNG.rand(1:2, 900) - violin(fig[1, 2], categories, values, dodge = dodge, - color = map(d->d==1 ? :yellow : :orange, dodge), + violin(fig[1, 2], categories, values, dodge = dodge, + color = map(d->d==1 ? :yellow : :orange, dodge), strokewidth = 2, strokecolor = :black, gap = 0.1, dodge_gap = 0.5 ) @@ -1513,7 +1513,7 @@ end color = :gray, side = :left ) - violin!(categories, values, orientation = :horizontal, + violin!(categories, values, orientation = :horizontal, color = :yellow, side = :right, strokewidth = 2, strokecolor = :black, weights = abs.(values) ) @@ -1607,7 +1607,7 @@ end @reference_test "boxplot" begin fig = Figure() - + categories = vcat(fill(1, 300), fill(2, 300), fill(3, 300)) values = RNG.randn(900) .+ range(-1, 1, length=900) boxplot(fig[1, 1], categories, values) @@ -1646,16 +1646,16 @@ end @reference_test "crossbar" begin fig = Figure() - + xs = [1, 1, 2, 2, 3, 3] ys = RNG.rand(6) ymins = ys .- 1 ymaxs = ys .+ 1 dodge = [1, 2, 1, 2, 1, 2] - + crossbar(fig[1, 1], xs, ys, ymins, ymaxs, dodge = dodge, show_notch = true) - - crossbar(fig[1, 2], xs, ys, ymins, ymaxs, + + crossbar(fig[1, 2], xs, ys, ymins, ymaxs, dodge = dodge, dodge_gap = 0.25, gap = 0.05, midlinecolor = :blue, midlinewidth = 5, @@ -1679,7 +1679,7 @@ end w = @. x^2 * (1 - x)^2 ecdfplot(f[1, 2], x) ecdfplot!(x; weights = w, color=:orange) - + f end @@ -1710,18 +1710,18 @@ end data[201:500] .-= 3 data[501:end] .= 3 .* abs.(data[501:end]) .- 3 labels = vcat(fill("red", 500), fill("green", 500)) - + fig = Figure() rainclouds(fig[1, 1], labels, data, plot_boxplots = false, cloud_width = 2.0, markersize = 5.0) rainclouds(fig[1, 2], labels, data, color = labels, orientation = :horizontal, cloud_width = 2.0) - rainclouds(fig[2, 1], labels, data, clouds = hist, hist_bins = 30, boxplot_nudge = 0.1, + rainclouds(fig[2, 1], labels, data, clouds = hist, hist_bins = 30, boxplot_nudge = 0.1, center_boxplot = false, boxplot_width = 0.2, whiskerwidth = 1.0, strokewidth = 3.0) rainclouds(fig[2, 2], labels, data, color = labels, side = :right, violin_limits = extrema) fig end -@reference_test "series" begin +@reference_test "series" begin fig = Figure() data = cumsum(RNG.randn(4, 21), dims = 2) @@ -1729,7 +1729,7 @@ end linewidth = 4, linestyle = :dot, markersize = 15, solid_color = :black) axislegend(ax, position = :lt) - ax, sp = series(fig[2, 1], data, labels=["label $i" for i in 1:4], markersize = 10.0, + ax, sp = series(fig[2, 1], data, labels=["label $i" for i in 1:4], markersize = 10.0, marker = Circle, markercolor = :transparent, strokewidth = 2.0, strokecolor = :black) axislegend(ax, position = :lt) @@ -1741,11 +1741,11 @@ end xs = LinRange(0, 4pi, 21) ys = sin.(xs) - + stairs(f[1, 1], xs, ys) stairs(f[2, 1], xs, ys; step=:post, color=:blue, linestyle=:dash) stairs(f[3, 1], xs, ys; step=:center, color=:red, linestyle=:dot) - + f end @@ -1760,7 +1760,7 @@ end stemcolor = :red, color = :orange, markersize = 15, strokecolor = :red, strokewidth = 3, trunklinestyle = :dash, stemlinestyle = :dashdot) - + stem(f[2, 1], xs, sin.(xs), offset = LinRange(-0.5, 0.5, 30), color = LinRange(0, 1, 30), colorrange = (0, 0.5), @@ -1782,21 +1782,21 @@ end fig = Figure() waterfall(fig[1, 1], y) - waterfall(fig[1, 2], y, show_direction = true, marker_pos = :cross, + waterfall(fig[1, 2], y, show_direction = true, marker_pos = :cross, marker_neg = :hline, direction_color = :yellow) colors = Makie.wong_colors() x = repeat(1:2, inner=5) group = repeat(1:5, outer=2) - waterfall(fig[2, 1], x, y, dodge = group, color = colors[group], + waterfall(fig[2, 1], x, y, dodge = group, color = colors[group], show_direction = true, show_final = true, final_color=(colors[6], 1//3), dodge_gap = 0.1, gap = 0.05) x = repeat(1:5, outer=2) group = repeat(1:2, inner=5) - - waterfall(fig[2, 2], x, y, dodge = group, color = colors[group], + + waterfall(fig[2, 2], x, y, dodge = group, color = colors[group], show_direction = true, stack = :x, show_final = true) fig diff --git a/ReferenceTests/src/tests/specapi.jl b/ReferenceTests/src/tests/specapi.jl index ab5847915d9..f6bd6fb8651 100644 --- a/ReferenceTests/src/tests/specapi.jl +++ b/ReferenceTests/src/tests/specapi.jl @@ -117,12 +117,39 @@ end st end +AxNoTicks(;kw...) = S.Axis(; xticksvisible=false, + yticksvisible=false, yticklabelsvisible=false, + xticklabelsvisible=false, kw...) + +@reference_test "Moving Plots in SpecApi" begin + pl1 = S.Heatmap((1, 4), (1, 4), Makie.peaks(50)) + pl2 = S.Scatter(1:4; color=1:4, markersize=30, strokewidth=1, strokecolor=:black) + ax1 = AxNoTicks(; plots=[pl1, pl2]) + grid = S.GridLayout(AxNoTicks()) + f, _, pl = plot(S.GridLayout([ax1 grid]; colgaps=Fixed(4)); figure=(; figure_padding=2, size=(500, 100))) + cb1 = copy(colorbuffer(f)) + + pl1 = S.Heatmap((1, 4), (1, 4), Makie.peaks(50); colormap=:inferno) + ax1 = AxNoTicks() + grid = S.GridLayout(AxNoTicks(; plots=[pl1, pl2])) + pl[1] = S.GridLayout([ax1 grid]; colgaps=Fixed(4)) + cb2 = copy(colorbuffer(f)) + + pl1 = S.Heatmap((1, 4), (1, 4), Makie.peaks(50)) + ax1 = AxNoTicks(; plots=[pl1]) + ax2 = S.GridLayout(AxNoTicks(; plots=[pl2])) + pl[1] = S.GridLayout([ax1 ax2]; colgaps=Fixed(4)) + cb3 = copy(colorbuffer(f)) + + imgs = hcat(rotr90.((cb1, cb2, cb3))...) + s = Scene(; size=size(imgs)) + image!(s, imgs; space=:pixel) + s +end + function to_plot(plots) axes = map(permutedims(plots)) do plot - ax = S.Axis(; - plots=[plot], xticksvisible=false, - yticksvisible=false, yticklabelsvisible=false, - xticklabelsvisible=false) + ax = AxNoTicks(; plots=[plot]) return S.GridLayout([ax S.Colorbar(plot)]) end return S.GridLayout(axes) diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index b80a3b2ef12..1482da75cc3 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -175,7 +175,7 @@ end end @reference_test "event ticks in record" begin - # Checks whether record calculates and triggers event.tick by drawing a + # Checks whether record calculates and triggers event.tick by drawing a # Point at y = 1 for each frame where it does. The animation is irrelevant # here, so we can just check the final image. # The first point maybe at 0 depending on when the backend sets up it's @@ -192,6 +192,68 @@ end f end +@reference_test "Moving plots with move_to" begin + f, ax, pl1 = scatter(5:-1:1; markersize=20, axis=(; title="Axis 1")) + pl2 = poly!(ax, Rect2f(10, 10, 100, 100); color=:green, space=:pixel) + pl3 = scatter!(ax, 1:5; color=Float64[1:5;], markersize=0.5, colorrange=(1, 5), lowclip=:black, + highclip=:red, markerspace=:data) + pl4 = poly!(ax, Rect2f(0, 0, 1, 1); color=(:green, 0.5), strokewidth=2, strokecolor=:black) + f + img1 = copy(colorbuffer(f; px_per_unit=1)) + plots = [pl1, pl2, pl3, pl4] + get_listener_lengths() = map(plots) do x + arg_l = length(x[1].listeners) + attr_l = length(x.color.listeners) + return [arg_l, attr_l] + end + listener_lengths_1 = get_listener_lengths() + + ax2 = Axis(f[1, 2]; title="Axis 2") + ls = LScene(f[2, :]; show_axis=false) + scene = Makie.camrelative(ls.scene) + + Makie.move_to!(pl2, ax2.scene) + Makie.move_to!(pl3, ax2.scene) + Makie.move_to!(pl4, scene) + # Make sure updating still works for color + pl3.color = [-1, 2, 3, 4, 7] + pl3.colormap = :inferno + pl3.markersize = 1 + + @test listener_lengths_1 == get_listener_lengths() + + img2 = copy(colorbuffer(f; px_per_unit=1)) + @test length(ax.scene.plots) == 1 + @test ax.scene.plots[1] === pl1 + @test length(ax2.scene.plots) == 2 + @test pl2 in ax2.scene.plots + @test pl3 in ax2.scene.plots + @test pl4 in scene.plots + @test length(scene) == 1 + + # Move everything back + pl3.color = Float64[1:5;] + pl3.colormap = :viridis + pl3.markersize = 0.5 + Makie.move_to!(pl1, ax.scene) + Makie.move_to!(pl2, ax.scene) + Makie.move_to!(pl3, ax.scene) + Makie.move_to!(pl4, ax.scene) + # Make it easier to see similarity to first plot, by removing new scenes + delete!(ls) + delete!(ax2) + trim!(f.layout) + + img3 = copy(colorbuffer(f; px_per_unit=1)) + + @test listener_lengths_1 == get_listener_lengths() + + imgs = hcat(rotr90.((img1, img2, img3))...) + s = Scene(; size=size(imgs)) + image!(s, imgs; space=:pixel) + s +end + @reference_test "updating surface size" begin X = Observable(-5:5) Y = Observable(-5:5) @@ -200,8 +262,8 @@ end f = Figure(size = (800, 400)) surface(f[1, 1], X, Y, Z) surface(f[1, 2], map(collect, X), map(collect, Y), Z) - surface(f[1, 3], - map((X, Y) -> [x for x in X, y in Y], X, Y), + surface(f[1, 3], + map((X, Y) -> [x for x in X, y in Y], X, Y), map((X, Y) -> [y for x in X, y in Y], X, Y), Z) st = Stepper(f) Makie.step!(st) @@ -215,4 +277,4 @@ end Z.val = [0.01 * x*x * y*y for x in X.val, y in Y.val] notify(Z) Makie.step!(st) -end \ No newline at end of file +end diff --git a/ReferenceUpdater/src/local_server.jl b/ReferenceUpdater/src/local_server.jl index 668af3fd6d9..192ce6a9d65 100644 --- a/ReferenceUpdater/src/local_server.jl +++ b/ReferenceUpdater/src/local_server.jl @@ -28,7 +28,7 @@ function serve_update_page_from_dir(folder) @info "Downloading latest reference folder for $tag" tempdir = download_refimages(tag) - + @info "Updating files in $tempdir" for image in images_to_update @@ -253,7 +253,7 @@ end function group_files(path, input_filename, output_filename) isfile(joinpath(path, output_filename)) && return - + # Group files in new_files/missing_files into a table like layout: # GLMakie CairoMakie WGLMakie @@ -261,12 +261,12 @@ function group_files(path, input_filename, output_filename) data = Dict{String, Vector{Bool}}() open(joinpath(path, input_filename), "r") do file for filepath in eachline(file) - pieces = split(filepath, '/') + pieces = splitpath(filepath) backend = pieces[1] if !(backend in ("GLMakie", "CairoMakie", "WGLMakie")) - error("Failed to parse backend in \"$line\", got \"$backend\"") + error("Failed to parse backend in \"$pieces\", got \"$backend\"") end - + filename = join(pieces[2:end], '/') exists = get!(data, filename, [false, false, false]) diff --git a/WGLMakie/src/Serialization.js b/WGLMakie/src/Serialization.js index 09de2ff7e8f..3c3a876fdc0 100644 --- a/WGLMakie/src/Serialization.js +++ b/WGLMakie/src/Serialization.js @@ -2,6 +2,144 @@ import * as THREE from "./THREE.js"; import * as Camera from "./Camera.js"; import { create_line, create_linesegments } from "./Lines.js"; + +/** + * Updates the value of a given uniform with a new value. + * + * @param {THREE.Uniform} uniform - The uniform to update. + * @param {Object|Array} new_value - The new value to set for the uniform. If the uniform is a texture, this should be an array containing the size and texture data. + */ +function update_uniform(uniform, new_value) { + if (uniform.value.isTexture) { + const im_data = uniform.value.image; + const [size, tex_data] = new_value; + if (tex_data.length == im_data.data.length) { + im_data.data.set(tex_data); + } else { + const old_texture = uniform.value; + uniform.value = re_create_texture(old_texture, tex_data, size); + old_texture.dispose(); + } + uniform.value.needsUpdate = true; + } else { + if (is_three_fixed_array(uniform.value)) { + uniform.value.fromArray(new_value); + } else { + uniform.value = new_value; + } + } +} + + +class Plot { + mesh = undefined; + parent = undefined; + uuid = ""; + name = ""; + is_instanced = false; + geometry_needs_recreation = false; + plot_data = {}; + + constructor(scene, data) { + + this.plot_data = data; + + connect_plot(scene, this); + + if (data.plot_type === "lines") { + this.mesh = create_line(scene, this.plot_data); + } else if (data.plot_type === "linesegments") { + this.mesh = create_linesegments(scene, this.plot_data); + } else if ("instance_attributes" in data) { + this.is_instanced = true + this.mesh = create_instanced_mesh(scene, this.plot_data); + } else { + this.mesh = create_mesh(scene, this.plot_data); + } + + this.name = data.name; + this.uuid = data.uuid; + this.mesh.plot_uuid = data.uuid; + + this.mesh.frustumCulled = false; + this.mesh.matrixAutoUpdate = false; + this.mesh.renderOrder = data.zvalue; + + + data.uniform_updater.on(([name, data]) => { + this.update_uniform(name, data); + }); + + if ( + !(data.plot_type === "lines" || data.plot_type === "linesegments") + ) { + connect_attributes(this.mesh, data.attribute_updater); + } + this.parent = scene; + // Give mesh a reference to the plot object. + this.mesh.plot_object = this; + this.mesh.visible = data.visible.value; + data.visible.on(v=> { + this.mesh.visible = v; + }); + + } + + move_to(scene) { + if (scene === this.parent) { + return + } + this.parent.remove(this.mesh) + connect_plot(scene, this) + scene.add(this.mesh) + this.parent = scene + return + } + + update(attributes) { + attributes.keys().forEach(key=> { + const value = attributes[key] + if (value.type == "uniform") { + this.update_uniform(key, value.data); + } else if (value.type === "geometry") { + this.update_geometries(value.data) + } else if (value.type === "faces") { + this.update_faces(value.data) + } + }) + // For e.g. when we need to re-create the geometry + this.apply_updates() + } + + update_uniform(name, new_data) { + const uniform = this.mesh.material.uniforms[name]; + if (!uniform) { + throw new Error(`Uniform ${name} doesn't exist in Plot: ${this.name}`) + } + update_uniform(uniform, new_data); + } + + update_geometry(name, new_data) { + buffer = this.mesh.geometry.attributes[name]; + if (!buffer) { + throw new Error(`Buffer ${name} doesn't exist in Plot: ${this.name}`) + } + const old_length = buffer.count + if (new_data.length <= old_length) { + buffer.set(new_data.data); + buffer.needsUpdate = true; + } else { + // if we have a larger size we need resizing + recreation of the buffer geometry + buffer.to_update = new_data.data; + this.geometry_needs_recreation = true; + } + } + + update_faces(face_data) { + this.mesh.geometry.setIndex(new THREE.BufferAttribute(face_data, 1)); + } +} + // global scene cache to look them up for dynamic operations in Makie // e.g. insert!(scene, plot) / delete!(scene, plot) const scene_cache = {}; @@ -139,130 +277,71 @@ export function deserialize_uniforms(scene, data) { return result; } -export function deserialize_plot(scene, data) { - let mesh; - const update_visible = (v) => { - mesh.visible = v; - // don't return anything, since that will disable on_update callback - return; - }; - if (data.plot_type === "lines") { - mesh = create_line(scene, data); - } else if (data.plot_type === "linesegments") { - mesh = create_linesegments(scene, data); - } else if ("instance_attributes" in data) { - mesh = create_instanced_mesh(scene, data); - } else { - mesh = create_mesh(scene, data); - } - mesh.name = data.name; - mesh.frustumCulled = false; - mesh.matrixAutoUpdate = false; - mesh.plot_uuid = data.uuid; - mesh.renderOrder = data.zvalue; - update_visible(data.visible.value); - data.visible.on(update_visible); - connect_uniforms(mesh, data.uniform_updater); - if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { - connect_attributes(mesh, data.attribute_updater); - } - return mesh; -} - const ON_NEXT_INSERT = new Set(); export function on_next_insert(f) { ON_NEXT_INSERT.add(f); } -export function add_plot(scene, plot_data) { +/** + * Connects a plot to a scene by setting up the necessary camera uniforms. + * + * @param {THREE.Scene} scene - The scene object containing the camera and screen information. + * @param {Plot} plot - The plot object to be connected to the scene. + */ +function connect_plot(scene, plot) { // fill in the camera uniforms, that we don't sent in serialization per plot const cam = scene.wgl_camera; const identity = new THREE.Uniform(new THREE.Matrix4()); - if (plot_data.cam_space == "data") { - plot_data.uniforms.view = cam.view; - plot_data.uniforms.projection = cam.projection; - plot_data.uniforms.projectionview = cam.projectionview; - plot_data.uniforms.eyeposition = cam.eyeposition; - } else if (plot_data.cam_space == "pixel") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.pixel_space; - plot_data.uniforms.projectionview = cam.pixel_space; - } else if (plot_data.cam_space == "relative") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.relative_space; - plot_data.uniforms.projectionview = cam.relative_space; - } else { + const uniforms = plot.mesh ? plot.mesh.material.uniforms : plot.plot_data.uniforms; + const space = plot.plot_data.cam_space; + if (space == "data") { + uniforms.view = cam.view; + uniforms.projection = cam.projection; + uniforms.projectionview = cam.projectionview; + uniforms.eyeposition = cam.eyeposition; + } else if (space == "pixel") { + uniforms.view = identity; + uniforms.projection = cam.pixel_space; + uniforms.projectionview = cam.pixel_space; + } else if (space == "relative") { + uniforms.view = identity; + uniforms.projection = cam.relative_space; + uniforms.projectionview = cam.relative_space; + } else if (space == "clip") { // clip space - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = identity; - plot_data.uniforms.projectionview = identity; + uniforms.view = identity; + uniforms.projection = identity; + uniforms.projectionview = identity; + } else { + throw new Error(`Space ${space} not supported!`) } const { px_per_unit } = scene.screen; - plot_data.uniforms.resolution = cam.resolution; - plot_data.uniforms.px_per_unit = new THREE.Uniform(px_per_unit); + uniforms.resolution = cam.resolution; + uniforms.px_per_unit = new THREE.Uniform(px_per_unit); - if (plot_data.uniforms.preprojection) { - const { space, markerspace } = plot_data; - plot_data.uniforms.preprojection = cam.preprojection_matrix( + if (plot.plot_data.uniforms.preprojection) { + const { space, markerspace } = plot.plot_data; + uniforms.preprojection = cam.preprojection_matrix( space.value, markerspace.value ); } - if (scene.camera_relative_light) { - plot_data.uniforms.light_direction = cam.light_direction; - scene.light_direction.on((value) => { - cam.update_light_dir(value); - }); - } else { - // TODO how update? - const light_dir = new THREE.Vector3().fromArray( - scene.light_direction.value - ); - plot_data.uniforms.light_direction = new THREE.Uniform(light_dir); - scene.light_direction.on((value) => { - plot_data.uniforms.light_direction.value.fromArray(value); - }); - } + uniforms.light_direction = scene.light_direction; +} + - const p = deserialize_plot(scene, plot_data); - plot_cache[p.plot_uuid] = p; - scene.add(p); +export function add_plot(scene, plot_data) { + // fill in the camera uniforms, that we don't sent in serialization per plot + const p = new Plot(scene, plot_data); + plot_cache[p.uuid] = p.mesh; + scene.add(p.mesh); // execute all next insert callbacks const next_insert = new Set(ON_NEXT_INSERT); // copy next_insert.forEach((f) => f()); } -function connect_uniforms(mesh, updater) { - updater.on(([name, data]) => { - // this is the initial value, which shouldn't end up getting updated - - // TODO, figure out why this gets pushed!! - if (name === "none") { - return; - } - const uniform = mesh.material.uniforms[name]; - if (uniform.value.isTexture) { - const im_data = uniform.value.image; - const [size, tex_data] = data; - if (tex_data.length == im_data.data.length) { - im_data.data.set(tex_data); - } else { - const old_texture = uniform.value; - uniform.value = re_create_texture(old_texture, tex_data, size); - old_texture.dispose(); - } - uniform.value.needsUpdate = true; - } else { - if (is_three_fixed_array(uniform.value)) { - uniform.value.fromArray(data); - } else { - uniform.value = data; - } - } - }); -} - function convert_RGB_to_RGBA(rgbArray) { const length = rgbArray.length; const rgbaArray = new rgbArray.constructor((length / 3) * 4); @@ -365,6 +444,8 @@ function re_create_texture(old_texture, buffer, size) { } return tex; } + + function BufferAttribute(buffer) { const jsbuff = new THREE.BufferAttribute(buffer.flat, buffer.type_length); jsbuff.setUsage(THREE.DynamicDrawUsage); @@ -577,8 +658,6 @@ export function deserialize_scene(data, screen) { scene.backgroundcolor_alpha = data.backgroundcolor_alpha; scene.clearscene = data.clearscene; scene.visible = data.visible; - scene.camera_relative_light = data.camera_relative_light; - scene.light_direction = data.light_direction; const camera = new Camera.MakieCamera(); @@ -607,9 +686,23 @@ export function deserialize_scene(data, screen) { } update_cam(data.camera.value, true); // force update on first call + camera.update_light_dir(data.light_direction.value); data.camera.on(update_cam); + if (data.camera_relative_light) { + scene.light_direction = camera.light_direction; + } else { + const light_dir = new THREE.Vector3().fromArray( + data.light_direction.value + ); + scene.light_direction = new THREE.Uniform(light_dir); + data.light_direction.on((value) => { + plot_data.uniforms.light_direction.value.fromArray(value); + }); + } + + data.plots.forEach((plot_data) => { add_plot(scene, plot_data); }); diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 73717f390f9..cc9be787c70 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -339,18 +339,23 @@ function insert_scene!(session::Session, screen::Screen, scene::Scene) end function insert_plot!(session::Session, scene::Scene, @nospecialize(plot::Plot)) + @assert !haskey(plot, :__wgl_session) plot_data = serialize_plots(scene, Plot[plot]) plot_sub = Session(session) Bonito.init_session(plot_sub) - plot.__wgl_session = plot_sub js = js""" $(WGL).then(WGL=> { WGL.insert_plot($(js_uuid(scene)), $plot_data); })""" Bonito.evaljs_value(plot_sub, js; timeout=50) + @assert !haskey(plot.attributes, :__wgl_session) + plot.attributes[:__wgl_session] = plot_sub return end +function Base.insert!(screen::Screen, scene::Scene, @nospecialize(plot::PlotList)) + return nothing +end function Base.insert!(screen::Screen, scene::Scene, @nospecialize(plot::Plot)) session = get_screen_session(screen; error="Plot needs to be displayed to insert additional plots") if js_uuid(scene) in screen.displayed_scenes diff --git a/WGLMakie/src/picking.jl b/WGLMakie/src/picking.jl index 93bf7a56d9d..bd8c36b0355 100644 --- a/WGLMakie/src/picking.jl +++ b/WGLMakie/src/picking.jl @@ -56,7 +56,10 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) # E.g. if websocket got closed isnothing(session) && return Tuple{Plot,Int}[] selection = Bonito.evaljs_value(session, js""" - Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => WGL.pick_sorted(scene, $(xy_vec), $(range))) + Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => { + const picked = WGL.pick_sorted(scene, $(xy_vec), $(range)) + return picked + }) """) isnothing(selection) && return Tuple{Plot,Int}[] lookup = plot_lookup(scene) @@ -68,6 +71,7 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) end function Makie.pick(::Scene, screen::Screen, xy) + plot_matrix = pick_native(screen, Rect2i(xy..., 1, 1)) return plot_matrix[1, 1] end diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl index 64adb5bda0b..527e361750b 100644 --- a/WGLMakie/src/serialization.jl +++ b/WGLMakie/src/serialization.jl @@ -300,18 +300,20 @@ function serialize_scene(scene::Scene) light_dir = isnothing(dirlight) ? Observable(Vec3f(1)) : dirlight.direction cam_rel = isnothing(dirlight) ? false : dirlight.camera_relative - serialized = Dict(:viewport => pixel_area, - :backgroundcolor => lift(hexcolor, scene, scene.backgroundcolor), - :backgroundcolor_alpha => lift(Colors.alpha, scene, scene.backgroundcolor), - :clearscene => scene.clear, - :camera => serialize_camera(scene), - :light_direction => light_dir, - :camera_relative_light => cam_rel, - :plots => serialize_plots(scene, scene.plots), - :cam3d_state => cam3d_state, - :visible => scene.visible, - :uuid => js_uuid(scene), - :children => children) + serialized = Dict( + :viewport => pixel_area, + :backgroundcolor => lift(hexcolor, scene, scene.backgroundcolor), + :backgroundcolor_alpha => lift(Colors.alpha, scene, scene.backgroundcolor), + :clearscene => scene.clear, + :camera => serialize_camera(scene), + :light_direction => light_dir, + :camera_relative_light => cam_rel, + :plots => serialize_plots(scene, scene.plots), + :cam3d_state => cam3d_state, + :visible => scene.visible, + :uuid => js_uuid(scene), + :children => children + ) return serialized end diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl index c44683adca5..11ac0e34ab7 100644 --- a/WGLMakie/src/three_plot.jl +++ b/WGLMakie/src/three_plot.jl @@ -72,3 +72,20 @@ function three_display(screen::Screen, session::Session, scene::Scene) connect_scene_events!(screen, scene, comm) return wrapper, done_init end + +Makie.supports_move_to(::Screen) = true + +function Makie.move_to!(screen::Screen, plot::Plot, scene::Scene) + session = get_screen_session(screen) + # Make sure target scene is serialized + insert_scene!(session, screen, scene) + return evaljs(session, js""" + $(scene).then(scene=> { + $(plot).then(meshes=> { + meshes.forEach(m => { + m.plot_object.move_to(scene) + }) + }) + }) + """) +end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index a2e349a9981..89a9c13a822 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -21210,7 +21210,26 @@ class MakieCamera { } } } -const scene_cache = {}; +function update_uniform(uniform, new_value) { + if (uniform.value.isTexture) { + const im_data = uniform.value.image; + const [size, tex_data] = new_value; + if (tex_data.length == im_data.data.length) { + im_data.data.set(tex_data); + } else { + const old_texture = uniform.value; + uniform.value = re_create_texture(old_texture, tex_data, size); + old_texture.dispose(); + } + uniform.value.needsUpdate = true; + } else { + if (is_three_fixed_array(uniform.value)) { + uniform.value.fromArray(new_value); + } else { + uniform.value = new_value; + } + } +} function filter_by_key(dict, keys, default_value = false) { const result = {}; keys.forEach((key)=>{ @@ -21223,117 +21242,6 @@ function filter_by_key(dict, keys, default_value = false) { }); return result; } -const plot_cache = {}; -const TEXTURE_ATLAS = [ - undefined -]; -function add_scene(scene_id, three_scene) { - scene_cache[scene_id] = three_scene; -} -function find_scene(scene_id) { - return scene_cache[scene_id]; -} -function delete_scene(scene_id) { - const scene = scene_cache[scene_id]; - if (!scene) { - return; - } - delete_three_scene(scene); - while(scene.children.length > 0){ - scene.remove(scene.children[0]); - } - delete scene_cache[scene_id]; -} -function find_plots(plot_uuids) { - const plots = []; - plot_uuids.forEach((id)=>{ - const plot = plot_cache[id]; - if (plot) { - plots.push(plot); - } - }); - return plots; -} -function delete_scenes(scene_uuids, plot_uuids) { - plot_uuids.forEach((plot_id)=>{ - const plot = plot_cache[plot_id]; - if (plot) { - delete_plot(plot); - } - }); - scene_uuids.forEach((scene_id)=>{ - delete_scene(scene_id); - }); -} -function insert_plot(scene_id, plot_data) { - const scene = find_scene(scene_id); - plot_data.forEach((plot)=>{ - add_plot(scene, plot); - }); -} -function delete_plots(plot_uuids) { - const plots = find_plots(plot_uuids); - plots.forEach(delete_plot); -} -function convert_texture(scene, data) { - const tex = create_texture(scene, data); - tex.needsUpdate = true; - tex.minFilter = mod[data.minFilter]; - tex.magFilter = mod[data.magFilter]; - tex.anisotropy = data.anisotropy; - tex.wrapS = mod[data.wrapS]; - if (data.size.length > 1) { - tex.wrapT = mod[data.wrapT]; - } - if (data.size.length > 2) { - tex.wrapR = mod[data.wrapR]; - } - return tex; -} -function is_three_fixed_array(value) { - return value instanceof mod.Vector2 || value instanceof mod.Vector3 || value instanceof mod.Vector4 || value instanceof mod.Matrix4; -} -function to_uniform(scene, data) { - if (data.type !== undefined) { - if (data.type == "Sampler") { - return convert_texture(scene, data); - } - throw new Error(`Type ${data.type} not known`); - } - if (Array.isArray(data) || ArrayBuffer.isView(data)) { - if (!data.every((x)=>typeof x === "number")) { - return data; - } - if (data.length == 2) { - return new mod.Vector2().fromArray(data); - } - if (data.length == 3) { - return new mod.Vector3().fromArray(data); - } - if (data.length == 4) { - return new mod.Vector4().fromArray(data); - } - if (data.length == 16) { - const mat = new mod.Matrix4(); - mat.fromArray(data); - return mat; - } - } - return data; -} -function deserialize_uniforms(scene, data) { - const result = {}; - for(const name in data){ - const value = data[name]; - if (value instanceof mod.Uniform) { - result[name] = value; - } else { - const ser = to_uniform(scene, value); - result[name] = new mod.Uniform(ser); - } - } - return result; -} function lines_vertex_shader(uniforms, attributes, is_linesegments) { const attribute_decl = attributes_to_type_declaration(attributes); const uniform_decl = uniforms_to_type_declaration(uniforms); @@ -22301,37 +22209,20 @@ function lines_fragment_shader(uniforms, attributes) { } `; } -function create_line_material(scene, uniforms, attributes, is_linesegments) { - const uniforms_des = deserialize_uniforms(scene, uniforms); - const mat = new THREE.RawShaderMaterial({ - uniforms: uniforms_des, - glslVersion: THREE.GLSL3, - vertexShader: lines_vertex_shader(uniforms_des, attributes, is_linesegments), - fragmentShader: lines_fragment_shader(uniforms_des, attributes), - transparent: true, - blending: THREE.CustomBlending, - blendSrc: THREE.SrcAlphaFactor, - blendDst: THREE.OneMinusSrcAlphaFactor, - blendSrcAlpha: THREE.ZeroFactor, - blendDstAlpha: THREE.OneFactor, - blendEquation: THREE.AddEquation - }); - mat.uniforms.object_id = { - value: 1 - }; - return mat; +function create_line(scene, line_data) { + return _create_line(scene, line_data, false); } function attach_interleaved_line_buffer(attr_name, geometry, data, ndim, is_segments, is_position) { const skip_elems = is_segments ? 2 * ndim : ndim; - const buffer = new THREE.InstancedInterleavedBuffer(data, skip_elems, 1); - buffer.count = Math.max(0, is_segments ? Math.floor(buffer.count - 1) : buffer.count - 3); - geometry.setAttribute(attr_name + "_start", new THREE.InterleavedBufferAttribute(buffer, ndim, ndim)); - geometry.setAttribute(attr_name + "_end", new THREE.InterleavedBufferAttribute(buffer, ndim, 2 * ndim)); + const buffer1 = new THREE.InstancedInterleavedBuffer(data, skip_elems, 1); + buffer1.count = Math.max(0, is_segments ? Math.floor(buffer1.count - 1) : buffer1.count - 3); + geometry.setAttribute(attr_name + "_start", new THREE.InterleavedBufferAttribute(buffer1, ndim, ndim)); + geometry.setAttribute(attr_name + "_end", new THREE.InterleavedBufferAttribute(buffer1, ndim, 2 * ndim)); if (is_position) { - geometry.setAttribute(attr_name + "_prev", new THREE.InterleavedBufferAttribute(buffer, ndim, 0)); - geometry.setAttribute(attr_name + "_next", new THREE.InterleavedBufferAttribute(buffer, ndim, 3 * ndim)); + geometry.setAttribute(attr_name + "_prev", new THREE.InterleavedBufferAttribute(buffer1, ndim, 0)); + geometry.setAttribute(attr_name + "_next", new THREE.InterleavedBufferAttribute(buffer1, ndim, 3 * ndim)); } - return buffer; + return buffer1; } function create_line_instance_geometry() { const geometry = new THREE.InstancedBufferGeometry(); @@ -22403,116 +22294,274 @@ function _create_line(scene, line_data, is_segments) { attach_updates(mesh, buffers, line_data.attributes, is_segments); return mesh; } -function create_line(scene, line_data) { - return _create_line(scene, line_data, false); -} function create_linesegments(scene, line_data) { return _create_line(scene, line_data, true); } -function deserialize_plot(scene, data) { - let mesh; - const update_visible = (v)=>{ - mesh.visible = v; +class Plot { + mesh = undefined; + parent = undefined; + uuid = ""; + name = ""; + is_instanced = false; + geometry_needs_recreation = false; + plot_data = {}; + constructor(scene, data){ + this.plot_data = data; + connect_plot(scene, this); + if (data.plot_type === "lines") { + this.mesh = create_line(scene, this.plot_data); + } else if (data.plot_type === "linesegments") { + this.mesh = create_linesegments(scene, this.plot_data); + } else if ("instance_attributes" in data) { + this.is_instanced = true; + this.mesh = create_instanced_mesh(scene, this.plot_data); + } else { + this.mesh = create_mesh(scene, this.plot_data); + } + this.name = data.name; + this.uuid = data.uuid; + this.mesh.plot_uuid = data.uuid; + this.mesh.frustumCulled = false; + this.mesh.matrixAutoUpdate = false; + this.mesh.renderOrder = data.zvalue; + data.uniform_updater.on(([name, data])=>{ + this.update_uniform(name, data); + }); + if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { + connect_attributes(this.mesh, data.attribute_updater); + } + this.parent = scene; + this.mesh.plot_object = this; + this.mesh.visible = data.visible.value; + data.visible.on((v)=>{ + this.mesh.visible = v; + }); + } + move_to(scene) { + if (scene === this.parent) { + return; + } + this.parent.remove(this.mesh); + connect_plot(scene, this); + scene.add(this.mesh); + this.parent = scene; return; - }; - if (data.plot_type === "lines") { - mesh = create_line(scene, data); - } else if (data.plot_type === "linesegments") { - mesh = create_linesegments(scene, data); - } else if ("instance_attributes" in data) { - mesh = create_instanced_mesh(scene, data); - } else { - mesh = create_mesh(scene, data); - } - mesh.name = data.name; - mesh.frustumCulled = false; - mesh.matrixAutoUpdate = false; - mesh.plot_uuid = data.uuid; - mesh.renderOrder = data.zvalue; - update_visible(data.visible.value); - data.visible.on(update_visible); - connect_uniforms(mesh, data.uniform_updater); - if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { - connect_attributes(mesh, data.attribute_updater); } - return mesh; + update(attributes) { + attributes.keys().forEach((key)=>{ + const value = attributes[key]; + if (value.type == "uniform") { + this.update_uniform(key, value.data); + } else if (value.type === "geometry") { + this.update_geometries(value.data); + } else if (value.type === "faces") { + this.update_faces(value.data); + } + }); + this.apply_updates(); + } + update_uniform(name, new_data) { + const uniform = this.mesh.material.uniforms[name]; + if (!uniform) { + throw new Error(`Uniform ${name} doesn't exist in Plot: ${this.name}`); + } + update_uniform(uniform, new_data); + } + update_geometry(name, new_data) { + buffer = this.mesh.geometry.attributes[name]; + if (!buffer) { + throw new Error(`Buffer ${name} doesn't exist in Plot: ${this.name}`); + } + const old_length = buffer.count; + if (new_data.length <= old_length) { + buffer.set(new_data.data); + buffer.needsUpdate = true; + } else { + buffer.to_update = new_data.data; + this.geometry_needs_recreation = true; + } + } + update_faces(face_data) { + this.mesh.geometry.setIndex(new mod.BufferAttribute(face_data, 1)); + } +} +const scene_cache = {}; +const plot_cache = {}; +const TEXTURE_ATLAS = [ + undefined +]; +function add_scene(scene_id, three_scene) { + scene_cache[scene_id] = three_scene; +} +function find_scene(scene_id) { + return scene_cache[scene_id]; +} +function delete_scene(scene_id) { + const scene = scene_cache[scene_id]; + if (!scene) { + return; + } + delete_three_scene(scene); + while(scene.children.length > 0){ + scene.remove(scene.children[0]); + } + delete scene_cache[scene_id]; +} +function find_plots(plot_uuids) { + const plots = []; + plot_uuids.forEach((id)=>{ + const plot = plot_cache[id]; + if (plot) { + plots.push(plot); + } + }); + return plots; +} +function delete_scenes(scene_uuids, plot_uuids) { + plot_uuids.forEach((plot_id)=>{ + const plot = plot_cache[plot_id]; + if (plot) { + delete_plot(plot); + } + }); + scene_uuids.forEach((scene_id)=>{ + delete_scene(scene_id); + }); +} +function insert_plot(scene_id, plot_data1) { + const scene = find_scene(scene_id); + plot_data1.forEach((plot)=>{ + add_plot(scene, plot); + }); +} +function delete_plots(plot_uuids) { + const plots = find_plots(plot_uuids); + plots.forEach(delete_plot); +} +function convert_texture(scene, data) { + const tex = create_texture(scene, data); + tex.needsUpdate = true; + tex.minFilter = mod[data.minFilter]; + tex.magFilter = mod[data.magFilter]; + tex.anisotropy = data.anisotropy; + tex.wrapS = mod[data.wrapS]; + if (data.size.length > 1) { + tex.wrapT = mod[data.wrapT]; + } + if (data.size.length > 2) { + tex.wrapR = mod[data.wrapR]; + } + return tex; +} +function is_three_fixed_array(value) { + return value instanceof mod.Vector2 || value instanceof mod.Vector3 || value instanceof mod.Vector4 || value instanceof mod.Matrix4; +} +function to_uniform(scene, data) { + if (data.type !== undefined) { + if (data.type == "Sampler") { + return convert_texture(scene, data); + } + throw new Error(`Type ${data.type} not known`); + } + if (Array.isArray(data) || ArrayBuffer.isView(data)) { + if (!data.every((x)=>typeof x === "number")) { + return data; + } + if (data.length == 2) { + return new mod.Vector2().fromArray(data); + } + if (data.length == 3) { + return new mod.Vector3().fromArray(data); + } + if (data.length == 4) { + return new mod.Vector4().fromArray(data); + } + if (data.length == 16) { + const mat = new mod.Matrix4(); + mat.fromArray(data); + return mat; + } + } + return data; +} +function deserialize_uniforms(scene, data) { + const result = {}; + for(const name in data){ + const value = data[name]; + if (value instanceof mod.Uniform) { + result[name] = value; + } else { + const ser = to_uniform(scene, value); + result[name] = new mod.Uniform(ser); + } + } + return result; +} +function create_line_material(scene, uniforms, attributes, is_linesegments) { + const uniforms_des = deserialize_uniforms(scene, uniforms); + const mat = new THREE.RawShaderMaterial({ + uniforms: uniforms_des, + glslVersion: THREE.GLSL3, + vertexShader: lines_vertex_shader(uniforms_des, attributes, is_linesegments), + fragmentShader: lines_fragment_shader(uniforms_des, attributes), + transparent: true, + blending: THREE.CustomBlending, + blendSrc: THREE.SrcAlphaFactor, + blendDst: THREE.OneMinusSrcAlphaFactor, + blendSrcAlpha: THREE.ZeroFactor, + blendDstAlpha: THREE.OneFactor, + blendEquation: THREE.AddEquation + }); + mat.uniforms.object_id = { + value: 1 + }; + return mat; } const ON_NEXT_INSERT = new Set(); function on_next_insert(f) { ON_NEXT_INSERT.add(f); } -function add_plot(scene, plot_data) { +function connect_plot(scene, plot) { const cam = scene.wgl_camera; const identity = new mod.Uniform(new mod.Matrix4()); - if (plot_data.cam_space == "data") { - plot_data.uniforms.view = cam.view; - plot_data.uniforms.projection = cam.projection; - plot_data.uniforms.projectionview = cam.projectionview; - plot_data.uniforms.eyeposition = cam.eyeposition; - } else if (plot_data.cam_space == "pixel") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.pixel_space; - plot_data.uniforms.projectionview = cam.pixel_space; - } else if (plot_data.cam_space == "relative") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.relative_space; - plot_data.uniforms.projectionview = cam.relative_space; + const uniforms = plot.mesh ? plot.mesh.material.uniforms : plot.plot_data.uniforms; + const space = plot.plot_data.cam_space; + if (space == "data") { + uniforms.view = cam.view; + uniforms.projection = cam.projection; + uniforms.projectionview = cam.projectionview; + uniforms.eyeposition = cam.eyeposition; + } else if (space == "pixel") { + uniforms.view = identity; + uniforms.projection = cam.pixel_space; + uniforms.projectionview = cam.pixel_space; + } else if (space == "relative") { + uniforms.view = identity; + uniforms.projection = cam.relative_space; + uniforms.projectionview = cam.relative_space; + } else if (space == "clip") { + uniforms.view = identity; + uniforms.projection = identity; + uniforms.projectionview = identity; } else { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = identity; - plot_data.uniforms.projectionview = identity; + throw new Error(`Space ${space} not supported!`); } const { px_per_unit } = scene.screen; - plot_data.uniforms.resolution = cam.resolution; - plot_data.uniforms.px_per_unit = new mod.Uniform(px_per_unit); - if (plot_data.uniforms.preprojection) { - const { space , markerspace } = plot_data; - plot_data.uniforms.preprojection = cam.preprojection_matrix(space.value, markerspace.value); - } - if (scene.camera_relative_light) { - plot_data.uniforms.light_direction = cam.light_direction; - scene.light_direction.on((value)=>{ - cam.update_light_dir(value); - }); - } else { - const light_dir = new mod.Vector3().fromArray(scene.light_direction.value); - plot_data.uniforms.light_direction = new mod.Uniform(light_dir); - scene.light_direction.on((value)=>{ - plot_data.uniforms.light_direction.value.fromArray(value); - }); - } - const p = deserialize_plot(scene, plot_data); - plot_cache[p.plot_uuid] = p; - scene.add(p); + uniforms.resolution = cam.resolution; + uniforms.px_per_unit = new mod.Uniform(px_per_unit); + if (plot.plot_data.uniforms.preprojection) { + const { space , markerspace } = plot.plot_data; + uniforms.preprojection = cam.preprojection_matrix(space.value, markerspace.value); + } + uniforms.light_direction = scene.light_direction; +} +function add_plot(scene, plot_data1) { + const p = new Plot(scene, plot_data1); + plot_cache[p.uuid] = p.mesh; + scene.add(p.mesh); const next_insert = new Set(ON_NEXT_INSERT); next_insert.forEach((f)=>f()); } -function connect_uniforms(mesh, updater) { - updater.on(([name, data])=>{ - if (name === "none") { - return; - } - const uniform = mesh.material.uniforms[name]; - if (uniform.value.isTexture) { - const im_data = uniform.value.image; - const [size, tex_data] = data; - if (tex_data.length == im_data.data.length) { - im_data.data.set(tex_data); - } else { - const old_texture = uniform.value; - uniform.value = re_create_texture(old_texture, tex_data, size); - old_texture.dispose(); - } - uniform.value.needsUpdate = true; - } else { - if (is_three_fixed_array(uniform.value)) { - uniform.value.fromArray(data); - } else { - uniform.value = data; - } - } - }); -} function convert_RGB_to_RGBA(rgbArray) { const length = rgbArray.length; const rgbaArray = new rgbArray.constructor(length / 3 * 4); @@ -22525,24 +22574,24 @@ function convert_RGB_to_RGBA(rgbArray) { return rgbaArray; } function create_texture_from_data(data) { - let buffer = data.data; + let buffer1 = data.data; if (data.size.length == 3) { - const tex = new mod.Data3DTexture(buffer, data.size[0], data.size[1], data.size[2]); + const tex = new mod.Data3DTexture(buffer1, data.size[0], data.size[1], data.size[2]); tex.format = mod[data.three_format]; tex.type = mod[data.three_type]; return tex; } else { let format = mod[data.three_format]; if (data.three_format == "RGBFormat") { - buffer = convert_RGB_to_RGBA(buffer); + buffer1 = convert_RGB_to_RGBA(buffer1); format = mod.RGBAFormat; } - return new mod.DataTexture(buffer, data.size[0], data.size[1], format, mod[data.three_type]); + return new mod.DataTexture(buffer1, data.size[0], data.size[1], format, mod[data.three_type]); } } function create_texture(scene, data) { - const buffer = data.data; - if (buffer == "texture_atlas") { + const buffer1 = data.data; + if (buffer1 == "texture_atlas") { const { texture_atlas } = scene.screen; if (texture_atlas) { return texture_atlas; @@ -22565,14 +22614,14 @@ function create_texture(scene, data) { return create_texture_from_data(data); } } -function re_create_texture(old_texture, buffer, size) { +function re_create_texture(old_texture, buffer1, size) { let tex; if (size.length == 3) { - tex = new mod.Data3DTexture(buffer, size[0], size[1], size[2]); + tex = new mod.Data3DTexture(buffer1, size[0], size[1], size[2]); tex.format = old_texture.format; tex.type = old_texture.type; } else { - tex = new mod.DataTexture(buffer, size[0], size[1] ? size[1] : 1, old_texture.format, old_texture.type); + tex = new mod.DataTexture(buffer1, size[0], size[1] ? size[1] : 1, old_texture.format, old_texture.type); } tex.minFilter = old_texture.minFilter; tex.magFilter = old_texture.magFilter; @@ -22586,26 +22635,26 @@ function re_create_texture(old_texture, buffer, size) { } return tex; } -function BufferAttribute(buffer) { - const jsbuff = new mod.BufferAttribute(buffer.flat, buffer.type_length); +function BufferAttribute(buffer1) { + const jsbuff = new mod.BufferAttribute(buffer1.flat, buffer1.type_length); jsbuff.setUsage(mod.DynamicDrawUsage); return jsbuff; } -function InstanceBufferAttribute(buffer) { - const jsbuff = new mod.InstancedBufferAttribute(buffer.flat, buffer.type_length); +function InstanceBufferAttribute(buffer1) { + const jsbuff = new mod.InstancedBufferAttribute(buffer1.flat, buffer1.type_length); jsbuff.setUsage(mod.DynamicDrawUsage); return jsbuff; } function attach_geometry(buffer_geometry, vertexarrays, faces) { for(const name in vertexarrays){ const buff = vertexarrays[name]; - let buffer; + let buffer1; if (buff.to_update) { - buffer = new mod.BufferAttribute(buff.to_update, buff.itemSize); + buffer1 = new mod.BufferAttribute(buff.to_update, buff.itemSize); } else { - buffer = BufferAttribute(buff); + buffer1 = BufferAttribute(buff); } - buffer_geometry.setAttribute(name, buffer); + buffer_geometry.setAttribute(name, buffer1); } buffer_geometry.setIndex(faces); buffer_geometry.boundingSphere = new mod.Sphere(); @@ -22615,8 +22664,8 @@ function attach_geometry(buffer_geometry, vertexarrays, faces) { } function attach_instanced_geometry(buffer_geometry, instance_attributes) { for(const name in instance_attributes){ - const buffer = InstanceBufferAttribute(instance_attributes[name]); - buffer_geometry.setAttribute(name, buffer); + const buffer1 = InstanceBufferAttribute(instance_attributes[name]); + buffer_geometry.setAttribute(name, buffer1); } } function recreate_geometry(mesh, vertexarrays, faces) { @@ -22634,17 +22683,17 @@ function recreate_instanced_geometry(mesh) { ...mesh.geometry.index.array ]; Object.keys(mesh.geometry.attributes).forEach((name)=>{ - const buffer = mesh.geometry.attributes[name]; - const copy = buffer.to_update ? buffer.to_update : buffer.array.map((x)=>x); - if (buffer.isInstancedBufferAttribute) { + const buffer1 = mesh.geometry.attributes[name]; + const copy = buffer1.to_update ? buffer1.to_update : buffer1.array.map((x)=>x); + if (buffer1.isInstancedBufferAttribute) { instance_attributes[name] = { flat: copy, - type_length: buffer.itemSize + type_length: buffer1.itemSize }; } else { vertexarrays[name] = { flat: copy, - type_length: buffer.itemSize + type_length: buffer1.itemSize }; } }); @@ -22707,11 +22756,11 @@ function connect_attributes(mesh, updater) { function re_assign_buffers() { const attributes = mesh.geometry.attributes; Object.keys(attributes).forEach((name)=>{ - const buffer = attributes[name]; - if (buffer.isInstancedBufferAttribute) { - instance_buffers[name] = buffer; + const buffer1 = attributes[name]; + if (buffer1.isInstancedBufferAttribute) { + instance_buffers[name] = buffer1; } else { - geometry_buffers[name] = buffer; + geometry_buffers[name] = buffer1; } }); first_instance_buffer = first(instance_buffers); @@ -22723,7 +22772,7 @@ function connect_attributes(mesh, updater) { } re_assign_buffers(); updater.on(([name, new_values, length])=>{ - const buffer = mesh.geometry.attributes[name]; + const buffer1 = mesh.geometry.attributes[name]; let buffers; let real_length; let is_instance = false; @@ -22738,19 +22787,19 @@ function connect_attributes(mesh, updater) { real_length = real_geometry_length; } if (length <= real_length[0]) { - buffer.set(new_values); - buffer.needsUpdate = true; + buffer1.set(new_values); + buffer1.needsUpdate = true; if (is_instance) { mesh.geometry.instanceCount = length; } } else { - buffer.to_update = new_values; + buffer1.to_update = new_values; const all_have_same_length = Object.values(buffers).every((x)=>x.to_update && x.to_update.length / x.itemSize == length); if (all_have_same_length) { if (is_instance) { recreate_instanced_geometry(mesh); re_assign_buffers(); - mesh.geometry.instanceCount = new_values.length / buffer.itemSize; + mesh.geometry.instanceCount = new_values.length / buffer1.itemSize; } else { recreate_geometry(mesh, buffers, mesh.geometry.index); re_assign_buffers(); @@ -22771,8 +22820,6 @@ function deserialize_scene(data, screen) { scene.backgroundcolor_alpha = data.backgroundcolor_alpha; scene.clearscene = data.clearscene; scene.visible = data.visible; - scene.camera_relative_light = data.camera_relative_light; - scene.light_direction = data.light_direction; const camera = new MakieCamera(); scene.wgl_camera = camera; function update_cam(camera_matrices, force) { @@ -22790,8 +22837,17 @@ function deserialize_scene(data, screen) { update_cam(data.camera.value, true); camera.update_light_dir(data.light_direction.value); data.camera.on(update_cam); - data.plots.forEach((plot_data)=>{ - add_plot(scene, plot_data); + if (data.camera_relative_light) { + scene.light_direction = camera.light_direction; + } else { + const light_dir = new mod.Vector3().fromArray(data.light_direction.value); + scene.light_direction = new mod.Uniform(light_dir); + data.light_direction.on((value)=>{ + plot_data.uniforms.light_direction.value.fromArray(value); + }); + } + data.plots.forEach((plot_data1)=>{ + add_plot(scene, plot_data1); }); scene.scene_children = data.children.map((child)=>{ const childscene = deserialize_scene(child, screen); @@ -23278,8 +23334,8 @@ function pick_closest(scene, xy, range) { const y1 = Math.min(canvas.height, Math.ceil(px_per_unit * (xy[1] + range))); const dx = x1 - x0; const dy = y1 - y0; - const [plot_data, _] = pick_native(scene, x0, y0, dx, dy, false); - const plot_matrix = plot_data.data; + const [plot_data1, _] = pick_native(scene, x0, y0, dx, dy, false); + const plot_matrix = plot_data1.data; let min_dist = px_per_unit * px_per_unit * range * range; let selection = [ null, @@ -23319,11 +23375,11 @@ function pick_sorted(scene, xy, range) { const y1 = Math.min(canvas.height, Math.ceil(px_per_unit * (xy[1] + range))); const dx = x1 - x0; const dy = y1 - y0; - const [plot_data, selected] = pick_native(scene, x0, y0, dx, dy, false); + const [plot_data1, selected] = pick_native(scene, x0, y0, dx, dy, false); if (selected.length == 0) { return null; } - const plot_matrix = plot_data.data; + const plot_matrix = plot_data1.data; const distances = selected.map((x)=>1e30); const x = xy[0] * px_per_unit + 1 - x0; const y = xy[1] * px_per_unit + 1 - y0; diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index a54d8ebce69..f97f4463979 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -22,7 +22,7 @@ excludes = Set([ "Image on Surface Sphere", # TODO: texture rotated 180° # "heatmaps & surface", # TODO: fix direct NaN -> nancolor conversion "Array of Images Scatter", # scatter does not support texture images - + "Order Independent Transparency", "fast pixel marker", "Textured meshscatter", # not yet implemented @@ -52,6 +52,8 @@ end session = edisplay.browserdisplay.handler.session session_size = Base.summarysize(session) / 10^6 texture_atlas_size = Base.summarysize(WGLMakie.TEXTURE_ATLAS) / 10^6 + @show typeof.(last.(WGLMakie.TEXTURE_ATLAS.listeners)) + @show length(WGLMakie.TEXTURE_ATLAS.listeners) @show session_size texture_atlas_size @test session_size / 10^6 < 6 @test texture_atlas_size < 6 @@ -91,7 +93,7 @@ end rm(filename) end - + f, a, p = scatter(rand(10)); filename = "$(tempname()).mp4" try @@ -124,4 +126,4 @@ end @test events(f).tick[] == tick # TODO: test normal rendering -end \ No newline at end of file +end diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index f5e166ef393..8e3baed0fd6 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -184,23 +184,20 @@ mutable struct DataInspector root::Scene attributes::Attributes - temp_plots::Vector{AbstractPlot} + temp_plots::Vector{Plot} plot::Tooltip - selection::AbstractPlot + selection::Plot obsfuncs::Vector{Any} - lock::Threads.ReentrantLock end function DataInspector(scene::Scene, plot::AbstractPlot, attributes) - x = DataInspector(scene, attributes, AbstractPlot[], plot, plot, Any[], Threads.ReentrantLock()) - # finalizer(cleanup, x) # doesn't get triggered when this is dereferenced - x + return DataInspector(scene, attributes, Plot[], plot, plot, Any[]) end function cleanup(inspector::DataInspector) - off.(inspector.obsfuncs) + foreach(off, inspector.obsfuncs) empty!(inspector.obsfuncs) delete!(inspector.root, inspector.plot) clear_temporary_plots!(inspector, inspector.selection) @@ -281,9 +278,17 @@ function DataInspector(scene::Scene; priority = 100, kwargs...) # We delegate the hover processing to another channel, # So that we can skip queued up updates with empty_channel! # And also not slow down the processing of e.mouseposition/e.scroll + was_open = false channel = Channel{Nothing}(Inf) do ch for _ in ch - on_hover(inspector) + if isopen(scene) + was_open = true + on_hover(inspector) + end + if !isopen(scene) && was_open + close(ch) + break + end end end listeners = onany(e.mouseposition, e.scroll) do _, _ @@ -306,33 +311,31 @@ DataInspector(; kwargs...) = DataInspector(current_figure(); kwargs...) function on_hover(inspector) parent = inspector.root - lock(inspector.lock) do - (inspector.attributes.enabled[] && is_mouseinside(parent)) || return Consume(false) - - mp = mouseposition_px(parent) - should_clear = true - for (plt, idx) in pick_sorted(parent, mp, inspector.attributes.range[]) - if to_value(get(plt.attributes, :inspectable, true)) - # show_data should return true if it created a tooltip - if show_data_recursion(inspector, plt, idx) - should_clear = false - break - end + (inspector.attributes.enabled[] && is_mouseinside(parent)) || return Consume(false) + + mp = mouseposition_px(parent) + should_clear = true + for (plt, idx) in pick_sorted(parent, mp, inspector.attributes.range[]) + if to_value(get(plt.attributes, :inspectable, true)) + # show_data should return true if it created a tooltip + if show_data_recursion(inspector, plt, idx) + should_clear = false + break end end + end - if should_clear - plot = inspector.selection - if to_value(get(plot, :inspector_clear, automatic)) !== automatic - plot[:inspector_clear][](inspector, plot) - end - inspector.plot.visible[] = false - inspector.attributes.indicator_visible[] = false - inspector.plot.offset.val = inspector.attributes.offset[] + if should_clear + plot = inspector.selection + if to_value(get(plot, :inspector_clear, automatic)) !== automatic + plot[:inspector_clear][](inspector, plot) end - - return Consume(false) + inspector.plot.visible[] = false + inspector.attributes.indicator_visible[] = false + inspector.plot.offset.val = inspector.attributes.offset[] end + + return Consume(false) end diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index d1ee9652edf..1bb59e62a8e 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -15,11 +15,13 @@ end function Observables.connect!(parent::Transformation, child::Transformation; connect_func=true) tfuncs = [] + # Observables.clear(child.parent_model) obsfunc = on(parent.model; update=true) do m return child.parent_model[] = m end push!(tfuncs, obsfunc) if connect_func + # Observables.clear(child.transform_func) t2 = on(parent.transform_func; update=true) do f child.transform_func[] = f return diff --git a/src/scenes.jl b/src/scenes.jl index 5903e5a9e7c..fd1f12d16de 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -498,15 +498,13 @@ function free(plot::AbstractPlot) Observables.off(f) end foreach(free, plot.plots) - empty!(plot.plots) + # empty!(plot.plots) empty!(plot.deregister_callbacks) - empty!(plot.attributes) free(plot.transformation) return end function Base.delete!(scene::Scene, plot::AbstractPlot) - len = length(scene.plots) filter!(x -> x !== plot, scene.plots) # TODO, if we want to delete a subplot of a plot, # It won't be in scene.plots directly, but will still be deleted @@ -523,6 +521,46 @@ function Base.delete!(scene::Scene, plot::AbstractPlot) free(plot) end +supports_move_to(::MakieScreen) = false + +function supports_move_to(plot::Plot) + scene = get_scene(plot) + return all(scene.current_screens) do screen + return supports_move_to(screen) + end +end + +# function move_to!(screen::MakieScreen, plot::Plot, scene::Scene) +# # TODO, move without deleting! +# # Will be easier with Observable refactor +# delete!(screen, scene, plot) +# insert!(screen, scene, plot) +# return +# end + + +function move_to!(plot::Plot, scene::Scene) + if plot.parent === scene + return + end + + if is_space_compatible(plot, scene) + obsfunc = connect!(transformation(scene), transformation(plot)) + append!(plot.deregister_callbacks, obsfunc) + end + for screen in root(scene).current_screens + if supports_move_to(screen) + move_to!(screen, plot, scene) + end + end + current_parent = parent_scene(plot) + filter!(x -> x !== plot, current_parent.plots) + push!(scene.plots, plot) + plot.parent = scene + return +end + + events(x) = events(get_scene(x)) events(scene::Scene) = scene.events events(scene::SceneLike) = events(scene.parent) diff --git a/src/specapi.jl b/src/specapi.jl index 8f4bf7dd11e..5d03e321363 100644 --- a/src/specapi.jl +++ b/src/specapi.jl @@ -326,12 +326,13 @@ function distance_score(at::Tuple{Int,GP,GridLayoutSpec}, bt::Tuple{Int,GP,GridL end end -function find_min_distance(f, to_compare, list, scores) +function find_min_distance(f, to_compare, list, scores, penalty=(key, score)-> score) isempty(list) && return -1 minscore = 2.0 idx = -1 for key in keys(list) score = distance_score(to_compare, f(list[key], key), scores) + score = penalty(key, score) # apply custom penalty if score ≈ 0.0 # shortcuircit for exact matches return key end @@ -353,8 +354,15 @@ function find_layoutable( return (idx, layoutables[idx]...) end -function find_reusable_plot(plotspec::PlotSpec, plots::IdDict{PlotSpec,Plot}, scores) - idx = find_min_distance((_, spec) -> spec, plotspec, plots, scores) +function find_reusable_plot(scene::Scene, plotspec::PlotSpec, plots::IdDict{PlotSpec,Plot}, scores) + function penalty(key, score) + # penalize plots with different parents + # needs to be implemented via this penalty function, since parent scenes arent part of the spec + plot = plots[key] + move_to_penalty = ((!Makie.supports_move_to(plot)) * 100) + 1 + return norm(Float64[plot.parent !== scene, score]) * move_to_penalty + end + idx = find_min_distance((_, spec) -> spec, plotspec, plots, scores, penalty) idx == -1 && return nothing, nothing return plots[idx], idx end @@ -564,18 +572,21 @@ function push_without_add!(scene::Scene, plot) end end -function diff_plotlist!(scene::Scene, plotspecs::Vector{PlotSpec}, obs_to_notify, reusable_plots, - plotlist::Union{Nothing,PlotList}=nothing) - new_plots = IdDict{PlotSpec,Plot}() # needed to be mutated +function diff_plotlist!( + scene::Scene, plotspecs::Vector{PlotSpec}, + obs_to_notify, + plotlist::Union{Nothing,PlotList}=nothing, + reusable_plots = IdDict{PlotSpec, Plot}(), + new_plots = IdDict{PlotSpec,Plot}()) + # needed to be mutated empty!(scene.cycler.counters) # Global list of observables that need updating # Updating them all at once in the end avoids problems with triggering updates while updating # And at some point we may be able to optimize notify(list_of_observables) - empty!(obs_to_notify) scores = IdDict{Any, Float64}() 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, old_spec = find_reusable_plot(plotspec, reusable_plots, scores) + reused_plot, old_spec = find_reusable_plot(scene, plotspec, reusable_plots, scores) if isnothing(reused_plot) # Create new plot, store it into our `cached_plots` dictionary @debug("Creating new plot for spec") @@ -601,45 +612,57 @@ function diff_plotlist!(scene::Scene, plotspecs::Vector{PlotSpec}, obs_to_notify @debug("updating old plot with spec") # Delete the plots from reusable_plots, so that we don't re-use it multiple times! delete!(reusable_plots, old_spec) + if reused_plot.parent !== scene + @assert Makie.supports_move_to(reused_plot) + move_to!(reused_plot, scene) + end update_plot!(obs_to_notify, reused_plot, old_spec, plotspec) new_plots[plotspec] = reused_plot + end end return new_plots end -function update_plotspecs!(scene::Scene, list_of_plotspecs::Observable, plotlist::Union{Nothing, PlotList}=nothing) +function update_plotspecs!( + scene::Scene, list_of_plotspecs::Observable, + plotlist::Union{Nothing,PlotList}=nothing, + unused_plots=IdDict{PlotSpec,Plot}(), + new_plots=IdDict{PlotSpec,Plot}(), + own_plots=true + ) # 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. - unused_plots = IdDict{PlotSpec,Plot}() obs_to_notify = Observable[] - update_plotlist(spec::PlotSpec) = update_plotlist([spec]) function update_plotlist(plotspecs) # Global list of observables that need updating # Updating them all at once in the end avoids problems with triggering updates while updating # And at some point we may be able to optimize notify(list_of_observables) - empty!(obs_to_notify) empty!(scene.cycler.counters) # Reset Cycler # diff_plotlist! deletes all plots that get re-used from unused_plots # so, this will become our list of unused plots! - new_plots = diff_plotlist!(scene, plotspecs, obs_to_notify, unused_plots, plotlist) + diff_plotlist!(scene, plotspecs, obs_to_notify, plotlist, unused_plots, new_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) + if own_plots + for (_, plot) in unused_plots + if !isnothing(plotlist) + filter!(x -> x !== plot, plotlist.plots) + end + delete!(scene, plot) end - delete!(scene, plot) + # Transfer all new plots into unused_plots for the next update! + @assert !any(x-> x in unused_plots, new_plots) + empty!(unused_plots) + merge!(unused_plots, new_plots) + empty!(new_plots) + # finally, notify all changes at once end - # Transfer all new plots into unused_plots for the next update! - @assert !any(x-> x in unused_plots, new_plots) - empty!(unused_plots) - merge!(unused_plots, new_plots) - # finally, notify all changes at once foreach(notify, obs_to_notify) + empty!(obs_to_notify) return end l = Base.ReentrantLock() @@ -786,9 +809,7 @@ function update_layoutable!(block::T, plot_obs, old_spec::BlockSpec, spec::Block empty!(block.scene.cycler.counters) end if T <: AbstractAxis - if plot_obs[] != spec.plots - plot_obs[] = spec.plots - end + plot_obs[] = spec.plots scene = get_scene(block) if any(needs_tight_limits, scene.plots) tightlimits!(block) @@ -853,7 +874,7 @@ end function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::Union{Nothing, GridLayoutSpec}, - gridspec::GridLayoutSpec, previous_contents, new_layoutables) + gridspec::GridLayoutSpec, previous_contents, new_layoutables, global_unused_plots, new_plots) update_layoutable!(gridlayout, nothing, oldgridspec, gridspec) scores = IdDict{Any, Float64}() @@ -869,7 +890,7 @@ function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::U if new_layoutable isa AbstractAxis obs = Observable(spec.plots) scene = get_scene(new_layoutable) - update_plotspecs!(scene, obs) + update_plotspecs!(scene, obs, nothing, global_unused_plots, new_plots, false) if any(needs_tight_limits, scene.plots) tightlimits!(new_layoutable) end @@ -877,7 +898,7 @@ function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::U elseif new_layoutable isa GridLayout # Make sure all plots & blocks are inserted update_gridlayout!(new_layoutable, nesting + 1, spec, spec, previous_contents, - new_layoutables) + new_layoutables, global_unused_plots, new_plots) end push!(new_layoutables, (nesting, position, spec) => (new_layoutable, obs)) else @@ -888,7 +909,8 @@ function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::U (layoutable, plot_obs) = layoutable_obs gridlayout[position...] = layoutable if layoutable isa GridLayout - update_gridlayout!(layoutable, nesting + 1, old_spec, spec, previous_contents, new_layoutables) + update_gridlayout!(layoutable, nesting + 1, old_spec, spec, previous_contents, + new_layoutables, global_unused_plots, new_plots) else update_layoutable!(layoutable, plot_obs, old_spec, spec) update_state_before_display!(layoutable) @@ -913,13 +935,16 @@ function delete_layoutable!(grid::GridLayout) end function update_gridlayout!(target_layout::GridLayout, layout_spec::GridLayoutSpec, unused_layoutables, - new_layoutables) + new_layoutables, unused_plots, new_plots) # For each update we look into `unused_layoutables` to see if we can re-use a layoutable (GridLayout/Block). # Every re-used layoutable and every newly created gets pushed into `new_layoutables`, # while it gets removed from `unused_layoutables`. empty!(new_layoutables) + update_gridlayout!( + target_layout, 1, nothing, layout_spec, unused_layoutables, + new_layoutables, unused_plots, new_plots + ) - update_gridlayout!(target_layout, 1, nothing, layout_spec, unused_layoutables, new_layoutables) foreach(unused_layoutables) do (p, (block, obs)) # disconnect! all unused layoutables, so they dont show up anymore if block isa Block @@ -941,6 +966,16 @@ function update_gridlayout!(target_layout::GridLayout, layout_spec::GridLayoutSp GridLayoutBase.update!(l) end + for (_, plot) in unused_plots + delete!(plot.parent, plot) + end + # Transfer all new plots into unused_plots for the next update! + @assert isempty(unused_plots) || !any(x -> x in unused_plots, new_plots) + empty!(unused_plots) + merge!(unused_plots, new_plots) + empty!(new_plots) + # finally, notify all changes at once + # foreach(unused_layoutables) do (p, (block, obs)) # # Finally, disconnect all blocks that haven't been used! # disconnect!(block) @@ -962,9 +997,12 @@ function update_fig!(fig::Union{Figure,GridPosition,GridSubposition}, layout_obs sizehint!(new_layoutables, 50) l = Base.ReentrantLock() layout = get_layout!(fig) + unused_plots = IdDict{PlotSpec,Plot}() + new_plots = IdDict{PlotSpec,Plot}() on(get_topscene(fig), layout_obs; update=true) do layout_spec lock(l) do - update_gridlayout!(layout, layout_spec, unused_layoutables, new_layoutables) + update_gridlayout!(layout, layout_spec, unused_layoutables, new_layoutables, + unused_plots, new_plots) return end end diff --git a/test/specapi.jl b/test/specapi.jl index cea645640aa..9c0a6f23ac4 100644 --- a/test/specapi.jl +++ b/test/specapi.jl @@ -50,19 +50,19 @@ import Makie.SpecApi as S plotspecs = [S.Scatter(1:4; color=:red), S.Scatter(1:4; color=:red)] reusable_plots = IdDict{PlotSpec,Plot}() obs_to_notify = Observable[] - new_plots = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, reusable_plots) + new_plots = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, nothing, reusable_plots) @test length(new_plots) == 2 @test Set(scene.plots) == Set(values(new_plots)) @test isempty(obs_to_notify) - new_plots2 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, new_plots) + new_plots2 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, nothing, new_plots) @test isempty(new_plots) # they got all used up @test Set(scene.plots) == Set(values(new_plots2)) @test isempty(obs_to_notify) plotspecs = [S.Scatter(1:4; color=:yellow), S.Scatter(1:4; color=:green)] - new_plots3 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, new_plots2) + new_plots3 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, nothing, new_plots2) @test isempty(new_plots) # they got all used up @test Set(scene.plots) == Set(values(new_plots3))