diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index c988a3e785b..f94f3192e3f 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,18 +1,45 @@ name: CompatHelper on: schedule: - - cron: '0 0 * * *' # Everyday at midnight + - cron: 0 0 * * * workflow_dispatch: +permissions: + contents: write + pull-requests: write jobs: CompatHelper: runs-on: ubuntu-latest - permissions: - contents: write steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' - - name: CompatHelper.main() + - name: Check if Julia is already available in the PATH + id: julia_in_path + run: which julia + continue-on-error: true + - name: Install Julia, but only if it is not already available in the PATH + uses: julia-actions/setup-julia@v1 + with: + version: '1' + arch: ${{ runner.arch }} + if: steps.julia_in_path.outcome != 'success' + - name: "Add the General registry via Git" + run: | + import Pkg + ENV["JULIA_PKG_SERVER"] = "" + Pkg.Registry.add("General") + shell: julia --color=yes {0} + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "3" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} + - name: "Run CompatHelper" + run: | + import CompatHelper + CompatHelper.main(subdirs=["", "MakieCore", "GLMakie", "WGLMakie", "CairoMakie", "RPRMakie"]) + shell: julia --color=yes {0} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - run: julia -e 'using CompatHelper; CompatHelper.main()' + # COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index ab6c5a83942..39237841bea 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -7,6 +7,9 @@ on: branches: - master - sd/beta-20 +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: benchmark: name: ${{ matrix.package }} @@ -26,6 +29,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/.github/workflows/wglmakie.yaml b/.github/workflows/wglmakie.yaml index ad31bdfd0c9..6265a58419c 100644 --- a/.github/workflows/wglmakie.yaml +++ b/.github/workflows/wglmakie.yaml @@ -46,7 +46,6 @@ jobs: using Pkg; # dev mono repo versions pkg"dev . ./MakieCore ./WGLMakie ./ReferenceTests" - pkg"add JSServe#sd/fixes" - name: Run the tests continue-on-error: true run: > diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index 0a312e09419..89e4baa9016 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -13,8 +13,8 @@ FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" [compat] Cairo = "1.0.4" @@ -25,7 +25,10 @@ FreeType = "3, 4.0" GeometryBasics = "0.4.1" Makie = "=0.20.0" PrecompileTools = "1.0" +SHA = "0.7, 1.6, 1.7" julia = "1.3" +Base64 = "1.0, 1.6" +LinearAlgebra = "1.0, 1.6" [extras] Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 6a036a84727..06090653102 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -87,11 +87,9 @@ end function prepare_for_scene(screen::Screen, scene::Scene) - # get the root area to correct for its pixel size when translating - root_area = Makie.root(scene).px_area[] - - root_area_height = widths(root_area)[2] - scene_area = pixelarea(scene)[] + # get the root area to correct for its size when translating + root_area_height = widths(Makie.root(scene))[2] + scene_area = viewport(scene)[] scene_height = widths(scene_area)[2] scene_x_origin, scene_y_origin = scene_area.origin @@ -103,7 +101,7 @@ function prepare_for_scene(screen::Screen, scene::Scene) top_offset = root_area_height - scene_height - scene_y_origin Cairo.translate(screen.context, scene_x_origin, top_offset) - # clip the scene to its pixelarea + # clip the scene to its viewport Cairo.rectangle(screen.context, 0, 0, widths(scene_area)...) Cairo.clip(screen.context) @@ -116,7 +114,7 @@ function draw_background(screen::Screen, scene::Scene) if scene.clear[] bg = scene.backgroundcolor[] Cairo.set_source_rgba(cr, red(bg), green(bg), blue(bg), alpha(bg)); - r = pixelarea(scene)[] + r = viewport(scene)[] Cairo.rectangle(cr, origin(r)..., widths(r)...) # background fill(cr) end @@ -148,8 +146,8 @@ end function draw_plot_as_image(scene::Scene, screen::Screen, primitive::Combined, scale::Number = 1) # you can provide `p.rasterize = scale::Int` or `p.rasterize = true`, both of which are numbers - # Extract scene width in pixels - w, h = Int.(scene.px_area[].widths) + # Extract scene width in device indepentent units + w, h = size(scene) # Create a new Screen which renders directly to an image surface, # specifically for the plot's parent scene. scr = Screen(scene; px_per_unit = scale) @@ -178,3 +176,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/overrides.jl b/CairoMakie/src/overrides.jl index 9ccde4be16e..8bccdca5d3c 100644 --- a/CairoMakie/src/overrides.jl +++ b/CairoMakie/src/overrides.jl @@ -73,25 +73,26 @@ function draw_poly(scene::Scene, screen::Screen, poly, points_list::Vector{<:Vec end draw_poly(scene::Scene, screen::Screen, poly, rect::Rect2) = draw_poly(scene, screen, poly, [rect]) +draw_poly(scene::Scene, screen::Screen, poly, bezierpath::BezierPath) = draw_poly(scene, screen, poly, [bezierpath]) -function draw_poly(scene::Scene, screen::Screen, poly, rects::Vector{<:Rect2}) +function draw_poly(scene::Scene, screen::Screen, poly, shapes::Vector{<:Union{Rect2,BezierPath}}) model = poly.model[] space = to_value(get(poly, :space, :data)) - projected_rects = project_rect.(Ref(scene), space, rects, Ref(model)) + projected_shapes = project_shape.(Ref(scene), space, shapes, Ref(model)) color = to_cairo_color(poly.color[], poly) linestyle = Makie.convert_attribute(poly.linestyle[], key"linestyle"()) if isnothing(linestyle) linestyle_diffed = nothing - elseif linestyle isa AbstractVector{Float64} + elseif linestyle isa AbstractVector{<:Real} linestyle_diffed = diff(Float64.(linestyle)) else error("Wrong type for linestyle: $(poly.linestyle[]).") end strokecolor = to_cairo_color(poly.strokecolor[], poly) - broadcast_foreach(projected_rects, color, strokecolor, poly.strokewidth[]) do r, c, sc, sw - Cairo.rectangle(screen.context, origin(r)..., widths(r)...) + broadcast_foreach(projected_shapes, color, strokecolor, poly.strokewidth[]) do shape, c, sc, sw + create_shape_path!(screen.context, shape) set_source(screen.context, c) Cairo.fill_preserve(screen.context) isnothing(linestyle_diffed) || Cairo.set_dash(screen.context, linestyle_diffed .* sw) @@ -101,6 +102,31 @@ function draw_poly(scene::Scene, screen::Screen, poly, rects::Vector{<:Rect2}) end end +function project_shape(scene, space, shape::BezierPath, model) + commands = Makie.PathCommand[] + for cmd in shape.commands + if cmd isa EllipticalArc + bezier = Makie.elliptical_arc_to_beziers(cmd) + for b in bezier.commands + push!(commands, project_command(b, scene, space, model)) + end + else + push!(commands, project_command(cmd, scene, space, model)) + end + end + BezierPath(commands) +end + +function create_shape_path!(ctx, r::Rect2) + Cairo.rectangle(ctx, origin(r)..., widths(r)...) +end + +function create_shape_path!(ctx, b::BezierPath) + for cmd in b.commands + path_command(ctx, cmd) + end +end + function polypath(ctx, polygon) isempty(polygon) && return nothing ext = decompose(Point2f, polygon.exterior) diff --git a/CairoMakie/src/precompiles.jl b/CairoMakie/src/precompiles.jl index b2c6ab3155d..a654e938544 100644 --- a/CairoMakie/src/precompiles.jl +++ b/CairoMakie/src/precompiles.jl @@ -15,3 +15,9 @@ let include(shared_precompile) end end +precompile(draw_atomic_scatter, (Scene, Cairo.CairoContext, Tuple{typeof(identity),typeof(identity)}, + Vector{ColorTypes.RGBA{Float32}}, Vec{2,Float32}, ColorTypes.RGBA{Float32}, + Float32, BezierPath, Vec{2,Float32}, Quaternionf, + Mat4f, Vector{Point{2,Float32}}, + Mat4f, Makie.FreeTypeAbstraction.FTFont, Symbol, + Symbol)) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 13fb39e2727..3d920610c44 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -496,13 +496,14 @@ end function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Text{<:Tuple{<:Union{AbstractArray{<:Makie.GlyphCollection}, Makie.GlyphCollection}}})) ctx = screen.context @get_attribute(primitive, (rotation, model, space, markerspace, offset)) + transform_marker = to_value(get(primitive, :transform_marker, true))::Bool position = primitive.position[] # use cached glyph info glyph_collection = to_value(primitive[1]) draw_glyph_collection( scene, ctx, position, glyph_collection, remove_billboard(rotation), - model, space, markerspace, offset, primitive.transformation + model, space, markerspace, offset, primitive.transformation, transform_marker ) nothing @@ -511,21 +512,23 @@ end function draw_glyph_collection( scene, ctx, positions, glyph_collections::AbstractArray, rotation, - model::Mat, space, markerspace, offset, transformation + model::Mat, space, markerspace, offset, transformation, transform_marker ) # TODO: why is the Ref around model necessary? doesn't broadcast_foreach handle staticarrays matrices? broadcast_foreach(positions, glyph_collections, rotation, Ref(model), space, markerspace, offset) do pos, glayout, ro, mo, sp, msp, off - draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation) + draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation, transform_marker) end end _deref(x) = x _deref(x::Ref) = x[] -function draw_glyph_collection(scene, ctx, position, glyph_collection, rotation, _model, space, markerspace, offsets, transformation) +function draw_glyph_collection( + scene, ctx, position, glyph_collection, rotation, _model, space, + markerspace, offsets, transformation, transform_marker) glyphs = glyph_collection.glyphs glyphoffsets = glyph_collection.origins @@ -537,7 +540,7 @@ function draw_glyph_collection(scene, ctx, position, glyph_collection, rotation, strokecolors = glyph_collection.strokecolors model = _deref(_model) - model33 = model[Vec(1, 2, 3), Vec(1, 2, 3)] + model33 = transform_marker ? model[Vec(1, 2, 3), Vec(1, 2, 3)] : Mat3f(I) id = Mat4f(I) glyph_pos = let @@ -859,7 +862,7 @@ end nan2zero(x) = !isnan(x) * x -function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0) +function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0, rotation = Mat4f(I)) @get_attribute(attributes, (shading, diffuse, specular, shininess, faceculling)) matcap = to_value(get(attributes, :matcap, nothing)) @@ -876,44 +879,61 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f model = attributes.model[]::Mat4f space = to_value(get(attributes, :space, :data))::Symbol func = Makie.transform_func(attributes) + + # TODO: assume Symbol here after this has been deprecated for a while + if shading isa Bool + @warn "`shading::Bool` is deprecated. Use `shading = NoShading` instead of false and `shading = FastShading` or `shading = MultiLightShading` instead of true." + shading_bool = shading + else + shading_bool = shading != NoShading + end + draw_mesh3D( - scene, screen, space, func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, - model, shading::Bool, diffuse::Vec3f, + scene, screen, space, func, meshpoints, meshfaces, meshnormals, per_face_col, + pos, scale, rotation, + model, shading_bool::Bool, diffuse::Vec3f, specular::Vec3f, shininess::Float32, faceculling::Int ) end function draw_mesh3D( - scene, screen, space, transform_func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, + scene, screen, space, transform_func, meshpoints, meshfaces, meshnormals, per_face_col, + pos, scale, rotation, model, shading, diffuse, specular, shininess, faceculling ) ctx = screen.context - view = ifelse(is_data_space(space), scene.camera.view[], Mat4f(I)) - projection = Makie.space_to_clip(scene.camera, space, false) + projectionview = Makie.space_to_clip(scene.camera, space, true) + eyeposition = scene.camera.eyeposition[] i = Vec(1, 2, 3) - normalmatrix = transpose(inv(view[i, i] * model[i, i])) - - # Mesh data - # transform to view/camera space + normalmatrix = transpose(inv(model[i, i])) + local_model = rotation * Makie.scalematrix(Vec3f(scale)) # pass transform_func as argument to function, so that we get a function barrier # and have `transform_func` be fully typed inside closure vs = broadcast(meshpoints, (transform_func,)) do v, f # Should v get a nan2zero? v = Makie.apply_transform(f, v, space) - p4d = to_ndim(Vec4f, scale .* to_ndim(Vec3f, v, 0f0), 1f0) - view * (model * p4d .+ to_ndim(Vec4f, pos, 0f0)) + p4d = to_ndim(Vec4f, to_ndim(Vec3f, v, 0f0), 1f0) + model * (local_model * p4d .+ to_ndim(Vec4f, pos, 0f0)) end ns = map(n -> normalize(normalmatrix * n), meshnormals) # Light math happens in view/camera space - pointlight = Makie.get_point_light(scene) - lightposition = if !isnothing(pointlight) - pointlight.position[] + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + lightdirection = if dirlight.camera_relative + T = inv(scene.camera.view[][Vec(1,2,3), Vec(1,2,3)]) + normalize(T * dirlight.direction[]) + else + normalize(dirlight.direction[]) + end + c = dirlight.color[] + light_color = Vec3f(red(c), green(c), blue(c)) else - Vec3f(0) + lightdirection = Vec3f(0,0,-1) + light_color = Vec3f(0) end ambientlight = Makie.get_ambient_light(scene) @@ -924,11 +944,9 @@ function draw_mesh3D( Vec3f(0) end - lightpos = (view * to_ndim(Vec4f, lightposition, 1.0))[Vec(1, 2, 3)] - # Camera to screen space ts = map(vs) do v - clip = projection * v + clip = projectionview * v @inbounds begin p = (clip ./ clip[4])[Vec(1, 2)] p_yflip = Vec2f(p[1], -p[2]) @@ -938,6 +956,9 @@ function draw_mesh3D( return Vec3f(p[1], p[2], clip[3]) end + # vs are used as camdir (camera to vertex) for light calculation (in world space) + vs = map(v -> normalize(v[i] - eyeposition), vs) + # Approximate zorder average_zs = map(f -> average_z(ts, f), meshfaces) zorder = sortperm(average_zs) @@ -945,23 +966,23 @@ function draw_mesh3D( # Face culling zorder = filter(i -> any(last.(ns[meshfaces[i]]) .> faceculling), zorder) - draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightpos, shininess, diffuse, ambient, specular) + draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightdirection, light_color, shininess, diffuse, ambient, specular) return end -function _calculate_shaded_vertexcolors(N, v, c, lightpos, ambient, diffuse, specular, shininess) - L = normalize(lightpos .- v[Vec(1,2,3)]) - diff_coeff = max(dot(L, N), 0f0) - H = normalize(L + normalize(-v[Vec(1, 2, 3)])) - spec_coeff = max(dot(H, N), 0f0)^shininess +function _calculate_shaded_vertexcolors(N, v, c, lightdir, light_color, ambient, diffuse, specular, shininess) + L = lightdir + diff_coeff = max(dot(L, -N), 0f0) + H = normalize(L + v) + spec_coeff = max(dot(H, -N), 0f0)^shininess c = RGBAf(c) # if this is one expression it introduces allocations?? - new_c_part1 = (ambient .+ diff_coeff .* diffuse) .* Vec3f(c.r, c.g, c.b) #.+ - new_c = new_c_part1 .+ specular * spec_coeff + new_c_part1 = (ambient .+ light_color .* diff_coeff .* diffuse) .* Vec3f(c.r, c.g, c.b) #.+ + new_c = new_c_part1 .+ light_color .* specular * spec_coeff RGBAf(new_c..., c.alpha) end -function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightpos, shininess, diffuse, ambient, specular) +function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightdir, light_color, shininess, diffuse, ambient, specular) for k in reverse(zorder) f = meshfaces[k] @@ -969,7 +990,7 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, t1 = ts[f[1]] t2 = ts[f[2]] t3 = ts[f[3]] - + # skip any mesh segments with NaN points. if isnan(t1) || isnan(t2) || isnan(t3) continue @@ -984,7 +1005,7 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, N = ns[f[i]] v = vs[f[i]] c = facecolors[i] - _calculate_shaded_vertexcolors(N, v, c, lightpos, ambient, diffuse, specular, shininess) + _calculate_shaded_vertexcolors(N, v, c, lightdir, light_color, ambient, diffuse, specular, shininess) end else c1, c2, c3 = facecolors @@ -995,7 +1016,7 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, # c1 = RGB(n1...) # c2 = RGB(n2...) # c3 = RGB(n3...) - + pattern = Cairo.CairoPatternMesh() Cairo.mesh_pattern_begin_patch(pattern) @@ -1067,25 +1088,24 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki ) - if !(rotations isa Vector) - R = Makie.rotationmatrix4(to_rotation(rotations)) - submesh[:model] = model * R - end + submesh[:model] = model scales = primitive[:markersize][] for i in zorder p = pos[i] if color isa AbstractVector submesh[:calculated_colors] = color[i] end - if rotations isa Vector - R = Makie.rotationmatrix4(to_rotation(rotations[i])) - submesh[:model] = model * R - end scale = markersize isa Vector ? markersize[i] : markersize + rotation = if rotations isa Vector + Makie.rotationmatrix4(to_rotation(rotations[i])) + else + Makie.rotationmatrix4(to_rotation(rotations)) + end draw_mesh3D( scene, screen, submesh, marker, pos = p, - scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0) + scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0), + rotation = rotation ) end diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index 2de2e552910..c11f5861af9 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 @@ -306,6 +306,7 @@ function Makie.colorbuffer(screen::Screen) end function Makie.colorbuffer(screen::Screen{IMAGE}) + Makie.push_screen!(screen.scene, screen) empty!(screen) cairo_draw(screen, screen.scene) return PermutedDimsArray(screen.surface.data, (2, 1)) diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 2008d01a0a6..e22b8938dc0 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -47,7 +47,7 @@ function project_scale(scene::Scene, space, s, model = Mat4f(I)) end end -function project_rect(scenelike, space, rect::Rect, model) +function project_shape(scenelike, space, rect::Rect, model) mini = project_position(scenelike, space, minimum(rect), model) maxi = project_position(scenelike, space, maximum(rect), model) return Rect(mini, maxi .- mini) @@ -226,7 +226,7 @@ function per_face_colors(_color, matcap, faces, normals, uv) wsize = reverse(size(color)) wh = wsize .- 1 cvec = map(uv) do uv - x, y = clamp.(round.(Int, Tuple(uv) .* wh) .+ 1, 1, wh) + x, y = clamp.(round.(Int, Tuple(uv) .* wh) .+ 1, 1, wsize) return color[end - (y - 1), x] end # TODO This is wrong and doesn't actually interpolate diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index c38d9bba068..d3dcc4c6c93 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -37,7 +37,7 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) @testset "saving pdf two times" begin # https://github.com/MakieOrg/Makie.jl/issues/2433 - fig = Figure(resolution=(480, 792)) + fig = Figure(size = (480, 792)) ax = Axis(fig[1, 1]) # The IO was shared between screens, which left the second figure empty save("fig.pdf", fig, pt_per_unit=0.5) @@ -52,14 +52,14 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) # https://github.com/MakieOrg/Makie.jl/issues/2438 # This bug was caused by using the screen size of the pdf screen, which # has a different device_scaling_factor, and therefore a different screen size - fig = scatter(1:4, figure=(; resolution=(800, 800))) + fig = scatter(1:4, figure=(; size = (800, 800))) save("test.pdf", fig) size(Makie.colorbuffer(fig)) == (800, 800) rm("test.pdf") end @testset "switching from pdf screen to png, save" begin - fig = scatter(1:4, figure=(; resolution=(800, 800))) + fig = scatter(1:4, figure=(; size = (800, 800))) save("test.pdf", fig) save("test.png", fig) @test size(load("test.png")) == (1600, 1600) @@ -89,7 +89,7 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) @testset "changing resolution of same format" begin # see: https://github.com/MakieOrg/Makie.jl/issues/2433 # and: https://github.com/MakieOrg/AlgebraOfGraphics.jl/pull/441 - scene = Scene(resolution=(800, 800)); + scene = Scene(size = (800, 800)); load_save(s; kw...) = (save("test.png", s; kw...); load("test.png")) @test size(load_save(scene, px_per_unit=2)) == (1600, 1600) @test size(load_save(scene, px_per_unit=1)) == (800, 800) @@ -122,7 +122,7 @@ end @testset "VideoStream & screen options" begin N = 3 points = Observable(Point2f[]) - f, ax, pl = scatter(points, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(resolution=(600, 800),)) + f, ax, pl = scatter(points, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(size=(600, 800),)) vio = Makie.VideoStream(f; format="mp4", px_per_unit=2.0, backend=CairoMakie) @test vio.screen isa CairoMakie.Screen{CairoMakie.IMAGE} @test size(vio.screen) == size(f.scene) .* 2 diff --git a/CairoMakie/test/svg_tests.jl b/CairoMakie/test/svg_tests.jl index fd474a54d97..36a03929bee 100644 --- a/CairoMakie/test/svg_tests.jl +++ b/CairoMakie/test/svg_tests.jl @@ -27,6 +27,9 @@ end fig end) @test svg_isnt_rasterized(poly(Circle(Point2f(0, 0), 10))) + @test svg_isnt_rasterized(poly(BezierPath([ + MoveTo(0.0, 0.0), LineTo(1.0, 0.0), LineTo(1.0, 1.0), CurveTo(1.0, 1.0, 0.5, 1.0, 0.5, 0.5), ClosePath() + ]))) end @testset "reproducable svg ids" begin diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index 67a28fdb260..e3ac68e4827 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -16,9 +16,9 @@ Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" ModernGL = "66fc600b-dfda-50eb-8b99-91cfa97b1301" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" -PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] @@ -33,7 +33,10 @@ Makie = "=0.20.0" MeshIO = "0.4" ModernGL = "1" Observables = "0.5.1" -ShaderAbstractions = "0.4" PrecompileTools = "1.0" +ShaderAbstractions = "0.4" StaticArrays = "0.12, 1.0" julia = "1" +LinearAlgebra = "1.0, 1.6" +Markdown = "1.0, 1.6" +Printf = "1.0, 1.6" diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag index 8cac8ef53c9..6fdeda71b3f 100644 --- a/GLMakie/assets/shader/distance_shape.frag +++ b/GLMakie/assets/shader/distance_shape.frag @@ -31,7 +31,6 @@ uniform bool transparent_picking; flat in float f_viewport_from_u_scale; flat in float f_distancefield_scale; flat in vec4 f_color; -flat in vec4 f_bg_color; flat in vec4 f_stroke_color; flat in vec4 f_glow_color; flat in uvec2 f_id; @@ -109,14 +108,10 @@ float ellipse(vec2 uv, vec2 scale) return (dot(p/wh,p/wh)>1.0) ? -d : d; } -void fill(vec4 fillcolor, Nothing image, vec2 uv, float infill, inout vec4 color){ - color = mix(color, fillcolor, infill); -} -void fill(vec4 c, sampler2D image, vec2 uv, float infill, inout vec4 color){ - color.rgba = mix(color, texture(image, uv.yx), infill); -} -void fill(vec4 c, sampler2DArray image, vec2 uv, float infill, inout vec4 color){ - color = mix(color, texture(image, vec3(uv.yx, f_primitive_index)), infill); +vec4 fill(vec4 fillcolor, Nothing image, vec2 uv) { return fillcolor; } +vec4 fill(vec4 c, sampler2D image, vec2 uv) { return texture(image, uv.yx); } +vec4 fill(vec4 c, sampler2DArray image, vec2 uv) { + return texture(image, vec3(uv.yx, f_primitive_index)); } @@ -132,8 +127,8 @@ void glow(vec4 glowcolor, float signed_distance, float inside, inout vec4 color) if (glow_width > 0.0){ float s_stroke_width = px_per_unit * stroke_width; float s_glow_width = px_per_unit * glow_width; - float outside = (abs(signed_distance)-s_stroke_width)/s_glow_width; - float alpha = 1-outside; + float outside = (abs(signed_distance) - s_stroke_width) / s_glow_width; + float alpha = 1 - outside; color = mix(vec4(glowcolor.rgb, glowcolor.a*alpha), color, inside); } } @@ -185,11 +180,18 @@ void main(){ float s_stroke_width = px_per_unit * stroke_width; float inside_start = max(-s_stroke_width, 0.0); float inside = aastep(inside_start, signed_distance); - vec4 final_color = f_bg_color; - fill(f_color, image, tex_uv, inside, final_color); + // For the initial coloring we can use the base pixel color and modulate + // its alpha value to create the shape set by the signed distance field. (i.e. inside) + vec4 final_color = fill(f_color, image, tex_uv); + final_color.a = final_color.a * inside; + + // Stroke and glow need to also modulate colors (rgb) to smoothly transition + // from one to another. stroke(f_stroke_color, signed_distance, -s_stroke_width, final_color); glow(f_glow_color, signed_distance, aastep(-s_stroke_width, signed_distance), final_color); + + // TODO: In 3D, we should arguably discard fragments outside the sprite // But note that this may interfere with object picking. // if (final_color == f_bg_color) diff --git a/GLMakie/assets/shader/fragment_output.frag b/GLMakie/assets/shader/fragment_output.frag index 7fb14e78a9a..837d5ccc3ec 100644 --- a/GLMakie/assets/shader/fragment_output.frag +++ b/GLMakie/assets/shader/fragment_output.frag @@ -13,7 +13,7 @@ layout(location=1) out uvec2 fragment_groupid; in vec3 o_view_pos; -in vec3 o_normal; +in vec3 o_view_normal; void write2framebuffer(vec4 color, uvec2 id){ if(color.a <= 0.0) @@ -34,7 +34,7 @@ void write2framebuffer(vec4 color, uvec2 id){ // // if transparency == false && ssao = true // fragment_color = color; // fragment_position = o_view_pos; - // fragment_normal_occlusion.xyz = o_normal; + // fragment_normal_occlusion.xyz = o_view_normal; // // else // fragment_color = color; diff --git a/GLMakie/assets/shader/heatmap.vert b/GLMakie/assets/shader/heatmap.vert index df15cab1327..380bf802328 100644 --- a/GLMakie/assets/shader/heatmap.vert +++ b/GLMakie/assets/shader/heatmap.vert @@ -13,7 +13,7 @@ out vec2 o_uv; flat out uvec2 o_objectid; out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; ivec2 ind2sub(ivec2 dim, int linearindex){ return ivec2(linearindex % dim.x, linearindex / dim.x); @@ -22,7 +22,7 @@ ivec2 ind2sub(ivec2 dim, int linearindex){ void main(){ //Outputs for ssao, which we don't use for 2d shaders like heatmap/image o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); int index = gl_InstanceID; vec2 offset = vertices; diff --git a/GLMakie/assets/shader/lighting.frag b/GLMakie/assets/shader/lighting.frag new file mode 100644 index 00000000000..015ad495f97 --- /dev/null +++ b/GLMakie/assets/shader/lighting.frag @@ -0,0 +1,212 @@ +{{GLSL_VERSION}} +{{GLSL_EXTENSIONS}} + +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + + +// Shared uniforms, inputs and functions +#if defined FAST_SHADING || defined MULTI_LIGHT_SHADING + +// Generic uniforms +uniform vec3 diffuse; +uniform vec3 specular; +uniform float shininess; + +uniform float backlight; + +in vec3 o_camdir; +in vec3 o_world_pos; + +float smooth_zero_max(float x) { + // This is a smoothed version of max(value, 0.0) where -1 <= value <= 1 + // This comes from: + // c = 2 ^ -a # normalizes power w/o swaps + // xswap = (1 / c / a)^(1 / (a - 1)) - 1 # xval with derivative 1 + // yswap = c * (xswap+1) ^ a # yval with derivative 1 + // ifelse.(xs .< yswap, c .* (xs .+ 1 .+ xswap .- yswap) .^ a, xs) + // a = 16 constants: (harder edge) + // const float c = 0.0000152587890625, xswap = 0.7411011265922482, yswap = 0.10881882041201549; + // a = 8 constants: (softer edge) + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + return x < yswap ? c * pow(x + shift, 8) : x; +} + +vec3 blinn_phong(vec3 light_color, vec3 light_dir, vec3 camdir, vec3 normal, vec3 color) { + // diffuse coefficient (how directly does light hits the surface) + float diff_coeff = smooth_zero_max(dot(light_dir, -normal)) + + backlight * smooth_zero_max(dot(light_dir, normal)); + + // DEBUG - visualize diff_coeff, i.e. the angle between light and normals + // if (diff_coeff > 0.999) + // return vec3(0, 0, 1); + // else + // return vec3(1 - diff_coeff,diff_coeff, 0.05); + + // specular coefficient (does reflected light bounce into camera?) + vec3 H = normalize(light_dir + camdir); + float spec_coeff = pow(max(dot(H, -normal), 0.0), shininess) + + backlight * pow(max(dot(H, normal), 0.0), shininess); + if (diff_coeff <= 0.0 || isnan(spec_coeff)) + spec_coeff = 0.0; + + return light_color * vec3(diffuse * diff_coeff * color + specular * spec_coeff); +} + +#else // glsl fails to compile if the shader is just empty + +vec3 illuminate(vec3 normal, vec3 base_color); + +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// FAST_SHADING // +//////////////////////////////////////////////////////////////////////////////// + + +#ifdef FAST_SHADING + +uniform vec3 ambient; +uniform vec3 light_color; +uniform vec3 light_direction; + +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + vec3 shaded_color = blinn_phong(light_color, light_direction, camdir, normal, base_color); + return ambient * base_color + shaded_color; +} + +vec3 illuminate(vec3 normal, vec3 base_color) { + return illuminate(o_world_pos, normalize(o_camdir), normal, base_color); +} + +#endif + + +//////////////////////////////////////////////////////////////////////////////// +// MULTI_LIGHT_SHADING // +//////////////////////////////////////////////////////////////////////////////// + + +#ifdef MULTI_LIGHT_SHADING + +{{MAX_LIGHTS}} +{{MAX_LIGHT_PARAMETERS}} + +// differentiating different light sources +const int UNDEFINED = 0; +const int Ambient = 1; +const int PointLight = 2; +const int DirectionalLight = 3; +const int SpotLight = 4; +const int RectLight = 5; + +// light parameters (maybe invalid depending on light type) +uniform int N_lights; +uniform int light_types[MAX_LIGHTS]; +uniform vec3 light_colors[MAX_LIGHTS]; +uniform float light_parameters[MAX_LIGHT_PARAMETERS]; + +vec3 calc_point_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 position = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec2 param = vec2(light_parameters[idx+3], light_parameters[idx+4]); + + // calculate light direction and distance + vec3 light_vec = world_pos - position; + + float dist = length(light_vec); + vec3 light_dir = normalize(light_vec); + + // How weak has the light gotten due to distance + // float attentuation = 1.0 / (1.0 + dist * dist * dist); + float attentuation = 1.0 / (1.0 + param.x * dist + param.y * dist * dist); + + return attentuation * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_directional_light(vec3 light_color, int idx, vec3 camdir, vec3 normal, vec3 color) { + vec3 light_dir = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + return blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_spot_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 position = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec3 spot_light_dir = normalize(vec3(light_parameters[idx+3], light_parameters[idx+4], light_parameters[idx+5])); + float inner_angle = light_parameters[idx+6]; // cos applied + float outer_angle = light_parameters[idx+7]; // cos applied + + vec3 light_dir = normalize(world_pos - position); + float intensity = smoothstep(outer_angle, inner_angle, dot(light_dir, spot_light_dir)); + + return intensity * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 calc_rect_light(vec3 light_color, int idx, vec3 world_pos, vec3 camdir, vec3 normal, vec3 color) { + // extract args + vec3 origin = vec3(light_parameters[idx], light_parameters[idx+1], light_parameters[idx+2]); + vec3 u1 = vec3(light_parameters[idx+3], light_parameters[idx+4], light_parameters[idx+5]); + vec3 u2 = vec3(light_parameters[idx+6], light_parameters[idx+7], light_parameters[idx+8]); + vec3 light_dir = vec3(light_parameters[idx+9], light_parameters[idx+10], light_parameters[idx+11]); + + // Find t such that = + // to find the point p = world_pos + t * light_dir = origin + w1 * u1 + w2 * u2 + 0 * light_dir. + // Then check if p is inside the rectangle by computing w1 and w2. + float t = dot(origin - world_pos, light_dir); + vec3 dir = world_pos + t * light_dir - origin; + float w1 = dot(dir, u1) / dot(u1, u1); + float w2 = dot(dir, u2) / dot(u2, u2); + + // mask out light rays that do not come from inside the shape + float intensity = smoothstep(0.45, 0.55, 1-abs(w1)) * smoothstep(0.45, 0.55, 1-abs(w2)); + + // If we do not mask the plane we may want to consider light rays coming from + // the closest edge. + // vec3 position = origin + clamp(w1, -0.5, 0.5) * u1 + clamp(w2, -0.5, 0.5) * u2; + // vec3 light_dir = normalize(world_pos - position); + + return intensity * blinn_phong(light_color, light_dir, camdir, normal, color); +} + +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + vec3 final_color = vec3(0); + int idx = 0; + for (int i = 0; i < min(N_lights, MAX_LIGHTS); i++) { + switch (light_types[i]) { + case Ambient: + final_color += light_colors[i] * base_color; + break; + case PointLight: + final_color += calc_point_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 5; // 3 position, 2 attenuation params + break; + case DirectionalLight: + final_color += calc_directional_light(light_colors[i], idx, camdir, normal, base_color); + idx += 3; // 3 direction + break; + case SpotLight: + final_color += calc_spot_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 8; // 3 position, 3 direction, 1 parameter + break; + case RectLight: + final_color += calc_rect_light(light_colors[i], idx, world_pos, camdir, normal, base_color); + idx += 12; + break; + default: + return vec3(1,0,1); // debug magenta + } + } + return final_color; +} + +vec3 illuminate(vec3 normal, vec3 base_color) { + return illuminate(o_world_pos, normalize(o_camdir), normal, base_color); +} + +#endif diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom index 2c0f8d473ef..ad8b9399b13 100644 --- a/GLMakie/assets/shader/line_segment.geom +++ b/GLMakie/assets/shader/line_segment.geom @@ -47,12 +47,12 @@ void emit_vertex(vec3 position, vec2 uv, int index) } out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; void main(void) { o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // get the four vertices passed to the shader: vec3 p0 = screen_space(gl_in[0].gl_Position); // start of previous segment @@ -72,8 +72,8 @@ void main(void) vec3 AA_offset = AA_THICKNESS * v0; float AA = AA_THICKNESS * px2u; - /* 0 v0 l - | --> | + /* 0 v0 l + | --> | -thickness_aa0 - .----------------------------------. - -thickness_aa1 -g_thickness[0] - | .------------------------------. | - -g_thickness[1] | | | | @@ -95,8 +95,8 @@ void main(void) emit_vertex(p1 + thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, -thickness_aa1), 1); emit_vertex(p1 - thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, thickness_aa1), 1); #else - // For patterned lines AA is mostly done by the pattern sampling. We - // still set f_uv_minmax here to ensure that cut off patterns als have + // For patterned lines AA is mostly done by the pattern sampling. We + // still set f_uv_minmax here to ensure that cut off patterns als have // anti-aliasing at the start/end of this segment f_uv_minmax = vec2(0, u); emit_vertex(p0 + thickness_aa0 * n0 - AA_offset, vec2( - AA, -thickness_aa0), 0); diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index 833d435ebcc..f5683063430 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -24,7 +24,7 @@ flat out uvec2 f_id; flat out vec2 f_uv_minmax; out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; uniform vec2 resolution; uniform float pattern_length; @@ -773,7 +773,7 @@ void main(void) { // These need to be set but don't have reasonable values here o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // we generate very thin lines for linewidth 0, so we manually skip them: if (g_thickness[1] == 0.0 && g_thickness[2] == 0.0) { diff --git a/GLMakie/assets/shader/mesh.frag b/GLMakie/assets/shader/mesh.frag index 26df220316d..5480da20008 100644 --- a/GLMakie/assets/shader/mesh.frag +++ b/GLMakie/assets/shader/mesh.frag @@ -4,15 +4,11 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d bool _; //empty structs are not allowed }; -uniform vec3 ambient; -uniform vec3 diffuse; -uniform vec3 specular; -uniform float shininess; -uniform float backlight; +// Sets which shading procedures to use +{{shading}} -in vec3 o_normal; -in vec3 o_lightdir; -in vec3 o_camdir; +in vec3 o_world_normal; +in vec3 o_view_normal; in vec4 o_color; in vec2 o_uv; flat in uvec2 o_id; @@ -68,7 +64,8 @@ vec4 get_color(sampler2D intensity, vec2 uv, vec2 color_norm, sampler1D color_ma return get_color_from_cmap(i, color_map, color_norm); } vec4 matcap_color(sampler2D matcap){ - vec2 muv = o_normal.xy * 0.5 + vec2(0.5, 0.5); + // TODO should matcaps use view space normals? + vec2 muv = o_view_normal.xy * 0.5 + vec2(0.5, 0.5); return texture(matcap, vec2(1.0-muv.y, muv.x)); } vec4 get_color(Nothing image, vec2 uv, Nothing color_norm, Nothing color_map, sampler2D matcap){ @@ -100,25 +97,11 @@ vec4 get_pattern_color(sampler2D color){ // Needs to exist for opengl to be happy vec4 get_pattern_color(Nothing color){return vec4(1,0,1,1);} -vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); - - // specular coefficient - vec3 H = normalize(L + V); - - float spec_coeff = pow(max(dot(H, N), 0.0), shininess); - if (diff_coeff <= 0.0 || isnan(spec_coeff)) - spec_coeff = 0.0; - - // final lighting model - return vec3( - diffuse * diff_coeff * color + - specular * spec_coeff - ); -} - void write2framebuffer(vec4 color, uvec2 id); +#ifndef NO_SHADING +vec3 illuminate(vec3 normal, vec3 base_color); +#endif void main(){ vec4 color; @@ -128,6 +111,8 @@ void main(){ }else{ color = get_color(image, o_uv, color_norm, color_map, matcap); } - {{light_calc}} + #ifndef NO_SHADING + color.rgb = illuminate(normalize(o_world_normal), color.rgb); + #endif write2framebuffer(color, o_id); } diff --git a/GLMakie/assets/shader/mesh.vert b/GLMakie/assets/shader/mesh.vert index 75d3f0d8b3a..018248c0c5c 100644 --- a/GLMakie/assets/shader/mesh.vert +++ b/GLMakie/assets/shader/mesh.vert @@ -14,10 +14,9 @@ uniform bool interpolate_in_fragment_shader = false; in vec3 normals; -uniform vec3 lightposition; uniform mat4 projection, view, model; -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange); uniform uint objectid; @@ -63,5 +62,5 @@ void main() o_uv = vec2(1.0 - tex_uv.y, tex_uv.x) * uv_scale; o_color = to_color(vertex_color, color_map, color_norm); vec3 v = to_3d(vertices); - render(model * vec4(v, 1), normals, view, projection, lightposition); + render(model * vec4(v, 1), normals, view, projection); } diff --git a/GLMakie/assets/shader/particles.vert b/GLMakie/assets/shader/particles.vert index 8676a2a682f..2d26785e54d 100644 --- a/GLMakie/assets/shader/particles.vert +++ b/GLMakie/assets/shader/particles.vert @@ -28,7 +28,6 @@ in vec3 vertices; in vec3 normals; {{texturecoordinates_type}} texturecoordinates; -uniform vec3 lightposition; uniform mat4 view, model, projection; uniform uint objectid; uniform int len; @@ -91,7 +90,7 @@ vec4 get_particle_color(sampler2D color, Nothing intensity, Nothing color_map, N return vec4(0); } -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec2 get_uv(Nothing x){return vec2(0.0);} vec2 get_uv(vec2 x){return vec2(1.0 - x.y, x.x);} @@ -108,5 +107,5 @@ void main(){ o_color = o_color * to_color(vertex_color); o_uv = get_uv(texturecoordinates); rotate(rotation, index, V, N); - render(model * vec4(pos + V, 1), N, view, projection, lightposition); + render(model * vec4(pos + V, 1), N, view, projection); } diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index d34d131a187..fc771ef63b5 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -85,7 +85,6 @@ void emit_vertex(vec4 vertex, vec2 uv) f_uv_texture_bbox = g_uv_texture_bbox[0]; f_primitive_index = g_primitive_index[0]; f_color = g_color[0]; - f_bg_color = vec4(g_color[0].rgb, 0); f_stroke_color = g_stroke_color[0]; f_glow_color = g_glow_color[0]; f_id = g_id[0]; @@ -99,12 +98,12 @@ mat2 diagm(vec2 v){ } out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; void main(void) { o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); // emit quad as triangle strip // v3. ____ . v4 diff --git a/GLMakie/assets/shader/sprites.vert b/GLMakie/assets/shader/sprites.vert index 2489dddd040..ceee24efe7f 100644 --- a/GLMakie/assets/shader/sprites.vert +++ b/GLMakie/assets/shader/sprites.vert @@ -72,6 +72,7 @@ vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_ {{stroke_color_type}} stroke_color; {{glow_color_type}} glow_color; +uniform bool scale_primitive; uniform mat4 preprojection; uniform mat4 model; uniform uint objectid; @@ -96,7 +97,10 @@ void main(){ vec3 pos; {{position_calc}} vec4 p = preprojection * model * vec4(pos, 1); - g_position = p.xyz / p.w + mat3(model) * marker_offset; + if (scale_primitive) + g_position = p.xyz / p.w + mat3(model) * marker_offset; + else + g_position = p.xyz / p.w + marker_offset; g_offset_width.xy = quad_offset.xy; g_offset_width.zw = scale.xy; g_color = _color(color, intensity, color_map, color_norm, g_primitive_index, len); diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index e268131452a..b8b16eb98d6 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -19,8 +19,6 @@ in vec2 vertices; {{position_y_type}} position_y; uniform sampler2D position_z; -uniform vec3 lightposition; - {{image_type}} image; {{color_map_type}} color_map; {{color_norm_type}} color_norm; @@ -36,7 +34,7 @@ uniform vec3 scale; uniform mat4 view, model, projection; // See util.vert for implementations -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition); +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); ivec2 ind2sub(ivec2 dim, int linearindex); vec2 grid_pos(Grid2D pos, vec2 uv); vec2 linear_index(ivec2 dims, int index); @@ -172,6 +170,5 @@ void main() if (isnan(pos.z)) { pos.z = 0.0; } - - render(model * vec4(pos, 1), normalvec, view, projection, lightposition); + render(model * vec4(pos, 1), normalvec, view, projection); } diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index be3bdadfcc1..afb2c379945 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -1,5 +1,12 @@ {{GLSL_VERSION}} +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; @@ -241,33 +248,47 @@ vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm return get_color_from_cmap(intensity, color_map, color_norm); } -out vec3 o_view_pos; -out vec3 o_normal; -out vec3 o_lightdir; -out vec3 o_camdir; + +uniform float depth_shift; + +// TODO maybe ifdef SSAO this stuff? // transpose(inv(view * model)) // Transformation for vectors (rather than points) -uniform mat3 normalmatrix; -uniform vec3 lightposition; +uniform mat3 view_normalmatrix; +out vec3 o_view_pos; +out vec3 o_view_normal; + + +#if defined(FAST_SHADING) || defined(MULTI_LIGHT_SHADING) +// transpose(inv(model)) +uniform mat3 world_normalmatrix; uniform vec3 eyeposition; -uniform float depth_shift; +out vec3 o_world_pos; +out vec3 o_world_normal; +out vec3 o_camdir; +#endif -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition) +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { - // normal in world space - o_normal = normalmatrix * normal; // position in view space (as seen from camera) vec4 view_pos = view * position_world; + view_pos /= view_pos.w; + // position in clip space (w/ depth) gl_Position = projection * view_pos; gl_Position.z += gl_Position.w * depth_shift; - // direction to light - o_lightdir = normalize(view*vec4(lightposition, 1.0) - view_pos).xyz; - // direction to camera - // This is equivalent to - // normalize(view*vec4(eyeposition, 1.0) - view_pos).xyz - // (by definition `view * eyeposition = 0`) - o_camdir = normalize(-view_pos).xyz; + + // for lighting +#if defined(FAST_SHADING) || defined(MULTI_LIGHT_SHADING) + o_world_pos = position_world.xyz / position_world.w; + o_world_normal = world_normalmatrix * normal; + // direction from camera to vertex + o_camdir = position_world.xyz / position_world.w - eyeposition; +#endif + + // for SSAO o_view_pos = view_pos.xyz / view_pos.w; + // SSAO + matcap + o_view_normal = view_normalmatrix * normal; } diff --git a/GLMakie/assets/shader/volume.frag b/GLMakie/assets/shader/volume.frag index 4fdcadf3d25..1e4eef28b6d 100644 --- a/GLMakie/assets/shader/volume.frag +++ b/GLMakie/assets/shader/volume.frag @@ -1,10 +1,16 @@ {{GLSL_VERSION}} +// Sets which shading procedures to use +// Options: +// NO_SHADING - skip shading calculation, handled outside +// FAST_SHADING - single point light (forward rendering) +// MULTI_LIGHT_SHADING - simple shading with multiple lights (forward rendering) +{{shading}} + struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; in vec3 frag_vert; -in vec3 o_light_dir; {{volumedata_type}} volumedata; @@ -15,11 +21,6 @@ in vec3 o_light_dir; uniform float absorption = 1.0; uniform vec3 eyeposition; -uniform vec3 ambient; -uniform vec3 diffuse; -uniform vec3 specular; -uniform float shininess; - uniform mat4 modelinv; uniform int algorithm; uniform float isovalue; @@ -72,9 +73,17 @@ vec4 color_lookup(Nothing colormap, int index) return vec4(0); } -vec3 gennormal(vec3 uvw, float d) +vec3 gennormal(vec3 uvw, float d, vec3 o) { + // uvw samples positions (0..1 values) + // d is the sampling step. Could be any small value here + // o is half the uvw distance between two voxels. A distance smaller than + // that will result in equal positions when sampling on the edge of the + // volume, generating broken normals. vec3 a, b; + + float eps = 0.001; + // handle normals at edges! if(uvw.x + d >= 1.0){ return vec3(1, 0, 0); @@ -96,32 +105,33 @@ vec3 gennormal(vec3 uvw, float d) return vec3(0, 0, -1); } - a.x = texture(volumedata, uvw - vec3(d,0.0,0.0)).r; - b.x = texture(volumedata, uvw + vec3(d,0.0,0.0)).r; + a.x = texture(volumedata, uvw - vec3(o.x, 0.0, 0.0)).r; + b.x = texture(volumedata, uvw + vec3(o.x, 0.0, 0.0)).r; - a.y = texture(volumedata, uvw - vec3(0.0,d,0.0)).r; - b.y = texture(volumedata, uvw + vec3(0.0,d,0.0)).r; + a.y = texture(volumedata, uvw - vec3(0.0, o.y, 0.0)).r; + b.y = texture(volumedata, uvw + vec3(0.0, o.y, 0.0)).r; - a.z = texture(volumedata, uvw - vec3(0.0,0.0,d)).r; - b.z = texture(volumedata, uvw + vec3(0.0,0.0,d)).r; - return normalize(a-b); + a.z = texture(volumedata, uvw - vec3(0.0, 0.0, o.z)).r; + b.z = texture(volumedata, uvw + vec3(0.0, 0.0, o.z)).r; + + vec3 diff = a - b; + float n = length(diff); + + if (n < 0.000000000001) // 1e-12 + return diff; + + return diff / n; } -// Includes front and back-facing normals (N, -N) -vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0) + max(dot(L, -N), 0.0); - // specular coefficient - vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0) + max(dot(H, -N), 0.0), shininess); - if (diff_coeff <= 0.0 || isnan(spec_coeff)) - spec_coeff = 0.0; - // final lighting model - return vec3( - ambient * color + - diffuse * diff_coeff * color + - specular * spec_coeff - ); +#ifndef NO_SHADING +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color); +#endif + +#ifdef NO_SHADING +vec3 illuminate(vec3 world_pos, vec3 camdir, vec3 normal, vec3 base_color) { + return normal; } +#endif // Simple random generator found: http://stackoverflow.com/questions/4200224/random-noise-functions-for-glsl float rand(){ @@ -208,7 +218,8 @@ vec4 contours(vec3 front, vec3 dir) float T = 1.0; vec3 Lo = vec3(0.0); int i = 0; - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); + vec3 edge_gap = 0.5 / textureSize(volumedata, 0); // see gennormal {{depth_init}} // may write: float depth = 100000.0; for (i; i < num_samples; ++i) { @@ -220,9 +231,9 @@ vec4 contours(vec3 front, vec3 dir) // may write // vec4 frag_coord = projectionview * model * vec4(pos, 1); // depth = min(depth, frag_coord.z / frag_coord.w); - vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); - vec3 opaque = blinnphong(N, camdir, L, density.rgb); + vec3 N = gennormal(pos, step_size, edge_gap); + vec4 world_pos = model * vec4(pos, 1); + vec3 opaque = illuminate(world_pos.xyz / world_pos.w, camdir, N, density.rgb); Lo += (T * opacity) * opaque; T *= 1.0 - opacity; if (T <= 0.01) @@ -242,7 +253,8 @@ vec4 isosurface(vec3 front, vec3 dir) vec4 c = vec4(0.0); int i = 0; vec4 diffuse_color = color_lookup(isovalue, color_map, color_norm, color); - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); + vec3 edge_gap = 0.5 / textureSize(volumedata, 0); // see gennormal {{depth_init}} // may write: float depth = 100000.0; for (i; i < num_samples; ++i){ @@ -252,10 +264,10 @@ vec4 isosurface(vec3 front, vec3 dir) // may write: // vec4 frag_coord = projectionview * model * vec4(pos, 1); // depth = min(depth, frag_coord.z / frag_coord.w); - vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 N = gennormal(pos, step_size, edge_gap); + vec4 world_pos = model * vec4(pos, 1); c = vec4( - blinnphong(N, camdir, L, diffuse_color.rgb), + illuminate(world_pos.xyz / world_pos.w, camdir, N, diffuse_color.rgb), diffuse_color.a ); break; diff --git a/GLMakie/assets/shader/volume.vert b/GLMakie/assets/shader/volume.vert index 72c60a336c8..2ca396b5763 100644 --- a/GLMakie/assets/shader/volume.vert +++ b/GLMakie/assets/shader/volume.vert @@ -3,24 +3,26 @@ in vec3 vertices; out vec3 frag_vert; -out vec3 o_light_dir; uniform mat4 projectionview, model; -uniform vec3 lightposition; uniform mat4 modelinv; uniform float depth_shift; +// SSAO out vec3 o_view_pos; -out vec3 o_normal; +out vec3 o_view_normal; + +// Lighting (unused and don't need to be available?) +// out vec3 o_world_pos; +// out vec3 o_world_normal; void main() { // TODO set these in volume.frag o_view_pos = vec3(0); - o_normal = vec3(0); + o_view_normal = vec3(0); vec4 world_vert = model * vec4(vertices, 1); frag_vert = world_vert.xyz; - o_light_dir = vec3(modelinv * vec4(lightposition, 1)); gl_Position = projectionview * world_vert; gl_Position.z += gl_Position.w * depth_shift; } 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..a51af4024bc 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -241,6 +241,7 @@ gl_convert(x::Mat{N, M, T}) where {N, M, T} = map(gl_promote(T), x) gl_convert(a::AbstractVector{<: AbstractFace}) = indexbuffer(s) gl_convert(t::Type{T}, a::T; kw_args...) where T <: NATIVE_TYPES = a gl_convert(::Type{<: GPUArray}, a::StaticVector) = gl_convert(a) +gl_convert(x::Vector) = x function gl_convert(T::Type{<: GPUArray}, a::AbstractArray{X, N}; kw_args...) where {X, N} T(convert(AbstractArray{gl_promote(X), N}, a); kw_args...) @@ -261,16 +262,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..5af488594da 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) @@ -54,8 +63,6 @@ function __init__() activate!() end -Base.@deprecate set_window_config!(; screen_config...) GLMakie.activate!(; screen_config...) - include("precompiles.jl") end diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 540e1e7d62d..a55ccc28593 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -2,6 +2,107 @@ using Makie: transform_func_obs, apply_transform using Makie: attribute_per_char, FastPixel, el32convert, Pixel using Makie: convert_arguments +function handle_lights(attr::Dict, screen::Screen, lights::Vector{Makie.AbstractLight}) + @inline function push_inplace!(trg, idx, src) + for i in eachindex(src) + trg[idx + i] = src[i] + end + return idx + length(src) + end + + MAX_LIGHTS = screen.config.max_lights + MAX_PARAMS = screen.config.max_light_parameters + + # Every light has a type and a color. Therefore we have these as independent + # uniforms with a max length of MAX_LIGHTS. + # Other parameters like position, direction, etc differe between light types. + # To avoid wasting a bunch of memory we squash all of them into one vector of + # size MAX_PARAMS. + attr[:N_lights] = Observable(0) + attr[:light_types] = Observable(sizehint!(Int32[], MAX_LIGHTS)) + attr[:light_colors] = Observable(sizehint!(RGBf[], MAX_LIGHTS)) + attr[:light_parameters] = Observable(sizehint!(Float32[], MAX_PARAMS)) + + on(screen.render_tick, priority = typemin(Int)) do _ + # derive number of lights from available lights. Both MAX_LIGHTS and + # MAX_PARAMS are considered for this. + n_lights = 0 + n_params = 0 + for light in lights + delta = 0 + if light isa PointLight + delta = 5 # 3 position + 2 attenuation + elseif light isa DirectionalLight + delta = 3 # 3 direction + elseif light isa SpotLight + delta = 8 # 3 position + 3 direction + 2 angles + elseif light isa RectLight + delta = 12 # 3 position + 2x 3 rect basis vectors + 3 direction + end + if n_params + delta > MAX_PARAMS || n_lights == MAX_LIGHTS + if n_params > MAX_PARAMS + @warn "Exceeded the maximum number of light parameters ($n_params > $MAX_PARAMS). Skipping lights beyond number $n_lights." + else + @warn "Exceeded the maximum number of lights ($n_lights > $MAX_LIGHTS). Skipping lights beyond number $n_lights." + end + break + end + n_params += delta + n_lights += 1 + end + + # Update number of lights + attr[:N_lights][] = n_lights + + # Update light types + trg = attr[:light_types][] + resize!(trg, n_lights) + map!(i -> Makie.light_type(lights[i]), trg, 1:n_lights) + notify(attr[:light_types]) + + # Update light colors + trg = attr[:light_colors][] + resize!(trg, n_lights) + map!(i -> Makie.light_color(lights[i]), trg, 1:n_lights) + notify(attr[:light_colors]) + + # Update other light parameters + # This precalculates world space pos/dir -> view/cam space pos/dir + parameters = attr[:light_parameters][] + resize!(parameters, n_params) + idx = 0 + for i in 1:n_lights + light = lights[i] + if light isa PointLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, light.attenuation[]) + elseif light isa DirectionalLight + if light.camera_relative + T = inv(attr[:view][][Vec(1,2,3), Vec(1,2,3)]) + dir = normalize(T * light.direction[]) + else + dir = normalize(light.direction[]) + end + idx = push_inplace!(parameters, idx, dir) + elseif light isa SpotLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, normalize(light.direction[])) + idx = push_inplace!(parameters, idx, cos.(light.angles[])) + elseif light isa RectLight + idx = push_inplace!(parameters, idx, light.position[]) + idx = push_inplace!(parameters, idx, light.u1[]) + idx = push_inplace!(parameters, idx, light.u2[]) + idx = push_inplace!(parameters, idx, normalize(light.direction[])) + end + end + notify(attr[:light_parameters]) + + return Consume(false) + end + + return attr +end + Makie.el32convert(x::GLAbstraction.Texture) = x gpuvec(x) = GPUVector(GLBuffer(x)) @@ -48,7 +149,17 @@ function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space] end # end end - get!(gl_attributes, :normalmatrix) do + + # for lighting + get!(gl_attributes, :world_normalmatrix) do + return lift(plot, gl_attributes[:model]) do m + i = Vec(1, 2, 3) + return transpose(inv(m[i, i])) + end + end + + # for SSAO + get!(gl_attributes, :view_normalmatrix) do return lift(plot, gl_attributes[:view], gl_attributes[:model]) do v, m i = Vec(1, 2, 3) return transpose(inv(v[i, i] * m[i, i])) @@ -56,7 +167,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 +191,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,48 +221,89 @@ 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 + on(plot, plot.model) do x + screen.requires_update = true + 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) - - pointlight = Makie.get_point_light(scene) - if !isnothing(pointlight) - gl_attributes[:lightposition] = pointlight.position + gl_attributes[:model] = plot.model + if haskey(plot, :markerspace) + gl_attributes[:markerspace] = plot.markerspace end + gl_attributes[:space] = plot.space + gl_attributes[:px_per_unit] = screen.px_per_unit - ambientlight = Makie.get_ambient_light(scene) - if !isnothing(ambientlight) - gl_attributes[:ambient] = ambientlight.color + handle_intensities!(screen, gl_attributes, plot) + connect_camera!(plot, gl_attributes, scene.camera, get_space(plot)) + + # TODO: remove depwarn & conversion after some time + if haskey(gl_attributes, :shading) && to_value(gl_attributes[:shading]) isa Bool + @warn "`shading::Bool` is deprecated. Use `shading = NoShading` instead of false and `shading = FastShading` or `shading = MultiLightShading` instead of true." + gl_attributes[:shading] = ifelse(gl_attributes[:shading][], FastShading, NoShading) + elseif haskey(gl_attributes, :shading) && gl_attributes[:shading] isa Observable + gl_attributes[:shading] = gl_attributes[:shading][] 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)) + shading = to_value(get(gl_attributes, :shading, NoShading)) + + if shading == FastShading + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + gl_attributes[:light_direction] = if dirlight.camera_relative + map(gl_attributes[:view], dirlight.direction) do view, dir + return normalize(inv(view[Vec(1,2,3), Vec(1,2,3)]) * dir) + end + else + map(normalize, dirlight.direction) + end + + gl_attributes[:light_color] = dirlight.color + else + gl_attributes[:light_direction] = Observable(Vec3f(0)) + gl_attributes[:light_color] = Observable(RGBf(0,0,0)) + end - robj = robj_func(gl_attributes) + ambientlight = Makie.get_ambient_light(scene) + if !isnothing(ambientlight) + gl_attributes[:ambient] = ambientlight.color + else + gl_attributes[:ambient] = Observable(RGBf(0,0,0)) + end + elseif shading == MultiLightShading + handle_lights(gl_attributes, screen, scene.lights) + end + robj = robj_func(gl_attributes) # <-- here 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 +311,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 +326,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 +345,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 +372,28 @@ 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 +407,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 +423,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 +438,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 +460,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 +477,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 +488,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 +505,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 - Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) + glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space) do pos, gc, offset, transfunc, space + return 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 +535,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 +543,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 +552,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 +567,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 +584,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 +607,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,21 +628,21 @@ 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 return 1.0f0 .- Vec2f(uv[2], uv[1]) end - gl_attributes[:shading] = false + get!(gl_attributes, :shading, NoShading) _interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = _interp ? :linear : :nearest if haskey(gl_attributes, :intensity) @@ -502,10 +654,10 @@ 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 + shading = to_value(gl_attributes[:shading])::Makie.MakieCore.ShadingAlgorithm + matcap_active = !isnothing(to_value(get(gl_attributes, :matcap, nothing))) color = pop!(gl_attributes, :color) interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = interp ? :linear : :nearest @@ -514,18 +666,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 @@ -547,30 +699,32 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, space=:data) if hasproperty(to_value(mesh), :uv) gl_attributes[:texturecoordinates] = lift(decompose_uv, mesh) end - if hasproperty(to_value(mesh), :normals) && shading + if hasproperty(to_value(mesh), :normals) && (shading !== NoShading || matcap_active) gl_attributes[:normals] = lift(decompose_normals, mesh) end return draw_mesh(screen, gl_attributes) end function draw_atomic(screen::Screen, scene::Scene, meshplot::Mesh) - return cached_robj!(screen, scene, meshplot) do gl_attributes + x = 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) + x = mesh_inner(screen, meshplot[1], t, gl_attributes, meshplot, space) + return x end + + return x 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 # We automatically insert x[3] into the color channel, so if it's equal we don't need to do anything 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 +737,17 @@ 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 +762,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 +783,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 +803,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..52f631095ce 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -194,7 +194,7 @@ function (p::MousePositionUpdater)(::Nothing) @print_error p.mouseposition[] = pos # notify!(e.mouseposition) end - return + return Consume(false) end """ @@ -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, priority = typemax(Int)) return end function Makie.disconnect!(screen::Screen, ::typeof(mouse_position)) diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index 1fb7a003f02..b8ed7a35dfd 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -75,5 +75,3 @@ include("rendering.jl") include("events.jl") include("drawing_primitives.jl") include("display.jl") - -Base.@deprecate_binding GLVisualize GLMakie true "The module `GLVisualize` has been removed and integrated into GLMakie, so simply replace all usage of `GLVisualize` with `GLMakie`." diff --git a/GLMakie/src/glshaders/image_like.jl b/GLMakie/src/glshaders/image_like.jl index 97c6fa990e8..0fd7ccbaa05 100644 --- a/GLMakie/src/glshaders/image_like.jl +++ b/GLMakie/src/glshaders/image_like.jl @@ -34,6 +34,7 @@ A matrix of Intensities will result in a contourf kind of plot function draw_heatmap(screen, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) + pop!(data, :shading, FastShading) @gen_defaults! data begin intensity = nothing => Texture color_map = nothing => Texture @@ -55,6 +56,8 @@ end function draw_volume(screen, main::VolumeTypes, data::Dict) geom = Rect3f(Vec3f(0), Vec3f(1)) to_opengl_mesh!(data, const_lift(GeometryBasics.triangle_mesh, geom)) + shading = pop!(data, :shading, FastShading) + pop!(data, :backlight, 0f0) # We overwrite this @gen_defaults! data begin volumedata = main => Texture model = Mat4f(I) @@ -67,12 +70,17 @@ function draw_volume(screen, main::VolumeTypes, data::Dict) absorption = 1f0 isovalue = 0.5f0 isorange = 0.01f0 + backlight = 1f0 enable_depth = true transparency = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "volume.vert", "volume.frag", + "util.vert", "volume.vert", + "fragment_output.frag", "lighting.frag", "volume.frag", view = Dict( + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "depth_init" => vol_depth_init(to_value(enable_depth)), "depth_default" => vol_depth_default(to_value(enable_depth)), "depth_main" => vol_depth_main(to_value(enable_depth)), diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 977cbc57c2b..877ddf90dea 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -27,7 +27,9 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) to_buffer(:uv, :texturecoordinates) to_buffer(:uvw, :texturecoordinates) # Only emit normals, when we shadin' - if to_value(get(result, :shading, true)) || !isnothing(to_value(get(result, :matcap, nothing))) + shading = get(result, :shading, NoShading)::Makie.MakieCore.ShadingAlgorithm + matcap_active = !isnothing(to_value(get(result, :matcap, nothing))) + if matcap_active || shading != NoShading to_buffer(:normals, :normals) end to_buffer(:attribute_id, :attribute_id) @@ -35,11 +37,11 @@ function to_opengl_mesh!(result, mesh_obs::TOrSignal{<: GeometryBasics.Mesh}) end function draw_mesh(screen, data::Dict) + shading = pop!(data, :shading, NoShading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin vertices = nothing => GLBuffer faces = nothing => indexbuffer normals = nothing => GLBuffer - shading = true backlight = 0f0 vertex_color = nothing => GLBuffer image = nothing => Texture @@ -53,9 +55,13 @@ function draw_mesh(screen, data::Dict) interpolate_in_fragment_shader = true shader = GLVisualizeShader( screen, - "util.vert", "mesh.vert", "mesh.frag", "fragment_output.frag", + "util.vert", "mesh.vert", + "fragment_output.frag", "mesh.frag", + "lighting.frag", view = Dict( - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index f44bec57305..90e3aab1575 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -57,9 +57,9 @@ function draw_mesh_particle(screen, p, data) scale = Vec3f(1) => TextureBuffer rotation = rot => TextureBuffer texturecoordinates = nothing - shading = true end + shading = pop!(data, :shading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin color_map = nothing => Texture color_norm = nothing @@ -71,16 +71,19 @@ function draw_mesh_particle(screen, p, data) fetch_pixel = false interpolate_in_fragment_shader = false uv_scale = Vec2f(1) + backlight = 0f0 instances = const_lift(length, position) - shading = true transparency = false shader = GLVisualizeShader( screen, - "util.vert", "particles.vert", "mesh.frag", "fragment_output.frag", + "util.vert", "particles.vert", + "fragment_output.frag", "lighting.frag", "mesh.frag", view = Dict( "position_calc" => position_calc(position, nothing, nothing, nothing, TextureBuffer), - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index 9750cd458e4..ef7810a96f8 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -12,17 +12,23 @@ function normal_calc(x::Bool, invert_normals::Bool = false) end end +# TODO this shouldn't be necessary function light_calc(x::Bool) - if x - """ - vec3 L = normalize(o_lightdir); - vec3 N = normalize(o_normal); - vec3 light1 = blinnphong(N, o_camdir, L, color.rgb); - vec3 light2 = blinnphong(N, o_camdir, -L, color.rgb); - color = vec4(ambient * color.rgb + light1 + backlight * light2, color.a); - """ + @error "shading::Bool is deprecated. Use `NoShading` instead of `false` and `FastShading` or `MultiLightShading` instead of true." + return light_calc(ifelse(x, FastShading, NoShading)) +end + +function light_calc(x::Makie.MakieCore.ShadingAlgorithm) + if x === NoShading + return "#define NO_SHADING" + elseif x === FastShading + return "#define FAST_SHADING" + elseif x === MultiLightShading + return "#define MULTI_LIGHT_SHADING" + # elseif x === :PBR # TODO? else - "" + @warn "Did not recognize shading value :$x. Defaulting to FastShading." + return "#define FAST_SHADING" end end @@ -78,7 +84,7 @@ function _position_calc( grid::Grid{2}, position_z::MatTypes{T}, target::Type{Texture} ) where T<:AbstractFloat """ - int index1D = index + offseti.x + offseti.y * dims.x + (index/(dims.x-1)); + int index1D = index + offseti.x + offseti.y * dims.x; // + (index/(dims.x-1)); ivec2 index2D = ind2sub(dims, index1D); vec2 index01 = (vec2(index2D) + 0.5) / (vec2(dims)); @@ -113,6 +119,7 @@ end function draw_surface(screen, main, data::Dict) primitive = triangle_mesh(Rect2(0f0,0f0,1f0,1f0)) to_opengl_mesh!(data, primitive) + shading = pop!(data, :shading, FastShading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin scale = nothing position = nothing @@ -120,8 +127,7 @@ function draw_surface(screen, main, data::Dict) position_y = nothing => Texture position_z = nothing => Texture image = nothing => Texture - shading = true - normal = shading + normal = shading != NoShading invert_normals = false backlight = 0f0 end @@ -141,12 +147,14 @@ function draw_surface(screen, main, data::Dict) transparency = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "surface.vert", - "mesh.frag", + "util.vert", "surface.vert", + "fragment_output.frag", "lighting.frag", "mesh.frag", view = Dict( "position_calc" => position_calc(position, position_x, position_y, position_z, Texture), "normal_calc" => normal_calc(normal, to_value(invert_normals)), - "light_calc" => light_calc(shading), + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) ) diff --git a/GLMakie/src/glshaders/visualize_interface.jl b/GLMakie/src/glshaders/visualize_interface.jl index 1633084e6b6..d9236e4f61c 100644 --- a/GLMakie/src/glshaders/visualize_interface.jl +++ b/GLMakie/src/glshaders/visualize_interface.jl @@ -176,7 +176,7 @@ function output_buffer_writes(screen::Screen, transparency = false) """ fragment_color = color; fragment_position = o_view_pos; - fragment_normal_occlusion.xyz = o_normal; + fragment_normal_occlusion.xyz = o_view_normal; """ else "fragment_color = color;" 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/postprocessing.jl b/GLMakie/src/postprocessing.jl index fa55afd4bfd..60c978e07cf 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -163,6 +163,7 @@ function ssao_postprocessor(framebuffer, shader_cache) glDrawBuffer(normal_occ_id) # occlusion buffer glViewport(0, 0, w, h) glEnable(GL_SCISSOR_TEST) + ppu = (x) -> round.(Int, screen.px_per_unit[] .* x) for (screenid, scene) in screen.screens # Select the area of one leaf scene @@ -170,8 +171,8 @@ function ssao_postprocessor(framebuffer, shader_cache) # scenes. It should be a leaf scene to avoid repeatedly shading # the same region (though this is not guaranteed...) isempty(scene.children) || continue - a = pixelarea(scene)[] - glScissor(minimum(a)..., widths(a)...) + a = viewport(scene)[] + glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms data1[:projection] = scene.camera.projection[] data1[:bias] = scene.ssao.bias[] @@ -184,8 +185,8 @@ function ssao_postprocessor(framebuffer, shader_cache) for (screenid, scene) in screen.screens # Select the area of one leaf scene isempty(scene.children) || continue - a = pixelarea(scene)[] - glScissor(minimum(a)..., widths(a)...) + a = viewport(scene)[] + glScissor(ppu(minimum(a))..., ppu(widths(a))...) # update uniforms data2[:blur_range] = scene.ssao.blur GLAbstraction.render(pass2) 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/rendering.jl b/GLMakie/src/rendering.jl index 2a47fbc6ea7..dda559f5ec3 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -7,7 +7,7 @@ function setup!(screen::Screen) glClear(GL_COLOR_BUFFER_BIT) for (id, scene) in screen.screens if scene.visible[] - a = pixelarea(scene)[] + a = viewport(scene)[] rt = (round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) glViewport(rt...) if scene.clear[] @@ -119,7 +119,7 @@ function GLAbstraction.render(filter_elem_func, screen::Screen) found || continue scene.visible[] || continue ppu = screen.px_per_unit[] - a = pixelarea(scene)[] + a = viewport(scene)[] glViewport(round.(Int, ppu .* minimum(a))..., round.(Int, ppu .* widths(a))...) render(elem) end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index e710bc6d444..8133ed61ed1 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -30,12 +30,14 @@ function renderloop end * `visible = true`: Whether or not the window should be visible when first created. * `scalefactor = automatic`: Sets the window scaling factor, such as `2.0` on HiDPI/Retina displays. It is set automatically based on the display, but may be any positive real number. -## Postprocessor +## Rendering constants & Postprocessor * `oit = false`: Whether to enable order independent transparency for the window. * `fxaa = true`: Whether to enable fxaa (anti-aliasing) for the window. * `ssao = true`: Whether to enable screen space ambient occlusion, which simulates natural shadowing at inner edges and crevices. * `transparency_weight_scale = 1000f0`: Adjusts a factor in the rendering shaders for order independent transparency. This should be the same for all of them (within one rendering pipeline) otherwise depth "order" will be broken. +* `max_lights = 64`: The maximum number of lights with `shading = MultiLightShading` +* `max_light_parameters = 5 * N_lights`: The maximum number of light parameters that can be uploaded. These include everything other than the light color (i.e. position, direction, attenuation, angles) in terms of scalar floats. """ mutable struct ScreenConfig # Renderloop @@ -57,11 +59,13 @@ mutable struct ScreenConfig visible::Bool scalefactor::Union{Nothing, Float32} - # Postprocessor + # Render Constants & Postprocessor oit::Bool fxaa::Bool ssao::Bool transparency_weight_scale::Float32 + max_lights::Int + max_light_parameters::Int function ScreenConfig( # Renderloop @@ -86,7 +90,9 @@ mutable struct ScreenConfig oit::Bool, fxaa::Bool, ssao::Bool, - transparency_weight_scale::Number) + transparency_weight_scale::Number, + max_lights::Int, + max_light_parameters::Int) return new( # Renderloop renderloop isa Makie.Automatic ? GLMakie.renderloop : renderloop, @@ -110,7 +116,9 @@ mutable struct ScreenConfig oit, fxaa, ssao, - transparency_weight_scale) + transparency_weight_scale, + max_lights, + max_light_parameters) end end @@ -164,7 +172,7 @@ mutable struct Screen{GLWindow} <: MakieScreen cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, AbstractPlot} framecache::Matrix{RGB{N0f8}} - render_tick::Observable{Nothing} + render_tick::Observable{Nothing} # listeners must not Consume(true) window_open::Observable{Bool} scalefactor::Observable{Float32} @@ -205,6 +213,8 @@ mutable struct Screen{GLWindow} <: MakieScreen end end +Makie.isvisible(screen::Screen) = screen.config.visible + # for e.g. closeall, track all created screens # gets removed in destroy!(screen) const ALL_SCREENS = Set{Screen}() @@ -354,6 +364,9 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B replace_processor!(config.ssao ? ssao_postprocessor : empty_postprocessor, 1) replace_processor!(config.oit ? OIT_postprocessor : empty_postprocessor, 2) replace_processor!(config.fxaa ? fxaa_postprocessor : empty_postprocessor, 3) + + # TODO: replace shader programs with lighting to update N_lights & N_light_parameters + # Set the config screen.config = config if start_renderloop @@ -374,7 +387,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 +413,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 @@ -433,8 +446,9 @@ end function pollevents(screen::Screen) ShaderAbstractions.switch_context!(screen.glscreen) - notify(screen.render_tick) GLFW.PollEvents() + notify(screen.render_tick) + return end Base.wait(x::Screen) = !isnothing(x.rendertask) && wait(x.rendertask) @@ -452,10 +466,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 @@ -713,7 +727,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma ctex = screen.framebuffer.buffers[:color] # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! - # GLFW.PollEvents() + pollevents(screen) # keep current buffer size to allows larger-than-window renders render_frame(screen, resize_buffers=false) # let it render if screen.config.visible @@ -912,9 +926,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..4c08416ffda 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,70 @@ 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 + +@reference_test "Complex Lighting - Ambient + SpotLights + PointLights" begin + angle2pos(phi) = Point3f(cosd(phi), sind(phi), 0) + lights = [ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + SpotLight(RGBf(2,0,0), angle2pos(0), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + SpotLight(RGBf(0,2,0), angle2pos(120), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + SpotLight(RGBf(0,0,2), angle2pos(240), Vec3f(0, 0, -1), Vec2f(pi/5, pi/4)), + PointLight(RGBf(1,1,1), Point3f(-4, -4, -2.5), 10.0), + PointLight(RGBf(1,1,0), Point3f(-4, 4, -2.5), 10.0), + PointLight(RGBf(1,0,1), Point3f( 4, 4, -2.5), 10.0), + PointLight(RGBf(0,1,1), Point3f( 4, -4, -2.5), 10.0), + ] + + scene = Scene(size = (400, 400), camera = cam3d!, lights = lights) + mesh!( + scene, + Rect3f(Point3f(-10, -10, -2.99), Vec3f(20, 20, 0.02)), + color = :white, shading = MultiLightShading, specular = Vec3f(0) + ) + center!(scene) + update_cam!(scene, Vec3f(0, 0, 10), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + scene +end + +@reference_test "Complex Lighting - DirectionalLight + specular reflection" begin + angle2dir(phi) = Vec3f(cosd(phi), sind(phi), -2) + lights = [ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + DirectionalLight(RGBf(1,0,0), angle2dir(0)), + DirectionalLight(RGBf(0,1,0), angle2dir(120)), + DirectionalLight(RGBf(0,0,1), angle2dir(240)), + ] + + scene = Scene(size = (400, 400), camera = cam3d!, center = false, lights = lights, backgroundcolor = :black) + mesh!( + scene, Sphere(Point3f(0), 1f0), color = :white, shading = MultiLightShading, + specular = Vec3f(1), shininess = 16f0 + ) + update_cam!(scene, Vec3f(0, 0, 3), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + scene +end + + +@reference_test "RectLight" begin + lights = Makie.AbstractLight[ + RectLight(RGBf(0.5, 0, 0), Point3f(-0.5, -1, 2), Vec3f(3, 0, 0), Vec3f(0, 3, 0)), + RectLight(RGBf(0, 0.5, 0), Rect2f(-1, 1, 1, 3)), + RectLight(RGBf(0, 0, 0.5), Point3f( 1, 0.5, 2), Vec3f(3, 0, 0), Vec3f(0, 3, 0)), + RectLight(RGBf(0.5, 0.5, 0.5), Point3f( 1, -1, 2), Vec3f(3, 0, 0), Vec3f(0, 3, 0), Vec3f(-0.3, 0.3, -1)), + ] + # Test transformations + translate!(lights[2], Vec3f(-1, 1, 2)) # translate to by default + scale!(lights[2], 3, 1) + + scene = Scene(lights = lights, camera = cam3d!, size = (400, 400)) + p = mesh!(scene, Rect3f(Point3f(-10, -10, 0.01), Vec3f(20, 20, 0.02)), color = :white) + update_cam!(scene, Vec3f(0, 0, 7), Vec3f(0, 0, 0), Vec3f(0, 1, 0)) + + scene +end \ No newline at end of file diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 05c4f31ff71..bb7b7b650cf 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -2,7 +2,7 @@ using GLMakie.Makie: getscreen function project_sp(scene, point) point_px = Makie.project(scene, point) - offset = Point2f(minimum(pixelarea(scene)[])) + offset = Point2f(minimum(viewport(scene)[])) return point_px .+ offset end @@ -213,7 +213,7 @@ end GLMakie.closeall() set_theme!() screens = map(1:10) do i - fig = Figure(resolution=(500, 500)) + fig = Figure(size=(500, 500)) rng = Random.MersenneTwister(0) ax, pl = image(fig[1, 1], 0..1, 0..1, rand(rng, 1000, 1000)) scatter!(ax, rand(rng, Point2f, 1000), color=:red) @@ -250,7 +250,7 @@ end @test screen.root_scene === nothing @test screen.rendertask === nothing - @test (Base.summarysize(screen) / 10^6) < 1.2 + @test (Base.summarysize(screen) / 10^6) < 1.22 end # All should go to pool after close @test all(x-> x in GLMakie.SCREEN_REUSE_POOL, screens) @@ -269,7 +269,7 @@ end N = 51 x = collect(range(0.0, 2π, length=N)) y = sin.(x) - fig, ax, pl = scatter(x, y, figure = (; resolution = (W, H))); + fig, ax, pl = scatter(x, y, figure = (; size = (W, H))); hidedecorations!(ax) # On OSX, the native window size has an underlying scale factor that we need to account diff --git a/MakieCore/Project.toml b/MakieCore/Project.toml index 98b07d16e10..2443de14e01 100644 --- a/MakieCore/Project.toml +++ b/MakieCore/Project.toml @@ -1,7 +1,7 @@ name = "MakieCore" uuid = "20f20a25-4f0e-4fdf-b5d1-57303727442b" authors = ["Simon Danisch"] -version = "0.6.8" +version = "0.6.9" [deps] Observables = "510215fc-4207-5dde-b226-833fc4488ee2" @@ -9,6 +9,7 @@ REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" [compat] Observables = "0.5.1" +REPL = "1" julia = "1" [extras] 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/basic_plots.jl b/MakieCore/src/basic_plots.jl index dad4f98ef28..d2262238edc 100644 --- a/MakieCore/src/basic_plots.jl +++ b/MakieCore/src/basic_plots.jl @@ -79,16 +79,17 @@ end """ ### 3D shading attributes -- `shading = true` enables lighting. -- `diffuse::Vec3f = Vec3f(0.4)` sets how strongly the red, green and blue channel react to diffuse (scattered) light. -- `specular::Vec3f = Vec3f(0.2)` sets how strongly the object reflects light in the red, green and blue channels. +- `shading = automatic` sets the lighting algorithm used. Options are `NoShading` (no lighting), `FastShading` (AmbientLight + PointLight) or `MultiLightShading` (Multiple lights, GLMakie only). Note that this does not affect RPRMakie. +- `diffuse::Vec3f = Vec3f(1.0)` sets how strongly the red, green and blue channel react to diffuse (scattered) light. +- `specular::Vec3f = Vec3f(0.4)` sets how strongly the object reflects light in the red, green and blue channels. - `shininess::Real = 32.0` sets how sharp the reflection is. +- `backlight::Float32 = 0f0` sets a weight for secondary light calculation with inverted normals. - `ssao::Bool = false` adjusts whether the plot is rendered with ssao (screen space ambient occlusion). Note that this only makes sense in 3D plots and is only applicable with `fxaa = true`. """ function shading_attributes!(attr) - attr[:shading] = true - attr[:diffuse] = 0.4 - attr[:specular] = 0.2 + attr[:shading] = automatic + attr[:diffuse] = 1.0 + attr[:specular] = 0.4 attr[:shininess] = 32.0f0 attr[:backlight] = 0f0 attr[:ssao] = false @@ -466,6 +467,7 @@ Plots one or multiple texts passed via the `text` keyword. - `glowwidth::Real = 0` sets the size of a glow effect around the marker. - `glowcolor::Union{Symbol, <:Colorant} = (:black, 0)` sets the color of the glow effect. - `word_wrap_with::Real = -1` specifies a linewidth limit for text. If a word overflows this limit, a newline is inserted before it. Negative numbers disable word wrapping. +- `transform_marker::Bool = false` controls whether the model matrix (without translation) applies to the glyph itself, rather than just the positions. (If this is true, `scale!` and `rotate!` will affect the text glyphs.) $(Base.Docs.doc(colormap_attributes!)) @@ -487,7 +489,7 @@ $(Base.Docs.doc(MakieCore.generic_plot_attributes!)) justification = automatic, lineheight = 1.0, markerspace = :pixel, - + transform_marker = false, offset = (0.0, 0.0), word_wrap_width = -1, ) @@ -537,7 +539,7 @@ $(Base.Docs.doc(MakieCore.generic_plot_attributes!)) strokewidth = theme(scene, :patchstrokewidth), linestyle = nothing, - shading = false, + shading = NoShading, fxaa = true, cycle = [:color => :patchcolor], diff --git a/MakieCore/src/conversion.jl b/MakieCore/src/conversion.jl index fdfff29f2d5..2b2b9635d2d 100644 --- a/MakieCore/src/conversion.jl +++ b/MakieCore/src/conversion.jl @@ -48,40 +48,40 @@ conversion_trait(::Type{<: Text}) = PointBased() GridBased is an abstract conversion trait for data that exists on a grid. -Child types: [`VertexBasedGrid`](@ref), [`CellBasedGrid`](@ref) +Child types: [`VertexGrid`](@ref), [`CellGrid`](@ref) See also: [`ImageLike`](@ref) Used for: Scatter, Lines """ abstract type GridBased <: ConversionTrait end """ - VertexBasedGrid() <: GridBased <: ConversionTrait + VertexGrid() <: GridBased <: ConversionTrait -Plots with the `VertexBasedGrid` trait convert their input data to +Plots with the `VertexGrid` trait convert their input data to `(xs::Vector{Float32}, ys::Vector{Float32}, zs::Matrix{Float32})` such that `(length(xs), length(ys)) == size(zs)`, or `(xs::Matrix{Float32}, ys::Matrix{Float32}, zs::Matrix{Float32})` such that `size(xs) == size(ys) == size(zs)`. -See also: [`CellBasedGrid`](@ref), [`ImageLike`](@ref) +See also: [`CellGrid`](@ref), [`ImageLike`](@ref) Used for: Surface """ -struct VertexBasedGrid <: GridBased end -conversion_trait(::Type{<: Surface}) = VertexBasedGrid() +struct VertexGrid <: GridBased end +conversion_trait(::Type{<: Surface}) = VertexGrid() """ - CellBasedGrid() <: GridBased <: ConversionTrait + CellGrid() <: GridBased <: ConversionTrait -Plots with the `CellBasedGrid` trait convert their input data to +Plots with the `CellGrid` trait convert their input data to `(xs::Vector{Float32}, ys::Vector{Float32}, zs::Matrix{Float32})` such that `(length(xs), length(ys)) == size(zs) .+ 1`. After the conversion the x and y values represent the edges of cells corresponding to z values. -See also: [`VertexBasedGrid`](@ref), [`ImageLike`](@ref) +See also: [`VertexGrid`](@ref), [`ImageLike`](@ref) Used for: Heatmap """ -struct CellBasedGrid <: GridBased end -conversion_trait(::Type{<: Heatmap}) = CellBasedGrid() +struct CellGrid <: GridBased end +conversion_trait(::Type{<: Heatmap}) = CellGrid() """ ImageLike() <: ConversionTrait @@ -90,18 +90,13 @@ Plots with the `ImageLike` trait convert their input data to `(xs::Interval, ys::Interval, zs::Matrix{Float32})` where xs and ys mark the limits of a quad containing zs. -See also: [`CellBasedGrid`](@ref), [`VertexBasedGrid`](@ref) +See also: [`CellGrid`](@ref), [`VertexGrid`](@ref) Used for: Image """ struct ImageLike <: ConversionTrait end conversion_trait(::Type{<: Image}) = ImageLike() # Rect2f(xmin, ymin, xmax, ymax) -# Deprecations -function ContinuousSurface() - error("ContinuousSurface has been deprecated. Use `ImageLike()` or `VertexBasedGrid()` instead.") -end -@deprecate DiscreteSurface CellBasedGrid() struct VolumeLike <: ConversionTrait end -conversion_trait(::Type{<: Volume}) = VolumeLike() \ No newline at end of file +conversion_trait(::Type{<: Volume}) = VolumeLike() 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/MakieCore/src/types.jl b/MakieCore/src/types.jl index 7dba1bf60ea..60a40aef6a1 100644 --- a/MakieCore/src/types.jl +++ b/MakieCore/src/types.jl @@ -122,3 +122,9 @@ end Billboard() = Billboard(0f0) Billboard(angle::Real) = Billboard(Float32(angle)) Billboard(angles::Vector) = Billboard(Float32.(angles)) + +@enum ShadingAlgorithm begin + NoShading + FastShading + MultiLightShading +end \ No newline at end of file diff --git a/NEWS.md b/NEWS.md index f0f6e805dc2..477438b6de5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -7,12 +7,20 @@ [#2544](https://github.com/MakieOrg/Makie.jl/pull/2544) - Fixed an issue where NaN was interpreted as zero when rendering `surface` through CairoMakie. [#2598](https://github.com/MakieOrg/Makie.jl/pull/2598) - Improved 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) -- Refactored the `SurfaceLike` family of traits into `VertexBasedGrid`, `CellBasedGrid` and `ImageLike`. [#3106](https://github.com/MakieOrg/Makie.jl/pull/3106) +- Refactored the `SurfaceLike` family of traits into `VertexBasedGrid`, `CellGrid` and `ImageLike`. [#3106](https://github.com/MakieOrg/Makie.jl/pull/3106) +- Added `shading = :verbose` in GLMakie to allow for multiple light sources. Also added more light types, fixed light directions for the previous lighting model (now `shading = :fast`) and adjusted `backlight` to affect normals. [#3246](https://github.com/MakieOrg/Makie.jl/pull/3246) +- Deprecated the `resolution` keyword in favor of `size` to reflect that this value is not a pixel resolution anymore [#3343](https://github.com/MakieOrg/Makie.jl/pull/3343). ## master -- Fix grouping of a zero-height bar in `barplot`. Now a zero-height bar shares the same properties of the previous bar, and if the bar is the first one, its height is treated as positive if and only if there exists a bar of positive height or all bars are zero-height. [#3058](https://github.com/MakieOrg/Makie.jl/pull/3058) -- Fixed a bug where Axis still consumes scroll events when interactions are disabled [#3272](https://github.com/MakieOrg/Makie.jl/pull/3272) +- Added `cornerradius` attribute to `Box` for rounded corners [#3346](https://github.com/MakieOrg/Makie.jl/pull/3346). + +## v0.19.12 + +- Fix grouping of a zero-height bar in `barplot`. Now a zero-height bar shares the same properties of the previous bar, and if the bar is the first one, its height is treated as positive if and only if there exists a bar of positive height or all bars are zero-height [#3058](https://github.com/MakieOrg/Makie.jl/pull/3058). +- Fixed a bug where Axis still consumes scroll events when interactions are disabled [#3272](https://github.com/MakieOrg/Makie.jl/pull/3272). +- Added `cornerradius` attribute to `Box` for rounded corners [#3308](https://github.com/MakieOrg/Makie.jl/pull/3308). +- Upgraded `StableHashTraits` from 1.0 to 1.1 [#3309](https://github.com/MakieOrg/Makie.jl/pull/3309). ## v0.19.11 @@ -22,8 +30,8 @@ ## v0.19.10 -- Fix bugs with Colorbar in recipes, add new API for creating a recipe colorbar and introduce experimental support for Categorical colormaps [#3090](https://github.com/MakieOrg/Makie.jl/pull/3090). -- Add experimental Datashader implementation [#2883](https://github.com/MakieOrg/Makie.jl/pull/2883). +- Fixed bugs with Colorbar in recipes, add new API for creating a recipe colorbar and introduce experimental support for Categorical colormaps [#3090](https://github.com/MakieOrg/Makie.jl/pull/3090). +- Added experimental Datashader implementation [#2883](https://github.com/MakieOrg/Makie.jl/pull/2883). - [Breaking] Changed the default order Polar arguments to (theta, r). [#3154](https://github.com/MakieOrg/Makie.jl/pull/3154) - General improvements to `PolarAxis`: full rlimtis & thetalimits, more controls and visual tweaks. See pr for more details.[#3154](https://github.com/MakieOrg/Makie.jl/pull/3154) diff --git a/Project.toml b/Project.toml index a8b103ef16c..b243978f9f0 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" @@ -34,7 +35,6 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" -Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" MathTeXEngine = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" @@ -69,6 +69,7 @@ Contour = "0.5, 0.6" DelaunayTriangulation = "0.8.7" Distributions = "0.17, 0.18, 0.19, 0.20, 0.21, 0.22, 0.23, 0.24, 0.25" DocStringExtensions = "0.8, 0.9" +Downloads = "1, 1.6" FileIO = "1.6" FixedPointNumbers = "0.6, 0.7, 0.8" Formatting = "0.4" @@ -82,10 +83,9 @@ Isoband = "0.1" KernelDensity = "0.5, 0.6" LaTeXStrings = "1.2" MacroTools = "0.5" -MakieCore = "=0.6.8" -Match = "1.1" +MakieCore = "=0.6.9" MathTeXEngine = "0.5" -Observables = "0.5.3" +Observables = "0.5.5" OffsetArrays = "1" Packing = "0.5" PlotUtils = "1" @@ -96,10 +96,20 @@ Setfield = "1" ShaderAbstractions = "0.4" Showoff = "0.3, 1.0.2" SignedDistanceFields = "0.4" -StableHashTraits = "1" +StableHashTraits = "1.1" +Statistics = "1, 1.6" StatsBase = "0.31, 0.32, 0.33, 0.34" StatsFuns = "0.9, 1.0" StructArrays = "0.3, 0.4, 0.5, 0.6" TriplotBase = "=0.1.0" UnicodeFun = "0.4" julia = "1.3" +Base64 = "1.0, 1.6" +CRC32c = "1.0, 1.6" +InteractiveUtils = "1.0, 1.6" +LinearAlgebra = "1.0, 1.6" +Markdown = "1.0, 1.6" +Printf = "1.0, 1.6" +REPL = "1.0, 1.6" +Random = "1.0, 1.6" +SparseArrays = "1.0, 1.6" diff --git a/README.md b/README.md index 49c5a3ccaa1..7c83c351647 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ The following examples are supposed to be self-explanatory. For further informat x = 1:0.1:10 fig = lines(x, x.^2; label = "Parabola", axis = (; xlabel = "x", ylabel = "y", title ="Title"), - figure = (; resolution = (800,600), fontsize = 22)) + figure = (; size = (800,600), fontsize = 22)) axislegend(; position = :lt) save("./assets/parabola.png", fig) fig @@ -167,8 +167,8 @@ with_theme(palette = (; patchcolor = cgrad(cmap, alpha=0.45))) do band!(x, sin.(x), approx .+= x .^ 5 / 120; label = L"n = 2") band!(x, sin.(x), approx .+= -x .^ 7 / 5040; label = L"n = 3") limits!(-3.8, 3.8, -1.5, 1.5) - axislegend(; position = :ct, bgcolor = (:white, 0.75), framecolor = :orange) - save("./assets/approxsin.png", fig, resolution = (800, 600)) + axislegend(; position = :ct, backgroundcolor = (:white, 0.75), framecolor = :orange) + save("./assets/approxsin.png", fig, size = (800, 600)) fig end ``` diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 6878df654fd..b02bc403b09 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -13,12 +13,14 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RadeonProRender = "27029320-176d-4a42-b57d-56729d2ad457" [compat] -julia = "1.3" Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.1" Makie = "=0.20.0" RadeonProRender = "0.3.0" +julia = "1.3" +LinearAlgebra = "1.0, 1.6" +Printf = "1.0, 1.6" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/RPRMakie/examples/bars.jl b/RPRMakie/examples/bars.jl index a6be9abe0e6..993be72e3eb 100644 --- a/RPRMakie/examples/bars.jl +++ b/RPRMakie/examples/bars.jl @@ -3,7 +3,7 @@ using Colors, FileIO, ImageShow using Colors: N0f8 RPRMakie.activate!(plugin=RPR.Northstar, resource=RPR.GPU0) -fig = Figure(; resolution=(800, 600), fontsize=26) +fig = Figure(; size=(800, 600), fontsize=26) radiance = 10000 lights = [EnvironmentLight(0.5, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(0, 0, 20), RGBf(radiance, radiance, radiance))] diff --git a/RPRMakie/examples/eart_topographie_sphere.jl b/RPRMakie/examples/eart_topographie_sphere.jl index 6bef6b10abc..e02ee0a2fc9 100644 --- a/RPRMakie/examples/eart_topographie_sphere.jl +++ b/RPRMakie/examples/eart_topographie_sphere.jl @@ -39,7 +39,7 @@ xetopo, yetopo, zetopo = lonlat3D(lonext, lat, dataext) begin r = 30 lights = [PointLight(Vec3f(2, 1, 3), RGBf(r, r, r))] - fig = Figure(; resolution=(1200, 1200), backgroundcolor=:black) + fig = Figure(; size=(1200, 1200), backgroundcolor=:black) ax = LScene(fig[1, 1]; show_axis=false)#, scenekw=(lights=lights,)) pltobj = surface!(ax, xetopo, yetopo, zetopo; color=dataext, colormap=:hot, colorrange=(-6000, 5000)) cam = cameracontrols(ax.scene) diff --git a/RPRMakie/examples/earth_topography.jl b/RPRMakie/examples/earth_topography.jl index 7f52cc43ca2..ceaf99ef70a 100644 --- a/RPRMakie/examples/earth_topography.jl +++ b/RPRMakie/examples/earth_topography.jl @@ -28,7 +28,7 @@ function glow_material(data_normed) end RPRMakie.activate!(iterations=32, plugin=RPR.Northstar) -fig = Figure(; resolution=(2000, 800)) +fig = Figure(; size=(2000, 800)) radiance = 30000 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(0, 100, 100), RGBf(radiance, radiance, radiance))] diff --git a/RPRMakie/examples/lego.jl b/RPRMakie/examples/lego.jl index cb364038271..fa9956c947d 100644 --- a/RPRMakie/examples/lego.jl +++ b/RPRMakie/examples/lego.jl @@ -69,7 +69,7 @@ lights = [ EnvironmentLight(1.5, rotl90(load(assetpath("sunflowers_1k.hdr"))')), PointLight(Vec3f(50, 0, 200), RGBf(radiance, radiance, radiance*1.1)), ] -s = Scene(resolution=(500, 500), lights=lights) +s = Scene(size=(500, 500), lights=lights) cam3d!(s) c = cameracontrols(s) diff --git a/RPRMakie/examples/lines.jl b/RPRMakie/examples/lines.jl index b3f2624e7f9..2fed509dc2d 100644 --- a/RPRMakie/examples/lines.jl +++ b/RPRMakie/examples/lines.jl @@ -12,7 +12,7 @@ function box!(ax, size) end begin - fig = Figure(; resolution=(1000, 1000)) + fig = Figure(; size=(1000, 1000)) radiance = 100 lights = Makie.AbstractLight[PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] ax = LScene(fig[1, 1]; scenekw=(; lights=lights), show_axis=false) diff --git a/RPRMakie/examples/material_x.jl b/RPRMakie/examples/material_x.jl index 415f87c7e7a..757fd825c90 100644 --- a/RPRMakie/examples/material_x.jl +++ b/RPRMakie/examples/material_x.jl @@ -7,7 +7,7 @@ img = begin radiance = 1000 lights = [EnvironmentLight(0.5, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(5), RGBf(radiance, radiance, radiance * 1.1))] - fig = Figure(; resolution=(1500, 700)) + fig = Figure(; size=(1500, 700)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=500, resource=RPR.GPU0) matsys = screen.matsys diff --git a/RPRMakie/examples/materials.jl b/RPRMakie/examples/materials.jl index ccdcb2209d7..80f7d9a0f2e 100644 --- a/RPRMakie/examples/materials.jl +++ b/RPRMakie/examples/materials.jl @@ -6,7 +6,7 @@ img = begin radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] - fig = Figure(; resolution=(1500, 700)) + fig = Figure(; size=(1500, 700)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=1000) diff --git a/RPRMakie/examples/opengl_interop.jl b/RPRMakie/examples/opengl_interop.jl index 04e7c2ccd10..6141b833658 100644 --- a/RPRMakie/examples/opengl_interop.jl +++ b/RPRMakie/examples/opengl_interop.jl @@ -11,9 +11,9 @@ radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] -fig = Figure(; resolution=(1500, 1000)) +fig = Figure(; size=(1500, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) -screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Northstar, resource=RPR.RPR_CREATION_FLAGS_ENABLE_GPU1) +screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Northstar, resource=RPR.RPR_CREATION_FLAGS_ENABLE_GPU0) material = RPR.UberMaterial(screen.matsys) surface!(ax, f.(u, v'), g.(u, v'), h.(u, v'); ambient=Vec3f(0.5), diffuse=Vec3f(1), specular=0.5, @@ -81,11 +81,12 @@ GLMakie.activate!(inline=false) display(fig; inline=false, backend=GLMakie) RPRMakie.activate!(iterations=1, plugin=RPR.Northstar, resource=RPR.GPU0) context, task = RPRMakie.replace_scene_rpr!(ax.scene, screen; refresh=refresh); +nothing # Change light parameters interactively -begin - lights[1].intensity[] = 1.5 - lights[2].radiance[] = RGBf(1000, 1000, 1000) - lights[2].position[] = Vec3f(3, 10, 10) - notify(refresh) -end +# begin +# lights[1].intensity[] = 1.5 +# lights[2].radiance[] = RGBf(1000, 1000, 1000) +# lights[2].position[] = Vec3f(3, 10, 10) +# notify(refresh) +# end diff --git a/RPRMakie/examples/sea_cables.jl b/RPRMakie/examples/sea_cables.jl index 5e1185b3653..0f78515347f 100644 --- a/RPRMakie/examples/sea_cables.jl +++ b/RPRMakie/examples/sea_cables.jl @@ -50,7 +50,7 @@ earth_img = load(Downloads.download("https://upload.wikimedia.org/wikipedia/comm # the actual plot ! RPRMakie.activate!(; iterations=100) scene = with_theme(theme_dark()) do - fig = Figure(; resolution=(1000, 1000)) + fig = Figure(; size=(1000, 1000)) radiance = 30 lights = [EnvironmentLight(0.5, load(RPR.assetpath("starmap_4k.tif"))), PointLight(Vec3f(1, 1, 3), RGBf(radiance, radiance, radiance))] diff --git a/RPRMakie/examples/volume.jl b/RPRMakie/examples/volume.jl index 7c4a86ddb63..c44b59b7dc1 100644 --- a/RPRMakie/examples/volume.jl +++ b/RPRMakie/examples/volume.jl @@ -8,7 +8,7 @@ brain = Float32.(niread(Makie.assetpath("brain.nii.gz")).raw) radiance = 5000 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] -fig = Figure(; resolution=(1000, 1000)) +fig = Figure(; size=(1000, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) Makie.volume!(ax, 0..3, 0..3.78, 0..3.18, brain, algorithm=:absorption, absorption=0.3) display(ax.scene; iterations=5000) diff --git a/RPRMakie/src/RPRMakie.jl b/RPRMakie/src/RPRMakie.jl index 74bf5fc7d4d..b83ae20599e 100644 --- a/RPRMakie/src/RPRMakie.jl +++ b/RPRMakie/src/RPRMakie.jl @@ -34,11 +34,16 @@ function ScreenConfig(iterations::Int, max_recursion::Int, render_resource, rend ) end + + include("scene.jl") include("lines.jl") include("meshes.jl") include("volume.jl") +Makie.apply_screen_config!(screen::RPRMakie.Screen, ::RPRMakie.ScreenConfig, args...) = screen +Base.empty!(::RPRMakie.Screen) = nothing + """ RPRMakie.activate!(; screen_config...) diff --git a/RPRMakie/src/lines.jl b/RPRMakie/src/lines.jl index a6c91665dd4..5f85f8db0c3 100644 --- a/RPRMakie/src/lines.jl +++ b/RPRMakie/src/lines.jl @@ -31,7 +31,9 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Lines) end function to_rpr_object(context, matsys, scene, plot::Makie.LineSegments) - points = decompose(Point3f, to_value(plot[1])) + arg1 = to_value(plot[1]) + isempty(arg1) && return nothing + points = decompose(Point3f, arg1) segments = TupleView{2,2}(RPR.rpr_int(0):RPR.rpr_int(length(points) - 1)) indices = RPR.rpr_int[] diff --git a/RPRMakie/src/meshes.jl b/RPRMakie/src/meshes.jl index 2c3c8af71d3..43bb3862d17 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -1,5 +1,5 @@ function extract_material(matsys, plot) - material = if haskey(plot, :material) + if haskey(plot, :material) if plot.material isa Attributes return RPR.Material(matsys, Dict(map(((k,v),)-> k => to_value(v), plot.material))) else @@ -11,26 +11,25 @@ function extract_material(matsys, plot) end function mesh_material(context, matsys, plot, color_obs = plot.color) - specular = plot.specular[] - shininess = plot.shininess[] color = to_value(color_obs) color_signal = if color isa AbstractMatrix{<:Number} tex = RPR.ImageTextureMaterial(matsys) - map(color_obs, plot.colormap, plot.colorrange) do color, cmap, crange - color_interp = Makie.interpolated_getindex.((to_colormap(cmap),), color, (crange,)) + calc_color = to_value(plot.calculated_colors) + lift(plot, color_obs, plot.colormap, plot.colorrange) do color, cmap, crange + color_interp = to_color(calc_color) img = RPR.Image(context, collect(color_interp')) tex.data = img return tex end elseif color isa AbstractMatrix{<:Colorant} tex = RPR.ImageTextureMaterial(matsys) - map(color_obs) do color + lift(plot, color_obs) do color img = RPR.Image(context, Makie.el32convert(color')) tex.data = img return tex end elseif color isa Colorant || color isa Union{String,Symbol} - map(to_color, color_obs) + lift(to_color, plot, color_obs) elseif color isa Nothing # ignore! color_obs @@ -39,7 +38,7 @@ function mesh_material(context, matsys, plot, color_obs = plot.color) end material = extract_material(matsys, plot) - map(color_signal) do color + on(plot, color_signal; update=true) do color if !isnothing(color) && hasproperty(material, :color) material.color = color end @@ -76,11 +75,9 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) push!(instances, inst) end - color = to_color(plot.color[]) - if color isa AbstractVector{<:Number} - cmap = to_colormap(plot.colormap[]) - crange = plot.colorrange[] - color_from_num = Makie.interpolated_getindex.((cmap,), color, (crange,)) + color = plot.calculated_colors[] + if color isa Makie.ColorMapping + color_from_num = to_color(color) object_id = RPR.InputLookupMaterial(matsys) object_id.value = RPR.RPR_MATERIAL_NODE_LOOKUP_OBJECT_ID @@ -90,9 +87,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) material.color = tex elseif color isa AbstractMatrix{<:Number} - cmap = to_colormap(plot.colormap[]) - crange = plot.colorrange[] - color_from_num = Makie.interpolated_getindex.((cmap,), color, (crange,)) + color_from_num = to_color(color) object_id = RPR.InputLookupMaterial(matsys) object_id.value = RPR.RPR_MATERIAL_NODE_LOOKUP_OBJECT_ID @@ -140,8 +135,8 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Surface) z = plot[3] function grid(x, y, z, trans) + space = to_value(get(plot, :space, :data)) g = map(CartesianIndices(z)) do i - space = to_value(get(plot, :space, :data)) p = Point3f(Makie.get_dim(x, i, 1, size(z)), Makie.get_dim(y, i, 2, size(z)), z[i]) return Makie.apply_transform(trans, p, space) end diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index 07e2d491961..8fe4c3ce43f 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -43,26 +43,87 @@ function insert_plots!(context, matsys, scene, mscene::Makie.Scene, @nospecializ end end -function to_rpr_light(context::RPR.Context, light::Makie.PointLight) +to_rpr_light(ctx, rpr_scene, light, scene) = to_rpr_light(ctx, rpr_scene, light) + +# TODO attenuation +function to_rpr_light(context::RPR.Context, matsys, light::Makie.PointLight) pointlight = RPR.PointLight(context) map(light.position) do pos transform!(pointlight, Makie.translationmatrix(pos)) end - map(light.radiance) do r - setradiantpower!(pointlight, red(r), green(r), blue(r)) + map(light.color) do c + setradiantpower!(pointlight, red(c), green(c), blue(c)) end return pointlight end -function to_rpr_light(context::RPR.Context, light::Makie.AmbientLight) +# TODO: Move to RadeonProRender.jl +function RPR.RPR.rprContextCreateSpotLight(context) + out_light = Ref{RPR.rpr_light}() + RPR.RPR.rprContextCreateSpotLight(context, out_light) + return out_light[] +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.DirectionalLight, scene) + directionallight = RPR.DirectionalLight(context) + map(light.direction) do dir + if light.camera_relative + T = inv(scene.camera.view[][Vec(1,2,3), Vec(1,2,3)]) + dir = normalize(T * dir) + else + dir = normalize(dir) + end + quart = Makie.rotation_between(dir, Vec3f(0,0,-1)) + transform!(directionallight, Makie.rotationmatrix4(quart)) + end + map(light.color) do c + setradiantpower!(directionallight, red(c), green(c), blue(c)) + end + return directionallight +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.RectLight) + mesh = lift(light.position, light.u1, light.u2) do center, u1, u2 + pos = center - 0.5u1 - 0.5u2 + points = Point3f[pos, pos + u1, pos + u1 + u2, pos + u2] + faces = [GLTriangleFace(1, 2, 3), GLTriangleFace(1, 3, 4)] + return GeometryBasics.Mesh(points, faces) + end + rpr_mesh = RPR.Shape(context, mesh[]) env_img = fill(light.color[], 1, 1) img = RPR.Image(context, env_img) env_light = RPR.EnvironmentLight(context) set!(env_light, img) + setintensityscale!(env_light, 0.1) + # TODO, this doesn't seem to properly create a rectangular portal -.- + setportal!(rpr_scene, env_light, rpr_mesh) return env_light end -function to_rpr_light(context::RPR.Context, light::Makie.EnvironmentLight) +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.SpotLight) + spotlight = RPR.SpotLight(context) + map(light.position, light.direction) do pos, dir + quart = Makie.rotation_between(dir, Vec3f(0,0,-1)) + transform!(spotlight, Makie.translationmatrix(pos) * Makie.rotationmatrix4(quart)) + end + map(light.color) do c + setradiantpower!(spotlight, red(c), green(c), blue(c)) + end + map(light.angles) do (inner, outer) + RadeonProRender.RPR.rprSpotLightSetConeShape(spotlight, inner, outer) + end + return spotlight +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.AmbientLight) + env_img = fill(light.color[], 1, 1) + img = RPR.Image(context, env_img) + env_light = RPR.EnvironmentLight(context) + set!(env_light, img) + return env_light +end + +function to_rpr_light(context::RPR.Context, rpr_scene, light::Makie.EnvironmentLight) env_light = RPR.EnvironmentLight(context) last_img = RPR.Image(context, light.image[]) set!(env_light, last_img) @@ -90,7 +151,7 @@ function to_rpr_scene(context::RPR.Context, matsys, mscene::Makie.Scene) RPR.rprSceneSetBackgroundImage(scene, img) end for light in mscene.lights - rpr_light = to_rpr_light(context, light) + rpr_light = to_rpr_light(context, scene, light, mscene) push!(scene, rpr_light) end @@ -120,23 +181,31 @@ function replace_scene_rpr!(scene::Makie.Scene, screen=Screen(scene); refresh=Ob # translate!(im, 0, 0, 1000) - clear = true + clear = Threads.Atomic{Bool}(true) onany(refresh, cam.projectionview) do _, _ - clear = true + clear[] = true return end RPR.rprContextSetParameterByKey1u(context, RPR.RPR_CONTEXT_ITERATIONS, 1) cam_values = (;) + task = @async while isopen(scene) + t = time() cam_values = update_rpr_camera!(cam_values, camera, cam_controls, cam) - framebuffer2 = render(screen; clear=clear, iterations=1) - if clear - clear = false + framebuffer2 = render(screen; clear=clear[], iterations=1) + if clear[] + clear[] = false end data = RPR.get_data(framebuffer2) im[1] = reverse(reshape(data, screen.fb_size); dims=2) - sleep(1/10) + tframe = time() - t + to_sleep = (1/10) - tframe + if to_sleep < 0.0 + yield() + else + sleep(to_sleep) + end end return context, task, rpr_scene end @@ -184,12 +253,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/RPRMakie/test/lines.jl b/RPRMakie/test/lines.jl index e36c4698486..9dba114232c 100644 --- a/RPRMakie/test/lines.jl +++ b/RPRMakie/test/lines.jl @@ -12,7 +12,7 @@ begin emissive = RPR.EmissiveMaterial(matsys) diffuse = RPR.DiffuseMaterial(matsys) - fig = Figure(resolution=(1000, 1000)) + fig = Figure(size=(1000, 1000)) ax = LScene(fig[1, 1], show_axis=false) for i in 4:4:12 n = i + 1 diff --git a/ReferenceTests/src/database.jl b/ReferenceTests/src/database.jl index 081478b2311..090a6ada7be 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)) + Makie.set_theme!(; size=(500, 500), + 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/attributes.jl b/ReferenceTests/src/tests/attributes.jl index 845a1d3e7e0..5b89c3d86bd 100644 --- a/ReferenceTests/src/tests/attributes.jl +++ b/ReferenceTests/src/tests/attributes.jl @@ -27,7 +27,7 @@ end end @reference_test "shading" begin - mesh(Sphere(Point3f(0), 1f0), color=:orange, shading=false) + mesh(Sphere(Point3f(0), 1f0), color=:orange, shading=NoShading) end @reference_test "visible" begin diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index a8ebbc782c5..8cc85e7e075 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -13,7 +13,7 @@ end end @reference_test "heatmap_interpolation" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) data = RNG.rand(32, 32) # the grayscale heatmap hides the problem that interpolation based on values # in GLMakie looks different than interpolation based on colors in CairoMakie @@ -110,7 +110,7 @@ end 5 8 9; ] color = [0.0, 0.0, 0.0, 0.0, -0.375, 0.0, 0.0, 0.0, 0.0] - fig, ax, meshplot = mesh(coordinates, connectivity, color=color, shading=false) + fig, ax, meshplot = mesh(coordinates, connectivity, color=color, shading=NoShading) wireframe!(ax, meshplot[1], color=(:black, 0.6), linewidth=3) fig end @@ -118,7 +118,7 @@ end @reference_test "colored triangle" begin mesh( [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)], color=[:red, :green, :blue], - shading=false + shading=NoShading ) end @@ -360,7 +360,7 @@ end @reference_test "Simple pie chart" begin - fig = Figure(resolution=(800, 800)) + fig = Figure(size=(800, 800)) pie(fig[1, 1], 1:5, color=collect(1:5), axis=(;aspect=DataAspect())) fig end @@ -420,7 +420,7 @@ end @reference_test "space 2D" begin # This should generate a regular grid with text in a circle in a box. All # sizes and positions are scaled to be equal across all options. - fig = Figure(resolution = (700, 700)) + fig = Figure(size = (700, 700)) ax = Axis(fig[1, 1], width = 600, height = 600) spaces = (:data, :pixel, :relative, :clip) xs = [ @@ -435,7 +435,7 @@ end s = 1.5scales[i] mesh!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, - shading = false, color = :blue) + shading = NoShading, color = :blue) lines!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, linewidth = 2, color = :red) @@ -462,7 +462,7 @@ end # - (x -> data) row should have stretched circle and text ain x direction # - (not data -> data) should keep aspect ratio for mesh and lines # - (data -> x) should be slightly missaligned with (not data -> x) - fig = Figure(resolution = (700, 700)) + fig = Figure(size = (700, 700)) ax = Axis(fig[1, 1], width = 600, height = 600) spaces = (:data, :pixel, :relative, :clip) xs = [ @@ -477,7 +477,7 @@ end s = 1.5scales[i] mesh!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, - shading = false, color = :blue) + shading = NoShading, color = :blue) lines!( ax, Rect2f(xs[i][i] - 2s, xs[i][j] - 2s, 4s, 4s), space = space, linewidth = 2, color = :red) @@ -496,7 +496,7 @@ end @reference_test "Scatter & Text transformations" begin # Check that transformations apply in `space = :data` fig, ax, p = scatter(Point2f(100, 0.5), marker = 'a', markersize=50) - t = text!(Point2f(100, 0.5), text = "Test", fontsize = 50) + t = text!(Point2f(100, 0.5), text = "Test", fontsize = 50, transform_marker=true) translate!(p, -100, 0, 0) translate!(t, -100, 0, 0) @@ -506,7 +506,7 @@ end scale!(p2, 0.5, 0.5, 1) # but do act on glyphs of text - t2 = text!(ax, 1, 0, text = "Test", fontsize = 50) + t2 = text!(ax, 1, 0, text = "Test", fontsize = 50, transform_marker=true) Makie.rotate!(t2, pi/4) scale!(t2, 0.5, 0.5, 1) @@ -530,11 +530,11 @@ end end @reference_test "2D surface with explicit color" begin - surface(1:10, 1:10, ones(10, 10); color = [RGBf(x*y/100, 0, 0) for x in 1:10, y in 1:10], shading = false) + surface(1:10, 1:10, ones(10, 10); color = [RGBf(x*y/100, 0, 0) for x in 1:10, y in 1:10], shading = NoShading) end @reference_test "heatmap and image colormap interpolation" begin - f = Figure(resolution=(500, 500)) + f = Figure(size=(500, 500)) crange = LinRange(0, 255, 10) len = length(crange) img = zeros(Float32, len, len + 2) @@ -561,7 +561,7 @@ end n = 100 categorical = [false, true] scales = [exp, identity, log, log10] - fig = Figure(resolution = (500, 250)) + fig = Figure(size = (500, 250)) ax = Axis(fig[1, 1]) for (i, cat) in enumerate(categorical) for (j, scale) in enumerate(scales) @@ -580,7 +580,7 @@ end @reference_test "colormap with specific values" begin cmap = cgrad([:black,:white,:orange],[0,0.2,1]) - fig = Figure(resolution=(400,200)) + fig = Figure(size=(400,200)) ax = Axis(fig[1,1]) x = range(0,1,length=50) scatter!(fig[1,1],Point2.(x,fill(0.,50)),color=x,colormap=cmap) @@ -633,7 +633,7 @@ end @reference_test "minor grid & scales" begin data = LinRange(0.01, 0.99, 200) - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) for (i, scale) in enumerate([log10, log2, log, sqrt, Makie.logit, identity]) row, col = fldmod1(i, 2) Axis(f[row, col], yscale = scale, title = string(scale), @@ -808,7 +808,7 @@ end end @reference_test "contour labels with transform_func" begin - f = Figure(resolution = (400, 400)) + f = Figure(size = (400, 400)) a = Axis(f[1, 1], xscale = log10) xs = 10 .^ range(0, 3, length=101) ys = range(1, 4, length=101) @@ -840,7 +840,7 @@ end @reference_test "trimspine" begin with_theme(Axis = (limits = (0.5, 5.5, 0.3, 3.4), spinewidth = 8, topspinevisible = false, rightspinevisible = false)) do - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) for (i, ts) in enumerate([(true, true), (true, false), (false, true), (false, false)]) Label(f[0, i], string(ts), tellwidth = false) @@ -859,7 +859,7 @@ end end @reference_test "hexbin bin int" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -875,7 +875,7 @@ end end @reference_test "hexbin bin tuple" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -893,7 +893,7 @@ end @reference_test "hexbin two cellsizes" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -909,7 +909,7 @@ end end @reference_test "hexbin one cellsize" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.rand(300) y = RNG.rand(300) @@ -925,7 +925,7 @@ end end @reference_test "hexbin threshold" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) x = RNG.randn(100000) y = RNG.randn(100000) @@ -974,7 +974,7 @@ end end @reference_test "Rich text" begin - f = Figure(fontsize = 30, resolution = (800, 600)) + f = Figure(fontsize = 30, size = (800, 600)) ax = Axis(f[1, 1], limits = (1, 100, 0.001, 1), xscale = log10, @@ -1066,10 +1066,10 @@ end end @reference_test "LaTeXStrings linesegment offsets" begin - s = Scene(camera = campixel!, resolution = (600, 600)) + s = Scene(camera = campixel!, size = (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 @@ -1078,7 +1078,7 @@ end end @reference_test "Scalar colors from colormaps" begin - f = Figure(resolution = (600, 600)) + f = Figure(size = (600, 600)) ax = Axis(f[1, 1]) hidedecorations!(ax) hidespines!(ax) @@ -1107,7 +1107,7 @@ end # It seems like we can't define recipes in `@reference_test` yet, # so we'll have to fake a recipe's structure. - fig = Figure(resolution = (600, 600)) + fig = Figure(size = (600, 600)) # Create a recipe plot ax, plot_top = heatmap(fig[1, 1], randn(10, 10)) # Plot some recipes at the level below the contour @@ -1351,7 +1351,7 @@ end end function ppu_test_plot(resolution, px_per_unit, scalefactor) - fig, ax, pl = scatter(1:4, markersize=100, color=1:4, figure=(; resolution=resolution), axis=(; titlesize=50, title="ppu: $px_per_unit, sf: $scalefactor")) + fig, ax, pl = scatter(1:4, markersize=100, color=1:4, figure=(; size=resolution), axis=(; titlesize=50, title="ppu: $px_per_unit, sf: $scalefactor")) DataInspector(ax) hidedecorations!(ax) return fig diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 259d18442e8..507b476b459 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -1,7 +1,7 @@ @reference_test "Image on Geometry (Moon)" begin moon = loadasset("moon.png") - fig, ax, meshplot = mesh(Sphere(Point3f(0), 1f0), color=moon, shading=false, axis = (;show_axis=false)) + fig, ax, meshplot = mesh(Sphere(Point3f(0), 1f0), color=moon, shading=NoShading, axis = (;show_axis=false)) update_cam!(ax.scene, Vec3f(-2, 2, 2), Vec3f(0)) fig end @@ -9,7 +9,7 @@ end @reference_test "Image on Geometry (Earth)" begin earth = loadasset("earth.png") m = uv_mesh(Tesselation(Sphere(Point3f(0), 1f0), 60)) - mesh(m, color=earth, shading=false) + mesh(m, color=earth, shading=NoShading) end @reference_test "Orthographic Camera" begin @@ -31,8 +31,9 @@ end scene = ax.scene cam = cameracontrols(scene) cam.settings[:projectiontype][] = Makie.Orthographic + cam.settings.center[] = false # This would be set by update_cam!() cam.upvector[] = (0.0, 0.0, 1.0) - cam.lookat[] = Vec3f(0.595, 2.5, 0.5) + cam.lookat[] = Vec3f(0.595, 1.5, 0.5) cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 0.61, cam.lookat[][3]) update_cam!(scene, cam) fig @@ -53,13 +54,13 @@ end rot = qrotation(Vec3f(1, 0, 0), 0.5pi) * qrotation(Vec3f(0, 1, 0), 0.7pi) meshscatter( 1:3, 1:3, fill(0, 3, 3), - marker=catmesh, color=img, markersize=1, rotation=rot, + marker=catmesh, color=img, markersize=1, rotations=rot, axis=(type=LScene, show_axis=false) ) end @reference_test "Load Mesh" begin - mesh(loadasset("cat.obj")) + mesh(loadasset("cat.obj"); color=:black) end @reference_test "Colored Mesh" begin @@ -193,7 +194,6 @@ end x = [cospi(φ) * sinpi(θ) for θ in θ, φ in φ] y = [sinpi(φ) * sinpi(θ) for θ in θ, φ in φ] z = [cospi(θ) for θ in θ, φ in φ] - RNG.rand([-1f0, 1f0], 3) pts = vec(Point3f.(x, y, z)) f, ax, p = surface(x, y, z, color=Makie.logo(), transparency=true) end @@ -274,7 +274,7 @@ end vy = -1:0.01:1 f(x, y) = (sin(x * 10) + cos(y * 10)) / 4 - scene = Scene(resolution=(500, 500), camera=cam3d!) + scene = Scene(size=(500, 500), camera=cam3d!) # One way to style the axis is to pass a nested dictionary / named tuple to it. psurf = surface!(scene, vx, vy, f) axis3d!(scene, frame = (linewidth = 2.0,)) @@ -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) @@ -525,7 +525,7 @@ end @reference_test "Depth Shift" begin # Up to some artifacts from fxaa the left side should be blue and the right red. - fig = Figure(resolution = (800, 400)) + fig = Figure(size = (800, 400)) prim = Rect3(Point3f(0), Vec3f(1)) ps = RNG.rand(Point3f, 10) .+ Point3f(0, 0, 1) @@ -593,7 +593,7 @@ end fig = Figure() for ax in [LScene(fig[1, 1]), Axis3(fig[1, 2])] mesh!(ax, Rect3(Point3f(-10), Vec3f(20)), color = :orange) - mesh!(ax, Rect2f(0.8, 0.1, 0.1, 0.8), space = :relative, color = :blue, shading = false) + mesh!(ax, Rect2f(0.8, 0.1, 0.1, 0.8), space = :relative, color = :blue, shading = NoShading) linesegments!(ax, Rect2f(-0.5, -0.5, 1, 1), space = :clip, color = :cyan, linewidth = 5) text!(ax, 0, 0.52, text = "Clip Space", align = (:center, :bottom), space = :clip) image!(ax, 0..40, 0..800, [x for x in range(0, 1, length=40), _ in 1:10], space = :pixel) diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index 2f857d415c3..e80c9d8629e 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -8,7 +8,7 @@ end @reference_test "Figure with Blocks" begin - fig = Figure(resolution = (900, 900)) + fig = Figure(size = (900, 900)) ax, sc = scatter(fig[1, 1][1, 1], RNG.randn(100, 2), axis = (;title = "Random Dots", xlabel = "Time")) sc2 = scatter!(ax, RNG.randn(100, 2) .+ 2, color = :red) ll = fig[1, 1][1, 2] = Legend(fig, [sc, sc2], ["Scatter", "Other"]) @@ -25,6 +25,17 @@ end fig end +@reference_test "Figure with boxes" begin + fig = Figure(resolution = (900, 900)) + Box(fig[1,1], color = :red, strokewidth = 3, linestyle = :solid, strokecolor = :black) + Box(fig[1,2], color = (:red, 0.5), strokewidth = 3, linestyle = :dash, strokecolor = :red) + Box(fig[1,3], color = :white, strokewidth = 3, linestyle = :dot, strokecolor = (:black, 0.5)) + Box(fig[2,1], color = :red, strokewidth = 3, linestyle = :solid, strokecolor = :black, cornerradius = 0) + Box(fig[2,2], color = (:red, 0.5), strokewidth = 3, linestyle = :dash, strokecolor = :red, cornerradius = 20) + Box(fig[2,3], color = :white, strokewidth = 3, linestyle = :dot, strokecolor = (:black, 0.5), cornerradius = (0, 10, 20, 30)) + fig +end + @reference_test "menus" begin fig = Figure() funcs = [sqrt, x->x^2, sin, cos] @@ -50,8 +61,8 @@ end @reference_test "Label with text wrapping" begin lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - fig = Figure(resolution = (1000, 660)) - m!(fig, lbl) = mesh!(fig.scene, lbl.layoutobservables.computedbbox, color = (:red, 0.5), shading=false) + fig = Figure(size = (1000, 660)) + m!(fig, lbl) = mesh!(fig.scene, lbl.layoutobservables.computedbbox, color = (:red, 0.5), shading=NoShading) lbl1 = Label(fig[1, 1:2], "HEADER "^10, fontsize = 40, word_wrap = true) m!(fig, lbl1) @@ -116,8 +127,8 @@ end lines!(ax,( 1:10) .* i, label = "$i") end # To verify that RGB values differ across entries - axislegend(ax, position = :lt, patchcolor = :red, patchsize = (100, 100), bgcolor = :gray50); - Legend(f[1, 2], ax, patchcolor = :gray80, patchsize = (100, 100), bgcolor = :gray50); + axislegend(ax, position = :lt, patchcolor = :red, patchsize = (100, 100), backgroundcolor = :gray50); + Legend(f[1, 2], ax, patchcolor = :gray80, patchsize = (100, 100), backgroundcolor = :gray50); f end end @@ -141,7 +152,7 @@ end f = Figure() ax = PolarAxis(f[1, 1]) zs = [r*cos(phi) for phi in range(0, 4pi, length=100), r in range(1, 2, length=100)] - p = surface!(ax, 0..2pi, 0..10, zs, shading = false, colormap = :coolwarm, colorrange=(-2, 2)) + p = surface!(ax, 0..2pi, 0..10, zs, shading = NoShading, colormap = :coolwarm, colorrange=(-2, 2)) rlims!(ax, 0, 11) # verify that r = 10 doesn't end up at r > 10 translate!(p, 0, 0, -200) Colorbar(f[1, 2], p) @@ -150,7 +161,7 @@ end # may fail in WGLMakie due to missing dashes @reference_test "PolarAxis scatterlines spine" begin - f = Figure(resolution = (800, 400)) + f = Figure(size = (800, 400)) ax1 = PolarAxis(f[1, 1], title = "No spine", spinevisible = false, theta_as_x = false) scatterlines!(ax1, range(0, 1, length=100), range(0, 10pi, length=100), color = 1:100) @@ -165,7 +176,7 @@ end # may fail in CairoMakie due to different text stroke handling # and in WGLMakie due to missing stroke @reference_test "PolarAxis decorations" begin - f = Figure(resolution = (400, 400), backgroundcolor = :black) + f = Figure(size = (400, 400), backgroundcolor = :black) ax = PolarAxis( f[1, 1], backgroundcolor = :black, @@ -184,7 +195,7 @@ end end @reference_test "PolarAxis limits" begin - f = Figure(resolution = (800, 600)) + f = Figure(size = (800, 600)) for (i, theta_0) in enumerate((0, -pi/6, pi/2)) for (j, thetalims) in enumerate(((0, 2pi), (-pi/2, pi/2), (0, pi/12))) po = PolarAxis(f[i, j], theta_0 = theta_0, thetalimits = thetalims, rlimits = (1 + 2(j-1), 7)) @@ -201,7 +212,7 @@ end end @reference_test "Axis3 axis reversal" begin - f = Figure(resolution = (1000, 1000)) + f = Figure(size = (1000, 1000)) revstr(dir, rev) = rev ? "$dir rev" : "" for (i, (x, y, z)) in enumerate(Iterators.product(fill((false, true), 3)...)) Axis3(f[fldmod1(i, 3)...], title = "$(revstr("x", x)) $(revstr("y", y)) $(revstr("z", z))", xreversed = x, yreversed = y, zreversed = z) @@ -211,7 +222,7 @@ end end @reference_test "Colorbar for recipes" begin - fig, ax, pl = barplot(1:3; color=1:3, colormap=Makie.Categorical(:viridis), figure=(;resolution=(800, 800))) + fig, ax, pl = barplot(1:3; color=1:3, colormap=Makie.Categorical(:viridis), figure=(;size=(800, 800))) Colorbar(fig[1, 2], pl; size=100) x = LinRange(-1, 1, 20) y = LinRange(-1, 1, 20) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 1f4ed54f998..7e6083f7c9c 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -1,6 +1,6 @@ @reference_test "lines and linestyles" begin # For now disabled until we fix GLMakie linestyle - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) scalar = 30 points = Point2f[(1, 1), (1, 2), (2, 3), (2, 1)] linestyles = [ @@ -13,6 +13,7 @@ scalar .* (points .+ Point2f(linewidth*2, i * 3.25)), linewidth = linewidth, linestyle = linestyle, + color=:black ) end end @@ -20,7 +21,7 @@ end @reference_test "lines with gaps" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) points = [ Point2f[(1, 0), (2, 0.5), (NaN, NaN), (4, 0.5), (5, 0)], Point2f[(NaN, NaN), (2, 0.5), (3, 0), (4, 0.5), (5, 0)], @@ -36,7 +37,7 @@ end end @reference_test "scatters" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) markersizes = 0:2:30 markers = [:circle, :rect, :cross, :utriangle, :dtriangle, @@ -49,6 +50,7 @@ end Point2f(i, j) .* 45, marker = m, markersize = ms, + color=:black ) end end @@ -56,7 +58,7 @@ end end @reference_test "scatter rotations" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) rotations = range(0, 2pi, length = 15) markers = [:circle, :rect, :cross, :utriangle, :dtriangle, @@ -71,6 +73,7 @@ end marker = m, markersize = 30, rotations = rot, + color=:black ) scatter!(s, p, color = :red, markersize = 6) end @@ -79,7 +82,7 @@ end end @reference_test "scatter with stroke" begin - s = Scene(resolution = (350, 700), camera = campixel!) + s = Scene(size = (350, 700), camera = campixel!) # half stroke, half glow strokes = range(1, 4, length=7) @@ -112,7 +115,7 @@ end end @reference_test "scatter with glow" begin - s = Scene(resolution = (350, 700), camera = campixel!) + s = Scene(size = (350, 700), camera = campixel!) # half stroke, half glow glows = range(4, 1, length=7) @@ -148,7 +151,7 @@ end @reference_test "scatter image markers" begin pixel_types = [ RGBA, RGBAf, RGBA{Float16}, ARGB, ARGB{Float16}, RGB, RGBf, RGB{Float16} ] rotations = [ 2pi/3 * (i-1) for i = 1:length(pixel_types) ] - s = Scene(resolution = (100+100*length(pixel_types), 400), camera = campixel!) + s = Scene(size = (100+100*length(pixel_types), 400), camera = campixel!) filename = Makie.assetpath("icon_transparent.png") marker_image = load(filename) for (i, (rot, pxtype)) in enumerate(zip(rotations, pixel_types)) @@ -166,7 +169,7 @@ end @reference_test "basic polygon shapes" begin - s = Scene(resolution = (800, 800), camera = campixel!) + s = Scene(size = (800, 800), camera = campixel!) scalefactor = 70 Pol = Makie.GeometryBasics.Polygon polys = [ @@ -219,7 +222,7 @@ end @reference_test "BezierPath markers" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) ax = Axis(f[1, 1]) markers = [ @@ -255,7 +258,7 @@ end # bb = Makie.bbox(Makie.DEFAULT_MARKER_MAP[marker]) # w, h = widths(bb) # ox, oy = origin(bb) - # xy = map(pv -> Makie.project(pv, Vec2f(widths(pixelarea(scene)[])), Point2f(5, i)), scene.camera.projectionview) + # xy = map(pv -> Makie.project(pv, Vec2f(widths(viewport(scene)[])), Point2f(5, i)), scene.camera.projectionview) # bb = map(xy -> Rect2f(xy .+ 30 * Vec2f(ox, oy), 30 * Vec2f(w, h)), xy) # lines!(bb, linewidth = 1, color = :orange, space = :pixel, linestyle = :dash) # end @@ -265,7 +268,7 @@ end end @reference_test "BezierPath marker stroke" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) ax = Axis(f[1, 1]) # Same as above @@ -287,7 +290,7 @@ end @reference_test "complex_bezier_markers" begin - f = Figure(resolution = (800, 800)) + f = Figure(size = (800, 800)) ax = Axis(f[1, 1]) arrow = BezierPath([ @@ -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) @@ -422,7 +425,7 @@ function draw_marker_test!(scene, marker, center; markersize=300) end @reference_test "marke glyph alignment" begin - scene = Scene(resolution=(1200, 1200)) + scene = Scene(size=(1200, 1200)) campixel!(scene) # marker is in front, so it should not be smaller than the background rectangle plot_row!(scene, 0, false) @@ -460,8 +463,8 @@ end f end - @reference_test "barplot with TeX-ed labels" begin - fig = Figure(resolution = (800, 800)) +@reference_test "barplot with TeX-ed labels" begin + fig = Figure(size = (800, 800)) lab1 = L"\int f(x) dx" lab2 = lab1 # lab2 = L"\frac{a}{b} - \sqrt{b}" # this will not work until #2667 is fixed 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/short_tests.jl b/ReferenceTests/src/tests/short_tests.jl index c939fc60626..b28d814a8ee 100644 --- a/ReferenceTests/src/tests/short_tests.jl +++ b/ReferenceTests/src/tests/short_tests.jl @@ -132,7 +132,7 @@ end highclip = :red, lowclip = :black, nan_color = (:green, 0.5), - shading = false, + shading = NoShading, ) surface!( Axis(fig[2, 2]), @@ -141,7 +141,7 @@ end highclip = :red, lowclip = :black, nan_color = (:green, 0.5), - shading = false, + shading = NoShading, ) fig end @@ -158,7 +158,7 @@ end @reference_test "lines linesegments width test" begin res = 200 - s = Scene(camera=campixel!, resolution=(res, res)) + s = Scene(camera=campixel!, size=(res, res)) half = res / 2 linewidth = 10 xstart = half - (half/2) diff --git a/ReferenceTests/src/tests/specapi.jl b/ReferenceTests/src/tests/specapi.jl new file mode 100644 index 00000000000..0e46d72be1e --- /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, color=Cycled(1)) + S.lines!(ax, 2:5; linewidth=7, color=Cycled(2)) + 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..04e4c30626a 100644 --- a/ReferenceTests/src/tests/text.jl +++ b/ReferenceTests/src/tests/text.jl @@ -1,5 +1,5 @@ @reference_test "heatmap_with_labels" begin - fig = Figure(resolution = (600, 600)) + fig = Figure(size = (600, 600)) ax = fig[1, 1] = Axis(fig) values = RNG.rand(10, 10) @@ -28,7 +28,7 @@ end end @reference_test "single_strings_single_positions" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) points = [Point(x, y) .* 200 for x in 1:3 for y in 1:3] scatter!(scene, points, marker = :circle, markersize = 10px) @@ -51,7 +51,7 @@ end @reference_test "multi_strings_multi_positions" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) angles = (-pi/6, 0.0, pi/6) points = [Point(x, y) .* 200 for x in 1:3 for y in 1:3 for angle in angles] @@ -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))]) @@ -76,10 +75,10 @@ end end @reference_test "single_strings_single_positions_justification" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (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) @@ -109,7 +108,7 @@ end end @reference_test "multi_boundingboxes" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) t1 = text!(scene, fill("makie", 4), @@ -137,7 +136,7 @@ end end @reference_test "single_boundingboxes" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) for a in pi/4:pi/2:7pi/4 @@ -182,7 +181,7 @@ end end @reference_test "empty_lines" begin - scene = Scene(camera = campixel!, resolution = (800, 800)) + scene = Scene(camera = campixel!, size = (800, 800)) t1 = text!(scene, "Line1\nLine 2\n\nLine4", position = (200, 400), align = (:center, :center), markerspace = :data) @@ -213,7 +212,7 @@ end @reference_test "Text offset" begin - f = Figure(resolution = (1000, 1000)) + f = Figure(size = (1000, 1000)) barplot(f[1, 1], 3:5) text!(1, 3, text = "bar 1", offset = (0, 10), align = (:center, :baseline)) text!([(2, 4), (3, 5)], text = ["bar 2", "bar 3"], @@ -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 @@ -350,7 +349,7 @@ end @reference_test "Word Wrapping" begin lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - fig = Figure(resolution=(600, 500)) + fig = Figure(size=(600, 500)) ax = Axis(fig[1, 1]) text!(ax, 0, 0, text = latexstring(L"$1$ " * lorem_ipsum), word_wrap_width=250, fontsize = 12, align = (:left, :bottom), justification = :left, color = :black) text!(ax, 0, 0, text = lorem_ipsum, word_wrap_width=250, fontsize = 12, align = (:left, :top), justification = :right, color = :black) diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index 4fd7f835587..d19c97647fd 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -43,7 +43,7 @@ end function generate_plot(N = 3) points = Observable(Point2f[]) color = Observable(RGBAf[]) - fig, ax, pl = scatter(points, color=color, markersize=1.0, marker=Circle, markerspace=:data, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(resolution=(800, 800),)) + fig, ax, pl = scatter(points, color=color, markersize=1.0, marker=Circle, markerspace=:data, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(size=(800, 800),)) function update_func(ij) push!(points.val, Point2f(Tuple(ij))) push!(color.val, RGBAf((Tuple(ij)./N)..., 0, 1)) @@ -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/Project.toml b/WGLMakie/Project.toml index 4c989338660..4da6b00f51d 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -14,9 +14,9 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" PNGFiles = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" -PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] @@ -25,15 +25,17 @@ FileIO = "1.1" FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.1" Hyperscript = "0.0.3, 0.0.4" -JSServe = "2.2" +JSServe = "v2.3" Makie = "=0.20.0" Observables = "0.5.1" +PNGFiles = "0.3, 0.4" +PrecompileTools = "1.0" RelocatableFolders = "0.1, 0.2, 0.3, 1.0" ShaderAbstractions = "0.4" -PrecompileTools = "1.0" StaticArrays = "0.12, 1.0" -PNGFiles = "0.3, 0.4" julia = "1.3" +LinearAlgebra = "1.0, 1.6" + [extras] MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" diff --git a/WGLMakie/assets/mesh.frag b/WGLMakie/assets/mesh.frag index 1355d41e6b6..efe137428e3 100644 --- a/WGLMakie/assets/mesh.frag +++ b/WGLMakie/assets/mesh.frag @@ -4,19 +4,31 @@ flat in int sample_frag_color; in vec3 o_normal; in vec3 o_camdir; -in vec3 o_lightdir; + +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); + float backlight = get_backlight(); + float diff_coeff = smooth_zero_max(dot(L, -N)) + + backlight * smooth_zero_max(dot(L, N)); // specular coefficient vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0), get_shininess()); + float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()) + + backlight * pow(max(dot(H, N), 0.0), get_shininess()); if (diff_coeff <= 0.0) spec_coeff = 0.0; + // final lighting model - return vec3( + return get_light_color() * vec3( get_diffuse() * diff_coeff * color + get_specular() * spec_coeff ); @@ -100,11 +112,10 @@ void main() { vec3 shaded_color = real_color.rgb; if(get_shading()){ - vec3 L = normalize(o_lightdir); + vec3 L = get_light_direction(); vec3 N = normalize(o_normal); - vec3 light1 = blinnphong(N, o_camdir, L, real_color.rgb); - vec3 light2 = blinnphong(N, o_camdir, -L, real_color.rgb); - shaded_color = get_ambient() * real_color.rgb + light1 + get_backlight() * light2; + vec3 light = blinnphong(N, normalize(o_camdir), L, real_color.rgb); + shaded_color = get_ambient() * real_color.rgb + light; } if (picking) { diff --git a/WGLMakie/assets/mesh.vert b/WGLMakie/assets/mesh.vert index 3b354497fd7..14341fbe452 100644 --- a/WGLMakie/assets/mesh.vert +++ b/WGLMakie/assets/mesh.vert @@ -1,12 +1,12 @@ out vec2 frag_uv; out vec3 o_normal; out vec3 o_camdir; -out vec3 o_lightdir; out vec4 frag_color; uniform mat4 projection; uniform mat4 view; +uniform vec3 eyeposition; vec3 tovec3(vec2 v){return vec3(v, 0.0);} vec3 tovec3(vec3 v){return v;} @@ -61,22 +61,15 @@ vec4 vertex_color(float value, vec2 colorrange, sampler2D colormap){ } } -void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 lightposition) +void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { // normal in world space o_normal = get_normalmatrix() * normal; - // position in view space (as seen from camera) - vec4 view_pos = view * position_world; // position in clip space (w/ depth) - gl_Position = projection * view_pos; + gl_Position = projection * view * position_world; // TODO consider using projectionview directly gl_Position.z += gl_Position.w * get_depth_shift(); - // direction to light - o_lightdir = normalize(view*vec4(lightposition, 1.0) - view_pos).xyz; // direction to camera - // This is equivalent to - // normalize(view*vec4(eyeposition, 1.0) - view_pos).xyz - // (by definition `view * eyeposition = 0`) - o_camdir = normalize(-view_pos).xyz; + o_camdir = position_world.xyz / position_world.w - eyeposition; } flat out uint frag_instance_id; @@ -90,7 +83,7 @@ void main(){ } vec4 position_world = model * vec4(vertex_position, 1); - render(position_world, get_normals(), view, projection, get_lightposition()); + render(position_world, get_normals(), view, projection); frag_uv = get_uv(); frag_uv = vec2(1.0 - frag_uv.y, frag_uv.x); frag_color = vertex_color(get_color(), get_colorrange(), colormap); diff --git a/WGLMakie/assets/particles.frag b/WGLMakie/assets/particles.frag index 5615fb356da..262a1fd9538 100644 --- a/WGLMakie/assets/particles.frag +++ b/WGLMakie/assets/particles.frag @@ -1,23 +1,34 @@ in vec4 frag_color; in vec3 frag_normal; in vec3 frag_position; -in vec3 frag_lightdir; +in vec3 o_camdir; + +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0); + float backlight = get_backlight(); + float diff_coeff = smooth_zero_max(dot(L, -N)) + + backlight * smooth_zero_max(dot(L, N)); // specular coefficient - vec3 H = normalize(L+V); + vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0), 8.0); + float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()) + + backlight * pow(max(dot(H, N), 0.0), get_shininess()); if (diff_coeff <= 0.0) spec_coeff = 0.0; // final lighting model - return vec3( - vec3(0.1) * vec3(0.3) + - vec3(0.9) * color * diff_coeff + - vec3(0.3) * spec_coeff + return get_light_color() * vec3( + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff ); } @@ -32,13 +43,12 @@ vec4 pack_int(uint id, uint index) { } void main() { - vec3 L, N, light1, light2, color; + vec3 L, N, light, color; if (get_shading()) { - L = normalize(frag_lightdir); + L = get_light_direction(); N = normalize(frag_normal); - light1 = blinnphong(N, frag_position, L, frag_color.rgb); - light2 = blinnphong(N, frag_position, -L, frag_color.rgb); - color = get_ambient() * frag_color.rgb + light1 + get_backlight() * light2; + light = blinnphong(N, normalize(o_camdir), L, frag_color.rgb); + color = get_ambient() * frag_color.rgb + light; } else { color = frag_color.rgb; } diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert index f2785d2aed4..e9bd0a356c3 100644 --- a/WGLMakie/assets/particles.vert +++ b/WGLMakie/assets/particles.vert @@ -2,13 +2,12 @@ precision mediump float; uniform mat4 projection; uniform mat4 view; +uniform vec3 eyeposition; out vec3 frag_normal; out vec3 frag_position; - out vec4 frag_color; -out vec3 frag_lightdir; - +out vec3 o_camdir; vec3 qmul(vec4 q, vec3 v){ return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v); @@ -31,16 +30,14 @@ void main(){ // get_* gets the global inputs (uniform, sampler, position array) // those functions will get inserted by the shader creation pipeline vec3 vertex_position = get_markersize() * to_vec3(get_position()); - vec3 lightpos = vec3(20,20,20); vec3 N = get_normals(); rotate(get_rotations(), vertex_position, N); vertex_position = to_vec3(get_offset()) + vertex_position; vec4 position_world = model * vec4(vertex_position, 1); frag_normal = N; - frag_lightdir = normalize(lightpos - position_world.xyz); frag_color = to_vec4(get_color()); // direction to camera - frag_position = -position_world.xyz; + o_camdir = position_world.xyz / position_world.w - eyeposition; // screen space coordinates of the position gl_Position = projection * view * position_world; gl_Position.z += gl_Position.w * get_depth_shift(); diff --git a/WGLMakie/assets/sprites.vert b/WGLMakie/assets/sprites.vert index c077fa62446..35f8eaedd86 100644 --- a/WGLMakie/assets/sprites.vert +++ b/WGLMakie/assets/sprites.vert @@ -61,15 +61,19 @@ void main(){ vec2 sprite_bbox_centre = get_quad_offset() + bbox_signed_radius; mat4 pview = projection * view; - // Compute transform for the offset vectors from the central point mat4 trans = get_transform_marker() ? model : mat4(1.0); - trans = (get_billboard() ? projection : pview) * qmat(get_rotations()) * trans; // Compute centre of billboard in clipping coordinates - vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); + // Always transform text/scatter position argument vec4 data_point = get_preprojection() * model * vec4(tovec3(get_pos()), 1); - data_point = vec4(data_point.xyz / data_point.w + mat3(model) * tovec3(get_marker_offset()), 1); + // maybe transform marker_offset + glyph offsets + data_point = vec4(data_point.xyz / data_point.w + mat3(trans) * tovec3(get_marker_offset()), 1); data_point = pview * data_point; + + // Compute transform for the offset vectors from the central point + trans = (get_billboard() ? projection : pview) * qmat(get_rotations()) * trans; + vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); + vec4 vclip = data_point + sprite_center; // Extra buffering is required around sprites which are antialiased so that diff --git a/WGLMakie/assets/volume.frag b/WGLMakie/assets/volume.frag index ec37e457671..d3048f8afee 100644 --- a/WGLMakie/assets/volume.frag +++ b/WGLMakie/assets/volume.frag @@ -2,7 +2,6 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d bool _; //empty structs are not allowed }; in vec3 frag_vert; -in vec3 o_light_dir; const float max_distance = 1.3; @@ -54,16 +53,25 @@ vec3 gennormal(vec3 uvw, float d) return normalize(a-b); } +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} + vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ - float diff_coeff = max(dot(L, N), 0.0) + max(dot(L, -N), 0.0); + // TODO use backlight here too? + float diff_coeff = smooth_zero_max(dot(L, -N)) + smooth_zero_max(dot(L, N)); // specular coefficient vec3 H = normalize(L + V); - float spec_coeff = pow(max(dot(H, N), 0.0) + max(dot(H, -N), 0.0), shininess); + float spec_coeff = pow(max(dot(H, -N), 0.0) + max(dot(H, N), 0.0), shininess); // final lighting model - return vec3( - ambient * color + - diffuse * diff_coeff * color + - specular * spec_coeff + return ambient * color + get_light_color() * vec3( + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff ); } @@ -122,14 +130,14 @@ vec4 contours(vec3 front, vec3 dir) float T = 1.0; vec3 Lo = vec3(0.0); int i = 0; - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); for (i; i < num_samples; ++i) { float intensity = texture(volumedata, pos).x; vec4 density = color_lookup(intensity, colormap, colorrange); float opacity = density.a; if(opacity > 0.0){ vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 L = get_light_direction(); vec3 opaque = blinnphong(N, camdir, L, density.rgb); Lo += (T * opacity) * opaque; T *= 1.0 - opacity; @@ -147,12 +155,12 @@ vec4 isosurface(vec3 front, vec3 dir) vec4 c = vec4(0.0); int i = 0; vec4 diffuse_color = color_lookup(isovalue, colormap, colorrange); - vec3 camdir = normalize(-dir); + vec3 camdir = normalize(dir); for (i; i < num_samples; ++i){ float density = texture(volumedata, pos).x; if(abs(density - isovalue) < isorange){ vec3 N = gennormal(pos, step_size); - vec3 L = normalize(o_light_dir - pos); + vec3 L = get_light_direction(); c = vec4( blinnphong(N, camdir, L, diffuse_color.rgb), diffuse_color.a diff --git a/WGLMakie/assets/volume.vert b/WGLMakie/assets/volume.vert index 42599e3e6a3..c9d00be85b8 100644 --- a/WGLMakie/assets/volume.vert +++ b/WGLMakie/assets/volume.vert @@ -1,5 +1,4 @@ out vec3 frag_vert; -out vec3 o_light_dir; uniform mat4 projection, view; @@ -7,7 +6,6 @@ void main() { frag_vert = position; vec4 world_vert = model * vec4(position, 1); - o_light_dir = vec3(modelinv * vec4(get_lightposition(), 1)); gl_Position = projection * view * world_vert; gl_Position.z += gl_Position.w * get_depth_shift(); } diff --git a/WGLMakie/src/Camera.js b/WGLMakie/src/Camera.js index cac43a96979..1a2b02d3655 100644 --- a/WGLMakie/src/Camera.js +++ b/WGLMakie/src/Camera.js @@ -1,7 +1,8 @@ import * as THREE from "./THREE.js"; +import { OrbitControls } from "./OrbitControls.js"; // Unitless is the scene pixel unit space -// so scene.px_area, or size(scene) +// so scene.viewport, or size(scene) // Which isn't the same as the framebuffer pixel size due to scalefactor/px_per_unit/devicePixelRatio export function events2unitless(screen, event) { const { canvas, winscale, renderer } = screen; @@ -13,7 +14,7 @@ export function events2unitless(screen, event) { export function to_world(scene, x, y) { const proj_inv = scene.wgl_camera.projectionview_inverse.value; - const [_x, _y, w, h] = scene.pixelarea.value; + const [_x, _y, w, h] = scene.viewport.value; const pix_space = new THREE.Vector4( ((x - _x) / w) * 2 - 1, ((y - _y) / h) * 2 - 1, @@ -34,29 +35,41 @@ function Identity4x4() { function in_scene(scene, mouse_event) { const [x, y] = events2unitless(scene.screen, mouse_event); - const [sx, sy, sw, sh] = scene.pixelarea.value; + const [sx, sy, sw, sh] = scene.viewport.value; return x >= sx && x < sx + sw && y >= sy && y < sy + sh; } // Taken from https://andreasrohner.at/posts/Web%20Development/JavaScript/Simple-orbital-camera-controls-for-THREE-js/ -export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { +export function attach_3d_camera( + canvas, + makie_camera, + cam3d, + light_dir, + scene +) { if (cam3d === undefined) { // we just support 3d cameras atm return; } const [w, h] = makie_camera.resolution.value; const camera = new THREE.PerspectiveCamera( - cam3d.fov, + cam3d.fov.value, w / h, - cam3d.near, - cam3d.far + 0.01, + 100.0 ); - 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); - function update() { + + const use_orbit_cam = () => + !(JSServe.can_send_to_julia && JSServe.can_send_to_julia()); + const controls = new OrbitControls(camera, canvas, use_orbit_cam, (e) => + in_scene(scene, e) + ); + controls.addEventListener("change", (e) => { const view = camera.matrixWorldInverse; const projection = camera.projectionMatrix; const [width, height] = cam3d.resolution.value; @@ -64,103 +77,14 @@ export function attach_3d_camera(canvas, makie_camera, cam3d, scene) { camera.aspect = width / height; camera.updateProjectionMatrix(); camera.updateWorldMatrix(); - makie_camera.update_matrices( view.elements, projection.elements, [width, height], [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 (!in_scene(scene, e)) { - return; - } - const delta = Math.sign(e.deltaY); - if (delta == -1) { - zoomOut(); - } else if (delta == 1) { - zoomIn(); - } - - e.preventDefault(); - } - function mouseDownHandler(e) { - if (!in_scene(scene, e)) { - return; - } - startDragX = e.clientX; - startDragY = e.clientY; - - e.preventDefault(); - } - function mouseMoveHandler(e) { - if (!in_scene(scene, e)) { - return; - } - if (startDragX === null || startDragY === null) return; - - if (drag) drag(e.clientX - startDragX, e.clientY - startDragY); - - startDragX = e.clientX; - startDragY = e.clientY; - e.preventDefault(); - } - function mouseUpHandler(e) { - if (!in_scene(scene, e)) { - return; - } - mouseMoveHandler.call(this, e); - startDragX = null; - startDragY = null; - e.preventDefault(); - } - domObject.addEventListener("wheel", mouseWheelHandler); - domObject.addEventListener("mousedown", mouseDownHandler); - domObject.addEventListener("mousemove", mouseMoveHandler); - domObject.addEventListener("mouseup", mouseUpHandler); - } - - function drag(deltaX, deltaY) { - const radPerPixel = Math.PI / 450; - const deltaPhi = radPerPixel * deltaX; - const deltaTheta = radPerPixel * deltaY; - const pos = camera.position.sub(center); - const radius = pos.length(); - let theta = Math.acos(pos.z / radius); - let phi = Math.atan2(pos.y, pos.x); - - // Subtract deltaTheta and deltaPhi - theta = Math.min(Math.max(theta - deltaTheta, 0), Math.PI); - phi -= deltaPhi; - - // Turn back into Cartesian coordinates - pos.x = radius * Math.sin(theta) * Math.cos(phi); - pos.y = radius * Math.sin(theta) * Math.sin(phi); - pos.z = radius * Math.cos(theta); - - camera.position.add(center); - camera.lookAt(center); - update(); - } - - function zoomIn() { - camera.position.sub(center).multiplyScalar(0.9).add(center); - update(); - } - - function zoomOut() { - camera.position.sub(center).multiplyScalar(1.1).add(center); - update(); - } - - addMouseHandler(canvas, drag, zoomIn, zoomOut); + makie_camera.update_light_dir(light_dir.value); + }); } function mul(a, b) { @@ -240,6 +164,12 @@ export class MakieCamera { // Lazy calculation, only if a plot type requests them // will be of the form: {[space, markerspace]: THREE.Uniform(...)} this.preprojections = {}; + + // For camera-relative light directions + // TODO: intial position wrong... + this.light_direction = new THREE.Uniform( + new THREE.Vector3(-1, -1, -1).normalize() + ); } calculate_matrices() { @@ -261,7 +191,8 @@ export class MakieCamera { // update all existing preprojection matrices Object.keys(this.preprojections).forEach((key) => { const [space, markerspace] = key.split(","); // jeez js, really just converting array keys to "elem,elem"? - this.preprojections[key].value = this.calculate_preprojection_matrix(space, markerspace); + this.preprojections[key].value = + this.calculate_preprojection_matrix(space, markerspace); }); } @@ -274,6 +205,13 @@ export class MakieCamera { return; } + update_light_dir(light_dir) { + const T = new THREE.Matrix3().setFromMatrix4(this.view.value).invert(); + const new_dir = new THREE.Vector3().fromArray(light_dir); + new_dir.applyMatrix3(T).normalize(); + this.light_direction.value = new_dir; + } + clip_to_space(space) { if (space === "data") { return this.projectionview_inverse.value; diff --git a/WGLMakie/src/Lines.js b/WGLMakie/src/Lines.js index 4f5926e4762..8fda06ae4eb 100644 --- a/WGLMakie/src/Lines.js +++ b/WGLMakie/src/Lines.js @@ -50,7 +50,7 @@ function linesegments_vertex_shader(uniforms, attributes) { vec3 screen_space(vec3 point) { vec4 vertex = projectionview * model * vec4(point, 1); - return vec3(vertex.xy * get_resolution() , vertex.z) / vertex.w; + return vec3(vertex.xy * get_resolution(), vertex.z + vertex.w * depth_shift) / vertex.w; } vec3 screen_space(vec2 point) { @@ -71,7 +71,7 @@ function linesegments_vertex_shader(uniforms, attributes) { vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); vec2 point = pointA + xBasis * position.x + yBasis * width * position.y; - gl_Position = vec4(point.xy / get_resolution(), p_a.z, 1.0); + gl_Position = vec4(point.xy / get_resolution(), position.x == 1.0 ? p_b.z : p_a.z, 1.0); } `; } diff --git a/WGLMakie/src/OrbitControls.js b/WGLMakie/src/OrbitControls.js new file mode 100644 index 00000000000..8a39fcb2332 --- /dev/null +++ b/WGLMakie/src/OrbitControls.js @@ -0,0 +1,1249 @@ +// Taken from three.js OrbitControls.js +// https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/jsm/controls/OrbitControls.js +import { + EventDispatcher, + MOUSE, + Quaternion, + Spherical, + TOUCH, + Vector2, + Vector3, + Plane, + Ray, + MathUtils, +} from "./THREE.js"; + +// OrbitControls performs orbiting, dollying (zooming), and panning. +// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). +// +// Orbit - left mouse / touch: one-finger move +// Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish +// Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move + +const _changeEvent = { type: "change" }; +const _startEvent = { type: "start" }; +const _endEvent = { type: "end" }; +const _ray = new Ray(); +const _plane = new Plane(); +const TILT_LIMIT = Math.cos(70 * MathUtils.DEG2RAD); + +class OrbitControls extends EventDispatcher { + constructor(object, domElement, allow_update, is_in_scene) { + super(); + + this.object = object; + this.domElement = domElement; + this.domElement.style.touchAction = "none"; // disable touch scroll + + // Set to false to disable this control + this.enabled = true; + + // "target" sets the location of focus, where the object orbits around + this.target = new Vector3(); + + // Sets the 3D cursor (similar to Blender), from which the maxTargetRadius takes effect + this.cursor = new Vector3(); + + // How far you can dolly in and out ( PerspectiveCamera only ) + this.minDistance = 0; + this.maxDistance = Infinity; + + // How far you can zoom in and out ( OrthographicCamera only ) + this.minZoom = 0; + this.maxZoom = Infinity; + + // Limit camera target within a spherical area around the cursor + this.minTargetRadius = 0; + this.maxTargetRadius = Infinity; + + // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + this.minPolarAngle = 0; // radians + this.maxPolarAngle = Math.PI; // radians + + // How far you can orbit horizontally, upper and lower limits. + // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) + this.minAzimuthAngle = -Infinity; // radians + this.maxAzimuthAngle = Infinity; // radians + + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + this.enableDamping = false; + this.dampingFactor = 0.05; + + // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + this.enableZoom = true; + this.zoomSpeed = 1.0; + + // Set to false to disable rotating + this.enableRotate = true; + this.rotateSpeed = 1.0; + + // Set to false to disable panning + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + this.zoomToCursor = false; + + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 + + // The four arrow keys + this.keys = { + LEFT: "ArrowLeft", + UP: "ArrowUp", + RIGHT: "ArrowRight", + BOTTOM: "ArrowDown", + }; + + // Mouse buttons + this.mouseButtons = { + LEFT: MOUSE.ROTATE, + MIDDLE: MOUSE.DOLLY, + RIGHT: MOUSE.PAN, + }; + + // Touch fingers + this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; + + // for reset + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + + // the target DOM element for key events + this._domElementKeyEvents = null; + + // + // public methods + // + + this.getPolarAngle = function () { + return spherical.phi; + }; + + this.getAzimuthalAngle = function () { + return spherical.theta; + }; + + this.getDistance = function () { + return this.object.position.distanceTo(this.target); + }; + + this.listenToKeyEvents = function (domElement) { + domElement.addEventListener("keydown", onKeyDown); + this._domElementKeyEvents = domElement; + }; + + this.stopListenToKeyEvents = function () { + this._domElementKeyEvents.removeEventListener("keydown", onKeyDown); + this._domElementKeyEvents = null; + }; + + this.saveState = function () { + scope.target0.copy(scope.target); + scope.position0.copy(scope.object.position); + scope.zoom0 = scope.object.zoom; + }; + + this.reset = function () { + scope.target.copy(scope.target0); + scope.object.position.copy(scope.position0); + scope.object.zoom = scope.zoom0; + + scope.object.updateProjectionMatrix(); + scope.dispatchEvent(_changeEvent); + + scope.update(); + + state = STATE.NONE; + }; + + // this method is exposed, but perhaps it would be better if we can make it private... + this.update = (function () { + const offset = new Vector3(); + + // so camera.up is the orbit axis + const quat = new Quaternion().setFromUnitVectors( + object.up, + new Vector3(0, 1, 0) + ); + const quatInverse = quat.clone().invert(); + + const lastPosition = new Vector3(); + const lastQuaternion = new Quaternion(); + const lastTargetPosition = new Vector3(); + + const twoPI = 2 * Math.PI; + + return function update(deltaTime = null) { + if (!allow_update()) { + return; + } + const position = scope.object.position; + + offset.copy(position).sub(scope.target); + + // rotate offset to "y-axis-is-up" space + offset.applyQuaternion(quat); + + // angle from z-axis around y-axis + spherical.setFromVector3(offset); + + if (scope.autoRotate && state === STATE.NONE) { + rotateLeft(getAutoRotationAngle(deltaTime)); + } + + if (scope.enableDamping) { + spherical.theta += + sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + } else { + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + } + + // restrict theta to be between desired limits + + let min = scope.minAzimuthAngle; + let max = scope.maxAzimuthAngle; + + if (isFinite(min) && isFinite(max)) { + if (min < -Math.PI) min += twoPI; + else if (min > Math.PI) min -= twoPI; + + if (max < -Math.PI) max += twoPI; + else if (max > Math.PI) max -= twoPI; + + if (min <= max) { + spherical.theta = Math.max( + min, + Math.min(max, spherical.theta) + ); + } else { + spherical.theta = + spherical.theta > (min + max) / 2 + ? Math.max(min, spherical.theta) + : Math.min(max, spherical.theta); + } + } + + // restrict phi to be between desired limits + spherical.phi = Math.max( + scope.minPolarAngle, + Math.min(scope.maxPolarAngle, spherical.phi) + ); + + spherical.makeSafe(); + + // move target to panned location + + if (scope.enableDamping === true) { + scope.target.addScaledVector( + panOffset, + scope.dampingFactor + ); + } else { + scope.target.add(panOffset); + } + + // Limit the target distance from the cursor to create a sphere around the center of interest + scope.target.sub(scope.cursor); + scope.target.clampLength( + scope.minTargetRadius, + scope.maxTargetRadius + ); + scope.target.add(scope.cursor); + + // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera + // we adjust zoom later in these cases + if ( + (scope.zoomToCursor && performCursorZoom) || + scope.object.isOrthographicCamera + ) { + spherical.radius = clampDistance(spherical.radius); + } else { + spherical.radius = clampDistance(spherical.radius * scale); + } + + offset.setFromSpherical(spherical); + + // rotate offset back to "camera-up-vector-is-up" space + offset.applyQuaternion(quatInverse); + + position.copy(scope.target).add(offset); + + scope.object.lookAt(scope.target); + + if (scope.enableDamping === true) { + sphericalDelta.theta *= 1 - scope.dampingFactor; + sphericalDelta.phi *= 1 - scope.dampingFactor; + + panOffset.multiplyScalar(1 - scope.dampingFactor); + } else { + sphericalDelta.set(0, 0, 0); + + panOffset.set(0, 0, 0); + } + + // adjust camera position + let zoomChanged = false; + if (scope.zoomToCursor && performCursorZoom) { + let newRadius = null; + if (scope.object.isPerspectiveCamera) { + // move the camera down the pointer ray + // this method avoids floating point error + const prevRadius = offset.length(); + newRadius = clampDistance(prevRadius * scale); + + const radiusDelta = prevRadius - newRadius; + scope.object.position.addScaledVector( + dollyDirection, + radiusDelta + ); + scope.object.updateMatrixWorld(); + } else if (scope.object.isOrthographicCamera) { + // adjust the ortho camera position based on zoom changes + const mouseBefore = new Vector3(mouse.x, mouse.y, 0); + mouseBefore.unproject(scope.object); + + scope.object.zoom = Math.max( + scope.minZoom, + Math.min(scope.maxZoom, scope.object.zoom / scale) + ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + const mouseAfter = new Vector3(mouse.x, mouse.y, 0); + mouseAfter.unproject(scope.object); + + scope.object.position.sub(mouseAfter).add(mouseBefore); + scope.object.updateMatrixWorld(); + + newRadius = offset.length(); + } else { + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled." + ); + scope.zoomToCursor = false; + } + + // handle the placement of the target + if (newRadius !== null) { + if (this.screenSpacePanning) { + // position the orbit target in front of the new camera position + scope.target + .set(0, 0, -1) + .transformDirection(scope.object.matrix) + .multiplyScalar(newRadius) + .add(scope.object.position); + } else { + // get the ray and translation plane to compute target + _ray.origin.copy(scope.object.position); + _ray.direction + .set(0, 0, -1) + .transformDirection(scope.object.matrix); + + // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid + // extremely large values + if ( + Math.abs(scope.object.up.dot(_ray.direction)) < + TILT_LIMIT + ) { + object.lookAt(scope.target); + } else { + _plane.setFromNormalAndCoplanarPoint( + scope.object.up, + scope.target + ); + _ray.intersectPlane(_plane, scope.target); + } + } + } + } else if (scope.object.isOrthographicCamera) { + scope.object.zoom = Math.max( + scope.minZoom, + Math.min(scope.maxZoom, scope.object.zoom / scale) + ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } + + scale = 1; + performCursorZoom = false; + + // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if ( + zoomChanged || + lastPosition.distanceToSquared(scope.object.position) > + EPS || + 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > + EPS || + lastTargetPosition.distanceToSquared(scope.target) > 0 + ) { + scope.dispatchEvent(_changeEvent); + + lastPosition.copy(scope.object.position); + lastQuaternion.copy(scope.object.quaternion); + lastTargetPosition.copy(scope.target); + + zoomChanged = false; + + return true; + } + + return false; + }; + })(); + + this.dispose = function () { + scope.domElement.removeEventListener("contextmenu", onContextMenu); + + scope.domElement.removeEventListener("pointerdown", onPointerDown); + scope.domElement.removeEventListener("pointercancel", onPointerUp); + scope.domElement.removeEventListener("wheel", onMouseWheel); + + scope.domElement.removeEventListener("pointermove", onPointerMove); + scope.domElement.removeEventListener("pointerup", onPointerUp); + + if (scope._domElementKeyEvents !== null) { + scope._domElementKeyEvents.removeEventListener( + "keydown", + onKeyDown + ); + scope._domElementKeyEvents = null; + } + + //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? + }; + + // + // internals + // + + const scope = this; + + const STATE = { + NONE: -1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6, + }; + + let state = STATE.NONE; + + const EPS = 0.000001; + + // current position in spherical coordinates + const spherical = new Spherical(); + const sphericalDelta = new Spherical(); + + let scale = 1; + const panOffset = new Vector3(); + + const rotateStart = new Vector2(); + const rotateEnd = new Vector2(); + const rotateDelta = new Vector2(); + + const panStart = new Vector2(); + const panEnd = new Vector2(); + const panDelta = new Vector2(); + + const dollyStart = new Vector2(); + const dollyEnd = new Vector2(); + const dollyDelta = new Vector2(); + + const dollyDirection = new Vector3(); + const mouse = new Vector2(); + let performCursorZoom = false; + + const pointers = []; + const pointerPositions = {}; + + function getAutoRotationAngle(deltaTime) { + if (deltaTime !== null) { + return ((2 * Math.PI) / 60) * scope.autoRotateSpeed * deltaTime; + } else { + return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed; + } + } + + function getZoomScale() { + return Math.pow(0.95, scope.zoomSpeed); + } + + function rotateLeft(angle) { + sphericalDelta.theta -= angle; + } + + function rotateUp(angle) { + sphericalDelta.phi -= angle; + } + + const panLeft = (function () { + const v = new Vector3(); + + return function panLeft(distance, objectMatrix) { + v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix + v.multiplyScalar(-distance); + + panOffset.add(v); + }; + })(); + + const panUp = (function () { + const v = new Vector3(); + + return function panUp(distance, objectMatrix) { + if (scope.screenSpacePanning === true) { + v.setFromMatrixColumn(objectMatrix, 1); + } else { + v.setFromMatrixColumn(objectMatrix, 0); + v.crossVectors(scope.object.up, v); + } + + v.multiplyScalar(distance); + + panOffset.add(v); + }; + })(); + + // deltaX and deltaY are in pixels; right and down are positive + const pan = (function () { + const offset = new Vector3(); + + return function pan(deltaX, deltaY) { + const element = scope.domElement; + + if (scope.object.isPerspectiveCamera) { + // perspective + const position = scope.object.position; + offset.copy(position).sub(scope.target); + let targetDistance = offset.length(); + + // half of the fov is center to top of screen + targetDistance *= Math.tan( + ((scope.object.fov / 2) * Math.PI) / 180.0 + ); + + // we use only clientHeight here so aspect ratio does not distort speed + panLeft( + (2 * deltaX * targetDistance) / element.clientHeight, + scope.object.matrix + ); + panUp( + (2 * deltaY * targetDistance) / element.clientHeight, + scope.object.matrix + ); + } else if (scope.object.isOrthographicCamera) { + // orthographic + panLeft( + (deltaX * (scope.object.right - scope.object.left)) / + scope.object.zoom / + element.clientWidth, + scope.object.matrix + ); + panUp( + (deltaY * (scope.object.top - scope.object.bottom)) / + scope.object.zoom / + element.clientHeight, + scope.object.matrix + ); + } else { + // camera neither orthographic nor perspective + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - pan disabled." + ); + scope.enablePan = false; + } + }; + })(); + + function dollyOut(dollyScale) { + if ( + scope.object.isPerspectiveCamera || + scope.object.isOrthographicCamera + ) { + scale /= dollyScale; + } else { + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled." + ); + scope.enableZoom = false; + } + } + + function dollyIn(dollyScale) { + if ( + scope.object.isPerspectiveCamera || + scope.object.isOrthographicCamera + ) { + scale *= dollyScale; + } else { + console.warn( + "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled." + ); + scope.enableZoom = false; + } + } + + function updateMouseParameters(event) { + if (!scope.zoomToCursor) { + return; + } + + performCursorZoom = true; + + const rect = scope.domElement.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const w = rect.width; + const h = rect.height; + + mouse.x = (x / w) * 2 - 1; + mouse.y = -(y / h) * 2 + 1; + + dollyDirection + .set(mouse.x, mouse.y, 1) + .unproject(scope.object) + .sub(scope.object.position) + .normalize(); + } + + function clampDistance(dist) { + return Math.max( + scope.minDistance, + Math.min(scope.maxDistance, dist) + ); + } + + // + // event callbacks - update the object state + // + + function handleMouseDownRotate(event) { + rotateStart.set(event.clientX, event.clientY); + } + + function handleMouseDownDolly(event) { + updateMouseParameters(event); + dollyStart.set(event.clientX, event.clientY); + } + + function handleMouseDownPan(event) { + panStart.set(event.clientX, event.clientY); + } + + function handleMouseMoveRotate(event) { + rotateEnd.set(event.clientX, event.clientY); + + rotateDelta + .subVectors(rotateEnd, rotateStart) + .multiplyScalar(scope.rotateSpeed); + + const element = scope.domElement; + + rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height + + rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight); + + rotateStart.copy(rotateEnd); + + scope.update(); + } + + function handleMouseMoveDolly(event) { + dollyEnd.set(event.clientX, event.clientY); + + dollyDelta.subVectors(dollyEnd, dollyStart); + + if (dollyDelta.y > 0) { + dollyOut(getZoomScale()); + } else if (dollyDelta.y < 0) { + dollyIn(getZoomScale()); + } + + dollyStart.copy(dollyEnd); + + scope.update(); + } + + function handleMouseMovePan(event) { + panEnd.set(event.clientX, event.clientY); + + panDelta + .subVectors(panEnd, panStart) + .multiplyScalar(scope.panSpeed); + + pan(panDelta.x, panDelta.y); + + panStart.copy(panEnd); + + scope.update(); + } + + function handleMouseWheel(event) { + updateMouseParameters(event); + + if (event.deltaY < 0) { + dollyIn(getZoomScale()); + } else if (event.deltaY > 0) { + dollyOut(getZoomScale()); + } + + scope.update(); + } + + function handleKeyDown(event) { + let needsUpdate = false; + + switch (event.code) { + case scope.keys.UP: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp( + (2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(0, scope.keyPanSpeed); + } + + needsUpdate = true; + break; + + case scope.keys.BOTTOM: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp( + (-2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(0, -scope.keyPanSpeed); + } + + needsUpdate = true; + break; + + case scope.keys.LEFT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft( + (2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(scope.keyPanSpeed, 0); + } + + needsUpdate = true; + break; + + case scope.keys.RIGHT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft( + (-2 * Math.PI * scope.rotateSpeed) / + scope.domElement.clientHeight + ); + } else { + pan(-scope.keyPanSpeed, 0); + } + + needsUpdate = true; + break; + } + + if (needsUpdate) { + // prevent the browser from scrolling on cursor keys + event.preventDefault(); + + scope.update(); + } + } + + function handleTouchStartRotate() { + if (pointers.length === 1) { + rotateStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + + rotateStart.set(x, y); + } + } + + function handleTouchStartPan() { + if (pointers.length === 1) { + panStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + + panStart.set(x, y); + } + } + + function handleTouchStartDolly() { + const dx = pointers[0].pageX - pointers[1].pageX; + const dy = pointers[0].pageY - pointers[1].pageY; + + const distance = Math.sqrt(dx * dx + dy * dy); + + dollyStart.set(0, distance); + } + + function handleTouchStartDollyPan() { + if (scope.enableZoom) handleTouchStartDolly(); + + if (scope.enablePan) handleTouchStartPan(); + } + + function handleTouchStartDollyRotate() { + if (scope.enableZoom) handleTouchStartDolly(); + + if (scope.enableRotate) handleTouchStartRotate(); + } + + function handleTouchMoveRotate(event) { + if (pointers.length == 1) { + rotateEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + + rotateEnd.set(x, y); + } + + rotateDelta + .subVectors(rotateEnd, rotateStart) + .multiplyScalar(scope.rotateSpeed); + + const element = scope.domElement; + + rotateLeft((2 * Math.PI * rotateDelta.x) / element.clientHeight); // yes, height + + rotateUp((2 * Math.PI * rotateDelta.y) / element.clientHeight); + + rotateStart.copy(rotateEnd); + } + + function handleTouchMovePan(event) { + if (pointers.length === 1) { + panEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + + panEnd.set(x, y); + } + + panDelta + .subVectors(panEnd, panStart) + .multiplyScalar(scope.panSpeed); + + pan(panDelta.x, panDelta.y); + + panStart.copy(panEnd); + } + + function handleTouchMoveDolly(event) { + const position = getSecondPointerPosition(event); + + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + + const distance = Math.sqrt(dx * dx + dy * dy); + + dollyEnd.set(0, distance); + + dollyDelta.set( + 0, + Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed) + ); + + dollyOut(dollyDelta.y); + + dollyStart.copy(dollyEnd); + } + + function handleTouchMoveDollyPan(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + + if (scope.enablePan) handleTouchMovePan(event); + } + + function handleTouchMoveDollyRotate(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + + if (scope.enableRotate) handleTouchMoveRotate(event); + } + + // + // event handlers - FSM: listen for events and reset state + // + + function onPointerDown(event) { + if (scope.enabled === false) return; + + if (pointers.length === 0) { + scope.domElement.setPointerCapture(event.pointerId); + + scope.domElement.addEventListener("pointermove", onPointerMove); + scope.domElement.addEventListener("pointerup", onPointerUp); + } + + // + + addPointer(event); + + if (event.pointerType === "touch") { + onTouchStart(event); + } else { + onMouseDown(event); + } + } + + function onPointerMove(event) { + if (scope.enabled === false) return; + if (!is_in_scene(event)) return; + if (event.pointerType === "touch") { + onTouchMove(event); + } else { + onMouseMove(event); + } + } + + function onPointerUp(event) { + removePointer(event); + + if (pointers.length === 0) { + scope.domElement.releasePointerCapture(event.pointerId); + + scope.domElement.removeEventListener( + "pointermove", + onPointerMove + ); + scope.domElement.removeEventListener("pointerup", onPointerUp); + } + + scope.dispatchEvent(_endEvent); + + state = STATE.NONE; + } + + function onMouseDown(event) { + let mouseAction; + + switch (event.button) { + case 0: + mouseAction = scope.mouseButtons.LEFT; + break; + + case 1: + mouseAction = scope.mouseButtons.MIDDLE; + break; + + case 2: + mouseAction = scope.mouseButtons.RIGHT; + break; + + default: + mouseAction = -1; + } + + switch (mouseAction) { + case MOUSE.DOLLY: + if (scope.enableZoom === false) return; + + handleMouseDownDolly(event); + + state = STATE.DOLLY; + + break; + + case MOUSE.ROTATE: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enablePan === false) return; + + handleMouseDownPan(event); + + state = STATE.PAN; + } else { + if (scope.enableRotate === false) return; + + handleMouseDownRotate(event); + + state = STATE.ROTATE; + } + + break; + + case MOUSE.PAN: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enableRotate === false) return; + + handleMouseDownRotate(event); + + state = STATE.ROTATE; + } else { + if (scope.enablePan === false) return; + + handleMouseDownPan(event); + + state = STATE.PAN; + } + + break; + + default: + state = STATE.NONE; + } + + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + + function onMouseMove(event) { + switch (state) { + case STATE.ROTATE: + if (scope.enableRotate === false) return; + + handleMouseMoveRotate(event); + + break; + + case STATE.DOLLY: + if (scope.enableZoom === false) return; + + handleMouseMoveDolly(event); + + break; + + case STATE.PAN: + if (scope.enablePan === false) return; + + handleMouseMovePan(event); + + break; + } + } + + function onMouseWheel(event) { + if ( + scope.enabled === false || + scope.enableZoom === false || + state !== STATE.NONE || + !is_in_scene(event) + ) + return; + + event.preventDefault(); + + scope.dispatchEvent(_startEvent); + + handleMouseWheel(event); + + scope.dispatchEvent(_endEvent); + } + + function onKeyDown(event) { + if (scope.enabled === false || scope.enablePan === false) return; + + handleKeyDown(event); + } + + function onTouchStart(event) { + trackPointer(event); + + switch (pointers.length) { + case 1: + switch (scope.touches.ONE) { + case TOUCH.ROTATE: + if (scope.enableRotate === false) return; + + handleTouchStartRotate(); + + state = STATE.TOUCH_ROTATE; + + break; + + case TOUCH.PAN: + if (scope.enablePan === false) return; + + handleTouchStartPan(); + + state = STATE.TOUCH_PAN; + + break; + + default: + state = STATE.NONE; + } + + break; + + case 2: + switch (scope.touches.TWO) { + case TOUCH.DOLLY_PAN: + if ( + scope.enableZoom === false && + scope.enablePan === false + ) + return; + + handleTouchStartDollyPan(); + + state = STATE.TOUCH_DOLLY_PAN; + + break; + + case TOUCH.DOLLY_ROTATE: + if ( + scope.enableZoom === false && + scope.enableRotate === false + ) + return; + + handleTouchStartDollyRotate(); + + state = STATE.TOUCH_DOLLY_ROTATE; + + break; + + default: + state = STATE.NONE; + } + + break; + + default: + state = STATE.NONE; + } + + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + + function onTouchMove(event) { + trackPointer(event); + + switch (state) { + case STATE.TOUCH_ROTATE: + if (scope.enableRotate === false) return; + + handleTouchMoveRotate(event); + + scope.update(); + + break; + + case STATE.TOUCH_PAN: + if (scope.enablePan === false) return; + + handleTouchMovePan(event); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) + return; + + handleTouchMoveDollyPan(event); + + scope.update(); + + break; + + case STATE.TOUCH_DOLLY_ROTATE: + if ( + scope.enableZoom === false && + scope.enableRotate === false + ) + return; + + handleTouchMoveDollyRotate(event); + + scope.update(); + + break; + + default: + state = STATE.NONE; + } + } + + function onContextMenu(event) { + if (scope.enabled === false) return; + + event.preventDefault(); + } + + function addPointer(event) { + pointers.push(event); + } + + function removePointer(event) { + delete pointerPositions[event.pointerId]; + + for (let i = 0; i < pointers.length; i++) { + if (pointers[i].pointerId == event.pointerId) { + pointers.splice(i, 1); + return; + } + } + } + + function trackPointer(event) { + let position = pointerPositions[event.pointerId]; + + if (position === undefined) { + position = new Vector2(); + pointerPositions[event.pointerId] = position; + } + + position.set(event.pageX, event.pageY); + } + + function getSecondPointerPosition(event) { + const pointer = + event.pointerId === pointers[0].pointerId + ? pointers[1] + : pointers[0]; + + return pointerPositions[pointer.pointerId]; + } + + // + + scope.domElement.addEventListener("contextmenu", onContextMenu); + + scope.domElement.addEventListener("pointerdown", onPointerDown); + scope.domElement.addEventListener("pointercancel", onPointerUp); + scope.domElement.addEventListener("wheel", onMouseWheel, { + passive: false, + }); + + // force an update at start + + this.update(); + } +} + +export { OrbitControls }; diff --git a/WGLMakie/src/Serialization.js b/WGLMakie/src/Serialization.js index 64bfa76268e..3fc7c66c5a3 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) { @@ -198,7 +197,7 @@ export function add_plot(scene, plot_data) { plot_data.uniforms.projection = identity; plot_data.uniforms.projectionview = identity; } - const {px_per_unit} = scene.screen; + const { px_per_unit } = scene.screen; plot_data.uniforms.resolution = cam.resolution; plot_data.uniforms.px_per_unit = new THREE.Uniform(px_per_unit); @@ -209,8 +208,25 @@ export function add_plot(scene, plot_data) { 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); + }); + } + 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 @@ -260,7 +276,6 @@ function convert_RGB_to_RGBA(rgbArray) { return rgbaArray; } - function create_texture(data) { const buffer = data.data; if (data.size.length == 3) { @@ -311,17 +326,17 @@ function re_create_texture(old_texture, buffer, size) { old_texture.type ); } - tex.minFilter = old_texture.minFilter - tex.magFilter = old_texture.magFilter - tex.anisotropy = old_texture.anisotropy - tex.wrapS = old_texture.wrapS + tex.minFilter = old_texture.minFilter; + tex.magFilter = old_texture.magFilter; + tex.anisotropy = old_texture.anisotropy; + tex.wrapS = old_texture.wrapS; if (size.length > 1) { - tex.wrapT = old_texture.wrapT + tex.wrapT = old_texture.wrapT; } if (size.length > 2) { - tex.wrapR = old_texture.wrapR + tex.wrapR = old_texture.wrapR; } - return tex + return tex; } function BufferAttribute(buffer) { const jsbuff = new THREE.BufferAttribute(buffer.flat, buffer.type_length); @@ -530,33 +545,51 @@ export function deserialize_scene(data, screen) { add_scene(data.uuid, scene); scene.scene_uuid = data.uuid; scene.frustumCulled = false; - scene.pixelarea = data.pixelarea; + scene.viewport = data.viewport; scene.backgroundcolor = data.backgroundcolor; + 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(); scene.wgl_camera = camera; - function update_cam(camera_matrices) { + function update_cam(camera_matrices, force) { + if (!force) { + // we use the threejs orbit controls, if the julia connection is gone + // at least for 3d ... 2d is still a todo, and will stay static right now + if (!(JSServe.can_send_to_julia && JSServe.can_send_to_julia())) { + return; + } + } const [view, projection, resolution, eyepos] = camera_matrices; camera.update_matrices(view, projection, resolution, eyepos); } - update_cam(data.camera.value); - if (data.cam3d_state) { - Camera.attach_3d_camera(canvas, camera, data.cam3d_state, scene); - } else { - data.camera.on(update_cam); + Camera.attach_3d_camera( + canvas, + camera, + data.cam3d_state, + data.light_direction, + scene + ); } + + update_cam(data.camera.value, true); // force update on first call + camera.update_light_dir(data.light_direction.value); + 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..b7fef72b073 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 @@ -48,22 +48,76 @@ end """ struct ScreenConfig framerate::Float64 # =30.0 - resize_to_body::Bool # false + resize_to::Any # nothing # We use nothing, since that serializes correctly to nothing in JS, which is important since that's where we calculate the defaults! # For the theming, we need to use Automatic though, since that's the Makie meaning for gets calculated somewhere else px_per_unit::Union{Nothing,Float64} # nothing, a.k.a the browser px_per_unit (devicePixelRatio) scalefactor::Union{Nothing,Float64} - function ScreenConfig(framerate::Number, resize_to_body::Bool, px_per_unit::Union{Number, Automatic, Nothing}, scalefactor::Union{Number, Automatic, Nothing}) + resize_to_body::Bool + function ScreenConfig( + framerate::Number, resize_to::Any, px_per_unit::Union{Number, Automatic, Nothing}, + scalefactor::Union{Number, Automatic, Nothing}, resize_to_body::Union{Nothing, Bool}) + if px_per_unit isa Automatic px_per_unit = nothing end if scalefactor isa Automatic scalefactor = nothing end - return new(framerate, resize_to_body, px_per_unit, scalefactor) + if resize_to_body isa Bool + @warn("`resize_to_body` is deprecated, use `resize_to = :body` instead") + if !(resize_to isa Nothing) + @warn("Setting `resize_to_body` and `resize_to` at the same time, only use resize_to") + else + resize_to = resize_to_body ? :body : nothing + end + end + ResizeType = Union{Nothing, Symbol} + if !(resize_to isa Union{ResizeType,Tuple{ResizeType,ResizeType}}) + error("Only nothing, :parent, or :body allowed, or a tuple of those for width/height.") + end + return new(framerate, resize_to, px_per_unit, scalefactor) end end + +""" + WithConfig(fig::Makie.FigureLike; screen_config...) + +Allows to pass a screenconfig to a figure, inside a JSServe.App. +This circumvents using `WGLMakie.activate!(; screen_config...)` inside an App, which modifies these values globally. +Example: + +```julia +App() do + f1 = scatter(1:4) + f2 = scatter(1:4; figure=(; backgroundcolor=:gray)) + wc = WGLMakie.WithConfig(f2; resize_to=:parent) + DOM.div(f1, DOM.div(wc; style="height: 200px; width: 50%")) +end +``` +""" +struct WithConfig + fig::Makie.FigureLike + config::ScreenConfig +end + +function WithConfig(fig::Makie.FigureLike; kw...) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol,Any}(kw)) + return WithConfig(fig, config) +end + +function JSServe.jsrender(session::Session, wconfig::WithConfig) + fig = wconfig.fig + Makie.update_state_before_display!(fig) + scene = Makie.get_scene(fig) + screen = Screen(scene, wconfig.config) + three, canvas, on_init = render_with_init(screen, session, scene) + return canvas +end + + + """ Screen(args...; screen_config...) @@ -96,7 +150,7 @@ function Base.show(io::IO, screen::Screen) sf = c.scalefactor print(io, """WGLMakie.Screen( framerate = $(c.framerate), - resize_to_body = $(c.resize_to_body), + resize_to = $(c.resize_to), px_per_unit = $(isnothing(ppu) ? :automatic : ppu), scalefactor = $(isnothing(sf) ? :automatic : sf) )""") @@ -118,8 +172,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 +244,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 +262,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 +307,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 +331,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 +365,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 +440,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 +463,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 +480,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..e5417f19fe7 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -6,29 +6,36 @@ 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))) - 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)) - per_vertex = Dict(:positions => positions, :faces => faces, :uv => uv, :normals => normals) + # TODO: Use Makie.surface2mesh + ps = lift(grid, plot, px, py, pz, transform_func_obs(plot), get(plot, :space, :data)) + positions = Buffer(ps) + rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), plot, pz) + fs = lift(r -> decompose(QuadFace{Int}, r), plot, rect) + fs = map((ps, fs) -> filter(f -> !any(i -> isnan(ps[i]), f), fs), plot, ps, fs) + faces = Buffer(fs) + # This adjusts uvs (compared to decompose_uv) so texture sampling starts at + # the center of a texture pixel rather than the edge, fixing + # https://github.com/MakieOrg/Makie.jl/pull/2598#discussion_r1152552196 + uv = Buffer(lift(plot, rect) do r + Nx, Ny = r.nvertices + f = Vec2f(1 / Nx, 1 / Ny) + [f .* Vec2f(0.5 + i, 0.5 + j) for j in Ny-1:-1:0 for i in 0:Nx-1] + end) + normals = Buffer(lift(Makie.nan_aware_normals, plot, ps, fs)) + per_vertex = Dict(:positions => positions, :faces => faces, :uv => uv, :normals => normals) uniforms = Dict(:uniform_color => color, :color => false) + return draw_mesh(mscene, per_vertex, plot, uniforms) 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 +52,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 +60,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, @@ -75,7 +82,8 @@ function create_shader(mscene::Scene, plot::Volume) :depth_shift => get(plot, :depth_shift, Observable(0.0f0)), # these get filled in later by serialization, but we need them # as dummy values here, so that the correct uniforms are emitted - :lightposition => Vec3f(1), + :light_direction => Vec3f(1), + :light_color => Vec3f(1), :eyeposition => Vec3f(1), :ambient => Vec3f(1), :picking => false, @@ -128,24 +136,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..9066d676374 100644 --- a/WGLMakie/src/lines.jl +++ b/WGLMakie/src/lines.jl @@ -3,6 +3,7 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) uniforms = Dict( :model => plot.model, :object_id => 1, + :depth_shift => plot.depth_shift, :picking => false, ) @@ -19,15 +20,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 +38,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..ded9dee2ab7 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,28 +55,32 @@ 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) get!(uniforms, :pattern, false) get!(uniforms, :model, plot.model) - get!(uniforms, :lightposition, Vec3f(1)) get!(uniforms, :ambient, Vec3f(1)) + get!(uniforms, :light_direction, Vec3f(1)) + get!(uniforms, :light_color, Vec3f(1)) uniforms[:interpolate_in_fragment_shader] = get(plot, :interpolate_in_fragment_shader, true) - get!(uniforms, :shading, get(plot, :shading, false)) + get!(uniforms, :shading, to_value(get(plot, :shading, NoShading)) != NoShading) - uniforms[:normalmatrix] = map(mscene.camera.view, plot.model) do v, m + uniforms[:normalmatrix] = map(plot.model) do m i = Vec(1, 2, 3) - return transpose(inv(v[i, i] * m[i, i])) + return transpose(inv(m[i, i])) end 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 +102,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..5c4a3bc681f 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -38,7 +38,8 @@ end const IGNORE_KEYS = Set([ :shading, :overdraw, :rotation, :distancefield, :space, :markerspace, :fxaa, :visible, :transformation, :alpha, :linewidth, :transparency, :marker, - :lightposition, :cycle, :label, :inspector_clear, :inspector_hover, + :light_direction, :light_color, + :cycle, :label, :inspector_clear, :inspector_hover, :inspector_label, :axis_cycler ]) @@ -49,7 +50,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)) @@ -77,13 +78,19 @@ function create_shader(scene::Scene, plot::MeshScatter) uniform_dict[:depth_shift] = get(plot, :depth_shift, Observable(0f0)) uniform_dict[:backlight] = plot.backlight - get!(uniform_dict, :ambient, Vec3f(1)) + # Make sure these exist + get!(uniform_dict, :ambient, Vec3f(0.1)) + get!(uniform_dict, :diffuse, Vec3f(0.9)) + get!(uniform_dict, :specular, Vec3f(0.3)) + get!(uniform_dict, :shininess, 8f0) + get!(uniform_dict, :light_direction, Vec3f(1)) + get!(uniform_dict, :light_color, Vec3f(1)) # id + picking gets filled in JS, needs to be here to emit the correct shader uniforms uniform_dict[:picking] = false uniform_dict[:object_id] = UInt32(0) - uniform_dict[:shading] = plot.shading + uniform_dict[:shading] = map(x -> x != NoShading, plot.shading) return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), instance, VertexArray(; per_instance...), uniform_dict) @@ -114,9 +121,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 +131,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 +170,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 +207,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 +237,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 +255,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..bee88140232 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, viewport(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 @@ -292,10 +292,17 @@ function serialize_scene(scene::Scene) children = map(child-> serialize_scene(child), scene.children) - serialized = Dict(:pixelarea => pixel_area, - :backgroundcolor => lift(hexcolor, scene.backgroundcolor), + dirlight = Makie.get_directional_light(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, @@ -304,7 +311,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 +326,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) @@ -331,11 +338,11 @@ function serialize_three(scene::Scene, plot::AbstractPlot) uniforms = mesh[:uniforms] updater = mesh[:uniform_updater] - pointlight = Makie.get_point_light(scene) - if !isnothing(pointlight) - uniforms[:lightposition] = serialize_three(pointlight.position[]) - on(pointlight.position) do value - updater[] = [:lightposition, serialize_three(value)] + dirlight = Makie.get_directional_light(scene) + if !isnothing(dirlight) + uniforms[:light_color] = serialize_three(dirlight.color[]) + on(plot, dirlight.color) do value + updater[] = [:light_color, serialize_three(value)] return end end @@ -343,7 +350,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..539ff7109bb 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, viewport(scene)) canvas = DOM.m("canvas"; tabindex="0", style="display: block") wrapper = DOM.div(canvas; style="width: 100%; height: 100%") comm = Observable(Dict{String,Any}()) @@ -55,7 +44,7 @@ function three_display(screen::Screen, session::Session, scene::Scene) try { const renderer = WGL.create_scene( $wrapper, $canvas, $canvas_width, $scene_serialized, $comm, $width, $height, - $(ta), $(config.framerate), $(config.resize_to_body), $(config.px_per_unit), $(config.scalefactor) + $(ta), $(config.framerate), $(config.resize_to), $(config.px_per_unit), $(config.scalefactor) ) const gl = renderer.getContext() const err = gl.getError() @@ -77,3 +66,4 @@ function three_display(screen::Screen, session::Session, scene::Scene) three = ThreeDisplay(session) return three, wrapper, done_init end +| diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index cef88fb9eee..cd50663ea6c 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -20258,6 +20258,736 @@ function attributes_to_type_declaration(attributes_dict) { } return result; } +const _changeEvent = { + type: "change" +}; +const _startEvent = { + type: "start" +}; +const _endEvent = { + type: "end" +}; +const _ray = new hi(); +const _plane = new mn(); +const TILT_LIMIT = Math.cos(70 * yv.DEG2RAD); +class OrbitControls extends sn { + constructor(object, domElement, allow_update, is_in_scene){ + super(); + this.object = object; + this.domElement = domElement; + this.domElement.style.touchAction = "none"; + this.enabled = true; + this.target = new A(); + this.cursor = new A(); + this.minDistance = 0; + this.maxDistance = Infinity; + this.minZoom = 0; + this.maxZoom = Infinity; + this.minTargetRadius = 0; + this.maxTargetRadius = Infinity; + this.minPolarAngle = 0; + this.maxPolarAngle = Math.PI; + this.minAzimuthAngle = -Infinity; + this.maxAzimuthAngle = Infinity; + this.enableDamping = false; + this.dampingFactor = 0.05; + this.enableZoom = true; + this.zoomSpeed = 1.0; + this.enableRotate = true; + this.rotateSpeed = 1.0; + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; + this.keyPanSpeed = 7.0; + this.zoomToCursor = false; + this.autoRotate = false; + this.autoRotateSpeed = 2.0; + this.keys = { + LEFT: "ArrowLeft", + UP: "ArrowUp", + RIGHT: "ArrowRight", + BOTTOM: "ArrowDown" + }; + this.mouseButtons = { + LEFT: zx.ROTATE, + MIDDLE: zx.DOLLY, + RIGHT: zx.PAN + }; + this.touches = { + ONE: Vx.ROTATE, + TWO: Vx.DOLLY_PAN + }; + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; + this._domElementKeyEvents = null; + this.getPolarAngle = function() { + return spherical.phi; + }; + this.getAzimuthalAngle = function() { + return spherical.theta; + }; + this.getDistance = function() { + return this.object.position.distanceTo(this.target); + }; + this.listenToKeyEvents = function(domElement) { + domElement.addEventListener("keydown", onKeyDown); + this._domElementKeyEvents = domElement; + }; + this.stopListenToKeyEvents = function() { + this._domElementKeyEvents.removeEventListener("keydown", onKeyDown); + this._domElementKeyEvents = null; + }; + this.saveState = function() { + scope.target0.copy(scope.target); + scope.position0.copy(scope.object.position); + scope.zoom0 = scope.object.zoom; + }; + this.reset = function() { + scope.target.copy(scope.target0); + scope.object.position.copy(scope.position0); + scope.object.zoom = scope.zoom0; + scope.object.updateProjectionMatrix(); + scope.dispatchEvent(_changeEvent); + scope.update(); + state = STATE.NONE; + }; + this.update = function() { + const offset = new A(); + const quat = new Ut().setFromUnitVectors(object.up, new A(0, 1, 0)); + const quatInverse = quat.clone().invert(); + const lastPosition = new A(); + const lastQuaternion = new Ut(); + const lastTargetPosition = new A(); + const twoPI = 2 * Math.PI; + return function update(deltaTime = null) { + if (!allow_update()) { + return; + } + const position = scope.object.position; + offset.copy(position).sub(scope.target); + offset.applyQuaternion(quat); + spherical.setFromVector3(offset); + if (scope.autoRotate && state === STATE.NONE) { + rotateLeft(getAutoRotationAngle(deltaTime)); + } + if (scope.enableDamping) { + spherical.theta += sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + } else { + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + } + let min = scope.minAzimuthAngle; + let max = scope.maxAzimuthAngle; + if (isFinite(min) && isFinite(max)) { + if (min < -Math.PI) min += twoPI; + else if (min > Math.PI) min -= twoPI; + if (max < -Math.PI) max += twoPI; + else if (max > Math.PI) max -= twoPI; + if (min <= max) { + spherical.theta = Math.max(min, Math.min(max, spherical.theta)); + } else { + spherical.theta = spherical.theta > (min + max) / 2 ? Math.max(min, spherical.theta) : Math.min(max, spherical.theta); + } + } + spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)); + spherical.makeSafe(); + if (scope.enableDamping === true) { + scope.target.addScaledVector(panOffset, scope.dampingFactor); + } else { + scope.target.add(panOffset); + } + scope.target.sub(scope.cursor); + scope.target.clampLength(scope.minTargetRadius, scope.maxTargetRadius); + scope.target.add(scope.cursor); + if (scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera) { + spherical.radius = clampDistance(spherical.radius); + } else { + spherical.radius = clampDistance(spherical.radius * scale); + } + offset.setFromSpherical(spherical); + offset.applyQuaternion(quatInverse); + position.copy(scope.target).add(offset); + scope.object.lookAt(scope.target); + if (scope.enableDamping === true) { + sphericalDelta.theta *= 1 - scope.dampingFactor; + sphericalDelta.phi *= 1 - scope.dampingFactor; + panOffset.multiplyScalar(1 - scope.dampingFactor); + } else { + sphericalDelta.set(0, 0, 0); + panOffset.set(0, 0, 0); + } + let zoomChanged = false; + if (scope.zoomToCursor && performCursorZoom) { + let newRadius = null; + if (scope.object.isPerspectiveCamera) { + const prevRadius = offset.length(); + newRadius = clampDistance(prevRadius * scale); + const radiusDelta = prevRadius - newRadius; + scope.object.position.addScaledVector(dollyDirection, radiusDelta); + scope.object.updateMatrixWorld(); + } else if (scope.object.isOrthographicCamera) { + const mouseBefore = new A(mouse.x, mouse.y, 0); + mouseBefore.unproject(scope.object); + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + const mouseAfter = new A(mouse.x, mouse.y, 0); + mouseAfter.unproject(scope.object); + scope.object.position.sub(mouseAfter).add(mouseBefore); + scope.object.updateMatrixWorld(); + newRadius = offset.length(); + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."); + scope.zoomToCursor = false; + } + if (newRadius !== null) { + if (this.screenSpacePanning) { + scope.target.set(0, 0, -1).transformDirection(scope.object.matrix).multiplyScalar(newRadius).add(scope.object.position); + } else { + _ray.origin.copy(scope.object.position); + _ray.direction.set(0, 0, -1).transformDirection(scope.object.matrix); + if (Math.abs(scope.object.up.dot(_ray.direction)) < TILT_LIMIT) { + object.lookAt(scope.target); + } else { + _plane.setFromNormalAndCoplanarPoint(scope.object.up, scope.target); + _ray.intersectPlane(_plane, scope.target); + } + } + } + } else if (scope.object.isOrthographicCamera) { + scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / scale)); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + } + scale = 1; + performCursorZoom = false; + if (zoomChanged || lastPosition.distanceToSquared(scope.object.position) > EPS || 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS || lastTargetPosition.distanceToSquared(scope.target) > 0) { + scope.dispatchEvent(_changeEvent); + lastPosition.copy(scope.object.position); + lastQuaternion.copy(scope.object.quaternion); + lastTargetPosition.copy(scope.target); + zoomChanged = false; + return true; + } + return false; + }; + }(); + this.dispose = function() { + scope.domElement.removeEventListener("contextmenu", onContextMenu); + scope.domElement.removeEventListener("pointerdown", onPointerDown); + scope.domElement.removeEventListener("pointercancel", onPointerUp); + scope.domElement.removeEventListener("wheel", onMouseWheel); + scope.domElement.removeEventListener("pointermove", onPointerMove); + scope.domElement.removeEventListener("pointerup", onPointerUp); + if (scope._domElementKeyEvents !== null) { + scope._domElementKeyEvents.removeEventListener("keydown", onKeyDown); + scope._domElementKeyEvents = null; + } + }; + const scope = this; + const STATE = { + NONE: -1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 + }; + let state = STATE.NONE; + const EPS = 0.000001; + const spherical = new Ou(); + const sphericalDelta = new Ou(); + let scale = 1; + const panOffset = new A(); + const rotateStart = new Z(); + const rotateEnd = new Z(); + const rotateDelta = new Z(); + const panStart = new Z(); + const panEnd = new Z(); + const panDelta = new Z(); + const dollyStart = new Z(); + const dollyEnd = new Z(); + const dollyDelta = new Z(); + const dollyDirection = new A(); + const mouse = new Z(); + let performCursorZoom = false; + const pointers = []; + const pointerPositions = {}; + function getAutoRotationAngle(deltaTime) { + if (deltaTime !== null) { + return 2 * Math.PI / 60 * scope.autoRotateSpeed * deltaTime; + } else { + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + } + } + function getZoomScale() { + return Math.pow(0.95, scope.zoomSpeed); + } + function rotateLeft(angle) { + sphericalDelta.theta -= angle; + } + function rotateUp(angle) { + sphericalDelta.phi -= angle; + } + const panLeft = function() { + const v = new A(); + return function panLeft(distance, objectMatrix) { + v.setFromMatrixColumn(objectMatrix, 0); + v.multiplyScalar(-distance); + panOffset.add(v); + }; + }(); + const panUp = function() { + const v = new A(); + return function panUp(distance, objectMatrix) { + if (scope.screenSpacePanning === true) { + v.setFromMatrixColumn(objectMatrix, 1); + } else { + v.setFromMatrixColumn(objectMatrix, 0); + v.crossVectors(scope.object.up, v); + } + v.multiplyScalar(distance); + panOffset.add(v); + }; + }(); + const pan = function() { + const offset = new A(); + return function pan(deltaX, deltaY) { + const element = scope.domElement; + if (scope.object.isPerspectiveCamera) { + const position = scope.object.position; + offset.copy(position).sub(scope.target); + let targetDistance = offset.length(); + targetDistance *= Math.tan(scope.object.fov / 2 * Math.PI / 180.0); + panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix); + panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix); + } else if (scope.object.isOrthographicCamera) { + panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix); + panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix); + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."); + scope.enablePan = false; + } + }; + }(); + function dollyOut(dollyScale) { + if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) { + scale /= dollyScale; + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); + scope.enableZoom = false; + } + } + function dollyIn(dollyScale) { + if (scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera) { + scale *= dollyScale; + } else { + console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); + scope.enableZoom = false; + } + } + function updateMouseParameters(event) { + if (!scope.zoomToCursor) { + return; + } + performCursorZoom = true; + const rect = scope.domElement.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const w = rect.width; + const h = rect.height; + mouse.x = x / w * 2 - 1; + mouse.y = -(y / h) * 2 + 1; + dollyDirection.set(mouse.x, mouse.y, 1).unproject(scope.object).sub(scope.object.position).normalize(); + } + function clampDistance(dist) { + return Math.max(scope.minDistance, Math.min(scope.maxDistance, dist)); + } + function handleMouseDownRotate(event) { + rotateStart.set(event.clientX, event.clientY); + } + function handleMouseDownDolly(event) { + updateMouseParameters(event); + dollyStart.set(event.clientX, event.clientY); + } + function handleMouseDownPan(event) { + panStart.set(event.clientX, event.clientY); + } + function handleMouseMoveRotate(event) { + rotateEnd.set(event.clientX, event.clientY); + rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); + const element = scope.domElement; + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); + rotateStart.copy(rotateEnd); + scope.update(); + } + function handleMouseMoveDolly(event) { + dollyEnd.set(event.clientX, event.clientY); + dollyDelta.subVectors(dollyEnd, dollyStart); + if (dollyDelta.y > 0) { + dollyOut(getZoomScale()); + } else if (dollyDelta.y < 0) { + dollyIn(getZoomScale()); + } + dollyStart.copy(dollyEnd); + scope.update(); + } + function handleMouseMovePan(event) { + panEnd.set(event.clientX, event.clientY); + panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + scope.update(); + } + function handleMouseWheel(event) { + updateMouseParameters(event); + if (event.deltaY < 0) { + dollyIn(getZoomScale()); + } else if (event.deltaY > 0) { + dollyOut(getZoomScale()); + } + scope.update(); + } + function handleKeyDown(event) { + let needsUpdate = false; + switch(event.code){ + case scope.keys.UP: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp(2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(0, scope.keyPanSpeed); + } + needsUpdate = true; + break; + case scope.keys.BOTTOM: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateUp(-2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(0, -scope.keyPanSpeed); + } + needsUpdate = true; + break; + case scope.keys.LEFT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft(2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(scope.keyPanSpeed, 0); + } + needsUpdate = true; + break; + case scope.keys.RIGHT: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + rotateLeft(-2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight); + } else { + pan(-scope.keyPanSpeed, 0); + } + needsUpdate = true; + break; + } + if (needsUpdate) { + event.preventDefault(); + scope.update(); + } + } + function handleTouchStartRotate() { + if (pointers.length === 1) { + rotateStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + rotateStart.set(x, y); + } + } + function handleTouchStartPan() { + if (pointers.length === 1) { + panStart.set(pointers[0].pageX, pointers[0].pageY); + } else { + const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); + const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); + panStart.set(x, y); + } + } + function handleTouchStartDolly() { + const dx = pointers[0].pageX - pointers[1].pageX; + const dy = pointers[0].pageY - pointers[1].pageY; + const distance = Math.sqrt(dx * dx + dy * dy); + dollyStart.set(0, distance); + } + function handleTouchStartDollyPan() { + if (scope.enableZoom) handleTouchStartDolly(); + if (scope.enablePan) handleTouchStartPan(); + } + function handleTouchStartDollyRotate() { + if (scope.enableZoom) handleTouchStartDolly(); + if (scope.enableRotate) handleTouchStartRotate(); + } + function handleTouchMoveRotate(event) { + if (pointers.length == 1) { + rotateEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + rotateEnd.set(x, y); + } + rotateDelta.subVectors(rotateEnd, rotateStart).multiplyScalar(scope.rotateSpeed); + const element = scope.domElement; + rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); + rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight); + rotateStart.copy(rotateEnd); + } + function handleTouchMovePan(event) { + if (pointers.length === 1) { + panEnd.set(event.pageX, event.pageY); + } else { + const position = getSecondPointerPosition(event); + const x = 0.5 * (event.pageX + position.x); + const y = 0.5 * (event.pageY + position.y); + panEnd.set(x, y); + } + panDelta.subVectors(panEnd, panStart).multiplyScalar(scope.panSpeed); + pan(panDelta.x, panDelta.y); + panStart.copy(panEnd); + } + function handleTouchMoveDolly(event) { + const position = getSecondPointerPosition(event); + const dx = event.pageX - position.x; + const dy = event.pageY - position.y; + const distance = Math.sqrt(dx * dx + dy * dy); + dollyEnd.set(0, distance); + dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, scope.zoomSpeed)); + dollyOut(dollyDelta.y); + dollyStart.copy(dollyEnd); + } + function handleTouchMoveDollyPan(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + if (scope.enablePan) handleTouchMovePan(event); + } + function handleTouchMoveDollyRotate(event) { + if (scope.enableZoom) handleTouchMoveDolly(event); + if (scope.enableRotate) handleTouchMoveRotate(event); + } + function onPointerDown(event) { + if (scope.enabled === false) return; + if (pointers.length === 0) { + scope.domElement.setPointerCapture(event.pointerId); + scope.domElement.addEventListener("pointermove", onPointerMove); + scope.domElement.addEventListener("pointerup", onPointerUp); + } + addPointer(event); + if (event.pointerType === "touch") { + onTouchStart(event); + } else { + onMouseDown(event); + } + } + function onPointerMove(event) { + if (scope.enabled === false) return; + if (!is_in_scene(event)) return; + if (event.pointerType === "touch") { + onTouchMove(event); + } else { + onMouseMove(event); + } + } + function onPointerUp(event) { + removePointer(event); + if (pointers.length === 0) { + scope.domElement.releasePointerCapture(event.pointerId); + scope.domElement.removeEventListener("pointermove", onPointerMove); + scope.domElement.removeEventListener("pointerup", onPointerUp); + } + scope.dispatchEvent(_endEvent); + state = STATE.NONE; + } + function onMouseDown(event) { + let mouseAction; + switch(event.button){ + case 0: + mouseAction = scope.mouseButtons.LEFT; + break; + case 1: + mouseAction = scope.mouseButtons.MIDDLE; + break; + case 2: + mouseAction = scope.mouseButtons.RIGHT; + break; + default: + mouseAction = -1; + } + switch(mouseAction){ + case zx.DOLLY: + if (scope.enableZoom === false) return; + handleMouseDownDolly(event); + state = STATE.DOLLY; + break; + case zx.ROTATE: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enablePan === false) return; + handleMouseDownPan(event); + state = STATE.PAN; + } else { + if (scope.enableRotate === false) return; + handleMouseDownRotate(event); + state = STATE.ROTATE; + } + break; + case zx.PAN: + if (event.ctrlKey || event.metaKey || event.shiftKey) { + if (scope.enableRotate === false) return; + handleMouseDownRotate(event); + state = STATE.ROTATE; + } else { + if (scope.enablePan === false) return; + handleMouseDownPan(event); + state = STATE.PAN; + } + break; + default: + state = STATE.NONE; + } + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + function onMouseMove(event) { + switch(state){ + case STATE.ROTATE: + if (scope.enableRotate === false) return; + handleMouseMoveRotate(event); + break; + case STATE.DOLLY: + if (scope.enableZoom === false) return; + handleMouseMoveDolly(event); + break; + case STATE.PAN: + if (scope.enablePan === false) return; + handleMouseMovePan(event); + break; + } + } + function onMouseWheel(event) { + if (scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE || !is_in_scene(event)) return; + event.preventDefault(); + scope.dispatchEvent(_startEvent); + handleMouseWheel(event); + scope.dispatchEvent(_endEvent); + } + function onKeyDown(event) { + if (scope.enabled === false || scope.enablePan === false) return; + handleKeyDown(event); + } + function onTouchStart(event) { + trackPointer(event); + switch(pointers.length){ + case 1: + switch(scope.touches.ONE){ + case Vx.ROTATE: + if (scope.enableRotate === false) return; + handleTouchStartRotate(); + state = STATE.TOUCH_ROTATE; + break; + case Vx.PAN: + if (scope.enablePan === false) return; + handleTouchStartPan(); + state = STATE.TOUCH_PAN; + break; + default: + state = STATE.NONE; + } + break; + case 2: + switch(scope.touches.TWO){ + case Vx.DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) return; + handleTouchStartDollyPan(); + state = STATE.TOUCH_DOLLY_PAN; + break; + case Vx.DOLLY_ROTATE: + if (scope.enableZoom === false && scope.enableRotate === false) return; + handleTouchStartDollyRotate(); + state = STATE.TOUCH_DOLLY_ROTATE; + break; + default: + state = STATE.NONE; + } + break; + default: + state = STATE.NONE; + } + if (state !== STATE.NONE) { + scope.dispatchEvent(_startEvent); + } + } + function onTouchMove(event) { + trackPointer(event); + switch(state){ + case STATE.TOUCH_ROTATE: + if (scope.enableRotate === false) return; + handleTouchMoveRotate(event); + scope.update(); + break; + case STATE.TOUCH_PAN: + if (scope.enablePan === false) return; + handleTouchMovePan(event); + scope.update(); + break; + case STATE.TOUCH_DOLLY_PAN: + if (scope.enableZoom === false && scope.enablePan === false) return; + handleTouchMoveDollyPan(event); + scope.update(); + break; + case STATE.TOUCH_DOLLY_ROTATE: + if (scope.enableZoom === false && scope.enableRotate === false) return; + handleTouchMoveDollyRotate(event); + scope.update(); + break; + default: + state = STATE.NONE; + } + } + function onContextMenu(event) { + if (scope.enabled === false) return; + event.preventDefault(); + } + function addPointer(event) { + pointers.push(event); + } + function removePointer(event) { + delete pointerPositions[event.pointerId]; + for(let i = 0; i < pointers.length; i++){ + if (pointers[i].pointerId == event.pointerId) { + pointers.splice(i, 1); + return; + } + } + } + function trackPointer(event) { + let position = pointerPositions[event.pointerId]; + if (position === undefined) { + position = new Z(); + pointerPositions[event.pointerId] = position; + } + position.set(event.pageX, event.pageY); + } + function getSecondPointerPosition(event) { + const pointer = event.pointerId === pointers[0].pointerId ? pointers[1] : pointers[0]; + return pointerPositions[pointer.pointerId]; + } + scope.domElement.addEventListener("contextmenu", onContextMenu); + scope.domElement.addEventListener("pointerdown", onPointerDown); + scope.domElement.addEventListener("pointercancel", onPointerUp); + scope.domElement.addEventListener("wheel", onMouseWheel, { + passive: false + }); + this.update(); + } +} function events2unitless(screen, event) { const { canvas , winscale , renderer } = screen; const rect = canvas.getBoundingClientRect(); @@ -20273,20 +21003,22 @@ function Identity4x4() { } function in_scene(scene, mouse_event) { const [x, y] = events2unitless(scene.screen, mouse_event); - const [sx, sy, sw, sh] = scene.pixelarea.value; + const [sx, sy, sw, sh] = scene.viewport.value; return x >= sx && x < sx + sw && y >= sy && y < sy + sh; } -function attach_3d_camera(canvas, makie_camera, cam3d, scene) { +function attach_3d_camera(canvas, makie_camera, cam3d, light_dir, scene) { if (cam3d === undefined) { 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, 0.01, 100.0); + const center = new A(...cam3d.lookat.value); + camera.up = new A(...cam3d.upvector.value); + camera.position.set(...cam3d.eyeposition.value); camera.lookAt(center); - function update() { + const use_orbit_cam = ()=>!(JSServe.can_send_to_julia && JSServe.can_send_to_julia()); + const controls = new OrbitControls(camera, canvas, use_orbit_cam, (e)=>in_scene(scene, e)); + controls.addEventListener("change", (e)=>{ const view = camera.matrixWorldInverse; const projection = camera.projectionMatrix; const [width, height] = cam3d.resolution.value; @@ -20302,82 +21034,8 @@ function attach_3d_camera(canvas, makie_camera, cam3d, scene) { 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 (!in_scene(scene, e)) { - return; - } - const delta = Math.sign(e.deltaY); - if (delta == -1) { - zoomOut(); - } else if (delta == 1) { - zoomIn(); - } - e.preventDefault(); - } - function mouseDownHandler(e) { - if (!in_scene(scene, e)) { - return; - } - startDragX = e.clientX; - startDragY = e.clientY; - e.preventDefault(); - } - function mouseMoveHandler(e) { - if (!in_scene(scene, e)) { - return; - } - if (startDragX === null || startDragY === null) return; - if (drag) drag(e.clientX - startDragX, e.clientY - startDragY); - startDragX = e.clientX; - startDragY = e.clientY; - e.preventDefault(); - } - function mouseUpHandler(e) { - if (!in_scene(scene, e)) { - return; - } - mouseMoveHandler.call(this, e); - startDragX = null; - startDragY = null; - e.preventDefault(); - } - domObject.addEventListener("wheel", mouseWheelHandler); - domObject.addEventListener("mousedown", mouseDownHandler); - domObject.addEventListener("mousemove", mouseMoveHandler); - domObject.addEventListener("mouseup", mouseUpHandler); - } - function drag(deltaX, deltaY) { - const radPerPixel = Math.PI / 450; - const deltaPhi = radPerPixel * deltaX; - const deltaTheta = radPerPixel * deltaY; - const pos = camera.position.sub(center); - const radius = pos.length(); - let theta = Math.acos(pos.z / radius); - let phi = Math.atan2(pos.y, pos.x); - theta = Math.min(Math.max(theta - deltaTheta, 0), Math.PI); - phi -= deltaPhi; - pos.x = radius * Math.sin(theta) * Math.cos(phi); - pos.y = radius * Math.sin(theta) * Math.sin(phi); - pos.z = radius * Math.cos(theta); - camera.position.add(center); - camera.lookAt(center); - update(); - } - function zoomIn() { - camera.position.sub(center).multiplyScalar(0.9).add(center); - update(); - } - function zoomOut() { - camera.position.sub(center).multiplyScalar(1.1).add(center); - update(); - } - addMouseHandler(canvas, drag, zoomIn, zoomOut); + makie_camera.update_light_dir(light_dir.value); + }); } function mul(a, b) { return b.clone().multiply(a); @@ -20458,6 +21116,7 @@ class MakieCamera { this.resolution = new Pu(new Z()); this.eyeposition = new Pu(new A()); this.preprojections = {}; + this.light_direction = new Pu(new A(-1, -1, -1).normalize()); } calculate_matrices() { const [w, h] = this.resolution.value; @@ -20480,6 +21139,12 @@ class MakieCamera { this.calculate_matrices(); return; } + update_light_dir(light_dir) { + const T = new He().setFromMatrix4(this.view.value).invert(); + const new_dir = new A().fromArray(light_dir); + new_dir.applyMatrix3(T).normalize(); + this.light_direction.value = new_dir; + } clip_to_space(space) { if (space === "data") { return this.projectionview_inverse.value; @@ -20555,6 +21220,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 +21238,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 +21253,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); @@ -20673,7 +21337,7 @@ function linesegments_vertex_shader(uniforms, attributes) { vec3 screen_space(vec3 point) { vec4 vertex = projectionview * model * vec4(point, 1); - return vec3(vertex.xy * get_resolution() , vertex.z) / vertex.w; + return vec3(vertex.xy * get_resolution(), vertex.z + vertex.w * depth_shift) / vertex.w; } vec3 screen_space(vec2 point) { @@ -20694,7 +21358,7 @@ function linesegments_vertex_shader(uniforms, attributes) { vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); vec2 point = pointA + xBasis * position.x + yBasis * width * position.y; - gl_Position = vec4(point.xy / get_resolution(), p_a.z, 1.0); + gl_Position = vec4(point.xy / get_resolution(), position.x == 1.0 ? p_b.z : p_a.z, 1.0); } `; } @@ -20930,8 +21594,20 @@ function add_plot(scene, plot_data) { 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(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()); @@ -21196,26 +21872,37 @@ function deserialize_scene(data, screen) { add_scene(data.uuid, scene); scene.scene_uuid = data.uuid; scene.frustumCulled = false; - scene.pixelarea = data.pixelarea; + scene.viewport = data.viewport; scene.backgroundcolor = data.backgroundcolor; + 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) { + function update_cam(camera_matrices, force) { + if (!force) { + if (!(JSServe.can_send_to_julia && JSServe.can_send_to_julia())) { + return; + } + } const [view, projection, resolution, eyepos] = camera_matrices; camera.update_matrices(view, projection, resolution, eyepos); } - update_cam(data.camera.value); if (data.cam3d_state) { - attach_3d_camera(canvas, camera, data.cam3d_state, scene); - } else { - data.camera.on(update_cam); + attach_3d_camera(canvas, camera, data.cam3d_state, data.light_direction, scene); } + 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); }); - 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,16 +21927,16 @@ 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) { return true; } renderer.autoClear = scene.clearscene.value; - const area = scene.pixelarea.value; + const area = scene.viewport.value; if (area) { const [x, y, w, h] = area.map((x)=>x * px_per_unit); renderer.setViewport(x, y, w, h); @@ -21259,7 +21946,8 @@ function render_scene(scene, picking = false) { renderer.setClearAlpha(0); renderer.setClearColor(new mod.Color(0), 0.0); } else { - renderer.setClearColor(scene.backgroundcolor.value); + const alpha = scene.backgroundcolor_alpha.value; + renderer.setClearColor(scene.backgroundcolor.value, alpha); } renderer.render(scene, camera); } @@ -21311,6 +21999,13 @@ function get_body_size() { height ]; } +function get_parent_size(canvas) { + const rect = canvas.parentElement.getBoundingClientRect(); + return [ + rect.width, + rect.height + ]; +} function wglerror(gl, error) { switch(error){ case gl.NO_ERROR: @@ -21361,7 +22056,7 @@ function on_shader_error(gl, program, glVertexShader, glFragmentShader) { const err = "THREE.WebGLProgram: Shader Error " + wglerror(gl, gl.getError()) + " - " + "VALIDATE_STATUS " + gl.getProgramParameter(program, gl.VALIDATE_STATUS) + "\n\n" + "Program Info Log:\n" + programLog + "\n" + vertexErrors + "\n" + fragmentErrors + "\n" + "Fragment log:\n" + fragmentLog + "Vertex log:\n" + vertexLog; JSServe.Connection.send_warning(err); } -function add_canvas_events(screen, comm, resize_to_body) { +function add_canvas_events(screen, comm, resize_to) { const { canvas , winscale } = screen; function mouse_callback(event) { const [x, y] = events2unitless(screen, event); @@ -21426,7 +22121,12 @@ function add_canvas_events(screen, comm, resize_to_body) { canvas.addEventListener("contextmenu", (e)=>e.preventDefault()); canvas.addEventListener("focusout", contextmenu); function resize_callback() { - const [width, height] = get_body_size(); + let width, height; + if (resize_to == "body") { + [width, height] = get_body_size(); + } else if (resize_to == "parent") { + [width, height] = get_parent_size(canvas); + } comm.notify({ resize: [ width / winscale, @@ -21434,7 +22134,7 @@ function add_canvas_events(screen, comm, resize_to_body) { ] }); } - if (resize_to_body) { + if (resize_to) { const resize_callback_throttled = throttle_function(resize_callback, 100); window.addEventListener("resize", (event)=>resize_callback_throttled()); resize_callback_throttled(); @@ -21497,7 +22197,7 @@ function add_picking_target(screen) { screen.picking_target = new mod.WebGLRenderTarget(w, h); return; } -function create_scene(wrapper, canvas, canvas_width, scenes, comm, width, height, texture_atlas_obs, fps, resize_to_body, px_per_unit, scalefactor) { +function create_scene(wrapper, canvas, canvas_width, scenes, comm, width, height, texture_atlas_obs, fps, resize_to, px_per_unit, scalefactor) { if (!scalefactor) { scalefactor = window.devicePixelRatio || 1.0; } @@ -21523,7 +22223,7 @@ function create_scene(wrapper, canvas, canvas_width, scenes, comm, width, height scalefactor, winscale }; - add_canvas_events(screen, comm, resize_to_body); + add_canvas_events(screen, comm, resize_to); set_render_size(screen, width, height); const three_scene = deserialize_scene(scenes, screen); start_renderloop(three_scene); @@ -21685,6 +22385,9 @@ function pick_sorted(scene, xy, range) { for(let i = 1; i <= dx; i++){ for(let j = 1; j <= dx; j++){ const d = x - i ^ 2 + (y - j) ^ 2; + if (plot_matrix.length <= pindex) { + continue; + } const [plot_uuid, index] = plot_matrix[pindex]; pindex = pindex + 1; const plot_index = selected.findIndex((x)=>x[0].plot_uuid == plot_uuid); diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index 8ba09b201f1..a9986046845 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 @@ -34,7 +34,7 @@ export function render_scene(scene, picking = false) { return true; } renderer.autoClear = scene.clearscene.value; - const area = scene.pixelarea.value; + const area = scene.viewport.value; if (area) { const [x, y, w, h] = area.map((x) => x * px_per_unit); renderer.setViewport(x, y, w, h); @@ -44,7 +44,8 @@ export function render_scene(scene, picking = false) { renderer.setClearAlpha(0); renderer.setClearColor(new THREE.Color(0), 0.0); } else { - renderer.setClearColor(scene.backgroundcolor.value); + const alpha = scene.backgroundcolor_alpha.value; + renderer.setClearColor(scene.backgroundcolor.value, alpha); } renderer.render(scene, camera); } @@ -130,7 +131,10 @@ function get_body_size() { const height = (window.innerHeight - height_padding); return [width, height]; } - +function get_parent_size(canvas) { + const rect = canvas.parentElement.getBoundingClientRect(); + return [rect.width, rect.height]; +} export function wglerror(gl, error) { switch (error) { @@ -219,7 +223,7 @@ function on_shader_error(gl, program, glVertexShader, glFragmentShader) { JSServe.Connection.send_warning(err); } -function add_canvas_events(screen, comm, resize_to_body) { +function add_canvas_events(screen, comm, resize_to) { const { canvas, winscale } = screen; function mouse_callback(event) { const [x, y] = events2unitless(screen, event); @@ -295,11 +299,16 @@ function add_canvas_events(screen, comm, resize_to_body) { canvas.addEventListener("focusout", contextmenu); function resize_callback() { - const [width, height] = get_body_size(); + let width, height; + if (resize_to == "body") { + [width, height] = get_body_size(); + } else if (resize_to == "parent") { + [width, height] = get_parent_size(canvas); + } // Send the resize event to Julia comm.notify({ resize: [width / winscale, height / winscale] }); } - if (resize_to_body) { + if (resize_to) { const resize_callback_throttled = throttle_function( resize_callback, 100 @@ -400,7 +409,7 @@ function create_scene( height, texture_atlas_obs, fps, - resize_to_body, + resize_to, px_per_unit, scalefactor ) { @@ -434,7 +443,7 @@ function create_scene( scalefactor, winscale, }; - add_canvas_events(screen, comm, resize_to_body); + add_canvas_events(screen, comm, resize_to); set_render_size(screen, width, height); const three_scene = deserialize_scene(scenes, screen); @@ -608,6 +617,9 @@ export function pick_sorted(scene, xy, range) { for (let i = 1; i <= dx; i++) { for (let j = 1; j <= dx; j++) { const d = (x - i) ^ (2 + (y - j)) ^ 2; + if (plot_matrix.length <= pindex) { + continue; + } const [plot_uuid, index] = plot_matrix[pindex]; pindex = pindex + 1; const plot_index = selected.findIndex( diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 3983aa86e81..22135765afd 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -31,20 +31,16 @@ excludes = Set([ "FEM mesh 2D", "FEM polygon 2D", # missing transparency & image - "Wireframe of a Surface", "Image on Surface Sphere", - "Surface with image", # Marker size seems wrong in some occasions: "Hbox", "UnicodeMarker", # Not sure, looks pretty similar to me! Maybe blend mode? "Test heatmap + image overlap", - "heatmaps & surface", - "OldAxis + Surface", + # "heatmaps & surface", # TODO: fix direct NaN -> nancolor conversion "Order Independent Transparency", "Record Video", "fast pixel marker", - "Animated surface and wireframe", "Array of Images Scatter", "Image Scatter different sizes", "scatter with stroke", @@ -65,13 +61,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 +76,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/explanations/backends/cairomakie.md b/docs/explanations/backends/cairomakie.md index d31748bcd57..2619d3f79c8 100644 --- a/docs/explanations/backends/cairomakie.md +++ b/docs/explanations/backends/cairomakie.md @@ -24,31 +24,6 @@ CairoMakie.activate!(type = "png") CairoMakie.activate!(type = "svg") ``` -#### Resolution Scaling - -When you save a CairoMakie figure, you can change the mapping from figure resolution to pixels (when saving to png) or points (when saving to svg or pdf). -This way you can easily scale the resulting image up or down without having to change any plot element sizes. - -Just specify `pt_per_unit` when saving vector formats and `px_per_unit` when saving pngs. -`px_per_unit` defaults to 1 and `pt_per_unit` defaults to 0.75. -When embedding svgs in websites, `1px` is equivalent to `0.75pt`. -This means that by default, saving a png or an svg results in an embedded image of the same apparent size. -If you require an exact size in `pt`, consider setting `pt_per_unit = 1`. - -Here's an example: - -```julia -fig = Figure(resolution = (800, 600)) - -save("normal.pdf", fig) # size = 600 x 450 pt -save("larger.pdf", fig, pt_per_unit = 2) # size = 1600 x 1200 pt -save("smaller.pdf", fig, pt_per_unit = 0.5) # size = 400 x 300 pt - -save("normal.png", fig) # size = 800 x 600 px -save("larger.png", fig, px_per_unit = 2) # size = 1600 x 1200 px -save("smaller.png", fig, px_per_unit = 0.5) # size = 400 x 300 px -``` - #### Z-Order CairoMakie as a 2D engine has no concept of z-clipping, therefore its 3D capabilities are quite limited. @@ -62,7 +37,7 @@ By setting the `rasterize` attribute of a plot, you can tell CairoMakie that thi Assuming that you have a `Plot` object `plt`, you can set `plt.rasterize = true` for simple rasterization, or you can set `plt.rasterize = scale::Int`, where `scale` represents the scaling factor for the image surface. -For example, if your Scene's resolution is `(800, 600)`, by setting `scale=2`, the rasterized image will have a resolution of `(1600, 1200)`. +For example, if your Scene's size is `(800, 600)`, by setting `scale=2`, the rasterized image embedded in the vector graphic will have a resolution of `(1600, 1200)`. You can deactivate this rasterization by setting `plt.rasterize = false`. diff --git a/docs/explanations/backends/glmakie.md b/docs/explanations/backends/glmakie.md index 0c0af8dfa3e..3ab4785869a 100644 --- a/docs/explanations/backends/glmakie.md +++ b/docs/explanations/backends/glmakie.md @@ -20,13 +20,13 @@ println("~~~") The sizes of figures are given in display-independent "logical" dimensions, and the GLMakie backend will scale the size of the displayed window on HiDPI/Retina displays automatically. -For example, the default `resolution = (800, 600)` will be shown in a 1600 × 1200 window +For example, the default `size = (800, 600)` will be shown in a 1600 × 1200 window on a HiDPI display which is configured with a 200% scaling factor. The scaling factor may be overridden by displaying the figure with a different `scalefactor` value: ```julia -fig = Figure(resolution = (800, 600)) +fig = Figure(size = (800, 600)) # ... display(fig, scalefactor = 1.5) ``` @@ -44,7 +44,7 @@ can be scaled to achieve HiDPI/Retina resolution renderings. The resolution scaling defaults to the same factor as the window scaling, but it may be independently overridden with the `px_per_unit` argument when showing a figure: ```julia -fig = Figure(resolution = (800, 600)) +fig = Figure(size = (800, 600)) # ... display(fig, px_per_unit = 2) ``` @@ -61,8 +61,7 @@ of results. #### Multiple Windows -GLMakie has experimental support for displaying multiple independent figures (or scenes). To open a new window, use `display(GLMakie.Screen(), figure_or_scene)`. - +GLMakie has experimental support for displaying multiple independent figures (or scenes). To open a new window, use `display(GLMakie.Screen(), figure_or_scene)`. To close all windows, use `GLMakie.closeall()`. ## Forcing Dedicated GPU Use In Linux diff --git a/docs/explanations/backends/rprmakie.md b/docs/explanations/backends/rprmakie.md index bbd4f31903c..8120a42dcc2 100644 --- a/docs/explanations/backends/rprmakie.md +++ b/docs/explanations/backends/rprmakie.md @@ -94,7 +94,7 @@ using Colors: N0f8 radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] -fig = Figure(; resolution=(1500, 700)); +fig = Figure(; size=(1500, 700)); ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights)) screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=400) @@ -178,7 +178,7 @@ function glow_material(data_normed) end RPRMakie.activate!(iterations=32, plugin=RPR.Northstar) -fig = Figure(; resolution=(2000, 800)) +fig = Figure(; size=(2000, 800)) radiance = 30000 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(0, 100, 100), RGBf(radiance, radiance, radiance))] @@ -232,7 +232,7 @@ radiance = 500 lights = [EnvironmentLight(1.0, load(RPR.assetpath("studio026.exr"))), PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] -fig = Figure(; resolution=(1500, 1000)) +fig = Figure(; size=(1500, 1000)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(; lights=lights)) screen = RPRMakie.Screen(size(ax.scene); plugin=RPR.Tahoe) material = RPR.UberMaterial(screen.matsys) @@ -397,7 +397,7 @@ lights = [ EnvironmentLight(1.5, rotl90(load(assetpath("sunflowers_1k.hdr"))')), PointLight(Vec3f(50, 0, 200), RGBf(radiance, radiance, radiance*1.1)), ] -s = Scene(resolution=(500, 500), lights=lights) +s = Scene(size=(500, 500), lights=lights) cam3d!(s) c = cameracontrols(s) @@ -491,7 +491,7 @@ earth_img = load(Downloads.download("https://upload.wikimedia.org/wikipedia/comm # the actual plot ! RPRMakie.activate!(; iterations=100) scene = with_theme(theme_dark()) do - fig = Figure(; resolution=(1000, 1000)) + fig = Figure(; size=(1000, 1000)) radiance = 30 lights = [EnvironmentLight(0.5, load(RPR.assetpath("starmap_4k.tif"))), PointLight(Vec3f(1, 1, 3), RGBf(radiance, radiance, radiance))] diff --git a/docs/explanations/backends/wglmakie.md b/docs/explanations/backends/wglmakie.md index 39c05e215bd..599c22b6776 100644 --- a/docs/explanations/backends/wglmakie.md +++ b/docs/explanations/backends/wglmakie.md @@ -378,7 +378,7 @@ end App() do session::Session # We can now use this wherever we want: - fig = Figure(resolution=(200, 200)) + fig = Figure(size=(200, 200)) contour(fig[1,1], rand(4,4)) card = GridCard( Slider(1:100), diff --git a/docs/explanations/colors.md b/docs/explanations/colors.md index 77b39c0e6ea..995ee1da1a6 100644 --- a/docs/explanations/colors.md +++ b/docs/explanations/colors.md @@ -36,7 +36,7 @@ theme = Attributes( with_theme(theme) do - f = Figure(resolution = (800, 1200)) + f = Figure(size = (800, 1200)) ax = Axis(f[1, 1], xautolimitmargin = (0.2, 0.2), yautolimitmargin = (0.1, 0.1)) hidedecorations!(ax) hidespines!(ax) diff --git a/docs/explanations/faq.md b/docs/explanations/faq.md index e94a9fede36..58bb69d0aef 100644 --- a/docs/explanations/faq.md +++ b/docs/explanations/faq.md @@ -162,7 +162,7 @@ using CairoMakie set_theme!(backgroundcolor = :gray90) -f = Figure(resolution = (800, 600)) +f = Figure(size = (800, 600)) for i in 1:3, j in 1:3 ax = Axis(f[i, j], title = "$i, $j", width = 100, height = 100) diff --git a/docs/explanations/figure.md b/docs/explanations/figure.md index cc529c9f9af..8d7e878191a 100644 --- a/docs/explanations/figure.md +++ b/docs/explanations/figure.md @@ -6,11 +6,11 @@ The `Figure` object contains a top-level `Scene` and a `GridLayout`, as well as ## Creating a Figure You can create a figure explicitly with the `Figure()` function, and set attributes of the underlying scene. -The most important one of which is the `resolution`. +The most important one of which is the `size`. ```julia f = Figure() -f = Figure(resolution = (600, 400)) +f = Figure(size = (600, 400)) ``` A figure is also created implicitly when you use simple, non-mutating plotting commands like `plot()`, `scatter()`, `lines()`, etc. @@ -31,7 +31,7 @@ figure, = scatter(rand(100, 2)) You can pass arguments to the created figure in a dict-like object to the special `figure` keyword: ```julia -scatter(rand(100, 2), figure = (resolution = (600, 400),)) +scatter(rand(100, 2), figure = (size = (600, 400),)) ``` ## Placing Blocks into a Figure @@ -167,55 +167,52 @@ contents(f[1, 1]) == [ax] content(f[1, 1]) == ax ``` -## Figure size +## Figure size and units -The size or resolution of a Figure is given without units, such as `resolution = (800, 600)`. -You can think of these values as "device-independent pixels". -Like the `px` unit in CSS, these values do not directly correspond to physical pixels of your screen or pixels in a png file. -Instead, they can be mapped to these device pixels using a scaling factor. +In Makie, figure size and attributes like line widths, font sizes, scatter marker extents, or layout column and row gaps are usually given as plain numbers, without an explicit unit attached. +What does it mean to have a `Figure` with `size = (600, 450)`, a line with `linewidth = 10` or a column gap of `30`? -Currently, these scaling factors are only directly supported by CairoMakie, but in the future they should be available for GLMakie and WGLMakie as well. -Right now, the implicit scaling factor of GLMakie and WGLMakie is 1, which means that a window of a figure with resolution 800 x 600 will actually have 800 x 600 pixels in its frame buffer. -In the future, this should be adjustable, for example for "retina" or high-dpi displays, where the frame buffer for a 800 x 600 window typically has 1600 x 1200 pixels. +The first underlying idea is that, no matter what your final output format is, these numbers are _relative_. +You can expect a `linewidth = 10` to cover 1/60th of the width `600` of the `Figure` and a column gap of `30` to span 1/20th of the Figure. +This holds, no matter if you later export that `Figure` as an image made out of pixels, or as a vector graphic that doesn't have pixels at all. -## Matching figure and font sizes to documents +The second idea is that, given some `Figure`, we want to be able to export an image at arbitrary resolution, or a vector graphic at any size from it, as long as the relative sizes of all elements stay intact. +So we need to _translate_ our abstract sizes to real sizes when we render. +In Makie, this is done with two scaling factors: `px_per_unit` for images and `pt_per_unit` for vector graphics. -Journal papers and other documents written in Word or LaTeX commonly use the `pt` unit to define font sizes. -The unit `pt` is a physical dimension and is typically defined as `1 inch / 72`. -To match font sizes of Makie plots with other text in these documents, you have to adjust both the figure size and font size together. +A line with `linewidth = 10` will be 10 pixels wide if rendered to an image file with `px_per_unit = 1`. It will be 5 pixels wide if `px_per_unit = 0.5` and 20 pixels if `px_per_unit = 2`. A `Figure` with `size = (600, 450)` will have 600 x 450 pixels when exported with `px_per_unit = 1`, 300 x 225 with `px_per_unit = 0.5` and 1200 x 900 with `px_per_unit = 2`. -First, you need to convert the physical target size of your figure in the document to device-independent pixels. -For this, you have to decide a `px_per_unit` value if you're exporting a bitmap, or a `pt_per_unit` value if you export vector graphics. -With those, you can convert the target font size into device-independent pixels as well. +It works exactly the same for vector graphics, just with a different target unit. A `pt` or point is a typographic unit that is defined as 1/72 of an inch, which comes out to about 0.353 mm. A line with `linewidth = 10` will be 10 points wide if rendered to an svg file with `pt_per_unit = 1`, it will be 5 points wide for `pt_per_unit = 0.5` and 20 points wide if `pt_per_unit = 2`. A `Figure` with `size = (600, 450)` will be 600 x 450 points in size when exported with `pt_per_unit = 1`, 300 x 225 with `pt_per_unit = 0.5` and 1200 x 900 with `pt_per_unit = 2`. -CairoMakie is the only backend that can export both bitmaps and vector graphics. -By default, its `px_per_unit` is `2` and `pt_per_unit` is `0.75`, but those values are chosen with interactive plotting with web-technology tools in mind. -The reason is that in normal web browsers, `1px` is equal to `0.75pt` and images with a density of 2 pixels for each device-independent `px` look sharper on modern high-dpi displays. -The default fontsize of `16` will by default look like `12pt` in web and print contexts this way. +### Defaults of `px_per_unit` and `pt_per_unit` -### Example +What are the default values of `px_per_unit` and `pt_per_unit` in each Makie backend, and why are they set that way? -Let's say we want to create a vector graphic for a scientific paper set with 12pt font size, and the figure size should be 5 x 4 inches which is equivalent to 360 x 288 pt (multiply by 72). +Let us start with `pt_per_unit` because this value is only relevant for one backend, which is CairoMakie. +The default value in CairoMakie is `pt_per_unit = 0.75`. So if you `save(figure, "output.svg")` a `Figure` with `size = (600, 450)`, this comes out as a vector graphic that is 450 x 337.5 pt large. -With the default `pt_per_unit = 0.75` we arrive at a necessary figure size of 480 x 384 device-independent pixels (divide by 0.75). +Why 0.75 and not simply 1? This has to do with web standards and device-independent pixels. Websites mix vector graphics and images, so they need some way to relate the sizes of both types to each other. In principle, a pixel in an image doesn't have a real-world width. But you don't want the images on your site to shrink relative to the other content when device pixels are small, or grow when device pixels are large. So web browsers don't directly map image pixels to device pixels. Instead, they use a concept called device-independent pixels. If you place an image with 600 x 450 pixels in a website, this image is interpreted by default to be 600 x 450 device-independent pixels wide. One device-independent pixel is defined to be 0.75 pt wide, that's where the factor 0.75 comes in. So an image with 600 x 450 device-independent pixels is the same apparent size as a vector graphic with size 450 x 337.5 pt. On high-resolution screens, browsers then simply render one device-independent pixel with multiple device pixels (for example 2x2 on an Apple Retina display) so that content stays at readable sizes and doesn't look tiny. -Equivalently, the font size we need to match 12pt is `12 / 0.75 = 16`. +For Makie, we decided that we want our abstract units to match device-independent pixels when used in web contexts, because that's very convenient and easy to predict for the end user. If you have a Jupyter or Pluto notebook, it's nice if a `Figure` comes out at the same apparent size, no matter if you're currently in CairoMakie's svg mode, or in the bitmap mode of any backend. Therefore, we annotate images with the original `Figure` size in device-independent pixels, so they are of the same apparent size, no matter what the `px_per_unit` value and therefore the effective pixel size is. And we give svg files the default scaling factor of 0.75 so that svgs always match images in apparent size. -Therefore, we can create our figure with `Figure(resolution = (480, 384), fontsize = 16)` and save with `save("figure.pdf", fig)`. +Now let us look at the default values for `px_per_unit`. In CairoMakie, the default is `px_per_unit = 2`. This means, a `Figure` with `size = (600, 450)` will be rendered as a 1200 x 900 pixel image. The reason it isn't `px_per_unit = 1` is that CairoMakie plots are often embedded in notebooks or websites, or looked at in image viewers or IDEs like VSCode. On websites, you don't know in advance what the pixel density of a reader's display is going to be. And in image viewers and IDEs, people like to zoom in to look at details. To cover these use cases by default, we decided `px_per_unit = 2` is a good compromise between sharp resolution and appropriate file size. Again, the _apparent_ size of output images in notebooks and websites (wherever the `MIME"text/html"` type is used) depends only on the `size`, because the output images are embedded with ` 0` can be helpful when visualizing a surface. (More precisely the light calculation is repeated with inverted normals and the result is mixed in with `backlight` as a prefactor.) + +!!! note + RPRMakie does not use these material attributes. + Instead it relies on RadeonProRender's material system, which is passed through the `material` attribute. + See the [RPRMakie page](https://docs.makie.org/stable/documentation/backends/rprmakie/) for examples. + + +## Lighting alogrithm + +Lights are controlled through the `lights` vector in a `scene` and by the `shading` attribute in a plot. +Generally you will not need to set `shading` yourself, as it is derived based on the lights vector. +The possible options for `shading` are: +- `shading = NoShading` disables light calculations, resulting in the plain color of an object being shown. +- `shading = FastShading` enables a simplified lighting model which only allows for one `AmbientLight` and one `DirectionalLight`. +- `shading = MultiLightShading` is a GLMakie exclusive option which enables multiple light sources (as set in the `ScreenConfig`, default up to 64) as well as `PointLight` and `SpotLight`. + +!!! note + You can access the underlying scene of an `Axis3` with `ax.scene`. + +For reference all the lighting calculations (except ambient) in GLMakie, WGLMakie and to some extend CairoMakie end up using the [Blinn-Phong reflection model](https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model) which boils down to + +```julia +function blinn_phong( + diffuse, specular, shininess, normal, object_color, + light_color, light_direction, camera_direction + ) + diffuse_coefficient = max(dot(light_direction, -normal), 0.0) + H = normalize(light_direction + camera_direction) + specular_coefficient = max(dot(H, -normal), 0.0)^shininess + return light_color * ( + diffuse * diffuse_coefficient * object_color + + specular * specular_coefficient + ) +end +``` + +The different light sources control the `light_direction` and may further adjust the result of this function. For example, `SpotLight` adds a factor which reduces light intensity outside its area. + + +## Types of Light + + +### AmbientLight + +{{doc AmbientLight}} + +\begin{examplefigure}{} +```julia +using CairoMakie +CairoMakie.activate!() # hide + +fig = Figure(size = (600, 600)) +ax11 = LScene(fig[1, 1], scenekw = (lights = [],)) +ax12 = LScene(fig[1, 2], scenekw = (lights = [AmbientLight(RGBf(0, 0, 0))],)) +ax21 = LScene(fig[2, 1], scenekw = (lights = [AmbientLight(RGBf(0.7, 0.7, 0.7))],)) +ax22 = LScene(fig[2, 2], scenekw = (lights = [AmbientLight(RGBf(0.8, 0.3, 0))],)) +for ax in (ax11, ax12, ax21, ax22) + mesh!(ax, Sphere(Point3f(0), 1f0), color = :white) +end +fig +``` +\end{examplefigure} + + +### DirectionalLight + +{{doc DirectionalLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure(size = (600, 600)) +ax11 = LScene(fig[1, 1], scenekw = (lights = [DirectionalLight(RGBf(0, 0, 0), Vec3f(-1, 0, 0))],)) +ax12 = LScene(fig[1, 2], scenekw = (lights = [DirectionalLight(RGBf(1, 1, 1), Vec3f(-1, 0, 0))],)) +lights = [ + DirectionalLight(RGBf(0, 0, 0.7), Vec3f(-1, -1, 0)), + DirectionalLight(RGBf(0.7, 0.2, 0), Vec3f(-1, 1, -1)), + DirectionalLight(RGBf(0.7, 0.7, 0.7), Vec3f(1, -1, -1)) +] +ax21 = LScene(fig[2, 1], scenekw = (lights = lights,)) +ax22 = LScene(fig[2, 2], scenekw = (lights = [DirectionalLight(RGBf(4, 2, 1), Vec3f(0, 0, -1))],)) +for ax in (ax11, ax12, ax21, ax22) + mesh!(ax, Sphere(Point3f(0), 1f0), color = :white) +end +fig +``` +\end{examplefigure} + +### PointLight + +{{doc PointLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +fig = Figure(size = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = [PointLight(RGBf(1, 1, 1), Point3f(0, 0, 0))],)) +ps = [Point3f(x, y, z) for x in (-1, 0, 1) for y in (-1, 0, 1) for z in (-1, 0, 1)] +meshscatter!(ax, ps, color = :white) +fig +``` +\end{examplefigure} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +lights = [ + PointLight(RGBf(1, 1, 1), Point3f(0, 0, 5), 50), + PointLight(RGBf(2, 0, 0), Point3f(-3, -3, 2), 10), + PointLight(RGBf(0, 2, 0), Point3f(-3, 3, 2), 10), + PointLight(RGBf(0, 0, 2), Point3f( 3, 3, 2), 10), + PointLight(RGBf(2, 2, 0), Point3f( 3, -3, 2), 10), +] + +fig = Figure(size = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = lights,)) +ps = [Point3f(x, y, 0) for x in -5:5 for y in -5:5] +meshscatter!(ax, ps, color = :white, markersize = 0.75) +scatter!(ax, map(l -> l.position[], lights), color = map(l -> l.color[], lights), strokewidth = 1, strokecolor = :black) +fig +``` +\end{examplefigure} + +With a strong PointLight and Attenuation you can create different colors at different distances. + +\begin{examplefigure}{} +```julia +using GLMakie, GeometryBasics +GLMakie.activate!() # hide + +ps = [ + Point3f(cosd(phi) * cosd(theta), sind(phi) * cosd(theta), sind(theta)) + for theta in range(-20, 20, length = 21) for phi in range(60, 340, length=30) +] +faces = [QuadFace(30j + i, 30j + mod1(i+1, 30), 30*(j+1) + mod1(i+1, 30), 30*(j+1) + i) for j in 0:19 for i in 1:29] +m = GeometryBasics.Mesh(meta(ps, normals = ps), decompose(GLTriangleFace, faces)) + +lights = [PointLight(RGBf(10, 4, 2), Point3f(0, 0, 0), 5)] + +fig = Figure(size = (600, 600), backgroundcolor = :black) +ax = LScene(fig[1, 1], scenekw = (lights = lights,), show_axis = false) +update_cam!(ax.scene, ax.scene.camera_controls, Rect3f(Point3f(-2), Vec3f(4))) +meshscatter!( + ax, [Point3f(0) for _ in 1:14], marker = m, markersize = 0.1:0.2:3.0, + color = :white, backlight = 1, transparency = false) +fig +``` +\end{examplefigure} + + +### SpotLight + +{{doc SpotLight}} + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide +GLMakie.closeall() # hide + +lights = [ + SpotLight(RGBf(1, 0, 0), Point3f(-3, 0, 3), Vec3f(0, 0, -1), Vec2f(0.0, 0.3pi)), + SpotLight(RGBf(0, 1, 0), Point3f( 0, 3, 3), Vec3f(0, -0.5, -1), Vec2f(0.2pi, 0.25pi)), + SpotLight(RGBf(0, 0, 1), Point3f( 3, 0, 3), Vec3f(0, 0, -1), Vec2f(0.25pi, 0.25pi)), +] + +fig = Figure(size = (600, 600)) +ax = LScene(fig[1, 1], scenekw = (lights = lights,)) +ps = [Point3f(x, y, 0) for x in -5:5 for y in -5:5] +meshscatter!(ax, ps, color = :white, markersize = 0.75) +scatter!(ax, map(l -> l.position[], lights), color = map(l -> l.color[], lights), strokewidth = 1, strokecolor = :black) +fig +``` +\end{examplefigure} + +### RectLight + +{{doc RectLight}} + +\begin{examplefigure}{} +```julia +using FileIO, GeometryBasics, LinearAlgebra, GLMakie + +# Create mesh from RectLight parameters +function to_mesh(l::RectLight) + n = -normalize(cross(l.u1[], l.u2[])) + p = l.position[] - 0.5 * l.u1[] - 0.5 * l.u2[] + positions = [p, p + l.u1[], p + l.u2[], p + l.u1[] + l.u2[]] + faces = GLTriangleFace[(1,2,3), (2,3,4)] + normals = [n,n,n,n] + return GeometryBasics.Mesh(meta(positions, normals = normals), faces) +end + +fig = Figure(backgroundcolor = :black) + +# Prepare lights +lights = Makie.AbstractLight[ + AmbientLight(RGBf(0.1, 0.1, 0.1)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f(-1.9, -1.9, 1.8, 1.8)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f(-1.9, 0.1, 1.8, 1.8)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f( 0.1, 0.1, 1.8, 1.8)), + RectLight(RGBf(0.9, 1, 0.8), Rect2f( 0.1, -1.9, 1.8, 1.8)), +] + +for l in lights + if l isa RectLight + angle = pi/4 + p = l.position[] + Makie.rotate!(l, Vec3f(0, 1, 0), angle) + + p = 3 * Vec3f(1+sin(angle), 0, cos(angle)) + + p[1] * normalize(l.u1[]) + + p[2] * normalize(l.u2[]) + translate!(l, p) + end +end + +# Set scene +scene = LScene( + fig[1, 1], show_axis = false, + scenekw=(lights = lights, backgroundcolor = :black, center = false), +) + +# floor +p = mesh!(scene, Rect3f(Point3f(-10, -10, 0.01), Vec3f(20, 20, 0.02)), color = :white) +translate!(p, 0, 0, -5) + +# Cat +cat_mesh = FileIO.load(Makie.assetpath("cat.obj")) +cat_texture = FileIO.load(Makie.assetpath("diffusemap.png")) +p2 = mesh!(scene, cat_mesh, color = cat_texture) +Makie.rotate!(p2, Vec3f(1,0,0), pi/2) +translate!(p2, -2, 2, -5) +scale!(p2, Vec3f(4)) + +# Window/light source markers +for l in lights + if l isa RectLight + m = to_mesh(l) + mesh!(m, color = :white, backlight = 1) + end +end + +# place camera +update_cam!(scene.scene, Vec3f(1.5, -13, 2), Vec3f(1, -2, 0), Vec3f(0, 0, 1)) + +fig +``` +\end{examplefigure} + +### EnvironmentLight + +{{doc EnvironmentLight}} diff --git a/docs/reference/scene/matcap.md b/docs/reference/scene/matcap.md new file mode 100644 index 00000000000..a9b320dc29b --- /dev/null +++ b/docs/reference/scene/matcap.md @@ -0,0 +1,17 @@ +# Matcap + +A matcap (material capture) is a texture which is applied based on the normals of a given mesh. They typically include complex materials and lighting and offer a cheap way to apply those to any mesh. You may pass a matcap via the `matcap` attribute of a `mesh`, `meshscatter` or `surface` plot. Setting `shading = NoShading` is suggested. You can find a lot matcaps [here](https://github.com/nidorx/matcaps). + +## Example + +\begin{examplefigure}{} +```julia +using FileIO +using GLMakie +GLMakie.activate!() # hide +catmesh = FileIO.load(assetpath("cat.obj")) +gold = FileIO.load(download("https://raw.githubusercontent.com/nidorx/matcaps/master/1024/E6BF3C_5A4719_977726_FCFC82.png")) + +mesh(catmesh, matcap=gold, shading = NoShading) +``` +\end{examplefigure} \ No newline at end of file diff --git a/docs/reference/specapi.md b/docs/reference/specapi.md new file mode 100644 index 00000000000..6c9d7bfbd80 --- /dev/null +++ b/docs/reference/specapi.md @@ -0,0 +1,212 @@ +# 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(; size=(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/tutorials/aspect-tutorial.md b/docs/tutorials/aspect-tutorial.md index b0e2f2c1a1a..9249e97daa1 100644 --- a/docs/tutorials/aspect-tutorial.md +++ b/docs/tutorials/aspect-tutorial.md @@ -16,7 +16,7 @@ CairoMakie.activate!() # hide set_theme!(backgroundcolor = :gray90) -f = Figure(resolution = (800, 500)) +f = Figure(size = (800, 500)) ax = Axis(f[1, 1], aspect = 1) Colorbar(f[1, 2]) f @@ -61,7 +61,7 @@ Let's try the example from above again, but this time we force the column of the \begin{examplefigure}{svg = true} ```julia -f = Figure(resolution = (800, 500)) +f = Figure(size = (800, 500)) ax = Axis(f[1, 1]) Colorbar(f[1, 2]) colsize!(f.layout, 1, Aspect(1, 1.0)) @@ -113,7 +113,7 @@ Let's return to our previous state with a square axis: \begin{examplefigure}{svg = true} ```julia # hide -f = Figure(resolution = (800, 500)) +f = Figure(size = (800, 500)) ax = Axis(f[1, 1]) Colorbar(f[1, 2]) colsize!(f.layout, 1, Aspect(1, 1.0)) diff --git a/docs/tutorials/basic-tutorial.md b/docs/tutorials/basic-tutorial.md index c91057ab8f2..c933635e7e9 100644 --- a/docs/tutorials/basic-tutorial.md +++ b/docs/tutorials/basic-tutorial.md @@ -55,12 +55,12 @@ f = Figure(backgroundcolor = :tomato) ``` \end{examplefigure} -Another common thing to do is to give a figure a different size or resolution. +Another common thing to do is to give a figure a different size. The default is 800x600, let's try halving the height: \begin{examplefigure}{svg = true} ```julia -f = Figure(backgroundcolor = :tomato, resolution = (800, 300)) +f = Figure(backgroundcolor = :tomato, size = (800, 300)) ``` \end{examplefigure} @@ -178,13 +178,13 @@ You can pass any kind of object with symbol-value pairs and these will be used a x = range(0, 10, length=100) y = sin.(x) scatter(x, y; - figure = (; resolution = (400, 400)), + figure = (; size = (400, 400)), axis = (; title = "Scatter plot", xlabel = "x label") ) ``` \end{examplefigure} -The `;` in `(; resolution = (400, 400))` is nothing special, it just clarifies that we want a one-element `NamedTuple` and not a variable called `resolution`. +The `;` in `(; size = (400, 400))` is nothing special, it just clarifies that we want a one-element `NamedTuple` and not a variable called `size`. It's good habit to include it but it's not needed for `NamedTuple`s with more than one entry. ## Argument conversions @@ -221,7 +221,7 @@ lines([Point(0, 0), Point(5, 10), Point(10, 5)]) The input arguments you can use with `lines` and `scatter` are mostly the same because they have the same conversion trait `PointBased`. Other plotting functions have different conversion traits, \myreflink{heatmap} for example expects two-dimensional grid data. -The respective trait is called `CellBasedGrid`. +The respective trait is called `CellGrid`. ## Layering multiple plots diff --git a/docs/tutorials/layout-tutorial.md b/docs/tutorials/layout-tutorial.md index c968e804212..6e4592ffd7d 100644 --- a/docs/tutorials/layout-tutorial.md +++ b/docs/tutorials/layout-tutorial.md @@ -17,7 +17,7 @@ using Makie.FileIO CairoMakie.activate!() # hide f = Figure(backgroundcolor = RGBf(0.98, 0.98, 0.98), - resolution = (1000, 700)) + size = (1000, 700)) ga = f[1, 1] = GridLayout() gb = f[2, 1] = GridLayout() gcd = f[1:2, 2] = GridLayout() @@ -170,7 +170,7 @@ using FileIO CairoMakie.activate!() # hide f = Figure(backgroundcolor = RGBf(0.98, 0.98, 0.98), - resolution = (1000, 700)) + size = (1000, 700)) ``` \end{examplefigure} diff --git a/docs/tutorials/scenes.md b/docs/tutorials/scenes.md index 2b5538f1094..99d4ff306f6 100644 --- a/docs/tutorials/scenes.md +++ b/docs/tutorials/scenes.md @@ -14,7 +14,7 @@ scene = Scene(; # set_theme!(lightposition=:eyeposition, ambient=RGBf(0.5, 0.5, 0.5))` lights = Makie.automatic, backgroundcolor = :gray, - resolution = (500, 500); + size = (500, 500); # gets filled in with the currently set global theme theme_kw... ) @@ -36,7 +36,7 @@ With scenes, one can create subwindows. The window extends are given by a `Rect{ using GLMakie, Makie GLMakie.activate!() scene = Scene(backgroundcolor=:gray) -subwindow = Scene(scene, px_area=Rect(100, 100, 200, 200), clear=true, backgroundcolor=:white) +subwindow = Scene(scene, viewport=Rect(100, 100, 200, 200), clear=true, backgroundcolor=:white) scene ``` \end{examplefigure} @@ -128,7 +128,7 @@ We can use those events to e.g. move the subwindow. If you execute the below in ```julia on(scene.events.mouseposition) do mousepos if ispressed(subwindow, Mouse.left & Keyboard.left_control) - subwindow.px_area[] = Rect(Int.(mousepos)..., 200, 200) + subwindow.viewport[] = Rect(Int.(mousepos)..., 200, 200) end end ``` @@ -249,12 +249,14 @@ The scene graph can be used to create rigid transformations, like for a robot ar ```julia GLMakie.activate!() # hide parent = Scene() -cam3d!(parent) +cam3d!(parent; clipping_mode = :static) # One can set the camera lookat and eyeposition, by getting the camera controls and using `update_cam!` camc = cameracontrols(parent) update_cam!(parent, camc, Vec3f(0, 8, 0), Vec3f(4.0, 0, 0)) - +# One may need to adjust the +# near and far clip plane when adjusting the camera manually +camc.far[] = 100f0 s1 = Scene(parent, camera=parent.camera) mesh!(s1, Rect3f(Vec3f(0, -0.1, -0.1), Vec3f(5, 0.2, 0.2))) s2 = Scene(s1, camera=parent.camera) @@ -368,7 +370,7 @@ lights = [ EnvironmentLight(1.5, rotl90(load(assetpath("sunflowers_1k.hdr"))')), PointLight(Vec3f(50, 0, 200), RGBf(radiance, radiance, radiance*1.1)), ] -s = Scene(resolution=(500, 500), lights=lights) +s = Scene(size=(500, 500), lights=lights) cam3d!(s) c = cameracontrols(s) c.near[] = 5 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/benchmark-ttfp.jl b/metrics/ttfp/benchmark-ttfp.jl index 456320f01d0..74a76215495 100644 --- a/metrics/ttfp/benchmark-ttfp.jl +++ b/metrics/ttfp/benchmark-ttfp.jl @@ -8,17 +8,15 @@ macro ctime(x) end t_using = @ctime @eval using $Package -get_colorbuffer(fig) = colorbuffer(fig; px_per_unit=1) - if Package === :WGLMakie import Electron WGLMakie.JSServe.use_electron_display() end -set_theme!(resolution=(800, 600)) +set_theme!(size=(800, 600)) create_time = @ctime fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -display_time = @ctime get_colorbuffer(fig) +display_time = @ctime colorbuffer(fig; px_per_unit=1) using BenchmarkTools using BenchmarkTools.JSON @@ -32,7 +30,7 @@ old = isfile(result) ? JSON.parse(read(result, String)) : [[], [], [], [], []] push!.(old[1:3], [t_using, create_time, display_time]) b1 = @benchmark fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -b2 = @benchmark get_colorbuffer(fig) setup=(fig=scatter(1:4)) +b2 = @benchmark colorbuffer(fig; px_per_unit=1) using Statistics diff --git a/metrics/ttfp/run-benchmark.jl b/metrics/ttfp/run-benchmark.jl index 46c11b19319..2fd1d2d1865 100644 --- a/metrics/ttfp/run-benchmark.jl +++ b/metrics/ttfp/run-benchmark.jl @@ -34,7 +34,7 @@ create_time = @ctime fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=2 display_time = @ctime Makie.colorbuffer(display(fig)) # Runtime create_time = @benchmark fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -display_time = @benchmark Makie.colorbuffer(display(fig)) +display_time = @benchmark Makie.colorbuffer(fig) ``` | | using | create | display | create | display | @@ -87,27 +87,28 @@ function analyze(pr, master) std_p = (std(pr) + std(master)) / 2 m_pr = mean(pr) m_m = mean(master) - mean_diff = mean(m_pr) - mean(m_m) - percent = (1 - (m_m / m_pr)) * 100 + mean_diff = m_pr - m_m + speedup = m_m / m_pr p = pvalue(tt) mean_diff_str = string(round(mean_diff; digits=2), unit) - + percent_change = (speedup - 1) * 100 result = if p < 0.05 if abs(d) > 0.2 - indicator = abs(percent) < 5 ? ["faster ✓", "slower X"] : ["**faster**✅", "**slower**❌"] + indicator = abs(percent_change) < 5 ? ["faster ✓", "slower X"] : ["**faster**✅", "**slower**❌"] indicator[d < 0 ? 1 : 2] else "*invariant*" end else - if abs(percent) < 5 + if abs(percent_change) < 5 "*invariant*" else "*noisy*🤷‍♀️" end end - return @sprintf("%s%.2f%s, %s %s (%.2fd, %.2fp, %.2fstd)", percent > 0 ? "+" : "-", abs(percent), "%", mean_diff_str, result, d, p, std_p) + return @sprintf("%.2fx %s, %s, (%.2fd, %.2fp, %.2fstd)", speedup, result, mean_diff_str, d, p, + std_p) end function summarize_stats(timings) @@ -153,6 +154,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 @@ -171,9 +175,12 @@ function make_or_edit_comment(ctx, pr, package_name, benchmarks) end end + +using Random + function run_benchmarks(projects; n=n_samples) benchmark_file = joinpath(@__DIR__, "benchmark-ttfp.jl") - for project in repeat(projects; outer=n) + for project in shuffle!(repeat(projects; outer=n)) run(`$(Base.julia_cmd()) --startup-file=no --project=$(project) $benchmark_file $Package`) project_name = basename(project) end diff --git a/precompile/shared-precompile.jl b/precompile/shared-precompile.jl index 5a38514dd70..efa7a3999cd 100644 --- a/precompile/shared-precompile.jl +++ b/precompile/shared-precompile.jl @@ -54,7 +54,7 @@ end @compile begin res = 200 - s = Scene(camera=campixel!, resolution=(res, res)) + s = Scene(camera=campixel!, size=(res, res)) half = res / 2 linewidth = 10 xstart = half - (half/2) 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..6528d8f0a11 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 @@ -74,19 +80,20 @@ using Observables: listeners, to_value, notify using MakieCore: SceneLike, MakieScreen, ScenePlot, AbstractScene, AbstractPlot, Transformable, Attributes, Combined, Theme, Plot using MakieCore: Arrows, Heatmap, Image, Lines, LineSegments, Mesh, MeshScatter, Poly, Scatter, Surface, Text, Volume, Wireframe -using MakieCore: ConversionTrait, NoConversion, PointBased, GridBased, VertexBasedGrid, CellBasedGrid, ImageLike, VolumeLike +using MakieCore: ConversionTrait, NoConversion, PointBased, GridBased, VertexGrid, CellGrid, ImageLike, VolumeLike using MakieCore: Key, @key_str, Automatic, automatic, @recipe using MakieCore: Pixel, px, Unit, Billboard +using MakieCore: NoShading, FastShading, MultiLightShading 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 export @L_str, @colorant_str -export ConversionTrait, NoConversion, PointBased, GridBased, VertexBasedGrid, CellBasedGrid, ImageLike, VolumeLike +export ConversionTrait, NoConversion, PointBased, GridBased, VertexGrid, CellGrid, ImageLike, VolumeLike export Pixel, px, Unit, plotkey, attributes, used_attributes export Linestyle @@ -108,6 +115,7 @@ include("interaction/liftmacro.jl") include("colorsampler.jl") include("patterns.jl") include("utilities/utilities.jl") # need Makie.AbstractPattern +include("lighting.jl") # Basic scene/plot/recipe interfaces + types include("scenes.jl") @@ -131,7 +139,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") @@ -224,7 +232,7 @@ export xtickrotation, ytickrotation, ztickrotation export xtickrotation!, ytickrotation!, ztickrotation! # Observable/Signal related -export Observable, Observable, lift, map_once, to_value, on, onany, @lift, off, connect! +export Observable, Observable, lift, to_value, on, onany, @lift, off, connect! # utilities and macros export @recipe, @extract, @extractvalue, @key_str, @get_attribute @@ -247,11 +255,11 @@ export SceneSpace, PixelSpace, Pixel export AbstractCamera, EmptyCamera, Camera, Camera2D, Camera3D, cam2d!, cam2d export campixel!, campixel, cam3d!, cam3d_cad!, old_cam3d!, old_cam3d_cad!, cam_relative! export update_cam!, rotate_cam!, translate_cam!, zoom! -export pixelarea, plots, cameracontrols, cameracontrols!, camera, events +export viewport, plots, cameracontrols, cameracontrols!, camera, events export to_world # picking + interactive use cases + events -export mouseover, onpick, pick, Events, Keyboard, Mouse, mouse_selection, is_mouseinside +export mouseover, onpick, pick, Events, Keyboard, Mouse, is_mouseinside export ispressed, Exclusively export connect_screen export window_area, window_open, mouse_buttons, mouse_position, mouseposition_px, @@ -263,6 +271,7 @@ export Consume # Raymarching algorithms export RaymarchAlgorithm, IsoValue, Absorption, MaximumIntensityProjection, AbsorptionRGBA, IndexedAbsorptionRGBA export Billboard +export NoShading, FastShading, MultiLightShading # Reexports of # Color/Vector types convenient for 3d/2d graphics @@ -340,7 +349,7 @@ export Arrows , Heatmap , Image , Lines , LineSegments , Mesh , MeshScatte export arrows , heatmap , image , lines , linesegments , mesh , meshscatter , poly , scatter , surface , text , volume , wireframe export arrows! , heatmap! , image! , lines! , linesegments! , mesh! , meshscatter! , poly! , scatter! , surface! , text! , volume! , wireframe! -export PointLight, EnvironmentLight, AmbientLight, SSAO +export AmbientLight, PointLight, DirectionalLight, SpotLight, EnvironmentLight, RectLight, SSAO include("precompiles.jl") diff --git a/src/basic_recipes/arrows.jl b/src/basic_recipes/arrows.jl index 83a2a38f76c..0537ee1c02f 100644 --- a/src/basic_recipes/arrows.jl +++ b/src/basic_recipes/arrows.jl @@ -146,7 +146,7 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher # for 2D arrows, compute the correct marker rotation given the projection / scene size # for the screen-space marker if is_pixel_space(arrowplot.markerspace[]) - rotations = lift(arrowplot, scene.camera.projectionview, scene.px_area, headstart) do pv, pxa, hs + rotations = lift(arrowplot, scene.camera.projectionview, scene.viewport, headstart) do pv, pxa, hs angles = map(hs) do (start, stop) pstart = project(scene, start) pstop = project(scene, stop) diff --git a/src/basic_recipes/axis.jl b/src/basic_recipes/axis.jl index 655b4a0fd75..fcb4f803539 100644 --- a/src/basic_recipes/axis.jl +++ b/src/basic_recipes/axis.jl @@ -334,10 +334,10 @@ function plot!(axis::Axis3D) getindex.(axis, (:showaxis, :showticks, :showgrid))..., titlevals..., framevals..., tvals..., axis.padding ) - map_once( + onany( draw_axis3d, Observable(textbuffer), Observable(linebuffer), scale(scene), - axis[1], axis.ticks.ranges_labels, Observable(axis.fonts), args... + axis[1], axis.ticks.ranges_labels, Observable(axis.fonts), args...; update=true ) return axis end diff --git a/src/basic_recipes/band.jl b/src/basic_recipes/band.jl index b0434868c1a..9a8de39b48d 100644 --- a/src/basic_recipes/band.jl +++ b/src/basic_recipes/band.jl @@ -13,7 +13,7 @@ $(ATTRIBUTES) default_theme(scene, Mesh)..., colorrange = automatic, ) - attr[:shading][] = false + attr[:shading][] = NoShading attr end diff --git a/src/basic_recipes/bracket.jl b/src/basic_recipes/bracket.jl index 34c2691bf85..c1d2dff71bb 100644 --- a/src/basic_recipes/bracket.jl +++ b/src/basic_recipes/bracket.jl @@ -55,7 +55,7 @@ function Makie.plot!(pl::Bracket) end onany(pl, points, scene.camera.projectionview, pl.model, transform_func(pl), - scene.px_area, pl.offset, pl.width, pl.orientation, realtextoffset, + scene.viewport, pl.offset, pl.width, pl.orientation, realtextoffset, pl.style) do points, _, _, _, _, offset, width, orientation, textoff, style empty!(bp[]) diff --git a/src/basic_recipes/contourf.jl b/src/basic_recipes/contourf.jl index 4936f25d372..89ff2ae4da3 100644 --- a/src/basic_recipes/contourf.jl +++ b/src/basic_recipes/contourf.jl @@ -56,7 +56,7 @@ function _get_isoband_levels(levels::AbstractVector{<:Real}, mi, ma) edges end -conversion_trait(::Type{<:Contourf}) = VertexBasedGrid() +conversion_trait(::Type{<:Contourf}) = VertexGrid() function _get_isoband_levels(::Val{:normal}, levels, values) return _get_isoband_levels(levels, extrema_nan(values)...) @@ -141,7 +141,7 @@ function Makie.plot!(c::Contourf{<:Tuple{<:AbstractVector{<:Real}, <:AbstractVec color = colors, strokewidth = 0, strokecolor = :transparent, - shading = false, + shading = NoShading, inspectable = c.inspectable, transparency = c.transparency ) diff --git a/src/basic_recipes/contours.jl b/src/basic_recipes/contours.jl index e2701997a87..639d5cc22ab 100644 --- a/src/basic_recipes/contours.jl +++ b/src/basic_recipes/contours.jl @@ -110,8 +110,8 @@ function to_levels(n::Integer, cnorm) range(zmin + dz; step = dz, length = n) end -conversion_trait(::Type{<: Contour3d}) = VertexBasedGrid() -conversion_trait(::Type{<: Contour}) = VertexBasedGrid() +conversion_trait(::Type{<: Contour3d}) = VertexGrid() +conversion_trait(::Type{<: Contour}) = VertexGrid() conversion_trait(::Type{<:Contour}, x, y, z, ::Union{Function, AbstractArray{<: Number, 3}}) = VolumeLike() conversion_trait(::Type{<: Contour}, ::AbstractArray{<: Number, 3}) = VolumeLike() @@ -247,10 +247,12 @@ function plot!(plot::T) where T <: Union{Contour, Contour3d} align = (:center, :center), fontsize = labelsize, font = labelfont, + transform_marker = false ) - lift(scene.camera.projectionview, scene.px_area, labels, labelcolor, labelformatter, - lev_pos_col) do _, _, labels, labelcolor, labelformatter, lev_pos_col + lift(scene.camera.projectionview, transformationmatrix(plot), scene.viewport, + labels, labelcolor, labelformatter, lev_pos_col + ) do _, _, _, labels, labelcolor, labelformatter, lev_pos_col labels || return pos = texts.positions.val; empty!(pos) rot = texts.rotation.val; empty!(rot) diff --git a/src/basic_recipes/convenience_functions.jl b/src/basic_recipes/convenience_functions.jl index 66d8b9702a7..dab4e6d0947 100644 --- a/src/basic_recipes/convenience_functions.jl +++ b/src/basic_recipes/convenience_functions.jl @@ -16,7 +16,7 @@ end showgradients( cgrads::AbstractVector{Symbol}; h = 0.0, offset = 0.2, fontsize = 0.7, - resolution = (800, length(cgrads) * 84) + size = (800, length(cgrads) * 84) )::Scene Plots the given colour gradients arranged as horizontal colourbars. @@ -27,11 +27,11 @@ function showgradients( h = 0.0, offset = 0.4, fontsize = 0.7, - resolution = (800, length(cgrads) * 84), + size = (800, length(cgrads) * 84), monospace = true )::Scene - scene = Scene(resolution = resolution) + scene = Scene(size = resolution) map(collect(cgrads)) do cmap c = to_colormap(cmap) diff --git a/src/basic_recipes/datashader.jl b/src/basic_recipes/datashader.jl index 27123b672be..dc06b83ad14 100644 --- a/src/basic_recipes/datashader.jl +++ b/src/basic_recipes/datashader.jl @@ -377,8 +377,8 @@ end function Makie.plot!(p::DataShader{<: Tuple{<: AbstractVector{<: Point}}}) scene = parent_scene(p) limits = lift(projview_to_2d_limits, p, scene.camera.projectionview; ignore_equal_values=true) - px_area = lift(identity, p, scene.px_area; ignore_equal_values=true) - canvas = canvas_obs(limits, px_area, p.agg, p.binsize) + viewport = lift(identity, p, scene.viewport; ignore_equal_values=true) + canvas = canvas_obs(limits, viewport, p.agg, p.binsize) p._boundingbox = lift(fast_bb, p.points, p.point_transform) on_func = p.async[] ? onany_latest : onany canvas_with_aggregation = Observable(canvas[]) # Canvas that only gets notified after get_aggregation happened @@ -432,8 +432,8 @@ end function Makie.plot!(p::DataShader{<:Tuple{Dict{String, Vector{Point{2, Float32}}}}}) scene = parent_scene(p) limits = lift(projview_to_2d_limits, p, scene.camera.projectionview; ignore_equal_values=true) - px_area = lift(identity, p, scene.px_area; ignore_equal_values=true) - canvas = canvas_obs(limits, px_area, Observable(AggCount{Float32}()), p.binsize) + viewport = lift(identity, p, scene.viewport; ignore_equal_values=true) + canvas = canvas_obs(limits, viewport, Observable(AggCount{Float32}()), p.binsize) p._boundingbox = lift(p.points, p.point_transform) do cats, func rects = map(points -> fast_bb(points, func), values(cats)) return reduce(union, rects) diff --git a/src/basic_recipes/error_and_rangebars.jl b/src/basic_recipes/error_and_rangebars.jl index e8c11cb98e6..95dc2ec39e1 100644 --- a/src/basic_recipes/error_and_rangebars.jl +++ b/src/basic_recipes/error_and_rangebars.jl @@ -28,7 +28,8 @@ $(ATTRIBUTES) colorscale = identity, colorrange = automatic, inspectable = theme(scene, :inspectable), - transparency = false + transparency = false, + cycle = [:color] ) end @@ -57,7 +58,8 @@ $(ATTRIBUTES) colorscale = identity, colorrange = automatic, inspectable = theme(scene, :inspectable), - transparency = false + transparency = false, + cycle = [:color] ) end @@ -198,7 +200,7 @@ function _plot_bars!(plot, linesegpairs, is_in_y_direction) scene = parent_scene(plot) whiskers = lift(plot, linesegpairs, scene.camera.projectionview, plot.model, - scene.px_area, transform_func(plot), whiskerwidth) do endpoints, _, _, _, _, whiskerwidth + scene.viewport, transform_func(plot), whiskerwidth) do endpoints, _, _, _, _, whiskerwidth screenendpoints = plot_to_screen(plot, endpoints) 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..29bace7a76c --- /dev/null +++ b/src/basic_recipes/specapi.jl @@ -0,0 +1,486 @@ +# 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) + if new_value isa Cycled + old_attr.val = to_color(parent_scene(plot), attribute, new_value) + else + @debug("updating kw $attribute") + old_attr.val = new_value + end + 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) + empty!(scene.cycler.counters) + 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 is_different(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/streamplot.jl b/src/basic_recipes/streamplot.jl index 9a999d27300..6fd20d88d3b 100644 --- a/src/basic_recipes/streamplot.jl +++ b/src/basic_recipes/streamplot.jl @@ -194,7 +194,7 @@ function plot!(p::StreamPlot) # Calculate arrow head rotations as angles. To avoid distortions from # (extreme) aspect ratios we need to project to pixel space and renormalize. scene = parent_scene(p) - rotations = lift(p, scene.camera.projectionview, scene.px_area, data) do pv, pxa, data + rotations = lift(p, scene.camera.projectionview, scene.viewport, data) do pv, pxa, data angles = map(data[1], data[2]) do pos, dir pstart = project(scene, pos) pstop = project(scene, pos + dir) diff --git a/src/basic_recipes/text.jl b/src/basic_recipes/text.jl index 01625e0cda5..ef23d1d14d5 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.viewport, 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/basic_recipes/tooltip.jl b/src/basic_recipes/tooltip.jl index ae0a516f04c..74c75241062 100644 --- a/src/basic_recipes/tooltip.jl +++ b/src/basic_recipes/tooltip.jl @@ -36,13 +36,13 @@ Creates a tooltip pointing at `position` displaying the given `string` """ @recipe(Tooltip, position) do scene Attributes(; - # General - text = "", + # General + text = "", offset = 10, placement = :above, align = 0.5, - xautolimits = false, - yautolimits = false, + xautolimits = false, + yautolimits = false, zautolimits = false, overdraw = false, depth_shift = 0f0, @@ -62,7 +62,7 @@ Creates a tooltip pointing at `position` displaying the given `string` # Background backgroundcolor = :white, triangle_size = 10, - + # Outline outline_color = :black, outline_linewidth = 2f0, @@ -103,7 +103,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) text_offset = map(p.offset, textpadding, p.triangle_size, p.placement, p.align) do o, pad, ts, placement, align l, r, b, t = pad - if placement === :left + if placement === :left return Vec2f(-o - r - ts, b - align * (b + t)) elseif placement === :right return Vec2f( o + l + ts, b - align * (b + t)) @@ -118,7 +118,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) end text_align = map(p.placement, p.align) do placement, align - if placement === :left + if placement === :left return (1.0, align) elseif placement === :right return (0.0, align) @@ -155,9 +155,9 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) # Text background mesh mesh!( - p, bbox, shading = false, space = :pixel, + p, bbox, shading = NoShading, space = :pixel, color = p.backgroundcolor, fxaa = false, - transparency = p.transparency, visible = p.visible, + transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, inspectable = p.inspectable ) @@ -170,8 +170,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) ) mp = mesh!( - p, triangle, shading = false, space = :pixel, - color = p.backgroundcolor, + p, triangle, shading = NoShading, space = :pixel, + color = p.backgroundcolor, transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, inspectable = p.inspectable @@ -179,8 +179,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) onany(bbox, p.triangle_size, p.placement, p.align) do bb, s, placement, align o = origin(bb); w = widths(bb) scale!(mp, s, s, s) - - if placement === :left + + if placement === :left translate!(mp, Vec3f(o[1] + w[1], o[2] + align * w[2], 0)) rotate!(mp, qrotation(Vec3f(0,0,1), 0.5pi)) elseif placement === :right @@ -212,45 +212,45 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) # | ____ # | | - shift = if placement === :left + shift = if placement === :left Vec2f[ - (l, b + 0.5h), (l, t), (r, t), - (r, b + align * h + 0.5s), - (r + s, b + align * h), + (l, b + 0.5h), (l, t), (r, t), + (r, b + align * h + 0.5s), + (r + s, b + align * h), (r, b + align * h - 0.5s), (r, b), (l, b), (l, b + 0.5h) ] elseif placement === :right Vec2f[ - (l + 0.5w, b), (l, b), - (l, b + align * h - 0.5s), - (l-s, b + align * h), + (l + 0.5w, b), (l, b), + (l, b + align * h - 0.5s), + (l-s, b + align * h), (l, b + align * h + 0.5s), (l, t), (r, t), (r, b), (l + 0.5w, b) ] elseif placement in (:below, :down, :bottom) Vec2f[ - (l, b + 0.5h), (l, t), - (l + align * w - 0.5s, t), - (l + align * w, t+s), - (l + align * w + 0.5s, t), + (l, b + 0.5h), (l, t), + (l + align * w - 0.5s, t), + (l + align * w, t+s), + (l + align * w + 0.5s, t), (r, t), (r, b), (l, b), (l, b + 0.5h) ] elseif placement in (:above, :up, :top) Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), - (l + align * w + 0.5s, b), - (l + align * w, b-s), - (l + align * w - 0.5s, b), + (l, b + 0.5h), (l, t), (r, t), (r, b), + (l + align * w + 0.5s, b), + (l + align * w, b-s), + (l + align * w - 0.5s, b), (l, b), (l, b + 0.5h) ] else @error "Tooltip placement $placement invalid. Assuming :above" Vec2f[ - (l, b + 0.5h), (l, t), (r, t), (r, b), - (l + align * w + 0.5s, b), - (l + align * w, b-s), - (l + align * w - 0.5s, b), + (l, b + 0.5h), (l, t), (r, t), (r, b), + (l + align * w + 0.5s, b), + (l + align * w, b-s), + (l + align * w - 0.5s, b), (l, b), (l, b + 0.5h) ] end @@ -259,8 +259,8 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) end lines!( - p, outline, - color = p.outline_color, space = :pixel, + p, outline, + color = p.outline_color, space = :pixel, linewidth = p.outline_linewidth, linestyle = p.outline_linestyle, transparency = p.transparency, visible = p.visible, overdraw = p.overdraw, depth_shift = p.depth_shift, diff --git a/src/basic_recipes/tricontourf.jl b/src/basic_recipes/tricontourf.jl index e808c34103e..72f90865605 100644 --- a/src/basic_recipes/tricontourf.jl +++ b/src/basic_recipes/tricontourf.jl @@ -4,7 +4,7 @@ struct DelaunayTriangulation end tricontourf(triangles::Triangulation, zs; kwargs...) tricontourf(xs, ys, zs; kwargs...) -Plots a filled tricontour of the height information in `zs` at the horizontal positions `xs` and +Plots a filled tricontour of the height information in `zs` at the horizontal positions `xs` and vertical positions `ys`. A `Triangulation` from DelaunayTriangulation.jl can also be provided instead of `xs` and `ys` for specifying the triangles, otherwise an unconstrained triangulation of `xs` and `ys` is computed. @@ -54,16 +54,16 @@ function Makie.used_attributes(::Type{<:Tricontourf}, ::AbstractVector{<:Real}, return (:triangulation,) end -function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}; +function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}; triangulation=DelaunayTriangulation()) z = elconvert(Float32, z) points = [x'; y'] if triangulation isa DelaunayTriangulation tri = DelTri.triangulate(points) elseif !(triangulation isa DelTri.Triangulation) - # Wrap user's provided triangulation into a Triangulation. Their triangulation must be such that DelTri.add_triangle! is defined. - if typeof(triangulation) <: AbstractMatrix{<:Int} && size(triangulation, 1) != 3 - triangulation = triangulation' + # Wrap user's provided triangulation into a Triangulation. Their triangulation must be such that DelTri.add_triangle! is defined. + if typeof(triangulation) <: AbstractMatrix{<:Int} && size(triangulation, 1) != 3 + triangulation = triangulation' end tri = DelTri.Triangulation(points) triangles = DelTri.get_triangles(tri) @@ -198,7 +198,7 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:DelTri.Triangulation, <:AbstractVe color = colors, strokewidth = 0, strokecolor = :transparent, - shading = false, + shading = NoShading, inspectable = c.inspectable, transparency = c.transparency ) diff --git a/src/bezier.jl b/src/bezier.jl index 81f18f506cc..a7754525fea 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 @@ -264,27 +277,29 @@ function parse_bezier_commands(svg) commands = PathCommand[] lastcomm = nothing function lastp() - c = commands[end] if isnothing(lastcomm) - Point(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 - elseif c isa EllipticalArc - let - ϕ = c.angle - a2 = c.a2 - rx = c.r1 - ry = c.r2 - m = Mat2(cos(ϕ), sin(ϕ), -sin(ϕ), cos(ϕ)) - m * Point(rx * cos(a2), ry * sin(a2)) + c.c - end + Point2d(0, 0) else - c.p + c = commands[end] + if 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 + elseif c isa EllipticalArc + let + ϕ = c.angle + a2 = c.a2 + rx = c.r1 + ry = c.r2 + m = Mat2(cos(ϕ), sin(ϕ), -sin(ϕ), cos(ϕ)) + return m * Point2d(rx * cos(a2), ry * sin(a2)) + c.c + end + else + return c.p + end end end @@ -300,27 +315,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 +345,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 +389,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 @@ -398,8 +413,8 @@ end function EllipticalArc(x1, y1, x2, y2, rx, ry, ϕ, largearc::Bool, sweepflag::Bool) # https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes - p1 = Point(x1, y1) - p2 = Point(x2, y2) + p1 = Point2d(x1, y1) + p2 = Point2d(x2, y2) m1 = Mat2(cos(ϕ), -sin(ϕ), sin(ϕ), cos(ϕ)) x1′, y1′ = m1 * (0.5 * (p1 - p2)) @@ -408,16 +423,16 @@ 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) vecangle(u, v) = sign(u[1] * v[2] - u[2] * v[1]) * acos(dot(u, v) / (norm(u) * norm(v))) - px(sign) = Point((sign * x1′ - c′[1]) / rx, (sign * y1′ - c′[2]) / rx) + px(sign) = Point2d((sign * x1′ - c′[1]) / rx, (sign * y1′ - c′[2]) / rx) - θ1 = vecangle(Point(1.0, 0.0), px(1)) + θ1 = vecangle(Point2d(1.0, 0.0), px(1)) Δθ_pre = mod(vecangle(px(1), px(-1)), 2pi) Δθ = if Δθ_pre > 0 && !sweepflag Δθ_pre - 2pi @@ -440,7 +455,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 +503,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 +526,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 +593,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 +608,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/camera/camera.jl b/src/camera/camera.jl index 56b122d16bd..2d75f3dc9ca 100644 --- a/src/camera/camera.jl +++ b/src/camera/camera.jl @@ -1,5 +1,5 @@ function Base.copy(x::Camera) - Camera(ntuple(8) do i + Camera(ntuple(9) do i getfield(x, i) end...) end @@ -18,6 +18,7 @@ function Base.show(io::IO, camera::Camera) println(io, " projection: ", camera.projection[]) println(io, " projectionview: ", camera.projectionview[]) println(io, " resolution: ", camera.resolution[]) + println(io, " lookat: ", camera.lookat[]) println(io, " eyeposition: ", camera.eyeposition[]) end @@ -67,8 +68,8 @@ function Observables.on(f, camera::Camera, observables::AbstractObservable...; p return f end -function Camera(px_area) - pixel_space = lift(px_area) do window_size +function Camera(viewport) + pixel_space = lift(viewport) do window_size nearclip = -10_000f0 farclip = 10_000f0 w, h = Float32.(widths(window_size)) @@ -82,7 +83,8 @@ function Camera(px_area) view, proj, proj_view, - lift(a-> Vec2f(widths(a)), px_area), + lift(a-> Vec2f(widths(a)), viewport), + Observable(Vec3f(0)), Observable(Vec3f(1)), ObserverFunction[], Dict{Symbol, Observable}() @@ -101,7 +103,7 @@ end is_mouseinside(x, target) = is_mouseinside(get_scene(x), target) function is_mouseinside(scene::Scene, target) scene === target && return false - Vec(scene.events.mouseposition[]) in pixelarea(scene)[] || return false + Vec(scene.events.mouseposition[]) in viewport(scene)[] || return false for child in r.children is_mouseinside(child, target) && return true end @@ -115,7 +117,7 @@ Returns true if the current mouseposition is inside the given scene. """ is_mouseinside(x) = is_mouseinside(get_scene(x)) function is_mouseinside(scene::Scene) - return Vec(scene.events.mouseposition[]) in pixelarea(scene)[] + return Vec(scene.events.mouseposition[]) in viewport(scene)[] # Check that mouse is not inside any other screen # for child in scene.children # is_mouseinside(child) && return false diff --git a/src/camera/camera2d.jl b/src/camera/camera2d.jl index c5d0110744a..d9d354a180f 100644 --- a/src/camera/camera2d.jl +++ b/src/camera/camera2d.jl @@ -11,8 +11,8 @@ end """ cam2d!(scene::SceneLike, kwargs...) -Creates a 2D camera for the given `scene`. The camera implements zooming by -scrolling and translation using mouse drag. It also implements rectangle +Creates a 2D camera for the given `scene`. The camera implements zooming by +scrolling and translation using mouse drag. It also implements rectangle selections. ## Keyword Arguments @@ -46,6 +46,7 @@ function cam2d!(scene::SceneLike; kw_args...) cam end +get_space(::Camera2D) = :data wscale(screenrect, viewrect) = widths(viewrect) ./ widths(screenrect) @@ -54,7 +55,14 @@ wscale(screenrect, viewrect) = widths(viewrect) ./ widths(screenrect) Updates the camera for the given `scene` to cover the given `area` in 2d. """ -update_cam!(scene::SceneLike, area) = update_cam!(scene, cameracontrols(scene), area) +function update_cam!(scene::SceneLike, area::Rect) + return update_cam!(scene, cameracontrols(scene), area) +end +function update_cam!(scene::SceneLike, area::Rect, center::Bool) + return update_cam!(scene, cameracontrols(scene), area, center) +end + + """ update_cam!(scene::SceneLike) @@ -69,7 +77,7 @@ function update_cam!(scene::Scene, cam::Camera2D, area3d::Rect) # ignore rects with width almost 0 any(x-> x ≈ 0.0, widths(area)) && return - pa = pixelarea(scene)[] + pa = viewport(scene)[] px_wh = normalize(widths(pa)) wh = normalize(widths(area)) ratio = px_wh ./ wh @@ -98,7 +106,7 @@ function update_cam!(scene::SceneLike, cam::Camera2D) end function correct_ratio!(scene, cam) - on(camera(scene), pixelarea(scene)) do area + on(camera(scene), viewport(scene)) do area neww = widths(area) change = neww .- cam.last_area[] if !(change ≈ Vec(0.0, 0.0)) @@ -132,7 +140,7 @@ function add_pan!(scene::SceneLike, cam::Camera2D) diff = startpos[] .- mp startpos[] = mp area = cam.area[] - diff = Vec(diff) .* wscale(pixelarea(scene)[], area) + diff = Vec(diff) .* wscale(viewport(scene)[], area) cam.area[] = Rectf(minimum(area) .+ diff, widths(area)) update_cam!(scene, cam) active[] = false @@ -150,7 +158,7 @@ function add_pan!(scene::SceneLike, cam::Camera2D) diff = startpos[] .- pos startpos[] = pos area = cam.area[] - diff = Vec(diff) .* wscale(pixelarea(scene)[], area) + diff = Vec(diff) .* wscale(viewport(scene)[], area) cam.area[] = Rectf(minimum(area) .+ diff, widths(area)) update_cam!(scene, cam) return Consume(true) @@ -165,7 +173,7 @@ function add_zoom!(scene::SceneLike, cam::Camera2D) @extractvalue cam (zoomspeed, zoombutton, area) zoom = Float32(x[2]) if zoom != 0 && ispressed(scene, zoombutton) && is_mouseinside(scene) - pa = pixelarea(scene)[] + pa = viewport(scene)[] z = (1f0 - zoomspeed)^zoom mp = Vec2f(e.mouseposition[]) - minimum(pa) mp = (mp .* wscale(pa, area)) + minimum(area) @@ -182,7 +190,7 @@ function add_zoom!(scene::SceneLike, cam::Camera2D) end function camspace(scene::SceneLike, cam::Camera2D, point) - point = Vec(point) .* wscale(pixelarea(scene)[], cam.area[]) + point = Vec(point) .* wscale(viewport(scene)[], cam.area[]) return Vec(point) .+ Vec(minimum(cam.area[])) end @@ -310,6 +318,7 @@ function add_restriction!(cam, window, rarea::Rect2, minwidths::Vec) end struct PixelCamera <: AbstractCamera end +get_space(::PixelCamera) = :pixel struct UpdatePixelCam @@ -317,6 +326,7 @@ struct UpdatePixelCam near::Float32 far::Float32 end +get_space(::UpdatePixelCam) = :pixel function (cam::UpdatePixelCam)(window_size) w, h = Float32.(widths(window_size)) @@ -327,7 +337,7 @@ end """ campixel!(scene; nearclip=-1000f0, farclip=1000f0) -Creates a pixel camera for the given `scene`. This means that the positional +Creates a pixel camera for the given `scene`. This means that the positional data of a plot will be interpreted in pixel units. This camera does not feature controls. """ @@ -335,21 +345,22 @@ function campixel!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) disconnect!(camera(scene)) update_once = Observable(false) closure = UpdatePixelCam(camera(scene), nearclip, farclip) - on(closure, camera(scene), pixelarea(scene)) + on(closure, camera(scene), viewport(scene)) cam = PixelCamera() # update once - closure(pixelarea(scene)[]) + closure(viewport(scene)[]) cameracontrols!(scene, cam) update_once[] = true return cam end struct RelativeCamera <: AbstractCamera end +get_space(::RelativeCamera) = :relative """ cam_relative!(scene) -Creates a camera for the given `scene` which maps the scene area to a 0..1 by +Creates a camera for the given `scene` which maps the scene area to a 0..1 by 0..1 range. This camera does not feature controls. """ function cam_relative!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 11935e7e3bb..dc789edadaf 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -1,5 +1,7 @@ abstract type AbstractCamera3D <: AbstractCamera end +get_space(::AbstractCamera3D) = :data + struct Camera3D <: AbstractCamera3D # User settings settings::Attributes @@ -38,7 +40,12 @@ Settings include anything that isn't a mouse or keyboard button. - `fixed_axis = true`: If true panning uses the (world/plot) z-axis instead of the camera up direction. - `zoom_shift_lookat = true`: If true keeps the data under the cursor when zooming. - `cad = false`: If true rotates the view around `lookat` when zooming off-center. -- `clipping_mode = :bbox_relative`: Controls how `near` and `far` get processed. With `:static` they get passed as is, with `:view_relative` they get scaled by `norm(eyeposition - lookat)` and with `:bbox_relative` they get scaled to be just outside the scene bounding box. (More specifically `far = 1` is scaled to the furthest point of a bounding sphere and `near` is generally overwritten to be the closest point.) +- `clipping_mode = :adaptive`: Controls how `near` and `far` get processed. Options: + - `:static` passes `near` and `far` as is + - `:adaptive` scales `near` by `norm(eyeposition - lookat)` and passes `far` as is + - `:view_relative` scales `near` and `far` by `norm(eyeposition - lookat)` + - `:bbox_relative` scales `near` and `far` to the scene bounding box as passed to the camera with `update_cam!(..., bbox)`. (More specifically `far = 1` is scaled to the furthest point of a bounding sphere and `near` is generally overwritten to be the closest point.) +- `center = true`: Controls whether the camera placement gets reset when calling `update_cam!(scene[, cam], bbox)`, which is called when a new plot is added. This is automatically set to `false` after calling `update_cam!(scene[, cam], eyepos, lookat[, up])`. - `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. - `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. @@ -160,7 +167,8 @@ function Camera3D(scene::Scene; kwargs...) zoom_shift_lookat = true, fixed_axis = true, cad = false, - clipping_mode = :bbox_relative + center = true, + clipping_mode = :adaptive ) replace!(settings, :Camera3D, scene, overwrites) @@ -170,7 +178,7 @@ function Camera3D(scene::Scene; kwargs...) elseif settings.clipping_mode[] === :bbox_relative far_default = 1f0 else - far_default = 10f0 # will be set when inserting a plot + far_default = 100f0 # will be set when inserting a plot end cam = Camera3D( @@ -246,7 +254,7 @@ function Camera3D(scene::Scene; kwargs...) update_cam!(scene, cam) end end - on(camera(scene), scene.px_area, cam.near, cam.far, settings.projectiontype) do _, _, _, _ + on(camera(scene), scene.viewport, cam.near, cam.far, settings.projectiontype) do _, _, _, _ update_cam!(scene, cam) end @@ -402,10 +410,10 @@ function add_mouse_controls!(scene, cam::Camera3D) if projectiontype[] == Perspective # TODO wrong scaling? :( ynorm = 2 * norm(cam.lookat[] - cam.eyeposition[]) * tand(0.5 * cam.fov[]) - return ynorm / widths(scene.px_area[])[2] * delta + return ynorm / size(scene, 2) * delta else viewnorm = norm(cam.eyeposition[] - cam.lookat[]) - return 2 * viewnorm / widths(scene.px_area[])[2] * delta + return 2 * viewnorm / size(scene, 2) * delta end end @@ -447,12 +455,13 @@ function add_mouse_controls!(scene, cam::Camera3D) # reposition if ispressed(scene, reposition_button[], event.button) && is_mouseinside(scene) plt, _, p = ray_assisted_pick(scene) - if p !== Point3f(NaN) && to_value(get(plt, :space, :data)) == :data && parent_scene(plt) == scene + p3d = to_ndim(Point3f, p, 0f0) + if !isnan(p3d) && to_value(get(plt, :space, :data)) == :data && parent_scene(plt) == scene # if translation/rotation happens with on-click reposition, # try uncommenting this # dragging[] = (false, false) - shift = p - cam.lookat[] - update_cam!(scene, cam, cam.eyeposition[] + shift, p) + shift = p3d - cam.lookat[] + update_cam!(scene, cam, cam.eyeposition[] + shift, p3d) end consume = true end @@ -617,7 +626,7 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) # recontextualize the (dy, dx, 0) from mouse rotations so that # drawing circles creates continuous rotations around the fixed axis mp = mouseposition_px(scene) - past_half = 0.5f0 .* widths(scene.px_area[]) .> mp + past_half = 0.5f0 .* size(scene) .> mp flip = 2f0 * past_half .- 1f0 angle = flip[1] * angles[1] + flip[2] * angles[2] angles = Vec3f(-angle, -angle, angle) @@ -655,14 +664,15 @@ function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat lookat = cam.lookat[] eyepos = cam.eyeposition[] viewdir = lookat - eyepos # -z - + vp = viewport(scene)[] + scene_width = widths(vp) if cad # Rotate view based on offset from center u_z = normalize(viewdir) u_x = normalize(cross(u_z, cam.upvector[])) u_y = normalize(cross(u_x, u_z)) - rel_pos = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 + rel_pos = 2.0f0 * mouseposition_px(scene) ./ scene_width .- 1.0f0 shift = rel_pos[1] * u_x + rel_pos[2] * u_y shift *= 0.1 * sign(1 - zoom_step) * norm(viewdir) @@ -673,8 +683,7 @@ function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat u_x = normalize(cross(u_z, cam.upvector[])) u_y = normalize(cross(u_x, u_z)) - ws = widths(scene.px_area[]) - rel_pos = (2.0 .* mouseposition_px(scene) .- ws) ./ ws[2] + rel_pos = (2.0 .* mouseposition_px(scene) .- scene_width) ./ scene_width[2] shift = (1 - zoom_step) * (rel_pos[1] * u_x + rel_pos[2] * u_y) if cam.settings.projectiontype[] == Makie.Orthographic @@ -717,11 +726,14 @@ function update_cam!(scene::Scene, cam::Camera3D) far_dist = center_dist + radius(bounding_sphere) near = max(view_dist * near, center_dist - radius(bounding_sphere)) far = far_dist * far + elseif cam.settings.clipping_mode[] === :adaptive + view_dist = norm(eyeposition - lookat) + near = view_dist * near; far = far elseif cam.settings.clipping_mode[] !== :static @error "clipping_mode = $(cam.settings.clipping_mode[]) not recognized, using :static." end - aspect = Float32((/)(widths(scene.px_area[])...)) + aspect = Float32((/)(widths(scene)...)) if cam.settings.projectiontype[] == Makie.Perspective proj = perspectiveprojection(fov, aspect, near, far) else @@ -732,11 +744,12 @@ function update_cam!(scene::Scene, cam::Camera3D) set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] + scene.camera.lookat[] = cam.lookat[] end # Update camera position via bbox -function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) +function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect, recenter::Bool = cam.settings.center[]) bb = Rect3f(area3d) width = widths(bb) center = maximum(bb) - 0.5f0 * width @@ -751,13 +764,18 @@ function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) dist = radius end - cam.lookat[] = center - cam.eyeposition[] = cam.lookat[] .+ dist * old_dir - cam.upvector[] = Vec3f(0, 0, 1) # Should we reset this? + if recenter + cam.lookat[] = center + cam.eyeposition[] = cam.lookat[] .+ dist * old_dir + cam.upvector[] = normalize(cross(old_dir, cross(cam.upvector[], old_dir))) + end if cam.settings.clipping_mode[] === :static cam.near[] = 0.1f0 * dist cam.far[] = 2f0 * dist + elseif cam.settings.clipping_mode[] === :adaptive + cam.near[] = 0.1f0 * dist / norm(cam.eyeposition[] - cam.lookat[]) + cam.far[] = 2f0 * dist end update_cam!(scene, cam) @@ -767,6 +785,7 @@ end # Update camera position via camera Position & Orientation function update_cam!(scene::Scene, camera::Camera3D, eyeposition::VecTypes, lookat::VecTypes, up::VecTypes = camera.upvector[]) + camera.settings.center[] = false camera.lookat[] = Vec3f(lookat) camera.eyeposition[] = Vec3f(eyeposition) camera.upvector[] = Vec3f(up) @@ -787,6 +806,7 @@ function update_cam!( radius::Real = norm(camera.eyeposition[] - camera.lookat[]), center = camera.lookat[] ) + camera.settings.center[] = false st, ct = sincos(theta) sp, cp = sincos(phi) v = Vec3f(ct * cp, ct * sp, st) @@ -807,4 +827,4 @@ function show_cam(scene) println("cam.upvector[] = ", round.(cam.upvector[], digits=2)) println("cam.fov[] = ", round.(cam.fov[], digits=2)) return -end \ No newline at end of file +end diff --git a/src/camera/old_camera3d.jl b/src/camera/old_camera3d.jl index e3167262327..33d381d2e45 100644 --- a/src/camera/old_camera3d.jl +++ b/src/camera/old_camera3d.jl @@ -46,13 +46,15 @@ function old_cam3d_cad!(scene::Scene; kw_args...) add_translation!(scene, cam, cam.pan_button, cam.move_key, false) add_rotation!(scene, cam, cam.rotate_button, cam.move_key, false) cameracontrols!(scene, cam) - on(camera(scene), scene.px_area) do area + on(camera(scene), scene.viewport) do area # update cam when screen ratio changes update_cam!(scene, cam) end cam end +get_space(::OldCamera3D) = :data + """ old_cam3d_turntable!(scene; kw_args...) @@ -82,7 +84,7 @@ function old_cam3d_turntable!(scene::Scene; kw_args...) add_translation!(scene, cam, cam.pan_button, cam.move_key, true) add_rotation!(scene, cam, cam.rotate_button, cam.move_key, true) cameracontrols!(scene, cam) - on(camera(scene), scene.px_area) do area + on(camera(scene), scene.viewport) do area # update cam when screen ratio changes update_cam!(scene, cam) end @@ -176,7 +178,7 @@ function add_translation!(scene, cam, key, button, zoom_shift_lookat::Bool) on(camera(scene), scene.events.scroll) do scroll if ispressed(scene, button[]) && is_mouseinside(scene) - cam_res = Vec2f(widths(scene.px_area[])) + cam_res = Vec2f(widths(scene)) mouse_pos_normalized = mouseposition_px(scene) ./ cam_res mouse_pos_normalized = 2*mouse_pos_normalized .- 1f0 zoom_step = scroll[2] @@ -237,7 +239,7 @@ function translate_cam!(scene::Scene, cam::OldCamera3D, _translation::VecTypes) dir = eyeposition - lookat dir_len = norm(dir) - cam_res = Vec2f(widths(scene.px_area[])) + cam_res = Vec2f(widths(scene)) z, x, y = translation z *= 0.1f0 * dir_len @@ -328,7 +330,7 @@ function update_cam!(scene::Scene, cam::OldCamera3D) # TODO this means you can't set FarClip... SAD! # TODO use boundingbox(scene) for optimal far/near far = max(zoom * 5f0, 30f0) - proj = projection_switch(scene.px_area[], fov, near, far, projectiontype, zoom) + proj = projection_switch(scene.viewport[], fov, near, far, projectiontype, zoom) view = Makie.lookat(eyeposition, lookat, upvector) set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] @@ -356,7 +358,9 @@ end Updates the camera's controls to point to the specified location. """ -update_cam!(scene::Scene, eyeposition, lookat, up = Vec3f(0, 0, 1)) = update_cam!(scene, cameracontrols(scene), eyeposition, lookat, up) +function update_cam!(scene::Scene, eyeposition::VecTypes{3}, lookat::VecTypes{3}, up::VecTypes{3} = Vec3f(0, 0, 1)) + return update_cam!(scene, cameracontrols(scene), eyeposition, lookat, up) +end function update_cam!(scene::Scene, camera::OldCamera3D, eyeposition, lookat, up = Vec3f(0, 0, 1)) camera.lookat[] = Vec3f(lookat) diff --git a/src/camera/projection_math.jl b/src/camera/projection_math.jl index 717224c6a6a..2451925c0ac 100644 --- a/src/camera/projection_math.jl +++ b/src/camera/projection_math.jl @@ -245,7 +245,7 @@ function to_world(scene::Scene, point::T) where T <: StaticVector inv(transformationmatrix(scene)[]) * inv(cam.view[]) * inv(cam.projection[]), - T(widths(pixelarea(scene)[])) + T(size(scene)) ) Point2f(x[1], x[2]) end @@ -280,7 +280,7 @@ end function project(scene::Scene, point::T) where T<:StaticVector cam = scene.camera - area = pixelarea(scene)[] + area = viewport(scene)[] # TODO, I think we need .+ minimum(area) # Which would be semi breaking at this point though, I suppose return project( @@ -344,6 +344,22 @@ function clip_to_space(cam::Camera, space::Symbol) end end +function get_space(scene::Scene) + space = get_space(cameracontrols(scene))::Symbol + space === :data ? (:data,) : (:data, space) +end +get_space(::AbstractCamera) = :data +# TODO: Should this be less specialized? ScenePlot? AbstractPlot? +get_space(plot::Combined) = to_value(get(plot, :space, :data))::Symbol + +is_space_compatible(a, b) = is_space_compatible(get_space(a), get_space(b)) +is_space_compatible(a::Symbol, b::Symbol) = a === b +is_space_compatible(a::Symbol, b::Union{Tuple, Vector}) = a in b +function is_space_compatible(a::Union{Tuple, Vector}, b::Union{Tuple, Vector}) + any(x -> is_space_compatible(x, b), a) +end +is_space_compatible(a::Union{Tuple, Vector}, b::Symbol) = is_space_compatible(b, a) + function project(cam::Camera, input_space::Symbol, output_space::Symbol, pos) input_space === output_space && return to_ndim(Point3f, pos, 0) clip_from_input = space_to_clip(cam, input_space) 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..0c947997ce2 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -328,14 +328,14 @@ function edges(v::AbstractVector) end end -function adjust_axes(::CellBasedGrid, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractMatrix) +function adjust_axes(::CellGrid, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractMatrix) x̂, ŷ = map((x, y), size(z)) do v, sz return length(v) == sz ? edges(v) : v end return x̂, ŷ, z end -adjust_axes(::VertexBasedGrid, x, y, z) = x, y, z +adjust_axes(::VertexGrid, x, y, z) = x, y, z """ convert_arguments(ct::GridBased, x::VecOrMat, y::VecOrMat, z::Matrix) @@ -352,7 +352,7 @@ function convert_arguments(ct::GridBased, x::AbstractVecOrMat{<: Number}, y::Abs return map(el32convert, adjust_axes(ct, x, y, z)) end -convert_arguments(ct::VertexBasedGrid, x::AbstractMatrix, y::AbstractMatrix) = convert_arguments(ct, x, y, zeros(size(y))) +convert_arguments(ct::VertexGrid, x::AbstractMatrix, y::AbstractMatrix) = convert_arguments(ct, x, y, zeros(size(y))) """ convert_arguments(P, x::RangeLike, y::RangeLike, z::AbstractMatrix) @@ -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)") @@ -1475,6 +1476,8 @@ end convert_attribute(value, ::key"diffuse") = Vec3f(value) convert_attribute(value, ::key"specular") = Vec3f(value) +convert_attribute(value, ::key"backlight") = Float32(value) + # SAMPLER overloads diff --git a/src/deprecated.jl b/src/deprecated.jl index 5bc7fe2b141..2115fb26a41 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -7,3 +7,17 @@ function backend_display(args...) @warn("`backend_display` is an internal deprecated function, which shouldn't be used outside Makie. if you must really use this function, it's now just `display(::Backend.Screen, figlike)`") end + +@deprecate DiscreteSurface CellGrid true +@deprecate ContinuousSurface VertexGrid true + + +function Base.getproperty(scene::Scene, field::Symbol) + if field === :px_area + @warn "`.px_area` got renamed to `.viewport`, and means the area the scene maps to in device indepentent units, not pixels. Note, `size(scene) == widths(scene.viewport[])`" + return scene.area + end + return getfield(scene, field) +end + +@deprecate pixelarea viewport true diff --git a/src/display.jl b/src/display.jl index a5b4440a322..5fc3b60903b 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 @@ -274,7 +274,7 @@ filetype(::FileIO.File{F}) where F = F # Allow format to be overridden with first argument """ - FileIO.save(filename, scene; resolution = size(scene), pt_per_unit = 0.75, px_per_unit = 1.0) + FileIO.save(filename, scene; size = size(scene), pt_per_unit = 0.75, px_per_unit = 1.0) Save a `Scene` with the specified filename and format. @@ -288,7 +288,7 @@ Save a `Scene` with the specified filename and format. ## All Backends -- `resolution`: `(width::Int, height::Int)` of the scene in dimensionless units (equivalent to `px` for GLMakie and WGLMakie). +- `size`: `(width::Int, height::Int)` of the scene in dimensionless units. - `update`: Whether the figure should be updated before saving. This resets the limits of all Axes in the figure. Defaults to `true`. - `backend`: Specify the `Makie` backend that should be used for saving. Defaults to the current backend. - Further keywords will be forwarded to the screen. @@ -307,14 +307,19 @@ end function FileIO.save( file::FileIO.Formatted, fig::FigureLike; - resolution = size(get_scene(fig)), + size = Base.size(get_scene(fig)), + resolution = nothing, backend = current_backend(), update = true, screen_config... ) scene = get_scene(fig) - if resolution != size(scene) - resize!(scene, resolution) + if resolution !== nothing + @warn "The keyword argument `resolution` for `save()` has been deprecated. Use `size` instead, which better reflects that this is a unitless size and not a pixel resolution." + size = resolution + end + if size != Base.size(scene) + resize!(scene, size) end filename = FileIO.filename(file) # Delete previous file if it exists and query only the file string for type. @@ -326,13 +331,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...) + visible = isvisible(getscreen(scene)) # if already has a screen, don't hide it! + 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 +407,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 @@ -421,13 +429,17 @@ function getscreen(backend::Union{Missing, Module}, scene::Scene, args...; scree end function get_sub_picture(image, format::ImageStorageFormat, rect) - xmin, ymin = minimum(rect) .- (1, 0) + xmin, ymin = minimum(rect) .- Vec(1, 0) xmax, ymax = maximum(rect) start = size(image, 1) - ymax stop = size(image, 1) - ymin return image[start:stop, xmin:xmax] end +# Needs to be overloaded by backends, with fallback true +isvisible(screen::MakieScreen) = true +isvisible(::Nothing) = false + """ colorbuffer(scene, format::ImageStorageFormat = JuliaNative; update=true, backend=current_backend(), screen_config...) @@ -445,11 +457,15 @@ or RGBA. function colorbuffer(fig::FigureLike, format::ImageStorageFormat = JuliaNative; update=true, backend = current_backend(), screen_config...) 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...) + # if already has a screen, use their visibility value, if no screen, returns false + visible = isvisible(getscreen(scene)) + 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)[]) + return get_sub_picture(img, format, viewport(scene)[]) else return img end 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/figures.jl b/src/figures.jl index 6e54b87c4a3..b349ca71635 100644 --- a/src/figures.jl +++ b/src/figures.jl @@ -26,11 +26,12 @@ if an axis is placed at that position (if not it errors) or it can reference an get_scene(fig::Figure) = fig.scene get_scene(fap::FigureAxisPlot) = fap.figure.scene +get_scene(gp::GridLayoutBase.GridPosition) = get_scene(get_figure(gp)) +get_scene(gp::GridLayoutBase.GridSubposition) = get_scene(get_figure(gp)) -const CURRENT_FIGURE = Ref{Union{Nothing, Figure}}(nothing) -Base.@deprecate_binding _current_figure CURRENT_FIGURE +const CURRENT_FIGURE = Ref{Union{Nothing, Figure}}(nothing) const CURRENT_FIGURE_LOCK = Base.ReentrantLock() """ @@ -196,7 +197,7 @@ end # Layouts are already hooked up to this, so it's very simple. """ resize!(fig::Figure, width, height) -Resizes the given `Figure` to the resolution given by `width` and `height`. +Resizes the given `Figure` to the size given by `width` and `height`. If you want to resize the figure to its current layout content, use `resize_to_layout!(fig)` instead. """ Makie.resize!(figure::Figure, width::Integer, height::Integer) = resize!(figure.scene, width, height) diff --git a/src/interaction/events.jl b/src/interaction/events.jl index 7d599680730..fbc08f862a5 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. @@ -264,7 +264,6 @@ ispressed(parent, result::Bool, waspressed = nothing) = result ispressed(parent, mb::Mouse.Button, waspressed = nothing) = ispressed(events(parent), mb, waspressed) ispressed(parent, key::Keyboard.Button, waspressed = nothing) = ispressed(events(parent), key, waspressed) -@deprecate ispressed(scene, ::Nothing) ispressed(parent, true) # Boolean Operator evaluation ispressed(parent, op::And, waspressed = nothing) = ispressed(parent, op.left, waspressed) && ispressed(parent, op.right, waspressed) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index b1b7465e609..9212cab8576 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -125,12 +125,12 @@ A --- B | | D --- C -this computes parameter `f` such that the line from `A + f * (B - A)` to -`D + f * (C - D)` crosses through the given point `P`. This assumes that `P` is +this computes parameter `f` such that the line from `A + f * (B - A)` to +`D + f * (C - D)` crosses through the given point `P`. This assumes that `P` is inside the quad and that none of the edges cross. """ function point_in_quad_parameter( - A::Point2, B::Point2, C::Point2, D::Point2, P::Point2; + A::Point2, B::Point2, C::Point2, D::Point2, P::Point2; iterations = 50, epsilon = 1e-6 ) @@ -166,9 +166,9 @@ end function shift_project(scene, pos) project( camera(scene).projectionview[], - Vec2f(widths(pixelarea(scene)[])), + Vec2f(size(scene)), pos - ) .+ Vec2f(origin(pixelarea(scene)[])) + ) .+ Vec2f(origin(viewport(scene)[])) end @@ -236,7 +236,7 @@ returning a label. See Makie documentation for more detail. - `enable_indicators = true)`: Enables or disables indicators - `depth = 9e3`: Depth value of the tooltip. This should be high so that the tooltip is always in front. -- `apply_tooltip_offset = true`: Enables or disables offsetting tooltips based +- `apply_tooltip_offset = true`: Enables or disables offsetting tooltips based on, for example, markersize. - and all attributes from `Tooltip` """ @@ -246,7 +246,7 @@ end function DataInspector(scene::Scene; priority = 100, kwargs...) parent = root(scene) - @assert origin(pixelarea(parent)[]) == Vec2f(0) + @assert origin(viewport(parent)[]) == Vec2f(0) attrib_dict = Dict(kwargs) base_attrib = Attributes( @@ -397,7 +397,7 @@ end function update_tooltip_alignment!(inspector, proj_pos) inspector.plot[1][] = proj_pos - wx, wy = widths(pixelarea(inspector.root)[]) + wx, wy = widths(viewport(inspector.root)[]) px, py = proj_pos placement = py < 0.75wy ? (:above) : (:below) @@ -515,8 +515,8 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i update_tooltip_alignment!(inspector, proj_pos) tt.offset[] = ifelse( - a.apply_tooltip_offset[], - sv_getindex(plot.linewidth[], idx) + 2, + a.apply_tooltip_offset[], + sv_getindex(plot.linewidth[], idx) + 2, a.offset[] ) @@ -560,7 +560,7 @@ function show_data(inspector::DataInspector, plot::Mesh, idx) end p = wireframe!( - scene, bbox, color = a.indicator_color, + scene, bbox, color = a.indicator_color, transformation = Transformation(), linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false @@ -661,7 +661,7 @@ function show_imagelike(inspector, plot, name, edge_based) a._color[] = if z isa AbstractFloat interpolated_getindex( to_colormap(plot.colormap[]), z, - to_value(get(plot.attributes, :colorrange, (0, 1))) + extract_colorrange(plot) ) else z @@ -672,16 +672,16 @@ function show_imagelike(inspector, plot, name, edge_based) if a.enable_indicators[] if plot.interpolate[] - if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || !(inspector.temp_plots[1] isa Scatter) clear_temporary_plots!(inspector, plot) p = scatter!( scene, pos, color = a._color, visible = a.indicator_visible, inspectable = false, model = plot.model, - # TODO switch to Rect with 2r-1 or 2r-2 markersize to have + # TODO switch to Rect with 2r-1 or 2r-2 markersize to have # just enough space to always detect the underlying image - marker=:rect, markersize = map(r -> 2r, a.range), + marker=:rect, markersize = map(r -> 2r, a.range), strokecolor = a.indicator_color, strokewidth = a.indicator_linewidth, depth_shift = -1f-3 @@ -694,7 +694,7 @@ function show_imagelike(inspector, plot, name, edge_based) end else bbox = _pixelated_image_bbox(plot[1][], plot[2][], plot[3][], i, j, edge_based) - if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || !(inspector.temp_plots[1] isa Wireframe) clear_temporary_plots!(inspector, plot) p = wireframe!( @@ -918,7 +918,7 @@ function show_poly(inspector, plot, poly, idx, source) clear_temporary_plots!(inspector, plot) p = lines!( - scene, line_collection, color = a.indicator_color, + scene, line_collection, color = a.indicator_color, transformation = Transformation(source), strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false, depth_shift = -1f-3 @@ -953,7 +953,7 @@ function show_data(inspector::DataInspector, plot::VolumeSlices, idx, child::Hea proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) tt[1][] = proj_pos - + world_pos = apply_transform_and_model(child, pos) if haskey(plot, :inspector_label) @@ -1003,7 +1003,7 @@ function show_data(inspector::DataInspector, plot::Band, idx::Integer, mesh::Mes clear_temporary_plots!(inspector, plot) p = lines!( scene, [P1, P2], transformation = Transformation(plot.transformation), - color = a.indicator_color, strokewidth = a.indicator_linewidth, + color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false, depth_shift = -1f-3 diff --git a/src/interaction/interactive_api.jl b/src/interaction/interactive_api.jl index 7ea6243d497..4fea246ad69 100644 --- a/src/interaction/interactive_api.jl +++ b/src/interaction/interactive_api.jl @@ -31,8 +31,6 @@ function onpick(f, scene::Scene, plots::AbstractPlot...; range=1) end end -@deprecate mouse_selection pick - """ mouse_in_scene(fig/ax/scene[, priority = 0]) @@ -44,7 +42,7 @@ function mouse_in_scene(scene::Scene; priority = 0) p = rootparent(scene) output = Observable(Vec2(0.0)) on(events(scene).mouseposition, priority = priority) do mp - output[] = Vec(mp) .- minimum(pixelarea(scene)[]) + output[] = Vec(mp) .- minimum(viewport(scene)[]) return Consume(false) end output @@ -175,7 +173,7 @@ Normalizes mouse position `pos` relative to the screen rectangle. """ screen_relative(x, mpos) = screen_relative(get_scene(x), mpos) function screen_relative(scene::Scene, mpos) - return Point2f(mpos) .- Point2f(minimum(pixelarea(scene)[])) + return Point2f(mpos) .- Point2f(minimum(viewport(scene)[])) end """ diff --git a/src/interaction/observables.jl b/src/interaction/observables.jl index a1d7b2d4465..6b384efa4d0 100644 --- a/src/interaction/observables.jl +++ b/src/interaction/observables.jl @@ -17,34 +17,11 @@ function safe_off(o::Observables.AbstractObservable, f) end end -""" - map_once(closure, inputs::Observable....)::Observable - -Like Reactive.foreach, in the sense that it will be preserved even if no reference is kept. -The difference is, that you can call map once multiple times with the same closure and it will -close the old result Observable and register a new one instead. - -``` -function test(s1::Observable) - s3 = map_once(x-> (println("1 ", x); x), s1) - s3 = map_once(x-> (println("2 ", x); x), s1) - -end -test(Observable(1), Observable(2)) -> - -""" -function map_once( - f, input::Observable, inputrest::Observable... - ) - for arg in (input, inputrest...) - safe_off(arg, f) - end - lift(f, input, inputrest...) +function on_latest(f, observable::Observable; update=false, spawn=false) + return on_latest(f, nothing, observable; update=update, spawn=spawn) end -function on_latest(f, observable; update=false, spawn=false) - # How does one create a finished task?? +function on_latest(f, to_track, observable::Observable; update=false, spawn=false) last_task = nothing has_changed = Threads.Atomic{Bool}(false) function run_f(new_value) @@ -61,15 +38,12 @@ function on_latest(f, observable; update=false, spawn=false) # we assume for now that `==` is prohibitive as the default if has_changed[] has_changed[] = false - run_f(observable[]) # needs to recursive + run_f(observable[]) # needs to be recursive end end - return on(observable; update=update) do new_value - if isnothing(last_task) - # run first task in sync - last_task = update ? (@async f(observable[])) : @async(nothing) - wait(last_task) - elseif istaskdone(last_task) + + function on_callback(new_value) + if isnothing(last_task) || istaskdone(last_task) if spawn last_task = Threads.@spawn run_f(new_value) else @@ -80,6 +54,14 @@ function on_latest(f, observable; update=false, spawn=false) return # Do nothing if working end end + + update && f(observable[]) + + if isnothing(to_track) + return on(on_callback, observable) + else + return on(on_callback, to_track, observable) + end end function onany_latest(f, observables...; update=false, spawn=false) @@ -87,3 +69,14 @@ function onany_latest(f, observables...; update=false, spawn=false) onany((args...)-> (result[] = args), observables...) on_latest((args)-> f(args...), result; update=update, spawn=spawn) end + +function map_latest!(f, result::Observable, observables...; update=false, spawn=false) + callback = Observables.MapCallback(f, result, observables) + return onany_latest(callback, observables...; update=update, spawn=spawn) +end + +function map_latest(f, observables...; spawn=false, ignore_equal_values=false) + result = Observable(f(map(to_value, observables)...); ignore_equal_values=ignore_equal_values) + map_latest!(f, result, observables...; update=update, spawn=spawn) + return result +end diff --git a/src/interaction/ray_casting.jl b/src/interaction/ray_casting.jl index 38d7f8f94e5..8b3c67f2515 100644 --- a/src/interaction/ray_casting.jl +++ b/src/interaction/ray_casting.jl @@ -21,7 +21,7 @@ end Ray(scene[, cam = cameracontrols(scene)], xy) Returns a `Ray` into the given `scene` passing through pixel position `xy`. Note -that the pixel position should be relative to the origin of the scene, as it is +that the pixel position should be relative to the origin of the scene, as it is when calling `mouseposition_px(scene)`. """ Ray(scene::Scene, xy::VecTypes{2}) = Ray(scene, cameracontrols(scene), xy) @@ -35,8 +35,8 @@ function Ray(scene::Scene, cam::Camera3D, xy::VecTypes{2}) u_z = normalize(viewdir) u_x = normalize(cross(u_z, cam.upvector[])) u_y = normalize(cross(u_x, u_z)) - - px_width, px_height = widths(scene.px_area[]) + + px_width, px_height = widths(scene) aspect = px_width / px_height rel_pos = 2 .* xy ./ (px_width, px_height) .- 1 @@ -51,7 +51,7 @@ function Ray(scene::Scene, cam::Camera3D, xy::VecTypes{2}) end function Ray(scene::Scene, cam::Camera2D, xy::VecTypes{2}) - rel_pos = xy ./ widths(scene.px_area[]) + rel_pos = xy ./ widths(scene) pv = scene.camera.projectionview[] m = Vec2f(pv[1, 1], pv[2, 2]) b = Vec2f(pv[1, 4], pv[2, 4]) @@ -64,16 +64,16 @@ function Ray(::Scene, ::PixelCamera, xy::VecTypes{2}) end function Ray(scene::Scene, ::RelativeCamera, xy::VecTypes{2}) - origin = xy ./ widths(scene.px_area[]) + origin = xy ./ widths(scene) return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) end Ray(scene::Scene, cam, xy::VecTypes{2}) = ray_from_projectionview(scene, xy) -# This method should always work +# This method should always work function ray_from_projectionview(scene::Scene, xy::VecTypes{2}) inv_view_proj = inv(camera(scene).projectionview[]) - area = pixelarea(scene)[] + area = viewport(scene)[] # This figures out the camera view direction from the projectionview matrix # and computes a ray from a near and a far point. @@ -126,7 +126,7 @@ end function ray_triangle_intersection(A::VecTypes, B::VecTypes, C::VecTypes, ray::Ray, ϵ = 1e-6) return ray_triangle_intersection( - to_ndim(Point3f, A, 0f0), to_ndim(Point3f, B, 0f0), to_ndim(Point3f, C, 0f0), + to_ndim(Point3f, A, 0f0), to_ndim(Point3f, B, 0f0), to_ndim(Point3f, C, 0f0), ray, ϵ ) end @@ -188,7 +188,7 @@ This function performs a `pick` at the given pixel position `xy` and returns the picked `plot`, `index` and world or input space `position::Point3f`. It is equivalent to ``` plot, idx = pick(fig/ax/scene, xy) -ray = Ray(parent_scene(plot), xy .- minimum(pixelarea(parent_scene(plot))[])) +ray = Ray(parent_scene(plot), xy .- minimum(viewport(parent_scene(plot))[])) position = position_on_plot(plot, idx, ray, apply_transform = true) ``` See [`position_on_plot`](@ref) for more information. @@ -197,7 +197,7 @@ function ray_assisted_pick(obj, xy = events(obj).mouseposition[]; apply_transfor plot, idx = pick(get_scene(obj), xy) isnothing(plot) && return (plot, idx, Point3f(NaN)) scene = parent_scene(plot) - ray = Ray(scene, xy .- minimum(pixelarea(scene)[])) + ray = Ray(scene, xy .- minimum(viewport(scene)[])) pos = position_on_plot(plot, idx, ray, apply_transform = apply_transform) return (plot, idx, pos) end @@ -206,23 +206,23 @@ end """ position_on_plot(plot, index[, ray::Ray; apply_transform = true]) -This function calculates the world or input space position of a ray - plot -intersection with the result `plot, idx = pick(...)` and a ray cast from the +This function calculates the world or input space position of a ray - plot +intersection with the result `plot, idx = pick(...)` and a ray cast from the picked position. If there is no intersection `Point3f(NaN)` will be returned. This should be called as ``` plot, idx = pick(ax, px_pos) -pos_in_ax = position_on_plot(plot, idx, Ray(ax, px_pos .- minimum(pixelarea(ax.scene)[]))) +pos_in_ax = position_on_plot(plot, idx, Ray(ax, px_pos .- minimum(viewport(ax.scene)[]))) ``` or more simply `plot, idx, pos_in_ax = ray_assisted_pick(ax, px_pos)`. -You can switch between getting a position in world space (after applying -transformations like `log`, `translate!()`, `rotate!()` and `scale!()`) and +You can switch between getting a position in world space (after applying +transformations like `log`, `translate!()`, `rotate!()` and `scale!()`) and input space (the raw position data of the plot) by adjusting `apply_transform`. -Note that `position_on_plot` is only implemented for primitive plot types, i.e. -the possible return types of `pick`. Depending on the plot type the calculation +Note that `position_on_plot` is only implemented for primitive plot types, i.e. +the possible return types of `pick`. Depending on the plot type the calculation differs: - `scatter` and `meshscatter` return the position of the picked marker/mesh - `text` is excluded, always returning `Point3f(NaN)` @@ -233,26 +233,28 @@ differs: """ function position_on_plot(plot::AbstractPlot, idx::Integer; apply_transform = true) return position_on_plot( - plot, idx, ray_at_cursor(parent_scene(plot)); + plot, idx, ray_at_cursor(parent_scene(plot)); apply_transform = apply_transform ) end function position_on_plot(plot::Union{Scatter, MeshScatter}, idx, ray::Ray; apply_transform = true) - pos = to_ndim(Point3f, plot[1][][idx], 0f0) - if apply_transform && !isnan(pos) - return apply_transform_and_model(plot, pos) + point = plot[1][][idx] + point3f = to_ndim(Point3f, point, 0.0f0) + point_t = if apply_transform && !isnan(point3f) + apply_transform_and_model(plot, point3f) else - return pos + point3f end + return to_ndim(typeof(point), point_t, 0.0f0) end function position_on_plot(plot::Union{Lines, LineSegments}, idx, ray::Ray; apply_transform = true) p0, p1 = apply_transform_and_model(plot, plot[1][][idx-1:idx]) pos = closest_point_on_line(p0, p1, ray) - + if apply_transform return pos else @@ -264,8 +266,8 @@ function position_on_plot(plot::Union{Lines, LineSegments}, idx, ray::Ray; apply end function position_on_plot(plot::Union{Heatmap, Image}, idx, ray::Ray; apply_transform = true) - # Heatmap and Image are always a Rect2f. The transform function is currently - # not allowed to change this, so applying it should be fine. Applying the + # Heatmap and Image are always a Rect2f. The transform function is currently + # not allowed to change this, so applying it should be fine. Applying the # model matrix may add a z component to the Rect2f, which we can't represent. # So we instead inverse-transform the ray space = to_value(get(plot, :space, :data)) @@ -274,7 +276,7 @@ function position_on_plot(plot::Union{Heatmap, Image}, idx, ray::Ray; apply_tran end ray = transform(inv(plot.model[]), ray) pos = ray_rect_intersection(Rect2f(p0, p1 - p0), ray) - + if apply_transform p4d = plot.model[] * to_ndim(Point4f, to_ndim(Point3f, pos, 0), 1) return p4d[Vec(1, 2, 3)] / p4d[4] @@ -305,7 +307,7 @@ function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) end end end - + @debug "Did not find intersection for index = $idx when casting a ray on mesh." return Point3f(NaN) @@ -335,7 +337,7 @@ function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) ray = transform(inv(plot.model[]), ray) tf = transform_func(plot) space = to_value(get(plot, :space, :data)) - + # This isn't the most accurate so we include some neighboring faces pos = Point3f(NaN) for i in _i-1:_i+1, j in _j-1:_j+1 @@ -393,4 +395,4 @@ function position_on_plot(plot::Volume, idx, ray::Ray; apply_transform = true) end position_on_plot(plot::Text, args...; kwargs...) = Point3f(NaN) -position_on_plot(plot::Nothing, args...; kwargs...) = Point3f(NaN) \ No newline at end of file +position_on_plot(plot::Nothing, args...; kwargs...) = Point3f(NaN) diff --git a/src/interfaces.jl b/src/interfaces.jl index 2bab5185fd8..32bdfc67ffc 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] @@ -213,10 +221,10 @@ function plot!(::Combined{F}) where {F} end end -function connect_plot!(scene::SceneLike, plot::Combined{F}) where {F} - plot.parent = scene +function connect_plot!(parent::SceneLike, plot::Combined{F}) where {F} + plot.parent = parent - apply_theme!(parent_scene(scene), plot) + apply_theme!(parent_scene(parent), plot) t_user = to_value(get(attributes(plot), :transformation, automatic)) if t_user isa Transformation plot.transformation = t_user @@ -228,11 +236,15 @@ function connect_plot!(scene::SceneLike, plot::Combined{F}) where {F} transform!(t, t_user) plot.transformation = t end - connect!(transformation(scene), transformation(plot)) + if is_space_compatible(plot, parent) + obsfunc = connect!(transformation(parent), transformation(plot)) + append!(plot.deregister_callbacks, obsfunc) + end end plot.model = transformationmatrix(plot) convert_arguments!(plot) calculated_attributes!(Combined{F}, plot) + default_shading!(plot, parent_scene(parent)) plot!(plot) return plot end diff --git a/src/layouting/data_limits.jl b/src/layouting/data_limits.jl index 676368c76fb..431ec09f184 100644 --- a/src/layouting/data_limits.jl +++ b/src/layouting/data_limits.jl @@ -166,6 +166,7 @@ function update_boundingbox!(bb_ref, bb::Rect) return end +# Default data_limits function data_limits(plot::AbstractPlot) # Assume primitive plot if isempty(plot.plots) @@ -181,7 +182,7 @@ function data_limits(plot::AbstractPlot) return bb_ref[] end -function _update_rect(rect::Rect{N, T}, point::Point{N, T}) where {N, T} +function _update_rect(rect::Rect{N, T}, point::VecTypes{N, T}) where {N, T} mi = minimum(rect) ma = maximum(rect) mis_mas = map(mi, ma, point) do _mi, _ma, _p @@ -201,6 +202,22 @@ function limits_from_transformed_points(points_iterator) return bb end +# include bbox from scaled markers +function limits_from_transformed_points(positions, scales, rotations, element_bbox) + isempty(positions) && return Rect3f() + + first_scale = attr_broadcast_getindex(scales, 1) + first_rot = attr_broadcast_getindex(rotations, 1) + full_bbox = Ref(first_rot * (element_bbox * first_scale) + first(positions)) + for (i, pos) in enumerate(positions) + scale, rot = attr_broadcast_getindex(scales, i), attr_broadcast_getindex(rotations, i) + transformed_bbox = rot * (element_bbox * scale) + pos + update_boundingbox!(full_bbox, transformed_bbox) + end + + return full_bbox[] +end + function data_limits(scenelike, exclude=(p)-> false) bb_ref = Base.RefValue(Rect3f()) foreach_plot(scenelike) do plot @@ -232,3 +249,20 @@ function data_limits(plot::Image) maxi = Vec3f(last.(mini_maxi)..., 0) return Rect3f(mini, maxi .- mini) end + +function data_limits(plot::MeshScatter) + # TODO: avoid mesh generation here if possible + @get_attribute plot (marker, markersize, rotations) + marker_bb = Rect3f(marker) + positions = iterate_transformed(plot) + scales = markersize + # fast path for constant markersize + if scales isa VecTypes{3} && rotations isa Quaternion + bb = limits_from_transformed_points(positions) + marker_bb = rotations * (marker_bb * scales) + return Rect3f(minimum(bb) + minimum(marker_bb), widths(bb) + widths(marker_bb)) + else + # TODO: optimize const scale, var rot and var scale, const rot + return limits_from_transformed_points(positions, scales, rotations, marker_bb) + end +end 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/lighting.jl b/src/lighting.jl new file mode 100644 index 00000000000..c4a0a814dd6 --- /dev/null +++ b/src/lighting.jl @@ -0,0 +1,278 @@ +abstract type AbstractLight end + +# GLMakie interface + +# These need to match up with light shaders to differentiate light types +module LightType + const UNDEFINED = 0 + const Ambient = 1 + const PointLight = 2 + const DirectionalLight = 3 + const SpotLight = 4 + const RectLight = 5 +end + +# Each light should implement +light_type(::AbstractLight) = LightType.UNDEFINED +light_color(::AbstractLight) = RGBf(0, 0, 0) +# Other attributes need to be handled explicitly in backends + + +""" + AmbientLight(color) <: AbstractLight + +A simple ambient light that uniformly lights every object based on its light color. + +Availability: +- All backends with `shading = FastShading` or `MultiLightShading` +""" +struct AmbientLight <: AbstractLight + color::Observable{RGBf} +end + +light_type(::AmbientLight) = LightType.Ambient +light_color(l::AmbientLight) = l.color[] + + +""" + PointLight(color, position[, attenuation = Vec2f(0)]) + PointLight(color, position, range::Real) + +A point-like light source placed at the given `position` with the given light +`color`. + +Optionally an attenuation parameter can be used to reduce the brightness of the +light source with distance. The reduction is given by +`1 / (1 + attenuation[1] * distance + attenuation[2] * distance^2)`. +Alternatively you can pass a light `range` to generate matching default +attenuation parameters. Note that you may need to set the light intensity, i.e. +the light color to values greater than 1 to get satisfying results. + +Availability: +- GLMakie with `shading = MultiLightShading` +- RPRMakie +""" +struct PointLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Vec3f} + attenuation::Observable{Vec2f} +end + +# no attenuation +function PointLight(color::Union{Colorant, Observable{<: Colorant}}, position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}) + return PointLight(color, position, Vec2f(0)) +end +# automatic attenuation +function PointLight(color::Union{Colorant, Observable{<: Colorant}}, position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}, range::Real) + return PointLight(color, position, default_attenuation(range)) +end + +@deprecate PointLight(position::Union{VecTypes{3}, Observable{<: VecTypes{3}}}, color::Union{Colorant, Observable{<: Colorant}}) PointLight(color, position) + +light_type(::PointLight) = LightType.PointLight +light_color(l::PointLight) = l.color[] + +# fit of values used on learnopengl/ogre3d +function default_attenuation(range::Real) + return Vec2f( + 4.690507869767646 * range ^ -1.009712247799057, + 82.4447791934059 * range ^ -2.0192061630628966 + ) +end + + +""" + DirectionalLight(color, direction[, camera_relative = false]) + +A light type which simulates a distant light source with parallel light rays +going in the given `direction`. + +Availability: +- All backends with `shading = FastShading` or `MultiLightShading` +""" +struct DirectionalLight <: AbstractLight + color::Observable{RGBf} + direction::Observable{Vec3f} + + # Usually a light source is placed in world space, i.e. unrelated to the + # camera. As a default however, we want to make sure that an object is + # always reasonably lit, which requires the light source to move with the + # camera. To keep this in sync in WGLMakie, the calculation needs to happen + # in javascript. This flag notives WGLMakie and other backends that this + # calculation needs to happen. + camera_relative::Bool + + DirectionalLight(col, dir, rel = false) = new(col, dir, rel) +end +light_type(::DirectionalLight) = LightType.DirectionalLight +light_color(l::DirectionalLight) = l.color[] + + +""" + SpotLight(color, position, direction, angles) + +Creates a spot light which illuminates objects in a light cone starting at +`position` pointing in `direction`. The opening angle is defined by an inner +and outer angle given in `angles`, between which the light intensity drops off. + +Availability: +- GLMakie with `shading = MultiLightShading` +- RPRMakie +""" +struct SpotLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Vec3f} + direction::Observable{Vec3f} + angles::Observable{Vec2f} +end + +light_type(::SpotLight) = LightType.SpotLight +light_color(l::SpotLight) = l.color[] + + +""" + EnvironmentLight(intensity, image) + +An environment light that uses a spherical environment map to provide lighting. +See: https://en.wikipedia.org/wiki/Reflection_mapping + +Availability: +- RPRMakie +""" +struct EnvironmentLight <: AbstractLight + intensity::Observable{Float32} + image::Observable{Matrix{RGBf}} +end + +""" + RectLight(color, r::Rect2[, direction = -normal]) + RectLight(color, center::Point3f, b1::Vec3f, b2::Vec3f[, direction = -normal]) + +Creates a RectLight with a given color. The first constructor derives the light +from a `Rect2` extending in x and y direction. The second specifies the `center` +of the rect (or more accurately parallelogram) with `b1` and `b2` specifying the +width and height vectors (including scale). + +Note that RectLight implements `translate!`, `rotate!` and `scale!` to simplify +adjusting the light. + +Availability: +- GLMakie with `Shading = MultiLightShading` +""" +struct RectLight <: AbstractLight + color::Observable{RGBf} + position::Observable{Point3f} + u1::Observable{Vec3f} + u2::Observable{Vec3f} + direction::Observable{Vec3f} +end + +RectLight(color, pos, u1, u2) = RectLight(color, pos, u1, u2, -normalize(cross(u1, u2))) +function RectLight(color, r::Rect2) + mini = minimum(r); ws = widths(r) + position = Observable(to_ndim(Point3f, mini + 0.5 * ws, 0)) + u1 = Observable(Vec3f(ws[1], 0, 0)) + u2 = Observable(Vec3f(0, ws[2], 0)) + return RectLight(color, position, u1, u2, normalize(Vec3f(0,0,-1))) +end + +# Implement Transformable interface (more or less) to simplify working with +# RectLights + +function translate!(::Type{T}, l::RectLight, v) where T + offset = to_ndim(Vec3f, Float32.(v), 0) + if T === Accum + l.position[] = l.position[] + offset + elseif T === Absolute + l.position[] = offset + else + error("Unknown translation type: $T") + end +end +translate!(l::RectLight, v) = translate!(Absolute, l, v) + +function rotate!(l::RectLight, q...) + rot = convert_attribute(q, key"rotation"()) + l.u1[] = rot * l.u1[] + l.u2[] = rot * l.u2[] + l.direction[] = rot * l.direction[] +end + +function scale!(::Type{T}, l::RectLight, s) where T + scale = to_ndim(Vec2f, Float32.(s), 0) + if T === Accum + l.u1[] = scale[1] * l.u1[] + l.u2[] = scale[2] * l.u2[] + elseif T === Absolute + l.u1[] = scale[1] * normalize(l.u1[]) + l.u2[] = scale[2] * normalize(l.u2[]) + else + error("Unknown translation type: $T") + end +end +scale!(l::RectLight, x::Real, y::Real) = scale!(Accum, l, Vec2f(x, y)) +scale!(l::RectLight, xy::VecTypes) = scale!(Accum, l, xy) + + +light_type(::RectLight) = LightType.RectLight +light_color(l::RectLight) = l.color[] + + +################################################################################ + + +function get_one_light(lights, Typ) + indices = findall(x-> x isa Typ, lights) + isempty(indices) && return nothing + return lights[indices[1]] +end + +function default_shading!(plot, lights::Vector{<: AbstractLight}) + # if the plot does not have :shading we assume the plot doesn't support it + haskey(plot.attributes, :shading) || return + + # Bad type + shading = to_value(plot.attributes[:shading]) + if !(shading isa MakieCore.ShadingAlgorithm || shading === automatic) + prev = shading + if (shading isa Bool) && (shading == false) + shading = NoShading + else + shading = automatic + end + @warn "`shading = $prev` is not valid. Use `automatic`, `NoShading`, `FastShading` or `MultiLightShading`. Defaulting to `$shading`." + end + + # automatic conversion + if shading === automatic + ambient_count = 0 + dir_light_count = 0 + + for light in lights + if light isa AmbientLight + ambient_count += 1 + elseif light isa DirectionalLight + dir_light_count += 1 + elseif light isa EnvironmentLight + continue + else + plot.attributes[:shading] = MultiLightShading + return + end + if ambient_count > 1 || dir_light_count > 1 + plot.attributes[:shading] = MultiLightShading + return + end + end + + if dir_light_count + ambient_count == 0 + shading = NoShading + else + shading = FastShading + end + end + + plot.attributes[:shading] = shading + + return +end \ No newline at end of file diff --git a/src/makielayout/MakieLayout.jl b/src/makielayout/MakieLayout.jl index bd5ae2c8692..ff3096c1022 100644 --- a/src/makielayout/MakieLayout.jl +++ b/src/makielayout/MakieLayout.jl @@ -1,5 +1,4 @@ import Formatting -using Match import Animations using GridLayoutBase using GridLayoutBase: GridSubposition @@ -93,5 +92,3 @@ export grid!, hgrid!, vgrid! export swap! export ncols, nrows export contents, content - -Base.@deprecate_binding MakieLayout Makie true "The module `MakieLayout` has been removed and integrated into Makie, so simply replace all usage of `MakieLayout` with `Makie`." diff --git a/src/makielayout/blocks.jl b/src/makielayout/blocks.jl index 18cfc4dde6b..4ccb223edc5 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) @@ -469,7 +475,7 @@ free(::Block) = nothing function Base.delete!(block::Block) free(block) block.parent === nothing && return - # detach plots, cameras, transformations, px_area + # detach plots, cameras, transformations, viewport empty!(block.blockscene) gc = GridLayoutBase.gridcontent(block) @@ -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 0655f236861..0fe287611a0 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` @@ -189,12 +182,18 @@ function initialize_block!(ax::Axis; palette = nothing) scenearea = sceneareanode!(ax.layoutobservables.computedbbox, finallimits, ax.aspect) - scene = Scene(blockscene, px_area=scenearea) + scene = Scene(blockscene, viewport=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) + background = poly!(blockscene, scenearea; color=ax.backgroundcolor, inspectable=false, shading=NoShading, strokecolor=:transparent) translate!(background, 0, 0, -100) elements[:background] = background @@ -258,7 +257,7 @@ function initialize_block!(ax::Axis; palette = nothing) # 3. Update the view onto the plot (camera matrices) onany(update_axis_camera, camera(scene), scene.transformation.transform_func, finallimits, ax.xreversed, ax.yreversed, priority = -2) - xaxis_endpoints = lift(blockscene, ax.xaxisposition, scene.px_area; + xaxis_endpoints = lift(blockscene, ax.xaxisposition, scene.viewport; ignore_equal_values=true) do xaxisposition, area if xaxisposition === :bottom return bottomline(Rect2f(area)) @@ -269,7 +268,7 @@ function initialize_block!(ax::Axis; palette = nothing) end end - yaxis_endpoints = lift(blockscene, ax.yaxisposition, scene.px_area; + yaxis_endpoints = lift(blockscene, ax.yaxisposition, scene.viewport; ignore_equal_values=true) do yaxisposition, area if yaxisposition === :left return leftline(Rect2f(area)) @@ -346,7 +345,7 @@ function initialize_block!(ax::Axis; palette = nothing) ax.yaxis = yaxis - xoppositelinepoints = lift(blockscene, scene.px_area, ax.spinewidth, ax.xaxisposition; + xoppositelinepoints = lift(blockscene, scene.viewport, ax.spinewidth, ax.xaxisposition; ignore_equal_values=true) do r, sw, xaxpos if xaxpos === :top y = bottom(r) @@ -361,7 +360,7 @@ function initialize_block!(ax::Axis; palette = nothing) end end - yoppositelinepoints = lift(blockscene, scene.px_area, ax.spinewidth, ax.yaxisposition; + yoppositelinepoints = lift(blockscene, scene.viewport, ax.spinewidth, ax.yaxisposition; ignore_equal_values=true) do r, sw, yaxpos if yaxpos === :right x = left(r) @@ -377,22 +376,22 @@ function initialize_block!(ax::Axis; palette = nothing) end xticksmirrored = lift(mirror_ticks, blockscene, xaxis.tickpositions, ax.xticksize, ax.xtickalign, - Ref(scene.px_area), :x, ax.xaxisposition[]) + Ref(scene.viewport), :x, ax.xaxisposition[]) xticksmirrored_lines = linesegments!(blockscene, xticksmirrored, visible = @lift($(ax.xticksmirrored) && $(ax.xticksvisible)), linewidth = ax.xtickwidth, color = ax.xtickcolor) translate!(xticksmirrored_lines, 0, 0, 10) yticksmirrored = lift(mirror_ticks, blockscene, yaxis.tickpositions, ax.yticksize, ax.ytickalign, - Ref(scene.px_area), :y, ax.yaxisposition[]) + Ref(scene.viewport), :y, ax.yaxisposition[]) yticksmirrored_lines = linesegments!(blockscene, yticksmirrored, visible = @lift($(ax.yticksmirrored) && $(ax.yticksvisible)), linewidth = ax.ytickwidth, color = ax.ytickcolor) translate!(yticksmirrored_lines, 0, 0, 10) xminorticksmirrored = lift(mirror_ticks, blockscene, xaxis.minortickpositions, ax.xminorticksize, - ax.xminortickalign, Ref(scene.px_area), :x, ax.xaxisposition[]) + ax.xminortickalign, Ref(scene.viewport), :x, ax.xaxisposition[]) xminorticksmirrored_lines = linesegments!(blockscene, xminorticksmirrored, visible = @lift($(ax.xticksmirrored) && $(ax.xminorticksvisible)), linewidth = ax.xminortickwidth, color = ax.xminortickcolor) translate!(xminorticksmirrored_lines, 0, 0, 10) yminorticksmirrored = lift(mirror_ticks, blockscene, yaxis.minortickpositions, ax.yminorticksize, - ax.yminortickalign, Ref(scene.px_area), :y, ax.yaxisposition[]) + ax.yminortickalign, Ref(scene.viewport), :y, ax.yaxisposition[]) yminorticksmirrored_lines = linesegments!(blockscene, yminorticksmirrored, visible = @lift($(ax.yticksmirrored) && $(ax.yminorticksvisible)), linewidth = ax.yminortickwidth, color = ax.yminortickcolor) translate!(yminorticksmirrored_lines, 0, 0, 10) @@ -409,31 +408,31 @@ function initialize_block!(ax::Axis; palette = nothing) elements[:yoppositeline] = yoppositeline translate!(yoppositeline, 0, 0, 20) - onany(blockscene, xaxis.tickpositions, scene.px_area) do tickpos, area + onany(blockscene, xaxis.tickpositions, scene.viewport) do tickpos, area local pxheight::Float32 = height(area) local offset::Float32 = ax.xaxisposition[] === :bottom ? pxheight : -pxheight update_gridlines!(xgridnode, Point2f(0, offset), tickpos) end - onany(blockscene, yaxis.tickpositions, scene.px_area) do tickpos, area + onany(blockscene, yaxis.tickpositions, scene.viewport) do tickpos, area local pxwidth::Float32 = width(area) local offset::Float32 = ax.yaxisposition[] === :left ? pxwidth : -pxwidth update_gridlines!(ygridnode, Point2f(offset, 0), tickpos) end - onany(blockscene, xaxis.minortickpositions, scene.px_area) do tickpos, area - local pxheight::Float32 = height(scene.px_area[]) + onany(blockscene, xaxis.minortickpositions, scene.viewport) do tickpos, area + local pxheight::Float32 = height(scene.viewport[]) local offset::Float32 = ax.xaxisposition[] === :bottom ? pxheight : -pxheight update_gridlines!(xminorgridnode, Point2f(0, offset), tickpos) end - onany(blockscene, yaxis.minortickpositions, scene.px_area) do tickpos, area - local pxwidth::Float32 = width(scene.px_area[]) + onany(blockscene, yaxis.minortickpositions, scene.viewport) do tickpos, area + local pxwidth::Float32 = width(scene.viewport[]) local offset::Float32 = ax.yaxisposition[] === :left ? pxwidth : -pxwidth update_gridlines!(yminorgridnode, Point2f(offset, 0), tickpos) end - subtitlepos = lift(blockscene, scene.px_area, ax.titlegap, ax.titlealign, ax.xaxisposition, + subtitlepos = lift(blockscene, scene.viewport, ax.titlegap, ax.titlealign, ax.xaxisposition, xaxis.protrusion; ignore_equal_values=true) do a, titlegap, align, xaxisposition, xaxisprotrusion @@ -462,7 +461,7 @@ function initialize_block!(ax::Axis; palette = nothing) markerspace = :data, inspectable = false) - titlepos = lift(calculate_title_position, blockscene, scene.px_area, ax.titlegap, ax.subtitlegap, + titlepos = lift(calculate_title_position, blockscene, scene.viewport, ax.titlegap, ax.subtitlegap, ax.titlealign, ax.xaxisposition, xaxis.protrusion, ax.subtitlelineheight, ax, subtitlet; ignore_equal_values=true) titlet = text!( @@ -505,7 +504,7 @@ function initialize_block!(ax::Axis; palette = nothing) # compute limits that adhere to the limit aspect ratio whenever the targeted # limits or the scene size change, because both influence the displayed ratio - onany(blockscene, scene.px_area, targetlimits) do pxa, lims + onany(blockscene, scene.viewport, targetlimits) do pxa, lims adjustlimits!(ax) end @@ -521,8 +520,8 @@ function initialize_block!(ax::Axis; palette = nothing) return ax end -function mirror_ticks(tickpositions, ticksize, tickalign, px_area, side, axisposition) - a = px_area[][] +function mirror_ticks(tickpositions, ticksize, tickalign, viewport, side, axisposition) + a = viewport[][] if side === :x opp = axisposition === :bottom ? top(a) : bottom(a) sign = axisposition === :bottom ? 1 : -1 @@ -689,17 +688,11 @@ function get_cycle_for_plottype(cycle_raw)::Cycle end end - -function to_color(cycle, attribute_name, cycler, palette) - if cycle.covary - palettes[current_symbol][mod1(index, length(palettes[isym]))] - else - cis = CartesianIndices(Tuple(length(p) for p in palettes)) - n = length(cis) - k = mod1(index, n) - idx = Tuple(cis[k]) - palettes[isym][idx[isym]] - end +function to_color(scene::Scene, attribute_name, cycled::Cycled) + palettes = to_value(scene.theme.palette) + attr_palette = to_value(palettes[attribute_name]) + index = cycled.i + return attr_palette[mod1(index, length(attr_palette))] end function add_cycle_attributes!(@nospecialize(plot), cycle::Cycle, cycler::Cycler, palette::Attributes) @@ -962,7 +955,7 @@ end function adjustlimits!(la) asp = la.autolimitaspect[] target = la.targetlimits[] - area = la.scene.px_area[] + area = la.scene.viewport[] # in the simplest case, just update the final limits with the target limits if isnothing(asp) || width(area) == 0 || height(area) == 0 @@ -1150,12 +1143,16 @@ with the symbols :l, :r, :b and :t. """ function hidespines!(la::Axis, spines::Symbol... = (:l, :r, :b, :t)...) for s in spines - @match s begin - :l => (la.leftspinevisible = false) - :r => (la.rightspinevisible = false) - :b => (la.bottomspinevisible = false) - :t => (la.topspinevisible = false) - x => error("Invalid spine identifier $x. Valid options are :l, :r, :b and :t.") + if s === :l + la.leftspinevisible = false + elseif s === :r + la.rightspinevisible = false + elseif s === :b + la.bottomspinevisible = false + elseif s === :t + la.topspinevisible = false + else + error("Invalid spine identifier $s. Valid options are :l, :r, :b and :t.") end end end @@ -1181,6 +1178,8 @@ function tight_xticklabel_spacing!(ax::Axis) end """ + tight_ticklabel_spacing(ax::Axis) + Sets the space allocated for the xticklabels and yticklabels of the `Axis` to the minimum that is needed. """ function tight_ticklabel_spacing!(ax::Axis) @@ -1206,7 +1205,7 @@ end function Makie.xlims!(ax::Axis, xlims) if length(xlims) != 2 error("Invalid xlims length of $(length(xlims)), must be 2.") - elseif xlims[1] == xlims[2] + elseif xlims[1] == xlims[2] && xlims[1] !== nothing error("Can't set x limits to the same value $(xlims[1]).") elseif all(x -> x isa Real, xlims) && xlims[1] > xlims[2] xlims = reverse(xlims) @@ -1214,8 +1213,9 @@ function Makie.xlims!(ax::Axis, xlims) else ax.xreversed[] = false end + mlims = convert_limit_attribute(ax.limits[]) - ax.limits.val = (xlims, ax.limits[][2]) + ax.limits.val = (xlims, mlims[2]) reset_limits!(ax, yauto = false) nothing end @@ -1223,7 +1223,7 @@ end function Makie.ylims!(ax::Axis, ylims) if length(ylims) != 2 error("Invalid ylims length of $(length(ylims)), must be 2.") - elseif ylims[1] == ylims[2] + elseif ylims[1] == ylims[2] && ylims[1] !== nothing error("Can't set y limits to the same value $(ylims[1]).") elseif all(x -> x isa Real, ylims) && ylims[1] > ylims[2] ylims = reverse(ylims) @@ -1231,22 +1231,95 @@ function Makie.ylims!(ax::Axis, ylims) else ax.yreversed[] = false end + mlims = convert_limit_attribute(ax.limits[]) - ax.limits.val = (ax.limits[][1], ylims) + ax.limits.val = (mlims[1], ylims) reset_limits!(ax, xauto = false) nothing end +""" + xlims!(ax, low, high) + xlims!(ax; low = nothing, high = nothing) + xlims!(ax, xlims) + +Set the x-axis limits of axis `ax` to `low` and `high` or a tuple +`xlims = (low,high)`. If the limits are ordered high-low, the axis orientation +will be reversed. If a limit is `nothing` it will be determined automatically +from the plots in the axis. +""" Makie.xlims!(ax, low, high) = Makie.xlims!(ax, (low, high)) +""" + ylims!(ax, low, high) + ylims!(ax; low = nothing, high = nothing) + ylims!(ax, ylims) + +Set the y-axis limits of axis `ax` to `low` and `high` or a tuple +`ylims = (low,high)`. If the limits are ordered high-low, the axis orientation +will be reversed. If a limit is `nothing` it will be determined automatically +from the plots in the axis. +""" Makie.ylims!(ax, low, high) = Makie.ylims!(ax, (low, high)) +""" + zlims!(ax, low, high) + zlims!(ax; low = nothing, high = nothing) + zlims!(ax, zlims) + +Set the z-axis limits of axis `ax` to `low` and `high` or a tuple +`zlims = (low,high)`. If the limits are ordered high-low, the axis orientation +will be reversed. If a limit is `nothing` it will be determined automatically +from the plots in the axis. +""" Makie.zlims!(ax, low, high) = Makie.zlims!(ax, (low, high)) +""" + xlims!(low, high) + xlims!(; low = nothing, high = nothing) + +Set the x-axis limits of the current axis to `low` and `high`. If the limits +are ordered high-low, this reverses the axis orientation. A limit set to +`nothing` will be determined automatically from the plots in the axis. +""" Makie.xlims!(low::Optional{<:Real}, high::Optional{<:Real}) = Makie.xlims!(current_axis(), low, high) +""" + ylims!(low, high) + ylims!(; low = nothing, high = nothing) + +Set the y-axis limits of the current axis to `low` and `high`. If the limits +are ordered high-low, this reverses the axis orientation. A limit set to +`nothing` will be determined automatically from the plots in the axis. +""" Makie.ylims!(low::Optional{<:Real}, high::Optional{<:Real}) = Makie.ylims!(current_axis(), low, high) +""" + zlims!(low, high) + zlims!(; low = nothing, high = nothing) + +Set the z-axis limits of the current axis to `low` and `high`. If the limits +are ordered high-low, this reverses the axis orientation. A limit set to +`nothing` will be determined automatically from the plots in the axis. +""" Makie.zlims!(low::Optional{<:Real}, high::Optional{<:Real}) = Makie.zlims!(current_axis(), low, high) +""" + xlims!(ax = current_axis()) + +Reset the x-axis limits to be determined automatically from the plots in the +axis. +""" Makie.xlims!(ax = current_axis(); low = nothing, high = nothing) = Makie.xlims!(ax, low, high) +""" + ylims!(ax = current_axis()) + +Reset the y-axis limits to be determined automatically from the plots in the +axis. +""" Makie.ylims!(ax = current_axis(); low = nothing, high = nothing) = Makie.ylims!(ax, low, high) +""" + zlims!(ax = current_axis()) + +Reset the z-axis limits to be determined automatically from the plots in the +axis. +""" Makie.zlims!(ax = current_axis(); low = nothing, high = nothing) = Makie.zlims!(ax, low, high) """ @@ -1772,7 +1845,7 @@ function colorbuffer(ax::Axis; include_decorations=true, update=true, colorbuffe bb = axis_bounds_with_decoration(ax) Rect2{Int}(round.(Int, minimum(bb)) .+ 1, round.(Int, widths(bb))) else - pixelarea(ax.scene)[] + viewport(ax.scene)[] end img = colorbuffer(root(ax.scene); update=false, colorbuffer_kws...) diff --git a/src/makielayout/blocks/axis3d.jl b/src/makielayout/blocks/axis3d.jl index 1449ffb4bbb..8361dbc46f9 100644 --- a/src/makielayout/blocks/axis3d.jl +++ b/src/makielayout/blocks/axis3d.jl @@ -38,12 +38,13 @@ function initialize_block!(ax::Axis3) return end - matrices = lift(calculate_matrices, scene, finallimits, scene.px_area, ax.elevation, ax.azimuth, + matrices = lift(calculate_matrices, scene, finallimits, scene.viewport, ax.elevation, ax.azimuth, ax.perspectiveness, ax.aspect, ax.viewmode, ax.xreversed, ax.yreversed, ax.zreversed) - on(scene, matrices) do (view, proj, eyepos) + on(scene, matrices) do (model, view, proj, eyepos) cam = camera(scene) Makie.set_proj_view!(cam, proj, view) + scene.transformation.model[] = model cam.eyeposition[] = eyepos end @@ -80,7 +81,7 @@ function initialize_block!(ax::Axis3) zticks, zticklabels, zlabel = add_ticks_and_ticklabels!(blockscene, scene, ax, 3, finallimits, ticknode_3, mi3, mi1, mi2, ax.azimuth, ax.xreversed, ax.yreversed, ax.zreversed) - titlepos = lift(scene, scene.px_area, ax.titlegap, ax.titlealign) do a, titlegap, align + titlepos = lift(scene, scene.viewport, ax.titlegap, ax.titlealign) do a, titlegap, align align_factor = halign2num(align, "Horizontal title align $align not supported.") x = a.origin[1] + align_factor * a.widths[1] @@ -105,9 +106,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) @@ -164,7 +162,7 @@ function initialize_block!(ax::Axis3) return end -function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect, +function calculate_matrices(limits, viewport, elev, azim, perspectiveness, aspect, viewmode, xreversed, yreversed, zreversed) ori = limits.origin @@ -197,7 +195,7 @@ function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect end |> Makie.scalematrix t2 = Makie.translationmatrix(-0.5 .* ws .* scales) - scale_matrix = t2 * s * t + model = t2 * s * t ang_max = 90 ang_min = 0.5 @@ -218,23 +216,16 @@ function calculate_matrices(limits, px_area, elev, azim, perspectiveness, aspect eyepos = Vec3{Float64}(x, y, z) - lookat_matrix = Makie.lookat( - eyepos, - Vec3{Float64}(0, 0, 0), - Vec3{Float64}(0, 0, 1)) - - w = width(px_area) - h = height(px_area) - - view_matrix = lookat_matrix * scale_matrix + lookat_matrix = lookat(eyepos, Vec3{Float64}(0), Vec3{Float64}(0, 0, 1)) - projection_matrix = projectionmatrix(view_matrix, limits, eyepos, radius, azim, elev, angle, w, h, scales, viewmode) + w = width(viewport) + h = height(viewport) - # for eyeposition dependent algorithms, we need to present the position as if - # there was no scaling applied - eyeposition = Vec3f(inv(scale_matrix) * Vec4f(eyepos..., 1)) + projection_matrix = projectionmatrix( + lookat_matrix * model, limits, eyepos, radius, azim, elev, angle, + w, h, scales, viewmode) - view_matrix, projection_matrix, eyeposition + return model, lookat_matrix, projection_matrix, eyepos end function projectionmatrix(viewmatrix, limits, eyepos, radius, azim, elev, angle, width, height, scales, viewmode) @@ -406,7 +397,7 @@ function add_gridlines_and_frames!(topscene, scene, ax, dim::Int, limits, tickno visible = attr(:gridvisible), inspectable = false) - framepoints = lift(limits, scene.camera.projectionview, scene.px_area, min1, min2, xreversed, yreversed, zreversed + framepoints = lift(limits, scene.camera.projectionview, scene.viewport, min1, min2, xreversed, yreversed, zreversed ) do lims, _, pxa, mi1, mi2, xrev, yrev, zrev o = pxa.origin @@ -443,7 +434,7 @@ end # this function projects a point from a 3d subscene into the parent space with a really # small z value function to_topscene_z_2d(p3d, scene) - o = scene.px_area[].origin + o = scene.viewport[].origin p2d = Point2f(o + Makie.project(scene, p3d)) # -10000 is an arbitrary weird constant that in preliminary testing didn't seem # to clip into plot objects anymore @@ -467,7 +458,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno ticksize = attr(:ticksize) tick_segments = lift(topscene, limits, tickvalues, miv, min1, min2, - scene.camera.projectionview, scene.px_area, ticksize, xreversed, yreversed, zreversed) do lims, ticks, miv, min1, min2, + scene.camera.projectionview, scene.viewport, ticksize, xreversed, yreversed, zreversed) do lims, ticks, miv, min1, min2, pview, pxa, tsize, xrev, yrev, zrev rev1 = (xrev, yrev, zrev)[d1] @@ -507,7 +498,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno # we are going to transform the 3d tick segments into 2d of the topscene # because otherwise they # be cut when they extend beyond the scene boundary - tick_segments_2dz = lift(topscene, tick_segments, scene.camera.projectionview, scene.px_area) do ts, _, _ + tick_segments_2dz = lift(topscene, tick_segments, scene.camera.projectionview, scene.viewport) do ts, _, _ map(ts) do p1_p2 to_topscene_z_2d.(p1_p2, Ref(scene)) end @@ -522,7 +513,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno translate!(ticks, 0, 0, -10000) labels_positions = Observable{Any}() - map!(topscene, labels_positions, scene.px_area, scene.camera.projectionview, + map!(topscene, labels_positions, scene.viewport, scene.camera.projectionview, tick_segments, ticklabels, attr(:ticklabelpad)) do pxa, pv, ticksegs, ticklabs, pad o = pxa.origin @@ -558,7 +549,7 @@ function add_ticks_and_ticklabels!(topscene, scene, ax, dim::Int, limits, tickno label_align = Observable((:center, :top)) onany(topscene, - scene.px_area, scene.camera.projectionview, limits, miv, min1, min2, + scene.viewport, scene.camera.projectionview, limits, miv, min1, min2, attr(:labeloffset), attr(:labelrotation), attr(:labelalign), xreversed, yreversed, zreversed ) do pxa, pv, lims, miv, min1, min2, labeloffset, lrotation, lalign, xrev, yrev, zrev @@ -686,7 +677,7 @@ function add_panel!(scene, ax, dim1, dim2, dim3, limits, min3) faces = [1 2 3; 3 4 1] - panel = mesh!(scene, vertices, faces, shading = false, inspectable = false, + panel = mesh!(scene, vertices, faces, shading = NoShading, inspectable = false, xautolimits = false, yautolimits = false, zautolimits = false, color = attr(:panelcolor), visible = attr(:panelvisible)) return panel @@ -742,6 +733,12 @@ function hideydecorations!(ax::Axis3; ax end +""" + hidezdecorations!(la::Axis; label = true, ticklabels = true, ticks = true, grid = true, + minorgrid = true, minorticks = true) + +Hide decorations of the z-axis: label, ticklabels, ticks and grid. +""" function hidezdecorations!(ax::Axis3; label = true, ticklabels = true, ticks = true, grid = true) @@ -844,7 +841,7 @@ end function Makie.xlims!(ax::Axis3, xlims::Tuple{Union{Real, Nothing}, Union{Real, Nothing}}) if length(xlims) != 2 error("Invalid xlims length of $(length(xlims)), must be 2.") - elseif xlims[1] == xlims[2] + elseif xlims[1] == xlims[2] && xlims[1] !== nothing error("Can't set x limits to the same value $(xlims[1]).") elseif all(x -> x isa Real, xlims) && xlims[1] > xlims[2] xlims = reverse(xlims) @@ -852,8 +849,9 @@ function Makie.xlims!(ax::Axis3, xlims::Tuple{Union{Real, Nothing}, Union{Real, else ax.xreversed[] = false end + mlims = convert_limit_attribute(ax.limits[]) - ax.limits.val = (xlims, ax.limits[][2], ax.limits[][3]) + ax.limits.val = (xlims, mlims[2], mlims[3]) reset_limits!(ax, yauto = false, zauto = false) nothing end @@ -861,7 +859,7 @@ end function Makie.ylims!(ax::Axis3, ylims::Tuple{Union{Real, Nothing}, Union{Real, Nothing}}) if length(ylims) != 2 error("Invalid ylims length of $(length(ylims)), must be 2.") - elseif ylims[1] == ylims[2] + elseif ylims[1] == ylims[2] && ylims[1] !== nothing error("Can't set y limits to the same value $(ylims[1]).") elseif all(x -> x isa Real, ylims) && ylims[1] > ylims[2] ylims = reverse(ylims) @@ -869,8 +867,9 @@ function Makie.ylims!(ax::Axis3, ylims::Tuple{Union{Real, Nothing}, Union{Real, else ax.yreversed[] = false end + mlims = convert_limit_attribute(ax.limits[]) - ax.limits.val = (ax.limits[][1], ylims, ax.limits[][3]) + ax.limits.val = (mlims[1], ylims, mlims[3]) reset_limits!(ax, xauto = false, zauto = false) nothing end @@ -878,25 +877,26 @@ end function Makie.zlims!(ax::Axis3, zlims) if length(zlims) != 2 error("Invalid zlims length of $(length(zlims)), must be 2.") - elseif zlims[1] == zlims[2] - error("Can't set y limits to the same value $(zlims[1]).") + elseif zlims[1] == zlims[2] && zlims[1] !== nothing + error("Can't set z limits to the same value $(zlims[1]).") elseif all(x -> x isa Real, zlims) && zlims[1] > zlims[2] zlims = reverse(zlims) ax.zreversed[] = true else ax.zreversed[] = false end + mlims = convert_limit_attribute(ax.limits[]) - ax.limits.val = (ax.limits[][1], ax.limits[][2], zlims) + ax.limits.val = (mlims[1], mlims[2], zlims) reset_limits!(ax, xauto = false, yauto = false) nothing end """ - limits!(ax::Axis3, xlims, ylims) + limits!(ax::Axis3, xlims, ylims, zlims) -Set the axis limits to `xlims` and `ylims`. +Set the axis limits to `xlims`, `ylims`, and `zlims`. If limits are ordered high-low, this reverses the axis orientation. """ function limits!(ax::Axis3, xlims, ylims, zlims) @@ -908,7 +908,8 @@ end """ limits!(ax::Axis3, x1, x2, y1, y2, z1, z2) -Set the axis x-limits to `x1` and `x2` and the y-limits to `y1` and `y2`. +Set the axis x-limits to `x1` and `x2`, the y-limits to `y1` and `y2`, and the +z-limits to `z1` and `z2`. If limits are ordered high-low, this reverses the axis orientation. """ function limits!(ax::Axis3, x1, x2, y1, y2, z1, z2) diff --git a/src/makielayout/blocks/box.jl b/src/makielayout/blocks/box.jl index 11f9a115231..77dd49be62f 100644 --- a/src/makielayout/blocks/box.jl +++ b/src/makielayout/blocks/box.jl @@ -5,14 +5,108 @@ function initialize_block!(box::Box) vis ? col : RGBAf(0, 0, 0, 0) end - ibbox = lift(round_to_IRect2D, blockscene, box.layoutobservables.computedbbox) + path = lift(blockscene, box.layoutobservables.computedbbox, box.cornerradius) do bbox, r + if r == 0 + BezierPath([ + MoveTo(topright(bbox)), + LineTo(topleft(bbox)), + LineTo(bottomleft(bbox)), + LineTo(bottomright(bbox)), + ClosePath() + ]) + else + w, h = widths(bbox) + _max = min(w/2, h/2) + r1, r2, r3, r4 = r isa NTuple{4, Real} ? r : r isa Real ? (r, r, r, r) : throw(ArgumentError("Invalid cornerradius value $r. Must be a `Real` or a tuple with 4 `Real`s.")) - poly!(blockscene, ibbox, color = box.color, visible = box.visible, + r1, r2, r3, r4 = min.(_max, (r1, r2, r3, r4)) + BezierPath([ + MoveTo(bbox.origin + Point(w, h/2)), + EllipticalArc(topright(bbox) - Point2f(r1, r1), r1, r1, 0.0, 0, pi/2), + EllipticalArc(topleft(bbox) + Point2f(r4, -r4), r4, r4, 0.0, pi/2, pi), + EllipticalArc(bottomleft(bbox) + Point2f(r3, r3), r3, r3, 0.0, pi, 3/2 * pi), + EllipticalArc(bottomright(bbox) + Point2f(-r2, r2), r2, r2, 0.0, 3/2 * pi, 2pi), + ClosePath(), + ]) + end + end + + + + poly!(blockscene, path, color = box.color, visible = box.visible, strokecolor = strokecolor_with_visibility, strokewidth = box.strokewidth, - inspectable = false) + inspectable = false, linestyle = box.linestyle) # trigger bbox box.layoutobservables.suggestedbbox[] = box.layoutobservables.suggestedbbox[] return end + + +function attribute_examples(::Type{Box}) + Dict( + :color => [ + Example( + name = "Colors", + code = """ + fig = Figure() + Box(fig[1, 1], color = :red) + Box(fig[1, 2], color = (:red, 0.5)) + Box(fig[2, 1], color = RGBf(0.2, 0.5, 0.7)) + Box(fig[2, 2], color = RGBAf(0.2, 0.5, 0.7, 0.5)) + fig + """ + ) + ], + :strokecolor => [ + Example( + name = "Stroke colors", + code = """ + fig = Figure() + Box(fig[1, 1], strokecolor = :red) + Box(fig[1, 2], strokecolor = (:red, 0.5)) + Box(fig[2, 1], strokecolor = RGBf(0.2, 0.5, 0.7)) + Box(fig[2, 2], strokecolor = RGBAf(0.2, 0.5, 0.7, 0.5)) + fig + """ + ) + ], + :strokewidth => [ + Example( + name = "Stroke widths", + code = """ + fig = Figure() + Box(fig[1, 1], strokewidth = 1) + Box(fig[1, 2], strokewidth = 10) + Box(fig[1, 3], strokewidth = 0) + fig + """ + ) + ], + :linestyle => [ + Example( + name = "Stroke style", + code = """ + fig = Figure() + Box(fig[1, 1], linestyle = :solid) + Box(fig[1, 2], linestyle = :dot) + Box(fig[1, 3], linestyle = :dash) + fig + """ + ) + ], + :cornerradius => [ + Example( + name = "Corner radius", + code = """ + fig = Figure() + Box(fig[1, 1], cornerradius = 0) + Box(fig[1, 2], cornerradius = 20) + Box(fig[1, 3], cornerradius = (0, 10, 20, 30)) + fig + """ + ) + ], + ) +end \ No newline at end of file diff --git a/src/makielayout/blocks/colorbar.jl b/src/makielayout/blocks/colorbar.jl index 1330308e364..1856ad4df73 100644 --- a/src/makielayout/blocks/colorbar.jl +++ b/src/makielayout/blocks/colorbar.jl @@ -25,6 +25,16 @@ function colorbar_check(keys, kwargs_keys) end end +function extract_colorrange(@nospecialize(plot::AbstractPlot))::Vec2{Float64} + if haskey(plot, :calculated_colors) && plot.calculated_colors[] isa Makie.ColorMapping + return plot.calculated_colors[].colorrange[] + elseif haskey(plot, :colorrange) && !(plot.colorrange[] isa Makie.Automatic) + return plot.colorrange[] + else + error("colorrange not found and calculated_colors for the plot is missing or is not a proper color map. Heatmaps and images should always contain calculated_colors[].colorrange") + end +end + function extract_colormap(@nospecialize(plot::AbstractPlot)) has_colorrange = haskey(plot, :colorrange) && !(plot.colorrange[] isa Makie.Automatic) if haskey(plot, :calculated_colors) && plot.calculated_colors[] isa Makie.ColorMapping @@ -243,7 +253,6 @@ function initialize_block!(cb::Colorbar) show_cats[] = true end end - heatmap!(blockscene, xrange, yrange, continous_pixels; colormap=colormap, @@ -401,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 b581994692c..8d9310f8ba4 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 @@ -12,16 +11,23 @@ function initialize_block!(leg::Legend, legend_area = lift(round_to_IRect2D, blockscene, leg.layoutobservables.computedbbox) - scene = Scene(blockscene, blockscene.px_area, camera = campixel!) + scene = Scene(blockscene, blockscene.viewport, camera = campixel!) # the rectangle in which the legend is drawn when margins are removed legendrect = lift(blockscene, legend_area, leg.margin) do la, lm enlarge(la, -lm[1], -lm[2], -lm[3], -lm[4]) end + backgroundcolor = if !isnothing(leg.bgcolor[]) + @warn("Keyword argument `bgcolor` is deprecated, use `backgroundcolor` instead.") + leg.bgcolor + else + leg.backgroundcolor + end + bg = poly!(scene, legendrect, - color = leg.bgcolor, strokewidth = leg.framewidth, visible = leg.framevisible, + color = backgroundcolor, strokewidth = leg.framewidth, visible = leg.framevisible, strokecolor = leg.framecolor, inspectable = false) translate!(bg, 0, 0, -7) # bg behind patches but before content at 0 (legend is at +10) @@ -461,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( @@ -480,17 +503,15 @@ function Legend(fig_or_scene, contents::AbstractVector, labels::AbstractVector, title = nothing; - kwargs...) - - if length(contents) != length(labels) - error("Number of elements not equal: $(length(contents)) content elements and $(length(labels)) labels.") - end + bbox=nothing, kwargs...) - 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 @@ -515,19 +536,14 @@ function Legend(fig_or_scene, contentgroups::AbstractVector{<:AbstractVector}, labelgroups::AbstractVector{<:AbstractVector}, titles::AbstractVector; - kwargs...) + bbox=nothing, 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 + 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 @@ -609,7 +625,7 @@ to one occurrence. """ function axislegend(ax, args...; position = :rt, kwargs...) Legend(ax.parent, args...; - bbox = ax.scene.px_area, + bbox = ax.scene.viewport, margin = (6, 6, 6, 6), legend_position_to_aligns(position)..., kwargs...) diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index 74def19a2c7..a88471351ff 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -59,7 +59,7 @@ function initialize_block!(m::Menu; default = 1) map!(blockscene, _direction, m.layoutobservables.computedbbox, m.direction) do bb, dir if dir == Makie.automatic - pxa = pixelarea(blockscene)[] + pxa = viewport(blockscene)[] bottomspace = abs(bottom(pxa) - bottom(bb)) topspace = abs(top(pxa) - top(bb)) # slight preference for down @@ -81,7 +81,7 @@ function initialize_block!(m::Menu; default = 1) left(bbox), right(bbox), d === :down ? max(0, bottom(bbox) - h) : top(bbox), - d === :down ? bottom(bbox) : min(top(bbox) + h, top(blockscene.px_area[])))) + d === :down ? bottom(bbox) : min(top(bbox) + h, top(blockscene.viewport[])))) end menuscene = Scene(blockscene, scenearea, camera = campixel!, clear=true) @@ -256,7 +256,7 @@ function initialize_block!(m::Menu; default = 1) m.is_open[] = !m.is_open[] if m.is_open[] t = translation(menuscene)[] - y_for_top_align = height(menuscene.px_area[]) - listheight[] + y_for_top_align = height(menuscene.viewport[]) - listheight[] translate!(menuscene, t[1], y_for_top_align, t[3]) end return Consume(true) @@ -295,7 +295,7 @@ function initialize_block!(m::Menu; default = 1) t = translation(menuscene)[] # Hack to differentiate mousewheel and trackpad scrolling step = m.scroll_speed[] * y - new_y = max(min(t[2] - step, 0), height(menuscene.px_area[]) - listheight[]) + new_y = max(min(t[2] - step, 0), height(menuscene.viewport[]) - listheight[]) translate!(menuscene, t[1], new_y, t[3]) return Consume(true) else diff --git a/src/makielayout/blocks/polaraxis.jl b/src/makielayout/blocks/polaraxis.jl index 4a5ef1a93b9..dc2d7aa240d 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) @@ -56,7 +53,7 @@ function initialize_block!(po::PolarAxis; palette=nothing) thetaticklabelplot.plots[1].fontsize, thetaticklabelplot.plots[1].font, po.thetaticklabelpad, - po.overlay.px_area + po.overlay.viewport ) do _, _, _, rpad, _, _, _, tpad, area # get maximum size of tick label @@ -87,7 +84,7 @@ function initialize_block!(po::PolarAxis; palette=nothing) po.target_rlims, po.target_thetalims, po.target_theta_0, po.direction, po.rticklabelsize, po.rticklabelpad, po.thetaticklabelsize, po.thetaticklabelpad, - po.overlay.px_area, po.overlay.camera.projectionview, + po.overlay.viewport, po.overlay.camera.projectionview, po.titlegap, po.titlesize, po.titlealign ) do rlims, thetalims, theta_0, dir, r_fs, r_pad, t_fs, t_pad, area, pv, gap, size, align @@ -255,14 +252,14 @@ function setup_camera_matrices!(po::PolarAxis) # update projection matrices # this just aspect-aware clip space (-1 .. 1, -h/w ... h/w, -max_z ... max_z) - on(po.blockscene, po.scene.px_area) do area + on(po.blockscene, po.scene.viewport) do area aspect = Float32((/)(widths(area)...)) w = 1f0 h = 1f0 / aspect camera(po.scene).projection[] = orthographicprojection(-w, w, -h, h, -max_z, max_z) end - on(po.blockscene, po.overlay.px_area) do area + on(po.blockscene, po.overlay.viewport) do area aspect = Float32((/)(widths(area)...)) w = 1f0 h = 1f0 / aspect @@ -383,7 +380,7 @@ function setup_camera_matrices!(po::PolarAxis) on(po.blockscene, e.mouseposition) do _ if drag_state[][3] - w = widths(po.scene.px_area[]) + w = widths(po.scene) p0 = (last_px_pos[] .- 0.5w) ./ w p1 = Point2f(mouseposition_px(po.scene) .- 0.5w) ./ w if norm(p0) * norm(p1) < 1e-6 @@ -766,7 +763,7 @@ function draw_axis!(po::PolarAxis, radius_at_origin) visible = po.clip, fxaa = false, transformation = Transformation(), # no polar transform for this - shading = false + shading = NoShading ) # inner clip is a (filled) circle sector which also needs to regenerate with @@ -788,7 +785,7 @@ function draw_axis!(po::PolarAxis, radius_at_origin) visible = po.clip, fxaa = false, transformation = Transformation(), - shading = false + shading = NoShading ) # handle placement with transform @@ -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/blocks/scene.jl b/src/makielayout/blocks/scene.jl index c4fa81663e6..e5cac177b0f 100644 --- a/src/makielayout/blocks/scene.jl +++ b/src/makielayout/blocks/scene.jl @@ -1,14 +1,9 @@ -function Makie.plot!(lscene::LScene, plot::AbstractPlot) - Makie.plot!(lscene.scene, plot) +function reset_limits!(lscene::LScene) notify(lscene.scene.theme.limits) center!(lscene.scene) - return plot -end - -function Makie.plot!(P::Makie.PlotFunc, ax::LScene, args...; kw_attributes...) - attributes = Makie.Attributes(kw_attributes) - return Makie.plot!(ax, P, attributes, args...) + return end +tightlimits!(::LScene) = nothing # TODO implement!? function initialize_block!(ls::LScene; scenekw = NamedTuple()) blockscene = ls.blockscene @@ -62,8 +57,3 @@ Makie.cam3d!(ax::LScene; kwargs...) = Makie.cam3d!(ax.scene; kwargs...) Makie.cam3d_cad!(ax::LScene; kwargs...) = Makie.cam3d_cad!(ax.scene; kwargs...) Makie.old_cam3d!(ax::LScene; kwargs...) = Makie.old_cam3d!(ax.scene; kwargs...) Makie.old_cam3d_cad!(ax::LScene; kwargs...) = Makie.old_cam3d_cad!(ax.scene; kwargs...) - - -function reset_limits!(ax::LScene) - # TODO -end diff --git a/src/makielayout/helpers.jl b/src/makielayout/helpers.jl index 378d067f231..2732f804cc9 100644 --- a/src/makielayout/helpers.jl +++ b/src/makielayout/helpers.jl @@ -138,7 +138,7 @@ function tightlimits!(la::Axis, ::Top) end function GridLayoutBase.GridLayout(scene::Scene, args...; kwargs...) - return GridLayout(args...; bbox=lift(Rect2f, pixelarea(scene)), kwargs...) + return GridLayout(args...; bbox=lift(Rect2f, viewport(scene)), kwargs...) end function axislines!(scene, rect, spinewidth, topspinevisible, rightspinevisible, diff --git a/src/makielayout/interactions.jl b/src/makielayout/interactions.jl index 0067ce7b4a6..e086e160bb7 100644 --- a/src/makielayout/interactions.jl +++ b/src/makielayout/interactions.jl @@ -124,9 +124,11 @@ end function _selection_vertices(ax_scene, outer, inner) _clamp(p, plow, phigh) = Point2f(clamp(p[1], plow[1], phigh[1]), clamp(p[2], plow[2], phigh[2])) - proj(point) = project(ax_scene, point) .+ minimum(ax_scene.px_area[]) - outer = positivize(outer) - inner = positivize(inner) + + proj(point) = project(ax_scene, point) .+ minimum(ax_scene.viewport[]) + transf = Makie.transform_func(ax_scene) + outer = positivize(Makie.apply_transform(transf, outer)) + inner = positivize(Makie.apply_transform(transf, inner)) obl = bottomleft(outer) obr = bottomright(outer) @@ -241,7 +243,7 @@ function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::Axis) cam = camera(scene) if zoom != 0 - pa = pixelarea(scene)[] + pa = viewport(scene)[] z = (1f0 - s.speed)^zoom @@ -304,7 +306,7 @@ function process_interaction(dp::DragPan, event::MouseEvent, ax) scene = ax.scene cam = camera(scene) - pa = pixelarea(scene)[] + pa = viewport(scene)[] mp_axscene = Vec4f((event.px .- pa.origin)..., 0, 1) mp_axscene_prev = Vec4f((event.prev_px .- pa.origin)..., 0, 1) diff --git a/src/makielayout/mousestatemachine.jl b/src/makielayout/mousestatemachine.jl index 472db916485..c7fe0ddadd1 100644 --- a/src/makielayout/mousestatemachine.jl +++ b/src/makielayout/mousestatemachine.jl @@ -3,27 +3,35 @@ module MouseEventTypes out enter over + leftdown rightdown middledown + leftup rightup middleup + leftdragstart rightdragstart middledragstart + leftdrag rightdrag middledrag + leftdragstop rightdragstop middledragstop + leftclick rightclick middleclick + leftdoubleclick rightdoubleclick middledoubleclick + downoutside end export MouseEventType @@ -125,6 +133,58 @@ end # don't react to buttons beyond the first three _isstandardmousebutton(b) = (b == Mouse.left || b == Mouse.middle || b == Mouse.right) +# TODO +# Make these enums so we can just do `Mouse.left & Mouse.drag` + +function to_drag_event(b::Mouse.Button) + b === Mouse.left && return MouseEventTypes.leftdrag + b === Mouse.right && return MouseEventTypes.rightdrag + b === Mouse.middle && return MouseEventTypes.middledrag + error("No recognized mouse button $b") +end + +function to_drag_start_event(b::Mouse.Button) + b === Mouse.left && return MouseEventTypes.leftdragstart + b === Mouse.right && return MouseEventTypes.rightdragstart + b === Mouse.middle && return MouseEventTypes.middledragstart + return error("No recognized mouse button $b") +end + +function to_drag_stop_event(b::Mouse.Button) + b === Mouse.left && return MouseEventTypes.leftdragstop + b === Mouse.right && return MouseEventTypes.rightdragstop + b === Mouse.middle && return MouseEventTypes.middledragstop + return error("No recognized mouse button $b") +end + +function to_down_event(b::Mouse.Button) + b === Mouse.left && return MouseEventTypes.leftdown + b === Mouse.right && return MouseEventTypes.rightdown + b === Mouse.middle && return MouseEventTypes.middledown + return error("No recognized mouse button $b") +end + +function to_up_event(b::Mouse.Button) + b === Mouse.left && return MouseEventTypes.leftup + b === Mouse.right && return MouseEventTypes.rightup + b === Mouse.middle && return MouseEventTypes.middleup + return error("No recognized mouse button $b") +end + +function to_doubleclick_event(b::Mouse.Button) + b === Mouse.left && return MouseEventTypes.leftdoubleclick + b === Mouse.right && return MouseEventTypes.rightdoubleclick + b === Mouse.middle && return MouseEventTypes.middledoubleclick + return error("No recognized mouse button $b") +end + +function to_click_event(b::Mouse.Button) + b === Mouse.left && return MouseEventTypes.leftclick + b === Mouse.right && return MouseEventTypes.rightclick + b === Mouse.middle && return MouseEventTypes.middleclick + return error("No recognized mouse button $b") +end + function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) Mouse = Makie.Mouse dblclick_max_interval = 0.2 @@ -163,12 +223,7 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) if drag_ongoing[] # continue the drag - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftdrag - Mouse.right => MouseEventTypes.rightdrag - Mouse.middle => MouseEventTypes.middledrag - x => error("No recognized mouse button $x") - end + event = to_drag_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) @@ -178,23 +233,13 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) # that means a drag started if mouse_downed_inside[] drag_ongoing[] = true - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftdragstart - Mouse.right => MouseEventTypes.rightdragstart - Mouse.middle => MouseEventTypes.middledragstart - x => error("No recognized mouse button $x") - end + event = to_drag_start_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) consumed = consumed || x - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftdrag - Mouse.right => MouseEventTypes.rightdrag - Mouse.middle => MouseEventTypes.middledrag - x => error("No recognized mouse button $x") - end + event = to_drag_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) @@ -250,12 +295,7 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) mouse_downed_button[] = button if mouse_was_inside[] - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftdown - Mouse.right => MouseEventTypes.rightdown - Mouse.middle => MouseEventTypes.middledown - x => error("No recognized mouse button $x") - end + event = to_down_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) @@ -281,12 +321,7 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) if downed_button_missing_from_pressed && some_mouse_button_had_been_downed if drag_ongoing[] - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftdragstop - Mouse.right => MouseEventTypes.rightdragstop - Mouse.middle => MouseEventTypes.middledragstop - x => error("No recognized mouse button $x") - end + event = to_drag_stop_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) @@ -295,13 +330,7 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) if mouse_was_inside[] # up after drag done over element - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftup - Mouse.right => MouseEventTypes.rightup - Mouse.middle => MouseEventTypes.middleup - x => error("No recognized mouse button $x") - end - + event = to_up_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) @@ -323,24 +352,14 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) if dt_last_click < dblclick_max_interval && !last_click_was_double[] && mouse_downed_button[] == b_last_click[] - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftdoubleclick - Mouse.right => MouseEventTypes.rightdoubleclick - Mouse.middle => MouseEventTypes.middledoubleclick - x => error("No recognized mouse button $x") - end + event = to_doubleclick_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) consumed = consumed || x last_click_was_double[] = true else - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftclick - Mouse.right => MouseEventTypes.rightclick - Mouse.middle => MouseEventTypes.middleclick - x => error("No recognized mouse button $x") - end + event = to_click_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) @@ -353,13 +372,7 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) mouse_downed_inside[] = false # up after click - event = @match mouse_downed_button[] begin - Mouse.left => MouseEventTypes.leftup - Mouse.right => MouseEventTypes.rightup - Mouse.middle => MouseEventTypes.middleup - x => error("No recognized mouse button $x") - end - + event = to_up_event(mouse_downed_button[]) x = setindex!(mouseevent, MouseEvent(event, t, data, px, prev_t[], prev_data[], prev_px[]) ) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index abd2be2058c..f9a71ebd212 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} @@ -671,7 +661,7 @@ function RectangleZoom(f::Function, ax::Axis; kw...) faces = [1 2 5; 5 2 6; 2 3 6; 6 3 7; 3 4 7; 7 4 8; 4 1 8; 8 1 5] # plot to blockscene, so ax.scene stays exclusive for user plots # That's also why we need to pass `ax.scene` to _selection_vertices, so it can project to that space - mesh = mesh!(ax.blockscene, selection_vertices, faces, color = (:black, 0.2), shading = false, + mesh = mesh!(ax.blockscene, selection_vertices, faces, color = (:black, 0.2), shading = NoShading, inspectable = false, visible=r.active, transparency=true) # translate forward so selection mesh and frame are never behind data translate!(mesh, 0, 0, 100) @@ -856,14 +846,16 @@ end valign = :center "The horizontal alignment of the rectangle in its suggested boundingbox" halign = :center - "The extra space added to the sides of the rectangle boundingbox." - padding = (0f0, 0f0, 0f0, 0f0) "The line width of the rectangle's border." strokewidth = 1f0 "Controls if the border of the rectangle is visible." strokevisible = true "The color of the border." strokecolor = RGBf(0, 0, 0) + "The linestyle of the rectangle border" + linestyle = nothing + "The radius of the rounded corner. One number is for all four corners, four numbers for going clockwise from top-right." + cornerradius = 0.0 "The width setting of the rectangle." width = nothing "The height setting of the rectangle." @@ -1187,8 +1179,10 @@ const EntryGroup = Tuple{Any, Vector{LegendEntry}} padding = (6f0, 6f0, 6f0, 6f0) "The additional space between the legend and its suggested boundingbox." margin = (0f0, 0f0, 0f0, 0f0) + "The background color of the legend. DEPRECATED - use `backgroundcolor` instead." + bgcolor = nothing "The background color of the legend." - bgcolor = :white + backgroundcolor = :white "The color of the legend border." framecolor = :black "The line width of the legend border." @@ -1356,8 +1350,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 @@ -1644,8 +1636,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..9bace9bdefd 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -37,33 +37,6 @@ function SSAO(; radius=nothing, bias=nothing, blur=nothing) return SSAO(_radius, _bias, _blur) end -abstract type AbstractLight end - -""" -A positional point light, shining at a certain color. -Color values can be bigger than 1 for brighter lights. -""" -struct PointLight <: AbstractLight - position::Observable{Vec3f} - radiance::Observable{RGBf} -end - -""" -An environment Light, that uses a spherical environment map to provide lighting. -See: https://en.wikipedia.org/wiki/Reflection_mapping -""" -struct EnvironmentLight <: AbstractLight - intensity::Observable{Float32} - image::Observable{Matrix{RGBf}} -end - -""" -A simple, one color ambient light. -""" -struct AmbientLight <: AbstractLight - color::Observable{RGBf} -end - """ Scene TODO document this @@ -81,7 +54,7 @@ mutable struct Scene <: AbstractScene events::Events "The current pixel area of the Scene." - px_area::Observable{Rect2i} + viewport::Observable{Rect2i} "Whether the scene should be cleared." clear::Observable{Bool} @@ -114,11 +87,12 @@ mutable struct Scene <: AbstractScene ssao::SSAO lights::Vector{AbstractLight} deregister_callbacks::Vector{Observables.ObserverFunction} + cycler::Cycler function Scene( parent::Union{Nothing, Scene}, events::Events, - px_area::Observable{Rect2i}, + viewport::Observable{Rect2i}, clear::Observable{Bool}, camera::Camera, camera_controls::AbstractCamera, @@ -130,12 +104,12 @@ mutable struct Scene <: AbstractScene backgroundcolor::Observable{RGBAf}, visible::Observable{Bool}, ssao::SSAO, - lights::Vector{AbstractLight} + lights::Vector ) scene = new( parent, events, - px_area, + viewport, clear, camera, camera_controls, @@ -147,8 +121,9 @@ mutable struct Scene <: AbstractScene backgroundcolor, visible, ssao, - lights, - Observables.ObserverFunction[] + convert(Vector{AbstractLight}, lights), + Observables.ObserverFunction[], + Cycler() ) finalizer(free, scene) return scene @@ -156,19 +131,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 +154,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) @@ -216,7 +191,7 @@ function Base.show(io::IO, scene::Scene) end function Scene(; - px_area::Union{Observable{Rect2i}, Nothing} = nothing, + viewport::Union{Observable{Rect2i}, Nothing} = nothing, events::Events = Events(), clear::Union{Automatic, Observable{Bool}, Bool} = automatic, transform_func=identity, @@ -239,12 +214,18 @@ function Scene(; bg = Observable{RGBAf}(to_color(m_theme.backgroundcolor[]); ignore_equal_values=true) - wasnothing = isnothing(px_area) + wasnothing = isnothing(viewport) if wasnothing - px_area = Observable(Recti(0, 0, m_theme.resolution[]); ignore_equal_values=true) + sz = if haskey(m_theme, :resolution) + @warn "Found `resolution` in the theme when creating a `Scene`. The `resolution` keyword for `Scene`s and `Figure`s has been deprecated. Use `Figure(; size = ...` or `Scene(; size = ...)` instead, which better reflects that this is a unitless size and not a pixel resolution. The key could also come from `set_theme!` calls or related theming functions." + m_theme.resolution[] + else + m_theme.size[] + end + viewport = Observable(Recti(0, 0, sz); ignore_equal_values=true) end - cam = camera isa Camera ? camera : Camera(px_area) + cam = camera isa Camera ? camera : Camera(viewport) _lights = lights isa Automatic ? AbstractLight[] : lights # if we have an opaque background, automatically set clear to true! @@ -254,7 +235,7 @@ function Scene(; clear = convert(Observable{Bool}, clear) end scene = Scene( - parent, events, px_area, clear, cam, camera_controls, + parent, events, viewport, clear, cam, camera_controls, transformation, plots, m_theme, children, current_screens, bg, visible, ssao, _lights ) @@ -262,51 +243,46 @@ function Scene(; if wasnothing on(events.window_area, priority = typemax(Int)) do w_area - if !any(x -> x ≈ 0.0, widths(w_area)) && px_area[] != w_area - px_area[] = w_area + if !any(x -> x ≈ 0.0, widths(w_area)) && viewport[] != w_area + viewport[] = w_area end return Consume(false) end end if lights isa Automatic - lightposition = to_value(get(m_theme, :lightposition, nothing)) - if !isnothing(lightposition) - position = if lightposition === :eyeposition - scene.camera.eyeposition - elseif lightposition isa Vec3 - m_theme.lightposition - else - error("Wrong lightposition type, use `:eyeposition` or `Vec3f(...)`") + haskey(m_theme, :lightposition) && @warn("`lightposition` is deprecated. Set `light_direction` instead.") + + if haskey(m_theme, :lights) + copyto!(scene.lights, m_theme.lights[]) + else + haskey(m_theme, :light_direction) || error("Theme must contain `light_direction::Vec3f` or an explicit `lights::Vector`!") + haskey(m_theme, :light_color) || error("Theme must contain `light_color::RGBf` or an explicit `lights::Vector`!") + haskey(m_theme, :camera_relative_light) || @warn("Theme should contain `camera_relative_light::Bool`.") + + if haskey(m_theme, :ambient) + push!(scene.lights, AmbientLight(m_theme[:ambient][])) end - push!(scene.lights, PointLight(position, RGBf(1, 1, 1))) - end - ambient = to_value(get(m_theme, :ambient, nothing)) - if !isnothing(ambient) - push!(scene.lights, AmbientLight(ambient)) + + push!(scene.lights, DirectionalLight( + m_theme[:light_color][], m_theme[:light_direction], + to_value(get(m_theme, :camera_relative_light, false)) + )) end end return scene end -function get_one_light(scene::Scene, Typ) - indices = findall(x-> x isa Typ, scene.lights) - isempty(indices) && return nothing - if length(indices) > 1 - @warn("Only one light supported by backend right now. Using only first light") - end - return scene.lights[indices[1]] -end - -get_point_light(scene::Scene) = get_one_light(scene, PointLight) -get_ambient_light(scene::Scene) = get_one_light(scene, AmbientLight) - +get_directional_light(scene::Scene) = get_one_light(scene.lights, DirectionalLight) +get_point_light(scene::Scene) = get_one_light(scene.lights, PointLight) +get_ambient_light(scene::Scene) = get_one_light(scene.lights, AmbientLight) +default_shading!(plot, scene::Scene) = default_shading!(plot, scene.lights) function Scene( parent::Scene; events=parent.events, - px_area=nothing, + viewport=nothing, clear=false, camera=nothing, camera_controls=parent.camera_controls, @@ -317,10 +293,10 @@ function Scene( if camera !== parent.camera camera_controls = EmptyCamera() end - child_px_area = px_area isa Observable ? px_area : Observable(Rect2i(0, 0, 0, 0); ignore_equal_values=true) + child_px_area = viewport isa Observable ? viewport : Observable(Rect2i(0, 0, 0, 0); ignore_equal_values=true) child = Scene(; events=events, - px_area=child_px_area, + viewport=child_px_area, clear=convert(Observable{Bool}, clear), camera=camera, camera_controls=camera_controls, @@ -330,13 +306,13 @@ function Scene( theme=theme(parent), kw... ) - if isnothing(px_area) - map!(identity, child, child_px_area, parent.px_area) - elseif px_area isa Rect2 - child_px_area[] = Rect2i(px_area) + if isnothing(viewport) + map!(identity, child, child_px_area, parent.viewport) + elseif viewport isa Rect2 + child_px_area[] = Rect2i(viewport) else - if !(px_area isa Observable) - error("px_area must be an Observable{Rect2} or a Rect2") + if !(viewport isa Observable) + error("viewport must be an Observable{Rect2} or a Rect2") end end push!(parent.children, child) @@ -346,7 +322,7 @@ end # legacy constructor function Scene(parent::Scene, area; kw...) - return Scene(parent; px_area=area, kw...) + return Scene(parent; viewport=area, kw...) end # Base overloads for Scene @@ -363,16 +339,17 @@ function root(scene::Scene) end parent_or_self(scene::Scene) = isroot(scene) ? scene : parent(scene) -GeometryBasics.widths(scene::Scene) = widths(to_value(pixelarea(scene))) +GeometryBasics.widths(scene::Scene) = widths(to_value(viewport(scene))) Base.size(scene::Scene) = Tuple(widths(scene)) Base.size(x::Scene, i) = size(x)[i] + function Base.resize!(scene::Scene, xy::Tuple{Number,Number}) resize!(scene, Recti(0, 0, xy)) end Base.resize!(scene::Scene, x::Number, y::Number) = resize!(scene, (x, y)) function Base.resize!(scene::Scene, rect::Rect2) - pixelarea(scene)[] = rect + viewport(scene)[] = rect if isroot(scene) for screen in scene.current_screens resize!(screen, widths(rect)...) @@ -423,7 +400,7 @@ end function free(scene::Scene) empty!(scene; free=true) - for field in [:backgroundcolor, :px_area, :visible] + for field in [:backgroundcolor, :viewport, :visible] Observables.clear(getfield(scene, field)) end for screen in copy(scene.current_screens) @@ -465,21 +442,20 @@ 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 function Base.delete!(screen::MakieScreen, ::Scene, ::AbstractPlot) - @warn "Deleting plots not implemented for backend: $(typeof(screen))" + @debug "Deleting plots not implemented for backend: $(typeof(screen))" end function Base.delete!(screen::MakieScreen, ::Scene) @@ -491,6 +467,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) @@ -530,9 +509,14 @@ end cameracontrols!(scene::SceneLike, cam) = cameracontrols!(parent(scene), cam) cameracontrols!(x, cam) = cameracontrols!(get_scene(x), cam) -pixelarea(x) = pixelarea(get_scene(x)) -pixelarea(scene::Scene) = scene.px_area -pixelarea(scene::SceneLike) = pixelarea(scene.parent) +viewport(x) = viewport(get_scene(x)) +""" + viewport(scene::Scene) + +Gets the viewport of the scene in device independent units as an `Observable{Rect2{Int}}`. +""" +viewport(scene::Scene) = scene.viewport +viewport(scene::SceneLike) = viewport(scene.parent) plots(x) = plots(get_scene(x)) plots(scene::SceneLike) = scene.plots @@ -549,7 +533,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 @@ -653,5 +638,3 @@ function collect_atomic_plots(scene::Scene, plots=AbstractPlot[]; is_atomic_plot collect_atomic_plots(scene.children, plots; is_atomic_plot=is_atomic_plot) plots end - -Base.@deprecate flatten_plots(scenelike) collect_atomic_plots(scenelike) 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/stats/hist.jl b/src/stats/hist.jl index 0ea0ea910b7..5c943a88ea2 100644 --- a/src/stats/hist.jl +++ b/src/stats/hist.jl @@ -144,7 +144,7 @@ function pick_hist_edges(vals, bins) if bins isa Int mi, ma = float.(extrema(vals)) if mi == ma - return [mi - 0.5, ma + 0.5] + return (mi - 0.5):(ma + 0.5) end # hist is right-open, so to include the upper data point, make the last bin a tiny bit bigger ma = nextfloat(ma) diff --git a/src/themes/theme_black.jl b/src/themes/theme_black.jl index 1f0b9965b59..3ee6e7fb23d 100644 --- a/src/themes/theme_black.jl +++ b/src/themes/theme_black.jl @@ -16,7 +16,7 @@ function theme_black() ), Legend = ( framecolor = :white, - bgcolor = :black, + backgroundcolor = :black, ), Axis3 = ( xgridcolor = RGBAf(1, 1, 1, 0.16), diff --git a/src/theming.jl b/src/theming.jl index 47450cdfa0c..b8ee0924d4d 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -23,8 +23,6 @@ const DEFAULT_PALETTES = Attributes( side = [:left, :right] ) -Base.@deprecate_binding default_palettes DEFAULT_PALETTES - const MAKIE_DEFAULT_THEME = Attributes( palette = DEFAULT_PALETTES, font = :regular, @@ -53,7 +51,7 @@ const MAKIE_DEFAULT_THEME = Attributes( patchcolor = RGBAf(0, 0, 0, 0.6), patchstrokecolor = :black, patchstrokewidth = 0, - resolution = (600, 450), # 4/3 aspect ratio + size = (600, 450), # 4/3 aspect ratio visible = true, Axis = Attributes(), Axis3 = Attributes(), @@ -68,10 +66,21 @@ const MAKIE_DEFAULT_THEME = Attributes( blur = Int32(2), # A (2blur+1) by (2blur+1) range is used for blurring # N_samples = 64, # number of samples (requires shader reload) ), - ambient = RGBf(0.55, 0.55, 0.55), - lightposition = :eyeposition, inspectable = true, + # Vec is equvalent to 36° right/east, 39° up/north from camera position + # The order here is Vec3f(right of, up from, towards) viewer/camera + light_direction = Vec3f(-0.45679495, -0.6293204, -0.6287243), + camera_relative_light = true, # Only applies to default DirectionalLight + light_color = RGBf(0.5, 0.5, 0.5), + ambient = RGBf(0.35, 0.35, 0.35), + + # Note: this can be set too + # lights = AbstractLight[ + # AmbientLight(RGBf(0.55, 0.55, 0.55)), + # DirectionalLight(RGBf(0.8, 0.8, 0.8), Vec3f(2/3, 2/3, 1/3)) + # ], + CairoMakie = Attributes( px_per_unit = 2.0, pt_per_unit = 0.75, @@ -100,19 +109,25 @@ const MAKIE_DEFAULT_THEME = Attributes( monitor = nothing, visible = true, - # Postproccessor + # Shader constants & Postproccessor oit = true, fxaa = true, ssao = false, # This adjusts a factor in the rendering shaders for order independent # transparency. This should be the same for all of them (within one rendering # pipeline) otherwise depth "order" will be broken. - transparency_weight_scale = 1000f0 + transparency_weight_scale = 1000f0, + # maximum number of lights with shading = :verbose + max_lights = 64, + max_light_parameters = 5 * 64 ), WGLMakie = Attributes( framerate = 30.0, - resize_to_body = false, + resize_to = nothing, + # DEPRECATED in favor of resize_to + # still needs to be here to gracefully deprecate it + resize_to_body = nothing, px_per_unit = automatic, scalefactor = automatic ), @@ -125,14 +140,9 @@ const MAKIE_DEFAULT_THEME = Attributes( ) ) -Base.@deprecate_binding minimal_default MAKIE_DEFAULT_THEME - - const CURRENT_DEFAULT_THEME = deepcopy(MAKIE_DEFAULT_THEME) const THEME_LOCK = Base.ReentrantLock() - - # Basically like deepcopy but while merging it into another Attribute dict function merge_without_obs!(result::Attributes, theme::Attributes) dict = attributes(result) @@ -203,7 +213,7 @@ restored afterwards, no matter if `f` succeeds or fails. Example: ```julia -my_theme = Theme(resolution = (500, 500), color = :red) +my_theme = Theme(size = (500, 500), color = :red) with_theme(my_theme, color = :blue, linestyle = :dashed) do scatter(randn(100, 2)) end @@ -224,6 +234,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..a3144183a7b 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 @@ -239,7 +241,12 @@ struct Camera resolution::Observable{Vec2f} """ - Eye position of the camera, sued for e.g. ray tracing. + Focal point of the camera, used for e.g. camera synchronized light direction. + """ + lookat::Observable{Vec3f} + + """ + Eye position of the camera, used for e.g. ray tracing. """ eyeposition::Observable{Vec3f} @@ -447,3 +454,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/units.jl b/src/units.jl index d93d71f8331..7105410199e 100644 --- a/src/units.jl +++ b/src/units.jl @@ -18,7 +18,7 @@ ######### function to_screen(scene::Scene, mpos) - return Point2f(mpos) .- Point2f(minimum(pixelarea(scene)[])) + return Point2f(mpos) .- Point2f(minimum(viewport(scene)[])) end number(x::Unit) = x.value diff --git a/src/utilities/quaternions.jl b/src/utilities/quaternions.jl index bcae57f50de..f5ccf832bc3 100644 --- a/src/utilities/quaternions.jl +++ b/src/utilities/quaternions.jl @@ -81,6 +81,17 @@ function Base.:(*)(quat::Quaternion{T}, vec::P) where {T, P <: StaticVector{3}} (num8 - num11) * vec[1] + (num9 + num10) * vec[2] + (1f0 - (num4 + num5)) * vec[3] ) end + +function Base.:(*)(quat::Quaternion, bb::Rect3{T}) where {T} + points = corners(bb) + first = points[1] + bb = Ref(Rect3{T}(quat * first, zero(first))) + for i in 2:length(points) + bb[] = _update_rect(bb[], Point3{T}(quat * points[i])) + end + return bb[] +end + Base.conj(q::Quaternion) = Quaternion(-q[1], -q[2], -q[3], q[4]) function Base.:(*)(q::Quaternion, w::Quaternion) diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index 00b6727330c..133d3344373 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -1,4 +1,4 @@ -const SERIALIZATION_FORMAT_VERSION = "v4" +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) @@ -187,8 +187,6 @@ end const DEFAULT_FONT = NativeFont[] const ALTERNATIVE_FONTS = NativeFont[] const FONT_LOCK = Base.ReentrantLock() -Base.@deprecate_binding _default_font DEFAULT_FONT -Base.@deprecate_binding _alternative_fonts ALTERNATIVE_FONTS function defaultfont() lock(FONT_LOCK) do @@ -291,19 +289,26 @@ function glyph_uv_width!(atlas::TextureAtlas, b::BezierPath) return atlas.uv_rectangles[glyph_index!(atlas, b)] end -crc(x, seed=UInt32(0)) = crc32c(collect(x), seed) + +# 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=crc) + 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=crc), 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 +439,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 +558,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 18da1690e7a..fd2d653eb56 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -179,6 +179,7 @@ attr_broadcast_getindex(x::Ref, i) = x[] # unwrap Refs just like in normal broad attr_broadcast_getindex(x::ScalarOrVector, i) = x.sv isa Vector ? x.sv[i] : x.sv is_vector_attribute(x::AbstractVector) = true +is_vector_attribute(x::Base.Generator) = is_vector_attribute(x.iter) is_vector_attribute(x::NativeFont) = false is_vector_attribute(x::Quaternion) = false is_vector_attribute(x::VecTypes) = false @@ -438,3 +439,18 @@ end sv_getindex(v::AbstractVector, i::Integer) = v[i] sv_getindex(x, ::Integer) = x sv_getindex(x::VecTypes, ::Integer) = x + +# TODO: move to GeometryBasics +function corners(rect::Rect2{T}) where T + o = minimum(rect) + w = widths(rect) + T0 = zero(T) + return Point{3,T}[o .+ Vec2{T}(x, y) for x in (T0, w[1]) for y in (T0, w[2])] +end + +function corners(rect::Rect3{T}) where T + o = minimum(rect) + w = widths(rect) + T0 = zero(T) + return Point{3,T}[o .+ Vec3{T}(x, y, z) for x in (T0, w[1]) for y in (T0, w[2]) for z in (T0, w[3])] +end diff --git a/test/bezier.jl b/test/bezier.jl new file mode 100644 index 00000000000..2111b8f235e --- /dev/null +++ b/test/bezier.jl @@ -0,0 +1,10 @@ +using Makie, Test + +# nice reference: https://www.nan.fyi/svg-paths +@testset "BezierPath construction" begin + @test_nowarn BezierPath("m 0,9 L 0,5138 0,9 z") + @test_broken BezierPath("m 0,1e-5 L 0,5138 0,9 z") isa BezierPath + @test_nowarn BezierPath("M 100,100 C 100,200 200,100 200,200 z") + @test_broken BezierPath("M 100,100 Q 50,150,100,100 z") isa BezierPath + @test_broken BezierPath("M 3.0 10.0 A 10.0 7.5 0.0 0.0 0.0 20.0 15.0 z") isa BezierPath +end diff --git a/test/boundingboxes.jl b/test/boundingboxes.jl index 284dcdce840..37aa126331e 100644 --- a/test/boundingboxes.jl +++ b/test/boundingboxes.jl @@ -34,8 +34,19 @@ end fig, ax, p = meshscatter([Point3f(x, y, z) for x in 1:5 for y in 1:5 for z in 1:5]) bb = boundingbox(p) - @test bb.origin ≈ Point3f(1) - @test bb.widths ≈ Vec3f(4) + # Note: awkwards numbers come from using mesh over Sphere + @test bb.origin ≈ Point3f(0.9011624, 0.9004657, 0.9) + @test bb.widths ≈ Vec3f(4.1986046, 4.199068, 4.2) + + fig, ax, p = meshscatter( + [Point3f(0) for _ in 1:3], + marker = Rect3f(Point3f(-0.1, -0.1, -0.1), Vec3f(0.2, 0.2, 1.2)), + markersize = Vec3f(1, 1, 2), + rotations = Makie.rotation_between.((Vec3f(0,0,1),), Vec3f[(1,0,0), (0,1,0), (0,0,1)]) + ) + bb = boundingbox(p) + @test bb.origin ≈ Point3f(-0.2) + @test bb.widths ≈ Vec3f(2.4) fig, ax, p = volume(rand(5, 5, 5)) bb = boundingbox(p) @@ -68,7 +79,7 @@ end @test bb.widths ≈ Vec3f(10.0, 10.0, 0) # text transforms to pixel space atm (TODO) - fig = Figure(resolution = (400, 400)) + fig = Figure(size = (400, 400)) ax = Axis(fig[1, 1]) p = text!(ax, Point2f(10), text = "test", fontsize = 20) bb = boundingbox(p) diff --git a/test/conversions.jl b/test/conversions.jl index 8288a6d83aa..bb9a9961974 100644 --- a/test/conversions.jl +++ b/test/conversions.jl @@ -302,16 +302,16 @@ end @testset "GridBased and ImageLike conversions" begin # type tree @test GridBased <: ConversionTrait - @test CellBasedGrid <: GridBased - @test VertexBasedGrid <: GridBased + @test CellGrid <: GridBased + @test VertexGrid <: GridBased @test ImageLike <: ConversionTrait # Plot to trait @test conversion_trait(Image) === ImageLike() - @test conversion_trait(Heatmap) === CellBasedGrid() - @test conversion_trait(Surface) === VertexBasedGrid() - @test conversion_trait(Contour) === VertexBasedGrid() - @test conversion_trait(Contourf) === VertexBasedGrid() + @test conversion_trait(Heatmap) === CellGrid() + @test conversion_trait(Surface) === VertexGrid() + @test conversion_trait(Contour) === VertexGrid() + @test conversion_trait(Contourf) === VertexGrid() m1 = [x for x in 1:10, y in 1:6] m2 = [y for x in 1:10, y in 1:6] @@ -337,7 +337,7 @@ end @test_throws ErrorException convert_arguments(Heatmap, m1, m2) end - @testset "VertexBasedGrid conversion" begin + @testset "VertexGrid conversion" begin vo1 = Float32.(v1) vo2 = Float32.(v2) mo1 = Float32.(m1) @@ -349,7 +349,7 @@ end @test convert_arguments(Surface, m1, m2) == (mo1, mo2, zeros(Float32, size(o3))) end - @testset "CellBasedGrid conversion" begin + @testset "CellGrid conversion" begin o1 = Float32.(0.5:1:10.5) o2 = Float32.(0.5:1:6.5) @test convert_arguments(Heatmap, m3) == (o1, o2, o3) diff --git a/test/events.jl b/test/events.jl index cc044d9e52b..41b7a94d440 100644 --- a/test/events.jl +++ b/test/events.jl @@ -135,7 +135,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right clipboard() = CLIP[] end @testset "copy_paste" begin - f = Figure(resolution=(640,480)) + f = Figure(size=(640,480)) tb = Textbox(f[1,1], placeholder="Copy/paste into me") e = events(f.scene) @@ -166,7 +166,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # Refresh figure to test right control + v combination empty!(f) - f = Figure(resolution=(640,480)) + f = Figure(size=(640,480)) tb = Textbox(f[1,1], placeholder="Copy/paste into me") e = events(f.scene) @@ -196,7 +196,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # This testset is based on the results the current camera system has. If # cam3d! is updated this is likely to break. @testset "cam3d!" begin - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) @@ -231,7 +231,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # Reset state so this is indepentent from the last checks - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) @@ -266,7 +266,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # Reset state - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) @@ -292,7 +292,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right end @testset "mouse state machine" begin - scene = Scene(resolution=(800, 600)); + scene = Scene(size=(800, 600)); e = events(scene) bbox = Observable(Rect2(200, 200, 400, 300)) msm = addmouseevents!(scene, bbox, priority=typemax(Int)) @@ -443,7 +443,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # TODO: test more @testset "Axis Interactions" begin - f = Figure(resolution = (400, 400)) + f = Figure(size = (400, 400)) a = Axis(f[1, 1]) e = events(f) diff --git a/test/figures.jl b/test/figures.jl index 4d844d115af..de8c1e8a315 100644 --- a/test/figures.jl +++ b/test/figures.jl @@ -155,8 +155,8 @@ end end @testset "Figure and axis kwargs validation" begin - @test_throws ArgumentError lines(1:10, axis = (aspect = DataAspect()), figure = (resolution = (100, 100))) - @test_throws ArgumentError lines(1:10, figure = (resolution = (100, 100))) + @test_throws ArgumentError lines(1:10, axis = (aspect = DataAspect()), figure = (size = (100, 100))) + @test_throws ArgumentError lines(1:10, figure = (size = (100, 100))) @test_throws ArgumentError lines(1:10, axis = (aspect = DataAspect())) # these just shouldn't error diff --git a/test/hist.jl b/test/hist.jl new file mode 100644 index 00000000000..96cbc15b01f --- /dev/null +++ b/test/hist.jl @@ -0,0 +1,16 @@ +@testset "Histogram plotting" begin + unequal_vec = [1; rand(2:9, rand(1:9))] + allequal_vec = fill(rand(1:9), rand(1:9)) + # normal range + @test_nowarn hist(0:rand(1:9)) + # initialize with unequal observable vector + v = Observable(unequal_vec) + @test_nowarn hist(v) + # change to allequal vector + @test_nowarn v[] = allequal_vec + # initialize with allequal observable vector + v = Observable(allequal_vec) + @test_nowarn hist(v) + # change to unequal vector + @test_nowarn v[] = unequal_vec +end diff --git a/test/makielayout.jl b/test/makielayout.jl index 4366a1081ff..29f2335bf26 100644 --- a/test/makielayout.jl +++ b/test/makielayout.jl @@ -115,6 +115,47 @@ end @test ax.finallimits[] == BBox(-5, 11, 5, 7) end +# issue 3240 +@testset "Axis limits 4-tuple" begin + fig = Figure() + ax = Axis(fig[1,1],limits=(0,600,0,15)) + xlims!(ax,100,400) + @test ax.limits[] == ((100,400),(0,15)) + xlims!() + @test ax.limits[] == ((nothing,nothing),(0,15)) + + ax = Axis(fig[1,1],limits=(0,600,0,15)) + ylims!(ax,1,13) + @test ax.limits[] == ((0,600),(1,13)) + ylims!() + @test ax.limits[] == ((0,600),(nothing,nothing)) + + ax = Axis(fig[1,1],limits=(0,600,0,15)) + limits!(ax,350,700,2,14) + @test ax.limits[] == ((350,700),(2,14)) +end + +@testset "Axis3 limits 6-tuple" begin + fig = Figure() + ax = Axis3(fig[1,1],limits=(0,1,0,2,0,3)) + xlims!(ax,1,2) + @test ax.limits[] == ((1,2),(0,2),(0,3)) + xlims!() + @test ax.limits[] == ((nothing,nothing),(0,2),(0,3)) + + ax = Axis3(fig[1,1],limits=(0,1,0,2,0,3)) + ylims!(ax,1,3) + @test ax.limits[] == ((0,1),(1,3),(0,3)) + ylims!() + @test ax.limits[] == ((0,1),(nothing,nothing),(0,3)) + + ax = Axis3(fig[1,1],limits=(0,1,0,2,0,3)) + zlims!(ax,1,5) + @test ax.limits[] == ((0,1),(0,2),(1,5)) + zlims!() + @test ax.limits[] == ((0,1),(0,2),(nothing,nothing)) +end + @testset "Colorbar plot object kwarg clash" begin for attr in (:colormap, :limits) f, ax, p = scatter(1:10, 1:10, color = 1:10, colorrange = (1, 10)) 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] diff --git a/test/ray_casting.jl b/test/ray_casting.jl index 135dd82d95d..73b5beaa20a 100644 --- a/test/ray_casting.jl +++ b/test/ray_casting.jl @@ -1,7 +1,7 @@ @testset "Ray Casting" begin @testset "View Rays" begin scene = Scene() - xy = 0.5 * widths(pixelarea(scene)[]) + xy = 0.5 * widths(viewport(scene)[]) orthographic_cam3d!(x) = cam3d!(x, perspectiveprojection = Makie.Orthographic) @@ -20,13 +20,13 @@ end - # transform() is used to apply a translation-rotation-scale matrix to rays + # transform() is used to apply a translation-rotation-scale matrix to rays # instead of point like data # Generate random point + transform rot = Makie.rotation_between(rand(Vec3f), rand(Vec3f)) model = Makie.transformationmatrix(rand(Vec3f), rand(Vec3f), rot) point = Point3f(1) + rand(Point3f) - + # Generate rate that passes through transformed point transformed = Point3f(model * Point4f(point..., 1)) direction = (1 + 10*rand()) * rand(Vec3f) @@ -71,61 +71,61 @@ end - # Note that these tests depend on the exact placement of plots and may + # Note that these tests depend on the exact placement of plots and may # error when cameras are adjusted @testset "position_on_plot()" begin - + # Lines (2D) & Linesegments (3D) ps = [exp(-0.01phi) * Point2f(cos(phi), sin(phi)) for phi in range(0, 20pi, length = 501)] - scene = Scene(resolution = (400, 400)) + scene = Scene(size = (400, 400)) p = lines!(scene, ps) cam2d!(scene) ray = Makie.Ray(scene, (325.0, 313.0)) pos = Makie.position_on_plot(p, 157, ray) @test pos ≈ Point3f(0.6087957666683925, 0.5513198993583837, 0.0) - - scene = Scene(resolution = (400, 400)) + + scene = Scene(size = (400, 400)) p = linesegments!(scene, ps) cam3d!(scene) ray = Makie.Ray(scene, (238.0, 233.0)) pos = Makie.position_on_plot(p, 178, ray) @test pos ≈ Point3f(-0.7850463447725504, -0.15125213957100314, 0.0) - - + + # Heatmap (2D) & Image (3D) - scene = Scene(resolution = (400, 400)) + scene = Scene(size = (400, 400)) p = heatmap!(scene, 0..1, -1..1, rand(10, 10)) cam2d!(scene) ray = Makie.Ray(scene, (228.0, 91.0)) pos = Makie.position_on_plot(p, 0, ray) @test pos ≈ Point3f(0.13999999, -0.54499996, 0.0) - - scene = Scene(resolution = (400, 400)) + + scene = Scene(size = (400, 400)) p = image!(scene, -1..1, -1..1, rand(10, 10)) cam3d!(scene) ray = Makie.Ray(scene, (309.0, 197.0)) pos = Makie.position_on_plot(p, 3, ray) @test pos ≈ Point3f(-0.7830243, 0.8614166, 0.0) - - + + # Mesh (3D) - scene = Scene(resolution = (400, 400)) + scene = Scene(size = (400, 400)) p = mesh!(scene, Rect3f(Point3f(0), Vec3f(1))) cam3d!(scene) ray = Makie.Ray(scene, (201.0, 283.0)) pos = Makie.position_on_plot(p, 15, ray) @test pos ≈ Point3f(0.029754717, 0.043159597, 1.0) - + # Surface (3D) - scene = Scene(resolution = (400, 400)) + scene = Scene(size = (400, 400)) p = surface!(scene, -2..2, -2..2, [sin(x) * cos(y) for x in -10:10, y in -10:10]) cam3d!(scene) ray = Makie.Ray(scene, (52.0, 238.0)) pos = Makie.position_on_plot(p, 57, ray) @test pos ≈ Point3f(0.80910987, -1.6090667, 0.137722) - + # Volume (3D) - scene = Scene(resolution = (400, 400)) + scene = Scene(size = (400, 400)) p = volume!(scene, rand(10, 10, 10)) cam3d!(scene) center!(scene) @@ -135,12 +135,12 @@ end # For recreating the above: - #= + #= # Scene setup from tests: - scene = Scene(resolution = (400, 400)) + scene = Scene(size = (400, 400)) p = surface!(scene, -2..2, -2..2, [sin(x) * cos(y) for x in -10:10, y in -10:10]) cam3d!(scene) - + pos = Observable(Point3f(0.5)) on(events(scene).mousebutton, priority = 100) do event if event.button == Mouse.left && event.action == Mouse.press @@ -160,4 +160,4 @@ scene =# -end \ No newline at end of file +end diff --git a/test/runtests.jl b/test/runtests.jl index c812f861316..24de4e41329 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -35,4 +35,6 @@ using Makie: volume include("ray_casting.jl") include("PolarAxis.jl") include("barplot.jl") + include("bezier.jl") + include("hist.jl") end diff --git a/test/scenes.jl b/test/scenes.jl index b28a6dd51b8..1758fdd183c 100644 --- a/test/scenes.jl +++ b/test/scenes.jl @@ -6,4 +6,68 @@ end @test theme(nothing, :nonexistant, default=1) == 1 @test theme(scene, :nonexistant, default=1) == 1 + + # test that deprecated `resolution keyword still works but throws warning` + logger = Test.TestLogger() + Base.with_logger(logger) do + scene = Scene(; resolution = (999, 999), size = (123, 123)) + @test scene.viewport[] == Rect2i((0, 0), (999, 999)) + end + @test occursin("The `resolution` keyword for `Scene`s and `Figure`s has been deprecated", logger.logs[1].message) +end + +@testset "Lighting" begin + @testset "Shading default" begin + plot = (attributes = Attributes(), ) # simplified "plot" + + # Based on number of lights + lights = Makie.AbstractLight[] + Makie.default_shading!(plot, lights) + @test !haskey(plot.attributes, :shading) + + plot.attributes[:shading] = Observable(Makie.automatic) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === NoShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, AmbientLight(RGBf(0.1, 0.1, 0.1))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === FastShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, DirectionalLight(RGBf(0.1, 0.1, 0.1), Vec3f(1))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === FastShading + + plot.attributes[:shading] = Observable(Makie.automatic) + push!(lights, PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0))) + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + # Based on light types + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [SpotLight(RGBf(0.1, 0.1, 0.1), Point3f(0), Vec3f(1), Vec2f(0.2, 0.3))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [EnvironmentLight(1.0, rand(2,2))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === NoShading # only affects RPRMakie so skipped here + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + plot.attributes[:shading] = Observable(Makie.automatic) + lights = [PointLight(RGBf(0.1, 0.1, 0.1), Point3f(0), Vec2f(0.1, 0.2))] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + + # keep existing shading type + lights = Makie.AbstractLight[] + Makie.default_shading!(plot, lights) + @test to_value(plot.attributes[:shading]) === MultiLightShading + end end diff --git a/test/zoom_pan.jl b/test/zoom_pan.jl index 8a5a3e5daa7..5fd708e80df 100644 --- a/test/zoom_pan.jl +++ b/test/zoom_pan.jl @@ -4,7 +4,7 @@ using Observables function cleanaxes() fig = Figure() ax = Axis(fig[1, 1]) - axbox = pixelarea(ax.scene)[] + axbox = viewport(ax.scene)[] lim = ax.finallimits[] e = events(ax.scene) return ax, axbox, lim, e @@ -79,7 +79,7 @@ end fig = Figure() ax = Axis(fig[1, 1]) plot!(ax, [10, 15, 20]) - axbox = pixelarea(ax.scene)[] + axbox = viewport(ax.scene)[] lim = ax.finallimits[] e = events(ax.scene)