From a5cd0524165acddee18a8aa8ed4cb38dd2138fb9 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 15 Jan 2023 21:05:07 +0530 Subject: [PATCH 01/80] Do not plot faces with NaN points in meshes Also allows NaN points to propagate ahead from surface to mesh. Solves https://www.github.com/MakieOrg/GeoMakie.jl/issues/133 --- CairoMakie/src/primitives.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 765295e6d0a..a2559bf86a9 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -859,13 +859,17 @@ end function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightpos, shininess, diffuse, ambient, specular) for k in reverse(zorder) - pattern = Cairo.CairoPatternMesh() f = meshfaces[k] # avoid SizedVector through Face indexing 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 + end facecolors = per_face_col[k] # light calculation @@ -887,6 +891,8 @@ 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) @@ -928,7 +934,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki end function surface2mesh(xs, ys, zs::AbstractMatrix) - ps = Makie.matrix_grid(p-> nan2zero.(p), xs, ys, zs) + ps = Makie.matrix_grid(identity, xs, ys, zs) rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) faces = decompose(QuadFace{Int}, rect) uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) From 079c553ceec8b9259d19587639a0a296491d6fa1 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 15 Jan 2023 21:15:14 +0530 Subject: [PATCH 02/80] Update NEWS.md --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index e372f9f3ec6..5373ee5263c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # News ## master +- Fixed an issue where NaN was interpreted as zero when rendering `surface` through CairoMakie. [#2598](https://github.com/MakieOrg/Makie.jl/pull/2598) - Fixed an issue where `poly` plots with `Vector{<: MultiPolygon}` inputs with per-polygon color were mistakenly rendered as meshes using CairoMakie. [#2590] From d8171553378579f8baf6cdcd6d072c4937b5209e Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 12:27:02 +0530 Subject: [PATCH 03/80] Skip NaN faces in the other mesh methods also --- CairoMakie/src/primitives.jl | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index a2559bf86a9..ed8f1f1b7c6 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -715,8 +715,15 @@ function draw_mesh2D(scene, screen, per_face_cols, space::Symbol, # This is a hack, which needs cleaning up in the Mesh plot type! for (f, (c1, c2, c3)) in zip(fs, per_face_cols) - pattern = Cairo.CairoPatternMesh() + t1, t2, t3 = project_position.(scene, space, vs[f], (model,)) #triangle points + + # don't draw any mesh faces with NaN components. + if isnan(t1) || isnan(t2) || isnan(t3) + continue + end + + pattern = Cairo.CairoPatternMesh() Cairo.mesh_pattern_begin_patch(pattern) Cairo.mesh_pattern_move_to(pattern, t1...) @@ -804,7 +811,8 @@ function draw_mesh3D( end ns = map(n -> normalize(normalmatrix * n), meshnormals) - # Liight math happens in view/camera space + + # Light math happens in view/camera space pointlight = Makie.get_point_light(scene) lightposition = if !isnothing(pointlight) pointlight.position[] @@ -871,6 +879,10 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, continue end + if isnan(t1) || isnan(t2) || isnan(t3) + continue + end + facecolors = per_face_col[k] # light calculation if shading From ce2a22b42944d53ef311285160a978817d8f2919 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 16:26:32 +0530 Subject: [PATCH 04/80] Add NaN-aware normal calculation code Basically replicates what's done in GLMakie's utils shader. Skips any combination of points which has a NaN when computing normals. --- CairoMakie/src/primitives.jl | 6 +- CairoMakie/src/utils.jl | 103 ++++++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index ed8f1f1b7c6..344c5167b50 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -762,7 +762,7 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f color = hasproperty(mesh, :color) ? mesh.color : color meshpoints = decompose(Point3f, mesh)::Vector{Point3f} meshfaces = decompose(GLTriangleFace, mesh)::Vector{GLTriangleFace} - meshnormals = decompose_normals(mesh)::Vector{Vec3f} + meshnormals = nan_aware_decompose_normals(mesh)::Vector{Vec3f} meshuvs = texturecoordinates(mesh)::Union{Nothing, Vector{Vec2f}} lowclip = get_color_attr(attributes, :lowclip) @@ -950,8 +950,8 @@ function surface2mesh(xs, ys, zs::AbstractMatrix) rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) faces = decompose(QuadFace{Int}, rect) uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) - uvm = GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv), faces) - return GeometryBasics.normal_mesh(uvm) + uvm = GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv), faces, ) + return nan_aware_normal_mesh(uvm) end ################################################################################ diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 41f26db3ffc..a490ecb554b 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -117,6 +117,15 @@ end to_uint32_color(c) = reinterpret(UInt32, convert(ARGB32, premultiplied_rgba(c))) +# handle patterns +function Cairo.CairoPattern(color::Makie.AbstractPattern) + # the Cairo y-coordinate are fliped + bitmappattern = reverse!(ARGB32.(Makie.to_image(color)); dims=2) + cairoimage = Cairo.CairoImageSurface(bitmappattern) + cairopattern = Cairo.CairoPattern(cairoimage) + return cairopattern +end + ######################################## # Image/heatmap -> ARGBSurface # ######################################## @@ -251,15 +260,95 @@ function mesh_pattern_set_corner_color(pattern, id, c::Colorant) Cairo.mesh_pattern_set_corner_color_rgba(pattern, id, rgbatuple(c)...) end -# not piracy -function Cairo.CairoPattern(color::Makie.AbstractPattern) - # the Cairo y-coordinate are fliped - bitmappattern = reverse!(ARGB32.(Makie.to_image(color)); dims=2) - cairoimage = Cairo.CairoImageSurface(bitmappattern) - cairopattern = Cairo.CairoPattern(cairoimage) - return cairopattern +# NaN-aware normal handling + + +""" + nan_aware_orthogonal_vector(v1, v2, v3) where N + +Returns an un-normalized normal vector for the triangle formed by the three input points. +Skips any combination of the inputs for which any point has a NaN component. +""" +function nan_aware_orthogonal_vector(v1, v2, v3) + centroid = Vec3f(((v1 .+ v2 .+ v3) ./ 3)...) + normal = [0.0, 0.0, 0.0] + # if the coord is NaN, then do not add. + (isnan(v1) | isnan(v2)) || (normal += cross(v2 .- centroid, v1 .- centroid)) + (isnan(v2) | isnan(v3)) || (normal += cross(v3 .- centroid, v2 .- centroid)) + (isnan(v3) | isnan(v1)) || (normal += cross(v1 .- centroid, v3 .- centroid)) + return Vec3f(normal).*-1 +end + +# Hijack GeometryBasics.jl machinery + +"A wrapper type which instructs `GeometryBasics.normals`` to use the NaN-aware path. Construct as `_NanAware{normaltype}()`." +struct _NanAware{T} +end + +# A NaN-aware version of GeometryBasics.normals. +function GeometryBasics.normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}, + ::_NanAware{N}) where {T,F<:NgonFace,N} + normals_result = zeros(N, length(vertices)) + free_verts = GeometryBasics.metafree.(vertices) + + for face in faces + + v1, v2, v3 = free_verts[face] + # we can get away with two edges since faces are planar. + n = nan_aware_orthogonal_vector(v1, v2, v3) + + for i in 1:length(F) + fi = face[i] + normals_result[fi] = normals_result[fi] + n + end + end + normals_result .= GeometryBasics.normalize.(normals_result) + return normals_result +end + +function GeometryBasics.normals(vertices::AbstractVector{<:AbstractPoint{2,T}}, faces::AbstractVector{F}, + normaltype::_NanAware{N}) where {T,F<:NgonFace,N} + return Vec2f.(GeometryBasics.normals(map(v -> Point3{T}(v..., 0), vertices), faces, normaltype)) +end + + +function GeometryBasics.normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D,T}}, faces::AbstractVector{F}, + normaltype::_NanAware{N}) where {D,T,F<:NgonFace,N} + return GeometryBasics.normals(collect(metafree.(vertices)), faces, normaltype) +end + +# Below are nan-aware versions of GeometryBasics functions. +# These are basically copied straight from GeometryBasics.jl, +# since the normal type on some of them is not exposed. + +function nan_aware_normal_mesh(primitive::GeometryBasics.Meshable{N}; nvertices=nothing) where {N} + if nvertices !== nothing + @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") + primitive = Tesselation(primitive, nvertices) + end + return GeometryBasics.mesh(primitive; pointtype=Point{N,Float32}, normaltype=_NanAware{Vec3f}(), + facetype=GLTriangleFace) +end + +function nan_aware_normal_mesh(points::AbstractVector{<:AbstractPoint}, + faces::AbstractVector{<:AbstractFace}) + _points = GeometryBasics.decompose(Point3f, points) + _faces = GeometryBasics.decompose(GeometryBasics.GLTriangleFace, faces) + return GeometryBasics.Mesh(GeometryBasics.meta(_points; normals=GeometryBasics.normals(_points, _faces, _NanAware{Vec3f}())), _faces) +end + +function nan_aware_decompose(NT::GeometryBasics.Normal{T}, primitive) where {T} + return GeometryBasics.collect_with_eltype(T, GeometryBasics.normals(GeometryBasics.coordinates(primitive), GeometryBasics.faces(primitive), _NanAware{T}())) end +nan_aware_decompose_normals(primitive) = nan_aware_decompose(GeometryBasics.Normal(), primitive) + + +################################################################################ +# Font handling # +################################################################################ + + """ Finds a font that can represent the unicode character! Returns Makie.defaultfont() if not representable! From 5440a236cb519bbfc48fa6ba10b7251e63b4f806 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 19:20:08 +0530 Subject: [PATCH 05/80] Apply nan code only to surfaces, not meshes Simplify the code a lot as well. --- CairoMakie/src/primitives.jl | 15 +++++------ CairoMakie/src/utils.jl | 48 +++++------------------------------- 2 files changed, 14 insertions(+), 49 deletions(-) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 344c5167b50..dbf453bc350 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -762,7 +762,7 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f color = hasproperty(mesh, :color) ? mesh.color : color meshpoints = decompose(Point3f, mesh)::Vector{Point3f} meshfaces = decompose(GLTriangleFace, mesh)::Vector{GLTriangleFace} - meshnormals = nan_aware_decompose_normals(mesh)::Vector{Vec3f} + meshnormals = decompose_normals(mesh)::Vector{Vec3f} # note: can be made NaN-aware. meshuvs = texturecoordinates(mesh)::Union{Nothing, Vector{Vec2f}} lowclip = get_color_attr(attributes, :lowclip) @@ -879,10 +879,6 @@ function draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, continue end - if isnan(t1) || isnan(t2) || isnan(t3) - continue - end - facecolors = per_face_col[k] # light calculation if shading @@ -946,12 +942,17 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki end function surface2mesh(xs, ys, zs::AbstractMatrix) + # crate a `Matrix{Point3}` ps = Makie.matrix_grid(identity, xs, ys, zs) + # create valid tessellations (triangulations) for the mesh + # knowing that it is a regular grid makes this simple rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) + # we use quad faces so that color handling is consistent faces = decompose(QuadFace{Int}, rect) + # create the uv (texture) vectors uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) - uvm = GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv), faces, ) - return nan_aware_normal_mesh(uvm) + # return a mesh with known uvs and normals. + return GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv, normals = nan_aware_normals(ps, faces)), faces, ) end ################################################################################ diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index a490ecb554b..882393c650c 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -279,16 +279,9 @@ function nan_aware_orthogonal_vector(v1, v2, v3) return Vec3f(normal).*-1 end -# Hijack GeometryBasics.jl machinery - -"A wrapper type which instructs `GeometryBasics.normals`` to use the NaN-aware path. Construct as `_NanAware{normaltype}()`." -struct _NanAware{T} -end - # A NaN-aware version of GeometryBasics.normals. -function GeometryBasics.normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}, - ::_NanAware{N}) where {T,F<:NgonFace,N} - normals_result = zeros(N, length(vertices)) +function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} + normals_result = zeros(Vec3f, length(vertices)) free_verts = GeometryBasics.metafree.(vertices) for face in faces @@ -306,44 +299,15 @@ function GeometryBasics.normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, return normals_result end -function GeometryBasics.normals(vertices::AbstractVector{<:AbstractPoint{2,T}}, faces::AbstractVector{F}, - normaltype::_NanAware{N}) where {T,F<:NgonFace,N} - return Vec2f.(GeometryBasics.normals(map(v -> Point3{T}(v..., 0), vertices), faces, normaltype)) +function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{2,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} + return Vec2f.(nan_aware_normals(map(v -> Point3{T}(v..., 0), vertices), faces)) end -function GeometryBasics.normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D,T}}, faces::AbstractVector{F}, - normaltype::_NanAware{N}) where {D,T,F<:NgonFace,N} - return GeometryBasics.normals(collect(metafree.(vertices)), faces, normaltype) -end - -# Below are nan-aware versions of GeometryBasics functions. -# These are basically copied straight from GeometryBasics.jl, -# since the normal type on some of them is not exposed. - -function nan_aware_normal_mesh(primitive::GeometryBasics.Meshable{N}; nvertices=nothing) where {N} - if nvertices !== nothing - @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") - primitive = Tesselation(primitive, nvertices) - end - return GeometryBasics.mesh(primitive; pointtype=Point{N,Float32}, normaltype=_NanAware{Vec3f}(), - facetype=GLTriangleFace) +function nan_aware_normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D,T}}, faces::AbstractVector{F}) where {D,T,F<:NgonFace} + return nan_aware_normals(collect(GeometryBasics.metafree.(vertices)), faces) end -function nan_aware_normal_mesh(points::AbstractVector{<:AbstractPoint}, - faces::AbstractVector{<:AbstractFace}) - _points = GeometryBasics.decompose(Point3f, points) - _faces = GeometryBasics.decompose(GeometryBasics.GLTriangleFace, faces) - return GeometryBasics.Mesh(GeometryBasics.meta(_points; normals=GeometryBasics.normals(_points, _faces, _NanAware{Vec3f}())), _faces) -end - -function nan_aware_decompose(NT::GeometryBasics.Normal{T}, primitive) where {T} - return GeometryBasics.collect_with_eltype(T, GeometryBasics.normals(GeometryBasics.coordinates(primitive), GeometryBasics.faces(primitive), _NanAware{T}())) -end - -nan_aware_decompose_normals(primitive) = nan_aware_decompose(GeometryBasics.Normal(), primitive) - - ################################################################################ # Font handling # ################################################################################ From dd60f10cbd803d755135e5fa2412b84f94be6827 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 19:20:15 +0530 Subject: [PATCH 06/80] Add a test --- ReferenceTests/src/tests/primitives.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 0d64146d5dd..6ef903825a4 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -414,3 +414,22 @@ end scene end + +@reference_test "Surface with NaN points" begin + N = 20 + + xs = LinRange(0, 6π, 10) + ys = LinRange(0, 6π, 10) + zs = sin.(xs) .* sin.(ys') + + zs[3:5, 5:8] .= NaN + + surface( + xs, ys, zs; + color = zs, + colormap = :RdBu, + shading = true # test shading as well. + ) +end + +end From 5978840857557876ec554d8bb0498dd310c9c990 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 20:25:16 +0530 Subject: [PATCH 07/80] Fix typo --- ReferenceTests/src/tests/primitives.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 6ef903825a4..e303676a1f6 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -1,4 +1,4 @@ -# @reference_test "lines and linestyles" begin +@reference_test "lines and linestyles" begin quote # For now disabled until we fix GLMakie linestyle s = Scene(resolution = (800, 800), camera = campixel!) From 9b14ec36c7a94ec31a1c354e7d2adc2b40eeec00 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 21:02:45 +0530 Subject: [PATCH 08/80] Move functionality from CairoMakie to Makie --- CairoMakie/src/utils.jl | 48 --------------------------- src/utilities/utilities.jl | 67 +++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 49 deletions(-) diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 882393c650c..0d074a544aa 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -260,54 +260,6 @@ function mesh_pattern_set_corner_color(pattern, id, c::Colorant) Cairo.mesh_pattern_set_corner_color_rgba(pattern, id, rgbatuple(c)...) end -# NaN-aware normal handling - - -""" - nan_aware_orthogonal_vector(v1, v2, v3) where N - -Returns an un-normalized normal vector for the triangle formed by the three input points. -Skips any combination of the inputs for which any point has a NaN component. -""" -function nan_aware_orthogonal_vector(v1, v2, v3) - centroid = Vec3f(((v1 .+ v2 .+ v3) ./ 3)...) - normal = [0.0, 0.0, 0.0] - # if the coord is NaN, then do not add. - (isnan(v1) | isnan(v2)) || (normal += cross(v2 .- centroid, v1 .- centroid)) - (isnan(v2) | isnan(v3)) || (normal += cross(v3 .- centroid, v2 .- centroid)) - (isnan(v3) | isnan(v1)) || (normal += cross(v1 .- centroid, v3 .- centroid)) - return Vec3f(normal).*-1 -end - -# A NaN-aware version of GeometryBasics.normals. -function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} - normals_result = zeros(Vec3f, length(vertices)) - free_verts = GeometryBasics.metafree.(vertices) - - for face in faces - - v1, v2, v3 = free_verts[face] - # we can get away with two edges since faces are planar. - n = nan_aware_orthogonal_vector(v1, v2, v3) - - for i in 1:length(F) - fi = face[i] - normals_result[fi] = normals_result[fi] + n - end - end - normals_result .= GeometryBasics.normalize.(normals_result) - return normals_result -end - -function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{2,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} - return Vec2f.(nan_aware_normals(map(v -> Point3{T}(v..., 0), vertices), faces)) -end - - -function nan_aware_normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D,T}}, faces::AbstractVector{F}) where {D,T,F<:NgonFace} - return nan_aware_normals(collect(GeometryBasics.metafree.(vertices)), faces) -end - ################################################################################ # Font handling # ################################################################################ diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index dcc2efa50bd..5cc1797a1b1 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -265,7 +265,7 @@ function to_vector(x::ClosedInterval, len, T) end """ -A colorsampler maps numnber values from a certain range to values of a colormap +A colorsampler maps number values from a certain range to values of a colormap ``` x = ColorSampler(colormap, (0.0, 1.0)) x[0.5] # returns color at half point of colormap @@ -322,6 +322,67 @@ function surface_normals(x, y, z) return vec(map(normal, CartesianIndices(z))) end + +############################################################ +# NaN-aware normal handling # +############################################################ + +""" + nan_aware_orthogonal_vector(v1, v2, v3) where N + +Returns an un-normalized normal vector for the triangle formed by the three input points. +Skips any combination of the inputs for which any point has a NaN component. +""" +function nan_aware_orthogonal_vector(v1, v2, v3) + centroid = Vec3f(((v1 .+ v2 .+ v3) ./ 3)...) + normal = [0.0, 0.0, 0.0] + # if the coord is NaN, then do not add. + (isnan(v1) | isnan(v2)) || (normal += cross(v2 .- centroid, v1 .- centroid)) + (isnan(v2) | isnan(v3)) || (normal += cross(v3 .- centroid, v2 .- centroid)) + (isnan(v3) | isnan(v1)) || (normal += cross(v1 .- centroid, v3 .- centroid)) + return Vec3f(normal).*-1 +end + +""" + nan_aware_normals(vertices::AbstractVector{<: Union{Point, PointMeta}}, faces::AbstractVector{F}) + +Computes the normals of a mesh defined by `vertices` and `faces` (a vector of `GeometryBasics.NgonFace`) +which ignores all contributions from points with `NaN` components. + +Equivalent in application to `GeometryBasics.normals`. +""" +function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} + normals_result = zeros(Vec3f, length(vertices)) + free_verts = GeometryBasics.metafree.(vertices) + + for face in faces + + v1, v2, v3 = free_verts[face] + # we can get away with two edges since faces are planar. + n = nan_aware_orthogonal_vector(v1, v2, v3) + + for i in 1:length(F) + fi = face[i] + normals_result[fi] = normals_result[fi] + n + end + end + normals_result .= GeometryBasics.normalize.(normals_result) + return normals_result +end + +function nan_aware_normals(vertices::AbstractVector{<:AbstractPoint{2,T}}, faces::AbstractVector{F}) where {T,F<:NgonFace} + return Vec2f.(nan_aware_normals(map(v -> Point3{T}(v..., 0), vertices), faces)) +end + + +function nan_aware_normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D,T}}, faces::AbstractVector{F}) where {D,T,F<:NgonFace} + return nan_aware_normals(collect(GeometryBasics.metafree.(vertices)), faces) +end + +############################################################ +# Matrix grid method for surface handling # +############################################################ + """ matrix_grid(f, x::AbstractArray, y::AbstractArray, z::AbstractMatrix)::Vector{Point3f} @@ -339,6 +400,10 @@ function matrix_grid(f, x::ClosedInterval, y::ClosedInterval, z::AbstractMatrix) matrix_grid(f, LinRange(extrema(x)..., size(z, 1)), LinRange(extrema(x)..., size(z, 2)), z) end +############################################################ +# Attribute key extraction # +############################################################ + function extract_keys(attributes, keys) attr = Attributes() for key in keys From c6725f66cef7d49da6e3efe3335d8a7c2d2911c6 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 21:02:58 +0530 Subject: [PATCH 09/80] Finally fix tests removed an extra 'end' --- ReferenceTests/src/tests/primitives.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index e303676a1f6..16e967f5368 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -1,4 +1,4 @@ -@reference_test "lines and linestyles" begin +# @reference_test "lines and linestyles" begin quote # For now disabled until we fix GLMakie linestyle s = Scene(resolution = (800, 800), camera = campixel!) @@ -431,5 +431,3 @@ end shading = true # test shading as well. ) end - -end From 3313da0d1f288d3d00b0549ca4628c147e285380 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Jan 2023 21:03:20 +0530 Subject: [PATCH 10/80] Complete the switch to Makie --- CairoMakie/src/primitives.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index dbf453bc350..a975fb8b456 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -952,7 +952,7 @@ function surface2mesh(xs, ys, zs::AbstractMatrix) # create the uv (texture) vectors uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) # return a mesh with known uvs and normals. - return GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv, normals = nan_aware_normals(ps, faces)), faces, ) + return GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv, normals = Makie.nan_aware_normals(ps, faces)), faces, ) end ################################################################################ From fd9354846689374d7df2be15722919c26e3c4b54 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 17 Jan 2023 16:42:18 +0100 Subject: [PATCH 11/80] simplify orthogonal_vector and avoid triangles in surface --- CairoMakie/src/primitives.jl | 2 ++ src/utilities/utilities.jl | 9 ++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index a975fb8b456..4afccd35c54 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -949,6 +949,8 @@ function surface2mesh(xs, ys, zs::AbstractMatrix) rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) # we use quad faces so that color handling is consistent faces = decompose(QuadFace{Int}, rect) + # and remove quads that contain a NaN coordinate to avoid drawing triangles + faces = filter(f -> !any(i -> isnan(ps[i]), f), faces) # create the uv (texture) vectors uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) # return a mesh with known uvs and normals. diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 5cc1797a1b1..901416501d2 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -334,13 +334,8 @@ Returns an un-normalized normal vector for the triangle formed by the three inpu Skips any combination of the inputs for which any point has a NaN component. """ function nan_aware_orthogonal_vector(v1, v2, v3) - centroid = Vec3f(((v1 .+ v2 .+ v3) ./ 3)...) - normal = [0.0, 0.0, 0.0] - # if the coord is NaN, then do not add. - (isnan(v1) | isnan(v2)) || (normal += cross(v2 .- centroid, v1 .- centroid)) - (isnan(v2) | isnan(v3)) || (normal += cross(v3 .- centroid, v2 .- centroid)) - (isnan(v3) | isnan(v1)) || (normal += cross(v1 .- centroid, v3 .- centroid)) - return Vec3f(normal).*-1 + (isnan(v1) || isnan(v2) || isnan(v3)) && return Vec3f(0) + return Vec3f(cross(v2 - v1, v3 - v1)) end """ From 9402fce5fbd048e37820d94b374b42765fafeb53 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 19 Jan 2023 08:01:14 +0530 Subject: [PATCH 12/80] Add a better test Co-committed-by: Frederic Freyer --- ReferenceTests/src/tests/primitives.jl | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 16e967f5368..116049ce386 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -416,18 +416,15 @@ end end @reference_test "Surface with NaN points" begin - N = 20 - - xs = LinRange(0, 6π, 10) - ys = LinRange(0, 6π, 10) - zs = sin.(xs) .* sin.(ys') - - zs[3:5, 5:8] .= NaN - - surface( - xs, ys, zs; - color = zs, - colormap = :RdBu, - shading = true # test shading as well. - ) + # prepare surface data + zs = rand(10, 10) + ns = copy(zs) + ns[4, 3:6] .= NaN + # plot surface + f, a, p = surface(1..10, 1..10, ns, colormap = [:lightblue, :lightblue]) + # plot a wireframe so we can see what's going on, and in which cells. + m = CairoMakie.surface2mesh(to_value.(p.converted)...) + scatter!(a, m.position, color = isnan.(m.normals), depth_shift = -1f-3) + wireframe!(a, m, depth_shift = -1f-3, color = :black) + f end From 4c2e4b40e04b3e915a4b63b996d118947c277d48 Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Tue, 24 Jan 2023 19:48:30 +0100 Subject: [PATCH 13/80] fix test --- ReferenceTests/src/tests/primitives.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 116049ce386..97c5c00a9de 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -423,7 +423,17 @@ end # plot surface f, a, p = surface(1..10, 1..10, ns, colormap = [:lightblue, :lightblue]) # plot a wireframe so we can see what's going on, and in which cells. - m = CairoMakie.surface2mesh(to_value.(p.converted)...) + m = let + xs, ys, zs = to_value.(p.converted) + ps = Makie.matrix_grid(identity, xs, ys, zs) + rect = Makie.Tesselation(Rect2f(0, 0, 1, 1), size(zs)) + faces = Makie.decompose(Makie.QuadFace{Int}, rect) + faces = filter(f -> !any(i -> isnan(ps[i]), f), faces) + uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), Makie.decompose_uv(rect)) + Makie.GeometryBasics.Mesh( + Makie.meta(ps; uv=uv, normals = Makie.nan_aware_normals(ps, faces)), faces, + ) + end scatter!(a, m.position, color = isnan.(m.normals), depth_shift = -1f-3) wireframe!(a, m, depth_shift = -1f-3, color = :black) f From db0e4005a58b1866d65118956d59ba9288bb9d5f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Jan 2023 16:38:50 +0100 Subject: [PATCH 14/80] fix NaN value rendering in GLMakie --- GLMakie/assets/shader/surface.vert | 8 +- GLMakie/assets/shader/util.vert | 119 +++++++++++++++++++++++++---- GLMakie/src/glshaders/surface.jl | 2 +- 3 files changed, 108 insertions(+), 21 deletions(-) diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index 0388819e0e1..1c5cc1be152 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -47,6 +47,10 @@ vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, vec2 uv); vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, vec2 uv); vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, vec2 uv); +vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, ivec2 uv); +vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, ivec2 uv); +vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv); + uniform uint objectid; uniform vec2 uv_scale; flat out uvec2 o_id; @@ -67,9 +71,5 @@ void main() vec3 normalvec = {{normal_calc}}; o_color = vec4(0.0); - // we still want to render NaN values... TODO: make value customizable? - if (isnan(pos.z)) { - pos.z = 0.0; - } render(model * vec4(pos, 1), normalvec, view, projection, lightposition); } diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index dfd42427ccb..ea5dd9dbd82 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -29,6 +29,12 @@ vec2 grid_pos(Grid2D position, vec2 uv){ ); } +vec2 grid_pos(Grid2D position, ivec2 uv, ivec2 size){ + return vec2( + (1 - uv.x / (size.x - 1)) * position.start[0] + uv.x / (size.x - 1) * position.stop[0], + (1 - uv.y / (size.y - 1)) * position.start[1] + uv.y / (size.y - 1) * position.stop[1] + ); +} // stretch is vec3 stretch(vec3 val, vec3 from, vec3 to){ @@ -265,22 +271,18 @@ vec3 normal_from_points( vec3 s0, vec3 s1, vec3 s2, vec3 s3, vec3 s4, vec2 uv, vec2 off1, vec2 off2, vec2 off3, vec2 off4 ){ - vec3 result = vec3(0); - if(isinbounds(off1) && isinbounds(off2)) - { - result += cross(s2-s0, s1-s0); - } - if(isinbounds(off2) && isinbounds(off3)) - { - result += cross(s3-s0, s2-s0); - } - if(isinbounds(off3) && isinbounds(off4)) - { - result += cross(s4-s0, s3-s0); - } - if(isinbounds(off4) && isinbounds(off1)) - { - result += cross(s1-s0, s4-s0); + vec3 result = vec3(0,0,0); + // isnan checks should avoid darkening around NaN positions but may not + // work with all systems + if (!isnan(s0.z)) { + bool check1 = isinbounds(off1) && !isnan(s1.z); + bool check2 = isinbounds(off2) && !isnan(s2.z); + bool check3 = isinbounds(off3) && !isnan(s3.z); + bool check4 = isinbounds(off4) && !isnan(s4.z); + if (check1 && check2) result += cross(s2-s0, s1-s0); + if (check2 && check3) result += cross(s3-s0, s2-s0); + if (check3 && check4) result += cross(s4-s0, s3-s0); + if (check4 && check1) result += cross(s1-s0, s4-s0); } // normal should be zero, but needs to be here, because the dead-code // elimanation of GLSL is overly enthusiastic @@ -354,6 +356,91 @@ vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, vec2 uv){ return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); } + +//////////////////////////////////////////////////////////////////////////////// +// Versions using ivec2 and texelFetch +//////////////////////////////////////////////////////////////////////////////// + + +bool isinbounds(ivec2 uv, ivec2 size) +{ + return (0 <= uv.x && uv.x < size.x && 0 <= uv.y && uv.y < size.y); +} + +vec3 normal_from_points( + vec3 s0, vec3 s1, vec3 s2, vec3 s3, vec3 s4, + ivec2 uv, ivec2 off1, ivec2 off2, ivec2 off3, ivec2 off4, ivec2 size + ){ + vec3 result = vec3(0,0,0); + // isnan checks should avoid darkening around NaN positions but may not + // work with all systems + if (!isnan(s0.z)) { + bool check1 = isinbounds(off1, size) && !isnan(s1.z); + bool check2 = isinbounds(off2, size) && !isnan(s2.z); + bool check3 = isinbounds(off3, size) && !isnan(s3.z); + bool check4 = isinbounds(off4, size) && !isnan(s4.z); + if (check1 && check2) result += cross(s2-s0, s1-s0); + if (check2 && check3) result += cross(s3-s0, s2-s0); + if (check3 && check4) result += cross(s4-s0, s3-s0); + if (check4 && check1) result += cross(s1-s0, s4-s0); + } + // normal should be zero, but needs to be here, because the dead-code + // elimanation of GLSL is overly enthusiastic + return normalize(result); +} + +vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2( 0, 1); + ivec2 off3 = uv + ivec2( 1, 0); + ivec2 off4 = uv + ivec2( 0, -1); + + s0 = vec3(texelFetch(xs, uv, 0).x, texelFetch(ys, uv, 0).x, texelFetch(zs, uv, 0).x); + s1 = vec3(texelFetch(xs, off1, 0).x, texelFetch(ys, off1, 0).x, texelFetch(zs, off1, 0).x); + s2 = vec3(texelFetch(xs, off2, 0).x, texelFetch(ys, off2, 0).x, texelFetch(zs, off2, 0).x); + s3 = vec3(texelFetch(xs, off3, 0).x, texelFetch(ys, off3, 0).x, texelFetch(zs, off3, 0).x); + s4 = vec3(texelFetch(xs, off4, 0).x, texelFetch(ys, off4, 0).x, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4, textureSize(zs, 0)); +} + + +vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, ivec2 uv){ + ivec2 size = textureSize(zs, 0).xy; + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2( 0, 1); + ivec2 off3 = uv + ivec2( 1, 0); + ivec2 off4 = uv + ivec2( 0, -1); + + s0 = vec3(grid_pos(pos, uv, size).xy, texelFetch(zs, uv, 0).x); + s1 = vec3(grid_pos(pos, off1, size).xy, texelFetch(zs, off1, 0).x); + s2 = vec3(grid_pos(pos, off2, size).xy, texelFetch(zs, off2, 0).x); + s3 = vec3(grid_pos(pos, off3, size).xy, texelFetch(zs, off3, 0).x); + s4 = vec3(grid_pos(pos, off4, size).xy, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4, size); +} + + +vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2( 0, 1); + ivec2 off3 = uv + ivec2( 1, 0); + ivec2 off4 = uv + ivec2( 0, -1); + + s0 = vec3(texelFetch(xs, uv.x, 0).x, texelFetch(ys, uv.y, 0).x, texelFetch(zs, uv, 0).x); + s1 = vec3(texelFetch(xs, off1.x, 0).x, texelFetch(ys, off1.y, 0).x, texelFetch(zs, off1, 0).x); + s2 = vec3(texelFetch(xs, off2.x, 0).x, texelFetch(ys, off2.y, 0).x, texelFetch(zs, off2, 0).x); + s3 = vec3(texelFetch(xs, off3.x, 0).x, texelFetch(ys, off3.y, 0).x, texelFetch(zs, off3, 0).x); + s4 = vec3(texelFetch(xs, off4.x, 0).x, texelFetch(ys, off4.y, 0).x, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4, textureSize(zs, 0)); +} + + uniform vec4 highclip; uniform vec4 lowclip; uniform vec4 nan_color; diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index c4d5961fc1a..300c525baf7 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -6,7 +6,7 @@ end function normal_calc(x::Bool, invert_normals::Bool = false) i = invert_normals ? "-" : "" if x - return "$(i)getnormal(position, position_x, position_y, position_z, o_uv);" + return "$(i)getnormal(position, position_x, position_y, position_z, index2D);" else return "vec3(0, 0, $(i)1);" end From 2af28458b867cadee18fd5ea86148a11582c319d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 28 Jan 2023 21:46:35 +0100 Subject: [PATCH 15/80] fix WGLMakie surfaces --- CairoMakie/src/primitives.jl | 17 +---------------- WGLMakie/src/imagelike.jl | 27 +++++++++++++++------------ src/utilities/utilities.jl | 20 +++++++++++++++++++- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index eef9a6c2ff2..27b32f7fb47 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -922,7 +922,7 @@ end function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Makie.Surface)) # Pretend the surface plot is a mesh plot and plot that instead - mesh = surface2mesh(primitive[1][], primitive[2][], primitive[3][]) + mesh = Makie.surface2mesh(primitive[1][], primitive[2][], primitive[3][]) old = primitive[:color] if old[] === nothing primitive[:color] = primitive[3] @@ -935,21 +935,6 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki return nothing end -function surface2mesh(xs, ys, zs::AbstractMatrix) - # crate a `Matrix{Point3}` - ps = Makie.matrix_grid(identity, xs, ys, zs) - # create valid tessellations (triangulations) for the mesh - # knowing that it is a regular grid makes this simple - rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) - # we use quad faces so that color handling is consistent - faces = decompose(QuadFace{Int}, rect) - # and remove quads that contain a NaN coordinate to avoid drawing triangles - faces = filter(f -> !any(i -> isnan(ps[i]), f), faces) - # create the uv (texture) vectors - uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) - # return a mesh with known uvs and normals. - return GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv, normals = Makie.nan_aware_normals(ps, faces)), faces, ) -end ################################################################################ # MeshScatter # diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl index d589034a546..5ab72f89957 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -90,26 +90,29 @@ end 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)) + + _mesh = map(Makie.surface2mesh, px, py, pz, transform_func_obs(plot), get(plot, :space, :data)) + positions = Buffer(map(m -> decompose(Point3f, m), _mesh)) + faces = Buffer(map(m -> decompose(GLTriangleFace, m), _mesh)) + uv = Buffer(map(decompose_uv, _mesh)) + normals = Buffer(map(decompose_normals, _mesh)) + vertices = GeometryBasics.meta(positions; uv=uv, normals=normals) + mesh = GeometryBasics.Mesh(vertices, faces) + plot_attributes = copy(plot.attributes) - pcolor = if haskey(plot, :color) && plot.color[] isa AbstractArray + minfilter = to_value(get(plot, :interpolate, true)) ? :linear : :nearest + color = if haskey(plot, :color) && plot.color[] isa AbstractArray if plot.color[] isa AbstractMatrix{<:Colorant} delete!(plot_attributes, :colormap) delete!(plot_attributes, :colorrange) end plot.color + Sampler(lift(x -> el32convert(to_color(permutedims(x))), plot.color), minfilter=minfilter) else - pz + Sampler(lift(x -> el32convert(to_color(x)), pz), minfilter=minfilter) end - minfilter = to_value(get(plot, :interpolate, true)) ? :linear : :nearest - color = Sampler(lift(x -> el32convert(to_color(permutedims(x))), pcolor), minfilter=minfilter) - normals = Buffer(lift(surface_normals, px, py, pz)) - vertices = GeometryBasics.meta(positions; uv=uv, normals=normals) - mesh = GeometryBasics.Mesh(vertices, faces) + + return draw_mesh(mscene, mesh, plot_attributes; uniform_color=color, color=false, shading=plot.shading, diffuse=plot.diffuse, specular=plot.specular, shininess=plot.shininess, diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 901416501d2..bf54abb4742 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -324,7 +324,7 @@ end ############################################################ -# NaN-aware normal handling # +# NaN-aware normal & mesh handling # ############################################################ """ @@ -374,6 +374,24 @@ function nan_aware_normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D return nan_aware_normals(collect(GeometryBasics.metafree.(vertices)), faces) end +function surface2mesh(xs, ys, zs::AbstractMatrix, transform_func = identity, space = :data) + # crate a `Matrix{Point3}` + # ps = matrix_grid(identity, xs, ys, zs) + ps = matrix_grid(p -> apply_transform(transform_func, p, space), xs, ys, zs) + # create valid tessellations (triangulations) for the mesh + # knowing that it is a regular grid makes this simple + rect = Tesselation(Rect2f(0, 0, 1, 1), size(zs)) + # we use quad faces so that color handling is consistent + faces = decompose(QuadFace{Int}, rect) + # and remove quads that contain a NaN coordinate to avoid drawing triangles + faces = filter(f -> !any(i -> isnan(ps[i]), f), faces) + # create the uv (texture) vectors + uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), decompose_uv(rect)) + # return a mesh with known uvs and normals. + return GeometryBasics.Mesh(GeometryBasics.meta(ps; uv=uv, normals = nan_aware_normals(ps, faces)), faces, ) +end + + ############################################################ # Matrix grid method for surface handling # ############################################################ From 7ef034c42941743ddbb48fa1df7a73bcef936615 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Jan 2023 01:08:41 +0100 Subject: [PATCH 16/80] fix the wrong uv instead --- GLMakie/assets/shader/surface.vert | 5 ++ GLMakie/assets/shader/util.vert | 85 ------------------------------ GLMakie/src/glshaders/surface.jl | 13 +++-- 3 files changed, 13 insertions(+), 90 deletions(-) diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index 1c5cc1be152..4ce47665eab 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -71,5 +71,10 @@ void main() vec3 normalvec = {{normal_calc}}; o_color = vec4(0.0); + // we still want to render NaN values... TODO: make value customizable? + if (isnan(pos.z)) { + pos.z = 0.0; + } + render(model * vec4(pos, 1), normalvec, view, projection, lightposition); } diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index ea5dd9dbd82..ac3af127260 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -356,91 +356,6 @@ vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, vec2 uv){ return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); } - -//////////////////////////////////////////////////////////////////////////////// -// Versions using ivec2 and texelFetch -//////////////////////////////////////////////////////////////////////////////// - - -bool isinbounds(ivec2 uv, ivec2 size) -{ - return (0 <= uv.x && uv.x < size.x && 0 <= uv.y && uv.y < size.y); -} - -vec3 normal_from_points( - vec3 s0, vec3 s1, vec3 s2, vec3 s3, vec3 s4, - ivec2 uv, ivec2 off1, ivec2 off2, ivec2 off3, ivec2 off4, ivec2 size - ){ - vec3 result = vec3(0,0,0); - // isnan checks should avoid darkening around NaN positions but may not - // work with all systems - if (!isnan(s0.z)) { - bool check1 = isinbounds(off1, size) && !isnan(s1.z); - bool check2 = isinbounds(off2, size) && !isnan(s2.z); - bool check3 = isinbounds(off3, size) && !isnan(s3.z); - bool check4 = isinbounds(off4, size) && !isnan(s4.z); - if (check1 && check2) result += cross(s2-s0, s1-s0); - if (check2 && check3) result += cross(s3-s0, s2-s0); - if (check3 && check4) result += cross(s4-s0, s3-s0); - if (check4 && check1) result += cross(s1-s0, s4-s0); - } - // normal should be zero, but needs to be here, because the dead-code - // elimanation of GLSL is overly enthusiastic - return normalize(result); -} - -vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, ivec2 uv){ - vec3 s0, s1, s2, s3, s4; - ivec2 off1 = uv + ivec2(-1, 0); - ivec2 off2 = uv + ivec2( 0, 1); - ivec2 off3 = uv + ivec2( 1, 0); - ivec2 off4 = uv + ivec2( 0, -1); - - s0 = vec3(texelFetch(xs, uv, 0).x, texelFetch(ys, uv, 0).x, texelFetch(zs, uv, 0).x); - s1 = vec3(texelFetch(xs, off1, 0).x, texelFetch(ys, off1, 0).x, texelFetch(zs, off1, 0).x); - s2 = vec3(texelFetch(xs, off2, 0).x, texelFetch(ys, off2, 0).x, texelFetch(zs, off2, 0).x); - s3 = vec3(texelFetch(xs, off3, 0).x, texelFetch(ys, off3, 0).x, texelFetch(zs, off3, 0).x); - s4 = vec3(texelFetch(xs, off4, 0).x, texelFetch(ys, off4, 0).x, texelFetch(zs, off4, 0).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4, textureSize(zs, 0)); -} - - -vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, ivec2 uv){ - ivec2 size = textureSize(zs, 0).xy; - vec3 s0, s1, s2, s3, s4; - ivec2 off1 = uv + ivec2(-1, 0); - ivec2 off2 = uv + ivec2( 0, 1); - ivec2 off3 = uv + ivec2( 1, 0); - ivec2 off4 = uv + ivec2( 0, -1); - - s0 = vec3(grid_pos(pos, uv, size).xy, texelFetch(zs, uv, 0).x); - s1 = vec3(grid_pos(pos, off1, size).xy, texelFetch(zs, off1, 0).x); - s2 = vec3(grid_pos(pos, off2, size).xy, texelFetch(zs, off2, 0).x); - s3 = vec3(grid_pos(pos, off3, size).xy, texelFetch(zs, off3, 0).x); - s4 = vec3(grid_pos(pos, off4, size).xy, texelFetch(zs, off4, 0).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4, size); -} - - -vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv){ - vec3 s0, s1, s2, s3, s4; - ivec2 off1 = uv + ivec2(-1, 0); - ivec2 off2 = uv + ivec2( 0, 1); - ivec2 off3 = uv + ivec2( 1, 0); - ivec2 off4 = uv + ivec2( 0, -1); - - s0 = vec3(texelFetch(xs, uv.x, 0).x, texelFetch(ys, uv.y, 0).x, texelFetch(zs, uv, 0).x); - s1 = vec3(texelFetch(xs, off1.x, 0).x, texelFetch(ys, off1.y, 0).x, texelFetch(zs, off1, 0).x); - s2 = vec3(texelFetch(xs, off2.x, 0).x, texelFetch(ys, off2.y, 0).x, texelFetch(zs, off2, 0).x); - s3 = vec3(texelFetch(xs, off3.x, 0).x, texelFetch(ys, off3.y, 0).x, texelFetch(zs, off3, 0).x); - s4 = vec3(texelFetch(xs, off4.x, 0).x, texelFetch(ys, off4.y, 0).x, texelFetch(zs, off4, 0).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4, textureSize(zs, 0)); -} - - uniform vec4 highclip; uniform vec4 lowclip; uniform vec4 nan_color; diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index 300c525baf7..714e174d772 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -6,7 +6,7 @@ end function normal_calc(x::Bool, invert_normals::Bool = false) i = invert_normals ? "-" : "" if x - return "$(i)getnormal(position, position_x, position_y, position_z, index2D);" + return "$(i)getnormal(position, position_x, position_y, position_z, index01);" else return "vec3(0, 0, $(i)1);" end @@ -32,7 +32,8 @@ function _position_calc( """ int index1D = index + offseti.x + offseti.y * dims.x + (index/(dims.x-1)); ivec2 index2D = ind2sub(dims, index1D); - vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); + vec2 index01 = (vec2(index2D) + 0.5) / (vec2(dims)); + pos = vec3( texelFetch(position_x, index2D, 0).x, texelFetch(position_y, index2D, 0).x, @@ -48,7 +49,8 @@ function _position_calc( """ int index1D = index + offseti.x + offseti.y * dims.x + (index/(dims.x-1)); ivec2 index2D = ind2sub(dims, index1D); - vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); + vec2 index01 = (vec2(index2D) + 0.5) / (vec2(dims)); + pos = vec3( texelFetch(position_x, index2D.x, 0).x, texelFetch(position_y, index2D.y, 0).x, @@ -78,8 +80,9 @@ function _position_calc( """ int index1D = index + offseti.x + offseti.y * dims.x + (index/(dims.x-1)); ivec2 index2D = ind2sub(dims, index1D); - vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); - float height = texture(position_z, index01).x; + vec2 index01 = (vec2(index2D) + 0.5) / (vec2(dims)); + + float height = texelFetch(position_z, index2D, 0).x; pos = vec3(grid_pos(position, index01), height); """ end From 8e00d1009ad11eb4f95af522f13bed8ea0d19d44 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Jan 2023 01:24:11 +0100 Subject: [PATCH 17/80] simplify test --- ReferenceTests/src/tests/primitives.jl | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 97c5c00a9de..00cad8c81f3 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -417,23 +417,13 @@ end @reference_test "Surface with NaN points" begin # prepare surface data - zs = rand(10, 10) + zs = [x^2 + y^2 for x in range(-2, 0, length=10), y in range(-2, 0, length=10)] ns = copy(zs) ns[4, 3:6] .= NaN # plot surface f, a, p = surface(1..10, 1..10, ns, colormap = [:lightblue, :lightblue]) # plot a wireframe so we can see what's going on, and in which cells. - m = let - xs, ys, zs = to_value.(p.converted) - ps = Makie.matrix_grid(identity, xs, ys, zs) - rect = Makie.Tesselation(Rect2f(0, 0, 1, 1), size(zs)) - faces = Makie.decompose(Makie.QuadFace{Int}, rect) - faces = filter(f -> !any(i -> isnan(ps[i]), f), faces) - uv = map(x-> Vec2f(1f0 - x[2], 1f0 - x[1]), Makie.decompose_uv(rect)) - Makie.GeometryBasics.Mesh( - Makie.meta(ps; uv=uv, normals = Makie.nan_aware_normals(ps, faces)), faces, - ) - end + m = Makie.surface2mesh(to_value.(p.converted)...) scatter!(a, m.position, color = isnan.(m.normals), depth_shift = -1f-3) wireframe!(a, m, depth_shift = -1f-3, color = :black) f From 8e79b41d6ac4e8caae3686de2569779a08fe4ac1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 29 Jan 2023 20:44:22 +0100 Subject: [PATCH 18/80] use texelFetch for normal calc --- GLMakie/assets/shader/surface.vert | 113 ++++++++++++++++++++++++++-- GLMakie/assets/shader/util.vert | 116 +---------------------------- GLMakie/src/glshaders/surface.jl | 2 +- 3 files changed, 108 insertions(+), 123 deletions(-) diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index 4ce47665eab..e268131452a 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -42,14 +42,111 @@ vec2 grid_pos(Grid2D pos, vec2 uv); vec2 linear_index(ivec2 dims, int index); vec2 linear_index(ivec2 dims, int index, vec2 offset); vec4 linear_texture(sampler2D tex, int index, vec2 offset); -// vec3 getnormal_fast(sampler2D zvalues, ivec2 uv); -vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, vec2 uv); -vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, vec2 uv); -vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, vec2 uv); - -vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, ivec2 uv); -vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, ivec2 uv); -vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv); + + +// Normal generation + +vec3 getnormal_fast(sampler2D zvalues, ivec2 uv) +{ + vec3 a = vec3(0, 0, 0); + vec3 b = vec3(1, 1, 0); + a.z = texelFetch(zvalues, uv, 0).r; + b.z = texelFetch(zvalues, uv + ivec2(1, 1), 0).r; + return normalize(a - b); +} + +bool isinbounds(ivec2 uv, ivec2 size) +{ + return (uv.x < size.x && uv.y < size.y && uv.x >= 0 && uv.y >= 0); +} + +/* +Computes normal at s0 based on four surrounding positions s1 ... s4 and the +respective uv coordinates uv, off1, ..., off4 + + s2 + s1 s0 s3 + s4 +*/ +vec3 normal_from_points( + vec3 s0, vec3 s1, vec3 s2, vec3 s3, vec3 s4, + ivec2 off1, ivec2 off2, ivec2 off3, ivec2 off4, ivec2 size + ){ + vec3 result = vec3(0,0,0); + // isnan checks should avoid darkening around NaN positions but may not + // work with all systems + if (!isnan(s0.z)) { + bool check1 = isinbounds(off1, size) && !isnan(s1.z); + bool check2 = isinbounds(off2, size) && !isnan(s2.z); + bool check3 = isinbounds(off3, size) && !isnan(s3.z); + bool check4 = isinbounds(off4, size) && !isnan(s4.z); + if (check1 && check2) result += cross(s2-s0, s1-s0); + if (check2 && check3) result += cross(s3-s0, s2-s0); + if (check3 && check4) result += cross(s4-s0, s3-s0); + if (check4 && check1) result += cross(s1-s0, s4-s0); + } + // normal should be zero, but needs to be here, because the dead-code + // elimanation of GLSL is overly enthusiastic + return normalize(result); +} + +// Overload for surface(Matrix, Matrix, Matrix) +vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2(0, 1); + ivec2 off3 = uv + ivec2(1, 0); + ivec2 off4 = uv + ivec2(0, -1); + + s0 = vec3(texelFetch(xs, uv, 0).x, texelFetch(ys, uv, 0).x, texelFetch(zs, uv, 0).x); + s1 = vec3(texelFetch(xs, off1, 0).x, texelFetch(ys, off1, 0).x, texelFetch(zs, off1, 0).x); + s2 = vec3(texelFetch(xs, off2, 0).x, texelFetch(ys, off2, 0).x, texelFetch(zs, off2, 0).x); + s3 = vec3(texelFetch(xs, off3, 0).x, texelFetch(ys, off3, 0).x, texelFetch(zs, off3, 0).x); + s4 = vec3(texelFetch(xs, off4, 0).x, texelFetch(ys, off4, 0).x, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, off1, off2, off3, off4, textureSize(zs, 0)); +} + + +// Overload for (range, range, Matrix) surface plots +// Though this is only called by surface(Matrix) +vec2 grid_pos(Grid2D position, ivec2 uv, ivec2 size); + +vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2(0, 1); + ivec2 off3 = uv + ivec2(1, 0); + ivec2 off4 = uv + ivec2(0, -1); + ivec2 size = textureSize(zs, 0); + + s0 = vec3(grid_pos(pos, uv, size).xy, texelFetch(zs, uv, 0).x); + s1 = vec3(grid_pos(pos, off1, size).xy, texelFetch(zs, off1, 0).x); + s2 = vec3(grid_pos(pos, off2, size).xy, texelFetch(zs, off2, 0).x); + s3 = vec3(grid_pos(pos, off3, size).xy, texelFetch(zs, off3, 0).x); + s4 = vec3(grid_pos(pos, off4, size).xy, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, off1, off2, off3, off4, size); +} + + +// Overload for surface(Vector, Vector, Matrix) +// Makie converts almost everything to this +vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv){ + vec3 s0, s1, s2, s3, s4; + ivec2 off1 = uv + ivec2(-1, 0); + ivec2 off2 = uv + ivec2(0, 1); + ivec2 off3 = uv + ivec2(1, 0); + ivec2 off4 = uv + ivec2(0, -1); + + s0 = vec3(texelFetch(xs, uv.x, 0).x, texelFetch(ys, uv.y, 0).x, texelFetch(zs, uv, 0).x); + s1 = vec3(texelFetch(xs, off1.x, 0).x, texelFetch(ys, off1.y, 0).x, texelFetch(zs, off1, 0).x); + s2 = vec3(texelFetch(xs, off2.x, 0).x, texelFetch(ys, off2.y, 0).x, texelFetch(zs, off2, 0).x); + s3 = vec3(texelFetch(xs, off3.x, 0).x, texelFetch(ys, off3.y, 0).x, texelFetch(zs, off3, 0).x); + s4 = vec3(texelFetch(xs, off4.x, 0).x, texelFetch(ys, off4.y, 0).x, texelFetch(zs, off4, 0).x); + + return normal_from_points(s0, s1, s2, s3, s4, off1, off2, off3, off4, textureSize(zs, 0)); +} uniform uint objectid; uniform vec2 uv_scale; diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index ac3af127260..58c14bf81e0 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -31,8 +31,8 @@ vec2 grid_pos(Grid2D position, vec2 uv){ vec2 grid_pos(Grid2D position, ivec2 uv, ivec2 size){ return vec2( - (1 - uv.x / (size.x - 1)) * position.start[0] + uv.x / (size.x - 1) * position.stop[0], - (1 - uv.y / (size.y - 1)) * position.start[1] + uv.y / (size.y - 1) * position.stop[1] + (1.0 - (uv.x + 0.5) / size.x) * position.start[0] + (uv.x + 0.5) / size.x * position.stop[0], + (1.0 - (uv.y + 0.5) / size.y) * position.start[1] + (uv.y + 0.5) / size.y * position.stop[1] ); } @@ -243,118 +243,6 @@ void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 l o_view_pos = view_pos.xyz / view_pos.w; } -// -vec3 getnormal_fast(sampler2D zvalues, ivec2 uv) -{ - vec3 a = vec3(0, 0, 0); - vec3 b = vec3(1, 1, 0); - a.z = texelFetch(zvalues, uv, 0).r; - b.z = texelFetch(zvalues, uv + ivec2(1, 1), 0).r; - return normalize(a - b); -} - -bool isinbounds(vec2 uv) -{ - return (uv.x <= 1.0 && uv.y <= 1.0 && uv.x >= 0.0 && uv.y >= 0.0); -} - - -/* -Computes normal at s0 based on four surrounding positions s1 ... s4 and the -respective uv coordinates uv, off1, ..., off4 - - s2 - s1 s0 s3 - s4 -*/ -vec3 normal_from_points( - vec3 s0, vec3 s1, vec3 s2, vec3 s3, vec3 s4, - vec2 uv, vec2 off1, vec2 off2, vec2 off3, vec2 off4 - ){ - vec3 result = vec3(0,0,0); - // isnan checks should avoid darkening around NaN positions but may not - // work with all systems - if (!isnan(s0.z)) { - bool check1 = isinbounds(off1) && !isnan(s1.z); - bool check2 = isinbounds(off2) && !isnan(s2.z); - bool check3 = isinbounds(off3) && !isnan(s3.z); - bool check4 = isinbounds(off4) && !isnan(s4.z); - if (check1 && check2) result += cross(s2-s0, s1-s0); - if (check2 && check3) result += cross(s3-s0, s2-s0); - if (check3 && check4) result += cross(s4-s0, s3-s0); - if (check4 && check1) result += cross(s1-s0, s4-s0); - } - // normal should be zero, but needs to be here, because the dead-code - // elimanation of GLSL is overly enthusiastic - return normalize(result); -} - -// Overload for surface(Matrix, Matrix, Matrix) -vec3 getnormal(Nothing pos, sampler2D xs, sampler2D ys, sampler2D zs, vec2 uv){ - // The +1e-6 fixes precision errors at the edge - float du = 1.0 / textureSize(zs,0).x + 1e-6; - float dv = 1.0 / textureSize(zs,0).y + 1e-6; - - vec3 s0, s1, s2, s3, s4; - vec2 off1 = uv + vec2(-du, 0); - vec2 off2 = uv + vec2(0, dv); - vec2 off3 = uv + vec2(du, 0); - vec2 off4 = uv + vec2(0, -dv); - - s0 = vec3(texture(xs, uv).x, texture(ys, uv).x, texture(zs, uv).x); - s1 = vec3(texture(xs, off1).x, texture(ys, off1).x, texture(zs, off1).x); - s2 = vec3(texture(xs, off2).x, texture(ys, off2).x, texture(zs, off2).x); - s3 = vec3(texture(xs, off3).x, texture(ys, off3).x, texture(zs, off3).x); - s4 = vec3(texture(xs, off4).x, texture(ys, off4).x, texture(zs, off4).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); -} - - -// Overload for (range, range, Matrix) surface plots -// Though this is only called by surface(Matrix) -vec3 getnormal(Grid2D pos, Nothing xs, Nothing ys, sampler2D zs, vec2 uv){ - // The +1e-6 fixes precision errors at the edge - float du = 1.0 / textureSize(zs,0).x + 1e-6; - float dv = 1.0 / textureSize(zs,0).y + 1e-6; - - vec3 s0, s1, s2, s3, s4; - vec2 off1 = uv + vec2(-du, 0); - vec2 off2 = uv + vec2(0, dv); - vec2 off3 = uv + vec2(du, 0); - vec2 off4 = uv + vec2(0, -dv); - - s0 = vec3(grid_pos(pos, uv).xy, texture(zs, uv).x); - s1 = vec3(grid_pos(pos, off1).xy, texture(zs, off1).x); - s2 = vec3(grid_pos(pos, off2).xy, texture(zs, off2).x); - s3 = vec3(grid_pos(pos, off3).xy, texture(zs, off3).x); - s4 = vec3(grid_pos(pos, off4).xy, texture(zs, off4).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); -} - - -// Overload for surface(Vector, Vector, Matrix) -// Makie converts almost everything to this -vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, vec2 uv){ - // The +1e-6 fixes precision errors at the edge - float du = 1.0 / textureSize(zs,0).x + 1e-6; - float dv = 1.0 / textureSize(zs,0).y + 1e-6; - - vec3 s0, s1, s2, s3, s4; - vec2 off1 = uv + vec2(-du, 0); - vec2 off2 = uv + vec2(0, dv); - vec2 off3 = uv + vec2(du, 0); - vec2 off4 = uv + vec2(0, -dv); - - s0 = vec3(texture(xs, uv.x).x, texture(ys, uv.y).x, texture(zs, uv).x); - s1 = vec3(texture(xs, off1.x).x, texture(ys, off1.y).x, texture(zs, off1).x); - s2 = vec3(texture(xs, off2.x).x, texture(ys, off2.y).x, texture(zs, off2).x); - s3 = vec3(texture(xs, off3.x).x, texture(ys, off3.y).x, texture(zs, off3).x); - s4 = vec3(texture(xs, off4.x).x, texture(ys, off4.y).x, texture(zs, off4).x); - - return normal_from_points(s0, s1, s2, s3, s4, uv, off1, off2, off3, off4); -} uniform vec4 highclip; uniform vec4 lowclip; diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index 714e174d772..9750cd458e4 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -6,7 +6,7 @@ end function normal_calc(x::Bool, invert_normals::Bool = false) i = invert_normals ? "-" : "" if x - return "$(i)getnormal(position, position_x, position_y, position_z, index01);" + return "$(i)getnormal(position, position_x, position_y, position_z, index2D);" else return "vec3(0, 0, $(i)1);" end From 066f0b7ebc9c2bf84107b05b12a1a31966b5b2fb Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 12 Mar 2023 19:23:35 +0100 Subject: [PATCH 19/80] switch back to translation zoom, cleanup/reorganize --- src/camera/camera3d.jl | 591 ++++++++++++++++++++----------------- src/utilities/utilities.jl | 30 +- 2 files changed, 344 insertions(+), 277 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index d7a8b6dace2..f0e1a83e639 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -1,15 +1,24 @@ -struct Camera3D <: AbstractCamera +abstract type AbstractCamera3D <: AbstractCamera end + +struct Camera3D <: AbstractCamera3D + # User settings + settings::Attributes + controls::Attributes + + # Interactivity + pulser::Observable{Float64} + selected::Observable{Bool} + # scroll_mod::Observable{Bool} + + # view matrix eyeposition::Observable{Vec3f} lookat::Observable{Vec3f} upvector::Observable{Vec3f} - - zoom_mult::Observable{Float32} - fov::Observable{Float32} # WGLMakie compat + + # perspective projection matrix + fov::Observable{Float32} near::Observable{Float32} far::Observable{Float32} - pulser::Observable{Float64} - - attributes::Attributes end """ @@ -81,71 +90,80 @@ You can also make adjustments to the camera position, rotation and zoom by calli - `zoom!(scene, zoom_step)` will change the zoom level of the scene without translating or rotating the scene. `zoom_step` applies multiplicatively to `cam.zoom_mult` which is used as a multiplier to the fov (perspective projection) or width and height (orthographic projection). """ function Camera3D(scene::Scene; kwargs...) - attr = merged_get!(:cam3d, scene, Attributes(kwargs)) do - Attributes( - # Keyboard controls - # Translations - up_key = Keyboard.r, - down_key = Keyboard.f, - left_key = Keyboard.a, - right_key = Keyboard.d, - forward_key = Keyboard.w, - backward_key = Keyboard.s, - # Zooms - zoom_in_key = Keyboard.u, - zoom_out_key = Keyboard.o, - stretch_view_key = Keyboard.page_up, - contract_view_key = Keyboard.page_down, - # Rotations - pan_left_key = Keyboard.j, - pan_right_key = Keyboard.l, - tilt_up_key = Keyboard.i, - tilt_down_key = Keyboard.k, - roll_clockwise_key = Keyboard.e, - roll_counterclockwise_key = Keyboard.q, - # Mouse controls - translation_button = Mouse.right, - scroll_mod = true, - rotation_button = Mouse.left, - # Shared controls - fix_x_key = Keyboard.x, - fix_y_key = Keyboard.y, - fix_z_key = Keyboard.z, - reset = Keyboard.home, - # Settings - keyboard_rotationspeed = 1f0, - keyboard_translationspeed = 0.5f0, - keyboard_zoomspeed = 1f0, - mouse_rotationspeed = 1f0, - mouse_translationspeed = 1f0, - mouse_zoomspeed = 1f0, - circular_rotation = (true, true, true), - fov = 45f0, # base fov - near = automatic, - far = automatic, - rotation_center = :lookat, - update_rate = 1/30, - projectiontype = Perspective, - fixed_axis = true, - zoom_shift_lookat = false, # doesn't really work with fov - cad = false, - # internal - selected = true - ) - end + overwrites = Attributes(kwargs) + + controls = Attributes( + # Keyboard controls + # Translations + up_key = Keyboard.r, + down_key = Keyboard.f, + left_key = Keyboard.a, + right_key = Keyboard.d, + forward_key = Keyboard.w, + backward_key = Keyboard.s, + # Zooms + zoom_in_key = Keyboard.u, + zoom_out_key = Keyboard.o, + # Rotations + pan_left_key = Keyboard.j, + pan_right_key = Keyboard.l, + tilt_up_key = Keyboard.i, + tilt_down_key = Keyboard.k, + roll_clockwise_key = Keyboard.e, + roll_counterclockwise_key = Keyboard.q, + # Mouse controls + translation_button = Mouse.right, + rotation_button = Mouse.left, + scroll_mod = true, + # Shared controls + fix_x_key = Keyboard.x, + fix_y_key = Keyboard.y, + fix_z_key = Keyboard.z, + reset = Keyboard.home + ) + + replace!(controls, :Camera3D, scene, overwrites) + + settings = Attributes( + keyboard_rotationspeed = 1f0, + keyboard_translationspeed = 0.5f0, + keyboard_zoomspeed = 1f0, + + mouse_rotationspeed = 1f0, + mouse_translationspeed = 1f0, + mouse_zoomspeed = 1f0, + + enable_translation = true, + enable_rotation = true, + enable_zoom = true, + + projectiontype = Makie.Perspective, + circular_rotation = (true, true, true), + rotation_center = :lookat, + update_rate = 1/30, + zoom_shift_lookat = true, + fixed_axis = true, + cad = false + ) + + replace!(settings, :Camera3D, scene, overwrites) cam = Camera3D( - pop!(attr, :eyeposition, Vec3f(3)), - pop!(attr, :lookat, Vec3f(0)), - pop!(attr, :upvector, Vec3f(0, 0, 1)), - - Observable(1f0), - Observable(attr[:fov][]), - Observable(attr[:near][] === automatic ? 0.1f0 : attr[:near][]), - Observable(attr[:far][] === automatic ? 100f0 : attr[:far][]), + settings, controls, + + # Internals - controls Observable(-1.0), + Observable(true), - attr + # Semi-Internal - view matrix + get(overwrites, :eyeposition, Observable(Vec3f(3, 3, 3))), + get(overwrites, :lookat, Observable(Vec3f(0, 0, 0))), + get(overwrites, :upvector, Observable(Vec3f(0, 0, 1))), + + # Semi-Internal - projection matrix + get(overwrites, :fov, Observable(45.0)), + get(overwrites, :near, Observable(0.1)), + get(overwrites, :far, Observable(10.0)), ) disconnect!(camera(scene)) @@ -154,9 +172,9 @@ function Camera3D(scene::Scene; kwargs...) # ticks every so often to get consistent position updates. on(cam.pulser) do prev_time current_time = time() - active = on_pulse(scene, cam, Float32(current_time - prev_time)) - @async if active && attr.selected[] - sleep(attr.update_rate[]) + active = on_pulse(scene, cam, current_time - prev_time) + @async if active && cam.selected[] + sleep(settings.update_rate[]) cam.pulser[] = current_time else cam.pulser.val = -1.0 @@ -165,7 +183,7 @@ function Camera3D(scene::Scene; kwargs...) keynames = ( :up_key, :down_key, :left_key, :right_key, :forward_key, :backward_key, - :zoom_in_key, :zoom_out_key, :stretch_view_key, :contract_view_key, + :zoom_in_key, :zoom_out_key, :pan_left_key, :pan_right_key, :tilt_up_key, :tilt_down_key, :roll_clockwise_key, :roll_counterclockwise_key ) @@ -173,7 +191,7 @@ function Camera3D(scene::Scene; kwargs...) # Start ticking if relevant keys are pressed on(camera(scene), events(scene).keyboardbutton) do event if event.action in (Keyboard.press, Keyboard.repeat) && cam.pulser[] == -1.0 && - attr.selected[] && any(key -> ispressed(scene, attr[key][]), keynames) + cam.selected[] && any(key -> ispressed(scene, controls[key][]), keynames) cam.pulser[] = time() return Consume(true) @@ -186,7 +204,7 @@ function Camera3D(scene::Scene; kwargs...) deselect_all_cameras!(root(scene)) on(camera(scene), events(scene).mousebutton, priority = 100) do event if event.action == Mouse.press - attr.selected[] = is_mouseinside(scene) + cam.selected[] = is_mouseinside(scene) end return Consume(false) end @@ -199,24 +217,21 @@ function Camera3D(scene::Scene; kwargs...) cameracontrols!(scene, cam) # Trigger updates on scene resize and settings change - on(camera(scene), scene.px_area, attr[:fov], attr[:projectiontype]) do _, _, _ - update_cam!(scene, cam) + on(camera(scene), cam.fov) do _ + if settings.projectiontype[] == Makie.Perspective + update_cam!(scene, cam) + end end - on(camera(scene), attr[:near], attr[:far]) do near, far - near === automatic || (cam.near[] = near) - far === automatic || (cam.far[] = far) + on(camera(scene), scene.px_area, cam.near, cam.far, settings.projectiontype) do _, _, _, _ update_cam!(scene, cam) end # reset on(camera(scene), events(scene).keyboardbutton) do event - if attr.selected[] && event.key == attr[:reset][] && event.action == Keyboard.release + if cam.selected[] && event.key == controls[:reset][] && event.action == Keyboard.release # center keeps the rotation of the camera so we reset that here # might make sense to keep user set lookat, upvector, eyeposition # around somewhere for this? - cam.lookat[] = Vec3f(0) - cam.upvector[] = Vec3f(0,0,1) - cam.eyeposition[] = Vec3f(3) center!(scene) return Consume(true) end @@ -235,7 +250,7 @@ cam3d_cad!(scene; cad = true, zoom_shift_lookat = false, fixed_axis = false, kwa function deselect_all_cameras!(scene) cam = cameracontrols(scene) - cam isa Camera3D && (cam.attributes.selected[] = false) + cam isa AbstractCamera3D && (cam.selected[] = false) for child in scene.children deselect_all_cameras!(child) end @@ -243,31 +258,109 @@ function deselect_all_cameras!(scene) end +################################################################################ +### Interactivity init +################################################################################ + + + +function on_pulse(scene, cam::Camera3D, timestep) + @extractvalue cam.controls ( + right_key, left_key, up_key, down_key, backward_key, forward_key, + tilt_up_key, tilt_down_key, pan_left_key, pan_right_key, roll_counterclockwise_key, roll_clockwise_key, + zoom_out_key, zoom_in_key + ) + @extractvalue cam.settings ( + keyboard_translationspeed, keyboard_rotationspeed, keyboard_zoomspeed, projectiontype + ) + + # translation + right = ispressed(scene, right_key) + left = ispressed(scene, left_key) + up = ispressed(scene, up_key) + down = ispressed(scene, down_key) + backward = ispressed(scene, backward_key) + forward = ispressed(scene, forward_key) + translating = right || left || up || down || backward || forward + + if translating + # translation in camera space x/y/z direction + if projectiontype == Perspective + viewnorm = norm(cam.lookat[] - cam.eyeposition[]) + xynorm = 2 * viewnorm * tand(0.5 * cam.fov[]) + translation = keyboard_translationspeed * timestep * Vec3f( + xynorm * (right - left), + xynorm * (up - down), + viewnorm * (backward - forward) + ) + else + # translation in camera space x/y/z direction + viewnorm = norm(cam.eyeposition[] - cam.lookat[]) + translation = 2 * viewnorm * keyboard_translationspeed * timestep * Vec3f( + right - left, up - down, backward - forward + ) + end + _translate_cam!(scene, cam, translation) + end + + # rotation + up = ispressed(scene, tilt_up_key) + down = ispressed(scene, tilt_down_key) + left = ispressed(scene, pan_left_key) + right = ispressed(scene, pan_right_key) + counterclockwise = ispressed(scene, roll_counterclockwise_key) + clockwise = ispressed(scene, roll_clockwise_key) + rotating = up || down || left || right || counterclockwise || clockwise + + if rotating + # rotations around camera space x/y/z axes + angles = keyboard_rotationspeed * timestep * + Vec3f(up - down, left - right, counterclockwise - clockwise) + + _rotate_cam!(scene, cam, angles) + end + + # zoom + zoom_out = ispressed(scene, zoom_out_key) + zoom_in = ispressed(scene, zoom_in_key) + zooming = zoom_out || zoom_in + + if zooming + zoom_step = (1f0 + keyboard_zoomspeed * timestep) ^ (zoom_out - zoom_in) + _zoom!(scene, cam, zoom_step, false) + end + + # if any are active, update matrices, else stop clock + if translating || rotating || zooming + update_cam!(scene, cam) + return true + else + return false + end +end + + function add_translation!(scene, cam::Camera3D) - translationspeed = cam.attributes[:mouse_translationspeed] - zoomspeed = cam.attributes[:mouse_zoomspeed] - shift_lookat = cam.attributes[:zoom_shift_lookat] - cad = cam.attributes[:cad] - button = cam.attributes[:translation_button] - scroll_mod = cam.attributes[:scroll_mod] + @extract cam.controls (translation_button, scroll_mod) + @extract cam.settings (mouse_translationspeed, mouse_zoomspeed, cad, projectiontype) last_mousepos = RefValue(Vec2f(0, 0)) dragging = RefValue(false) function compute_diff(delta) - if cam.attributes[:projectiontype][] == Orthographic - aspect = Float32((/)(widths(scene.px_area[])...)) - aspect_scale = Vec2f(1f0 + aspect, 1f0 + 1f0 / aspect) - return cam.zoom_mult[] * delta .* aspect_scale ./ widths(scene.px_area[]) + 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 else - viewdir = cam.lookat[] - cam.eyeposition[] - return 0.002f0 * cam.zoom_mult[] * norm(viewdir) * delta + viewnorm = norm(cam.eyeposition[] - cam.lookat[]) + return 2 * viewnorm / widths(scene.px_area[])[2] * delta end end # drag start/stop on(camera(scene), scene.events.mousebutton) do event - if ispressed(scene, button[]) + if ispressed(scene, translation_button[]) if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] last_mousepos[] = mouseposition_px(scene) dragging[] = true @@ -278,7 +371,7 @@ function add_translation!(scene, cam::Camera3D) diff = compute_diff(last_mousepos[] .- mousepos) last_mousepos[] = mousepos dragging[] = false - translate_cam!(scene, cam, translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) + translate_cam!(scene, cam, mouse_translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) return Consume(true) end return Consume(false) @@ -286,11 +379,11 @@ function add_translation!(scene, cam::Camera3D) # in drag on(camera(scene), scene.events.mouseposition) do mp - if dragging[] && ispressed(scene, button[]) + if dragging[] && ispressed(scene, translation_button[]) mousepos = screen_relative(scene, mp) diff = compute_diff(last_mousepos[] .- mousepos) last_mousepos[] = mousepos - translate_cam!(scene, cam, translationspeed[] * Vec3f(diff[1], diff[2], 0f0)) + translate_cam!(scene, cam, mouse_translationspeed[] * Vec3f(diff[1], diff[2], 0f0)) return Consume(true) end return Consume(false) @@ -298,24 +391,26 @@ function add_translation!(scene, cam::Camera3D) on(camera(scene), scene.events.scroll) do scroll if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) - zoom_step = (1f0 + 0.1f0 * zoomspeed[]) ^ -scroll[2] - zoom!(scene, cam, zoom_step, shift_lookat[], cad[]) + zoom_step = (1f0 + 0.1f0 * mouse_zoomspeed[]) ^ -scroll[2] + zoom!(scene, cam, zoom_step, cad[]) return Consume(true) end return Consume(false) end end + function add_rotation!(scene, cam::Camera3D) - rotationspeed = cam.attributes[:mouse_rotationspeed] - button = cam.attributes[:rotation_button] + @extract cam.controls (rotation_button, ) + @extract cam.settings (mouse_rotationspeed, ) + last_mousepos = RefValue(Vec2f(0, 0)) dragging = RefValue(false) e = events(scene) # drag start/stop on(camera(scene), e.mousebutton) do event - if ispressed(scene, button[]) + if ispressed(scene, rotation_button[]) if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] last_mousepos[] = mouseposition_px(scene) dragging[] = true @@ -324,7 +419,7 @@ function add_rotation!(scene, cam::Camera3D) elseif event.action == Mouse.release && dragging[] mousepos = mouseposition_px(scene) dragging[] = false - rot_scaling = rotationspeed[] * (e.window_dpi[] * 0.005) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) mp = (last_mousepos[] .- mousepos) .* 0.01f0 .* rot_scaling last_mousepos[] = mousepos rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) @@ -335,9 +430,9 @@ function add_rotation!(scene, cam::Camera3D) # in drag on(camera(scene), e.mouseposition) do mp - if dragging[] && ispressed(scene, button[]) + if dragging[] && ispressed(scene, rotation_button[]) mousepos = screen_relative(scene, mp) - rot_scaling = rotationspeed[] * (e.window_dpi[] * 0.005) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) mp = (last_mousepos[] .- mousepos) * 0.01f0 * rot_scaling last_mousepos[] = mousepos rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) @@ -348,77 +443,45 @@ function add_rotation!(scene, cam::Camera3D) end -function on_pulse(scene, cam, timestep) - attr = cam.attributes - - # translation - right = ispressed(scene, attr[:right_key][]) - left = ispressed(scene, attr[:left_key][]) - up = ispressed(scene, attr[:up_key][]) - down = ispressed(scene, attr[:down_key][]) - backward = ispressed(scene, attr[:backward_key][]) - forward = ispressed(scene, attr[:forward_key][]) - translating = right || left || up || down || backward || forward +################################################################################ +### Camera transformations +################################################################################ - if translating - # translation in camera space x/y/z direction - translation = attr[:keyboard_translationspeed][] * timestep * - Vec3f(right - left, up - down, backward - forward) - viewdir = cam.lookat[] - cam.eyeposition[] - _translate_cam!(scene, cam, cam.zoom_mult[] * norm(viewdir) * translation) - end - # rotation - up = ispressed(scene, attr[:tilt_up_key][]) - down = ispressed(scene, attr[:tilt_down_key][]) - left = ispressed(scene, attr[:pan_left_key][]) - right = ispressed(scene, attr[:pan_right_key][]) - counterclockwise = ispressed(scene, attr[:roll_counterclockwise_key][]) - clockwise = ispressed(scene, attr[:roll_clockwise_key][]) - rotating = up || down || left || right || counterclockwise || clockwise +# Simplified methods +function translate_cam!(scene, cam::Camera3D, t::VecTypes) + _translate_cam!(scene, cam, t) + update_cam!(scene, cam) + nothing +end - if rotating - # rotations around camera space x/y/z axes - angles = attr[:keyboard_rotationspeed][] * timestep * - Vec3f(up - down, left - right, counterclockwise - clockwise) +function rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) + _rotate_cam!(scene, cam, angles, from_mouse) + update_cam!(scene, cam) + nothing +end - _rotate_cam!(scene, cam, angles) - end +""" + zoom!(scene, zoom_step) - # zoom - zoom_out = ispressed(scene, attr[:zoom_out_key][]) - zoom_in = ispressed(scene, attr[:zoom_in_key][]) - zooming = zoom_out || zoom_in +Zooms the camera in or out based on the multiplier `zoom_step`. A `zoom_step` +of 1.0 is neutral, larger zooms out and lower zooms in. - if zooming - zoom_step = (1f0 + attr[:keyboard_zoomspeed][] * timestep) ^ (zoom_out - zoom_in) - _zoom!(scene, cam, zoom_step, false) - end +Note that this method only applies to Camera3D. +""" +zoom!(scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false) +function zoom!(scene, cam, zoom_step, cad = false) + _zoom!(scene, cam, zoom_step, cad) + update_cam!(scene, cam) + nothing +end - stretch = ispressed(scene, attr[:stretch_view_key][]) - contract = ispressed(scene, attr[:contract_view_key][]) - if stretch || contract - zoom_step = (1f0 + attr[:keyboard_zoomspeed][] * timestep) ^ (stretch - contract) - cam.eyeposition[] = cam.lookat[] + zoom_step * (cam.eyeposition[] - cam.lookat[]) - end - zooming = zooming || stretch || contract - # if any are active, update matrices, else stop clock - if translating || rotating || zooming - update_cam!(scene, cam) - return true - else - return false - end -end +function _translate_cam!(scene, cam::Camera3D, t) + cam.settings.enable_translation[] || return + @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) -function translate_cam!(scene::Scene, cam::Camera3D, t::VecTypes) - _translate_cam!(scene, cam, t) - update_cam!(scene, cam) - nothing -end -function _translate_cam!(scene, cam, t) # This uses a camera based coordinate system where # x expands right, y expands up and z expands towards the screen lookat = cam.lookat[] @@ -431,9 +494,9 @@ function _translate_cam!(scene, cam, t) trans = u_x * t[1] + u_y * t[2] + u_z * t[3] # apply world space restrictions - fix_x = ispressed(scene, cam.attributes[:fix_x_key][]) - fix_y = ispressed(scene, cam.attributes[:fix_y_key][]) - fix_z = ispressed(scene, cam.attributes[:fix_z_key][]) + fix_x = ispressed(scene, fix_x_key)::Bool + fix_y = ispressed(scene, fix_y_key)::Bool + fix_z = ispressed(scene, fix_z_key)::Bool if fix_x || fix_y || fix_z trans = Vec3f(fix_x, fix_y, fix_z) .* trans end @@ -444,12 +507,12 @@ function _translate_cam!(scene, cam, t) end -function rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) - _rotate_cam!(scene, cam, angles, from_mouse) - update_cam!(scene, cam) - nothing -end function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) + cam.settings.enable_rotation[] || return + + @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) + @extractvalue cam.settings (fixed_axis, circular_rotation, rotation_center) + # This applies rotations around the x/y/z axis of the camera coordinate system # x expands right, y expands up and z expands towards the screen lookat = cam.lookat[] @@ -459,38 +522,39 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) right = cross(viewdir, up) # +x x_axis = right - y_axis = cam.attributes[:fixed_axis][] ? Vec3f(0, 0, ifelse(up[3] < 0, -1, 1)) : up + y_axis = fixed_axis ? Vec3f(0, 0, ifelse(up[3] < 0, -1, 1)) : up z_axis = -viewdir - fix_x = ispressed(scene, cam.attributes[:fix_x_key][]) - fix_y = ispressed(scene, cam.attributes[:fix_y_key][]) - fix_z = ispressed(scene, cam.attributes[:fix_z_key][]) - cx, cy, cz = cam.attributes[:circular_rotation][] + fix_x = ispressed(scene, fix_x_key)::Bool + fix_y = ispressed(scene, fix_y_key)::Bool + fix_z = ispressed(scene, fix_z_key)::Bool + cx, cy, cz = circular_rotation + rotation = Quaternionf(0, 0, 0, 1) if !xor(fix_x, fix_y, fix_z) # if there are more or less than one restriction apply all rotations - rotation *= qrotation(y_axis, angles[2]) rotation *= qrotation(x_axis, angles[1]) + rotation *= qrotation(y_axis, angles[2]) rotation *= qrotation(z_axis, angles[3]) else # apply world space restrictions - if from_mouse && ((fix_x && (fix_x == cx)) || (fix_y && (fix_y == cy)) || (fix_z && (fix_z == cz))) + if from_mouse && ((fix_x && cx) || (fix_y && cy) || (fix_z && cz)) # 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 flip = 2f0 * past_half .- 1f0 angle = flip[1] * angles[1] + flip[2] * angles[2] - angles = Vec3f(-angle, angle, -angle) + angles = Vec3f(-angle, -angle, angle) # only one fix is true so this only rotates around one axis rotation *= qrotation( - Vec3f(fix_x, fix_z, fix_y) .* Vec3f(sign(right[1]), viewdir[2], sign(up[3])), + Vec3f(fix_x, fix_y, fix_z) .* Vec3f(sign(right[1]), viewdir[2], sign(up[3])), dot(Vec3f(fix_x, fix_y, fix_z), angles) ) else # restrict total quaternion rotation to one axis - rotation *= qrotation(y_axis, angles[2]) rotation *= qrotation(x_axis, angles[1]) + rotation *= qrotation(y_axis, angles[2]) rotation *= qrotation(z_axis, angles[3]) # the first three components are related to rotations around the x/y/z-axis rotation = Quaternionf(rotation.data .* (fix_x, fix_y, fix_z, 1)) @@ -502,138 +566,113 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) # TODO maybe generalize this to arbitrary center? # calculate positions from rotated vectors - if cam.attributes[:rotation_center][] === :lookat + if rotation_center === :lookat cam.eyeposition[] = lookat - viewdir else cam.lookat[] = eyepos + viewdir end + return end -""" - zoom!(scene, zoom_step) - -Zooms the camera in or out based on the multiplier `zoom_step`. A `zoom_step` -of 1.0 is neutral, larger zooms out and lower zooms in. +function _zoom!(scene, cam::Camera3D, zoom_step, cad = false) + cam.settings.enable_zoom[] || return -Note that this method only applies to Camera3D. -""" -zoom!(scene::Scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false, false) -function zoom!(scene::Scene, cam::Camera3D, zoom_step, shift_lookat = false, cad = false) - _zoom!(scene, cam, zoom_step, shift_lookat, cad) - update_cam!(scene, cam) - nothing -end -function _zoom!(scene::Scene, cam::Camera3D, zoom_step, shift_lookat = false, cad = false) + lookat = cam.lookat[] + eyepos = cam.eyeposition[] + viewdir = lookat - eyepos # -z + if cad - # move exeposition if mouse is not over the center - lookat = cam.lookat[] - eyepos = cam.eyeposition[] - up = cam.upvector[] # +y - viewdir = lookat - eyepos # -z - right = cross(viewdir, up) # +x + # 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 - shift = rel_pos[1] * normalize(right) + rel_pos[2] * normalize(up) - shifted = eyepos + 0.1f0 * sign(1f0 - zoom_step) * norm(viewdir) * shift - cam.eyeposition[] = lookat + norm(viewdir) * normalize(shifted - lookat) - elseif shift_lookat - lookat = cam.lookat[] - eyepos = cam.eyeposition[] - up = normalize(cam.upvector[]) - viewdir = lookat - eyepos - u_z = normalize(-viewdir) - u_x = normalize(cross(up, u_z)) - u_y = normalize(cross(u_z, u_x)) - - if cam.attributes[:projectiontype][] == Perspective - # translate both eyeposition and lookat to more or less keep data - # under the mouse in view - fov = cam.attributes[:fov][] - before = tan(clamp(cam.zoom_mult[] * fov, 0.01f0, 175f0) / 360f0 * Float32(pi)) - after = tan(clamp(cam.zoom_mult[] * zoom_step * fov, 0.01f0, 175f0) / 360f0 * Float32(pi)) - - aspect = Float32((/)(widths(scene.px_area[])...)) - rel_pos = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - shift = rel_pos[1] * u_x + rel_pos[2] * u_y - shift = -(after - before) * norm(viewdir) * normalize(aspect .* shift) - else - mx, my = 2f0 * mouseposition_px(scene) ./ widths(scene.px_area[]) .- 1f0 - aspect = Float32((/)(widths(scene.px_area[])...)) - w = 0.5f0 * (1f0 + aspect) * cam.zoom_mult[] - h = 0.5f0 * (1f0 + 1f0 / aspect) * cam.zoom_mult[] - shift = (1f0 - zoom_step) * (mx * w * u_x + my * h * u_y) - end + shift = rel_pos[1] * u_x + rel_pos[2] * u_y + shift *= 0.1 * sign(1 - zoom_step) * norm(viewdir) + + cam.eyeposition[] = lookat - zoom_step * viewdir + shift + elseif cam.settings.projectiontype[] == Makie.Orthographic && cam.settings.zoom_shift_lookat[] + # keep data under cursor + u_z = normalize(viewdir) + 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] + shift = (1 - zoom_step) * norm(viewdir) * (rel_pos[1] * u_x + rel_pos[2] * u_y) cam.lookat[] = lookat + shift - cam.eyeposition[] = eyepos + shift + cam.eyeposition[] = lookat - zoom_step * viewdir + shift + else + # just zoom in/out + cam.eyeposition[] = lookat - zoom_step * viewdir end - # apply zoom - cam.zoom_mult[] = cam.zoom_mult[] * zoom_step - return end +################################################################################ +### update_cam! methods +################################################################################ + + +# Update camera matrices function update_cam!(scene::Scene, cam::Camera3D) - @extractvalue cam (lookat, eyeposition, upvector) + @extractvalue cam (lookat, eyeposition, upvector, near, far, fov) - near = cam.near[]; far = cam.far[] - aspect = Float32((/)(widths(scene.px_area[])...)) + view = Makie.lookat(eyeposition, lookat, upvector) - if cam.attributes[:projectiontype][] == Perspective - fov = clamp(cam.zoom_mult[] * cam.attributes[:fov][], 0.01f0, 175f0) - cam.fov[] = fov - proj = perspectiveprojection(fov, aspect, near, far) + aspect = Float32((/)(widths(scene.px_area[])...)) + if cam.settings.projectiontype[] == Makie.Perspective + view_norm = norm(eyeposition - lookat) + proj = perspectiveprojection(fov, aspect, view_norm * near, view_norm * far) else - w = 0.5f0 * (1f0 + aspect) * cam.zoom_mult[] - h = 0.5f0 * (1f0 + 1f0 / aspect) * cam.zoom_mult[] - proj = orthographicprojection(-w, w, -h, h, near, far) + h = norm(eyeposition - lookat); w = h * aspect + proj = orthographicprojection(-w, w, -h, h, h * near, h * far) end - view = Makie.lookat(eyeposition, lookat, upvector) - set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] end -function update_cam!(scene::Scene, camera::Camera3D, area3d::Rect) - @extractvalue camera (lookat, eyeposition, upvector) + +# Update camera position via bbox +function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) bb = Rect3f(area3d) width = widths(bb) - half_width = width ./ 2f0 - middle = maximum(bb) - half_width - old_dir = normalize(eyeposition .- lookat) - camera.lookat[] = middle - neweyepos = middle .+ (1.2*norm(width) .* old_dir) - camera.eyeposition[] = neweyepos - camera.upvector[] = Vec3f(0,0,1) - if camera.attributes[:near][] === automatic - camera.near[] = 0.1f0 * norm(widths(bb)) - end - if camera.attributes[:far][] === automatic - camera.far[] = 3f0 * norm(widths(bb)) - end - if camera.attributes[:projectiontype][] == Orthographic - camera.zoom_mult[] = 0.6 * norm(width) + center = maximum(bb) - 0.5 * width + + old_dir = normalize(cam.eyeposition[] .- cam.lookat[]) + if cam.settings.projectiontype[] == Makie.Perspective + dist = 0.5 * norm(width) * (1.0 + 0.6 / tand(0.5 * cam.fov[])) else - camera.zoom_mult[] = 1f0 + dist = 0.6 * norm(width) end - update_cam!(scene, camera) + + cam.lookat[] = center + cam.eyeposition[] = cam.lookat[] .+ dist * old_dir + cam.upvector[] = Vec3f(0, 0, 1) # Should we reset this? + + update_cam!(scene, cam) + return end -function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = Vec3f(0, 0, 1)) - camera.lookat[] = Vec3f(lookat) +# Update camera position via camera Position & Orientation +function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = cam.upvector[]) + camera.lookat[] = Vec3f(lookat) camera.eyeposition[] = Vec3f(eyeposition) - camera.upvector[] = Vec3f(up) + camera.upvector[] = Vec3f(up) update_cam!(scene, camera) return end + function show_cam(scene) cam = cameracontrols(scene) println("cam=cameracontrols(scene)") diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index e6ef6b1bca2..7623799b11a 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -106,7 +106,19 @@ function extract_expr(extract_func, dictlike, args) end """ -usage @extract scene (a, b, c, d) + @extract scene (a, b, c, d) + +This becomes + +```julia +begin + a = scene[:a] + b = scene[:b] + c = scene[:d] + d = scene[:d] + (a, b, c, d) +end +``` """ macro extract(scene, args) extract_expr(getindex, scene, args) @@ -251,6 +263,22 @@ function merged_get!(defaults::Function, key, scene::SceneLike, input::Attribute return merge!(input, d) end +function Base.replace!(target::Attributes, key, scene::SceneLike, overwrite::Attributes) + if haskey(theme(scene), key) + _replace!(target, theme(scene, key)) + end + return _replace!(target, overwrite) +end + +function _replace!(target::Attributes, overwrite::Attributes) + for k in keys(target) + haskey(overwrite, k) && (target[k] = overwrite[k]) + end + return +end + + + to_vector(x::AbstractVector, len, T) = convert(Vector{T}, x) function to_vector(x::AbstractArray, len, T) if length(x) in size(x) # assert that just one dim != 1 From e4b056a6a77655e7195aa71b1ea346dbf48ed4ca Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 12 Mar 2023 19:46:57 +0100 Subject: [PATCH 20/80] fix typo --- src/camera/camera3d.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index f0e1a83e639..ccb9d7a7e99 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -664,7 +664,7 @@ function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) end # Update camera position via camera Position & Orientation -function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = cam.upvector[]) +function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = camera.upvector[]) camera.lookat[] = Vec3f(lookat) camera.eyeposition[] = Vec3f(eyeposition) camera.upvector[] = Vec3f(up) From 6cbca9e9d060bf1f3c0b202030b52064c879440b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 12 Mar 2023 19:55:32 +0100 Subject: [PATCH 21/80] add example --- docs/documentation/cameras.md | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/documentation/cameras.md b/docs/documentation/cameras.md index f935c92c29c..236764434cc 100644 --- a/docs/documentation/cameras.md +++ b/docs/documentation/cameras.md @@ -30,6 +30,59 @@ Note that this camera is not used by `Axis`. It is used, by default, for 2D `LSc {{doc Camera3D}} +## Example - Visualizing the cameras view box + +```julia +using GeometryBasics, LinearAlgebra + +function frustum_snapshot(cam) + r = Rect3f(Point3f(-1, -1, -1), Vec3f(2, 2, 2)) + rect_ps = coordinates(r) .|> Point3f + insert!(rect_ps, 13, Point3f(1, -1, 1)) # fix bad line + + inv_pv = inv(cam.projectionview[]) + return map(rect_ps) do p + p = inv_pv * to_ndim(Point4f, p, 1) + return p[Vec(1,2,3)] / p[4] + end +end + + +ex = Point3f(1,0,0) +ey = Point3f(0,1,0) +ez = Point3f(0,0,1) + +fig = Figure() +scene = LScene(fig[1, 1]) +cc = Makie.Camera3D(scene.scene, projectiontype = Makie.Perspective, far = 3.0) + +linesegments!(scene, Rect3f(Point3f(-1), Vec3f(2)), color = :black) +linesegments!(scene, + [-ex, ex, -ey, ey, -ez, ez], + color = [:red, :red, :green, :green, :blue, :blue] +) +center!(scene.scene) + +cam = scene.scene.camera +eyeposition = cc.eyeposition +lookat = cc.lookat +frustum = map(pv -> frustum_snapshot(cam), cam.projectionview) + +scene = LScene(fig[1, 2]) +_cc = Makie.Camera3D(scene.scene, projectiontype = Makie.Orthographic) +lines!(scene, frustum, color = :blue, linestyle = :dot) +scatter!(scene, eyeposition, color = :black) +scatter!(scene, lookat, color = :black) + +linesegments!(scene, + [-ex, ex, -ey, ey, -ez, ez], + color = [:red, :red, :green, :green, :blue, :blue] +) +linesegments!(scene, Rect3f(Point3f(-1), Vec3f(2)), color = :black) + +fig +``` + ## General Remarks To force a plot to be visualized in 3D, you can set the limits to have a nonzero \(z\)-axis interval, or ensure that a 3D camera type is used. From b54c0ae0947fa2a2df3860abc6615ce957c7a860 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 12 Mar 2023 20:00:54 +0100 Subject: [PATCH 22/80] remove enable translation/rotation/zoom can be achieved by removing hotkeys --- src/camera/camera3d.jl | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index ccb9d7a7e99..9ec826e4b42 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -34,7 +34,7 @@ The 3D camera is (or can be) unrestricted in terms of rotations and translations - `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. - `projectiontype = Perspective` sets the type of the projection. Can be `Orthographic` or `Perspective`. - `fixed_axis = false`: If true panning uses the (world/plot) z-axis instead of the camera up direction. -- `zoom_shift_lookat = true`: If true attempts to keep data under the cursor in view when zooming. +- `zoom_shift_lookat = true`: If true keeps the data under the cursor when zooming. Only applies to orthographic cameras. - `cad = false`: If true rotates the view around `lookat` when zooming off-center. The camera view follows from the position of the camera `eyeposition`, the point which the camera focuses `lookat` and the up direction of the camera `upvector`. These can be accessed as `cam.eyeposition` etc and adjusted via `update_cam!(scene, cameracontrols(scene), eyeposition, lookat[, upvector = Vec3f(0, 0, 1)])`. They can also be passed as keyword arguments when the camera is constructed. @@ -133,10 +133,6 @@ function Camera3D(scene::Scene; kwargs...) mouse_translationspeed = 1f0, mouse_zoomspeed = 1f0, - enable_translation = true, - enable_rotation = true, - enable_zoom = true, - projectiontype = Makie.Perspective, circular_rotation = (true, true, true), rotation_center = :lookat, @@ -478,8 +474,6 @@ end function _translate_cam!(scene, cam::Camera3D, t) - cam.settings.enable_translation[] || return - @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) # This uses a camera based coordinate system where @@ -508,8 +502,6 @@ end function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) - cam.settings.enable_rotation[] || return - @extractvalue cam.controls (fix_x_key, fix_y_key, fix_z_key) @extractvalue cam.settings (fixed_axis, circular_rotation, rotation_center) @@ -577,8 +569,6 @@ end function _zoom!(scene, cam::Camera3D, zoom_step, cad = false) - cam.settings.enable_zoom[] || return - lookat = cam.lookat[] eyepos = cam.eyeposition[] viewdir = lookat - eyepos # -z From 3e0728953461ee19d18d149e2da2499902ac39a3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 12 Mar 2023 23:37:22 +0100 Subject: [PATCH 23/80] update test values --- test/events.jl | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/test/events.jl b/test/events.jl index 31b2b484eb8..657242025b8 100644 --- a/test/events.jl +++ b/test/events.jl @@ -200,7 +200,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) cc = cameracontrols(scene) - + # Verify initial camera state @test cc.lookat[] == Vec3f(0) @test cc.eyeposition[] == Vec3f(3) @@ -218,15 +218,15 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # 2) Outside scene, in drag e.mouseposition[] = (1000, 450) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491522) - @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) + @test cc.eyeposition[] ≈ Vec3f(0.7128954, -4.1037745, 3.106576) + @test cc.upvector[] ≈ Vec3f(0.82216865, -0.17919835, 0.54030234) # 3) not in drag e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) e.mouseposition[] = (400, 250) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491522) - @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) + @test cc.eyeposition[] ≈ Vec3f(0.7128954, -4.1037745, 3.106576) + @test cc.upvector[] ≈ Vec3f(0.82216865, -0.17919835, 0.54030234) @@ -243,23 +243,24 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # translation # 1) In scene, in drag + e.mouseposition[] = (400, 250) e.mousebutton[] = MouseButtonEvent(Mouse.right, Mouse.press) e.mouseposition[] = (600, 250) - @test cc.lookat[] ≈ Vec3f(5.4697413, -3.3484206, -2.1213205) - @test cc.eyeposition[] ≈ Vec3f(8.469742, -0.34842062, 0.8786795) + @test cc.lookat[] ≈ Vec3f(1.0146117, -1.0146117, 0.0) + @test cc.eyeposition[] ≈ Vec3f(4.0146117, 1.9853883, 3.0) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) # 2) Outside scene, in drag e.mouseposition[] = (1000, 450) - @test cc.lookat[] ≈ Vec3f(9.257657, -5.4392805, -3.818377) - @test cc.eyeposition[] ≈ Vec3f(12.257658, -2.4392805, -0.81837714) + @test cc.lookat[] ≈ Vec3f(3.6296215, -2.4580488, -1.1715729) + @test cc.eyeposition[] ≈ Vec3f(6.6296215, 0.5419513, 1.8284271) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) # 3) not in drag e.mousebutton[] = MouseButtonEvent(Mouse.right, Mouse.release) e.mouseposition[] = (400, 250) - @test cc.lookat[] ≈ Vec3f(9.257657, -5.4392805, -3.818377) - @test cc.eyeposition[] ≈ Vec3f(12.257658, -2.4392805, -0.81837714) + @test cc.lookat[] ≈ Vec3f(3.6296215, -2.4580488, -1.1715729) + @test cc.eyeposition[] ≈ Vec3f(6.6296215, 0.5419513, 1.8284271) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) @@ -274,22 +275,20 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right @test cc.lookat[] == Vec3f(0) @test cc.eyeposition[] == Vec3f(3) @test cc.upvector[] == Vec3f(0, 0, 1) - @test cc.zoom_mult[] == 1f0 # Zoom + e.mouseposition[] = (400, 250) # for debugging e.scroll[] = (0.0, 4.0) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(3) + @test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) - @test cc.zoom_mult[] ≈ 0.6830134f0 # should not work outside the scene e.mouseposition[] = (1000, 450) e.scroll[] = (0.0, 4.0) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(3) + @test cc.eyeposition[] ≈ 0.6830134f0 * Vec3f(3) @test cc.upvector[] ≈ Vec3f(0.0, 0.0, 1.0) - @test cc.zoom_mult[] ≈ 0.6830134f0 end @testset "mouse state machine" begin From 11313dc211608cc8a6463d56557be5dbbb0d033e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 13 Mar 2023 00:03:05 +0100 Subject: [PATCH 24/80] cleanup docstring --- src/camera/camera3d.jl | 68 +++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 9ec826e4b42..ff449803b9f 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -8,7 +8,6 @@ struct Camera3D <: AbstractCamera3D # Interactivity pulser::Observable{Float64} selected::Observable{Bool} - # scroll_mod::Observable{Bool} # view matrix eyeposition::Observable{Vec3f} @@ -22,24 +21,37 @@ struct Camera3D <: AbstractCamera3D end """ - Camera3D(scene[; attributes...]) + Camera3D(scene[; kwargs...]) -Creates a 3d camera with a lot of controls. +Sets up a 3D camera with mouse and keyboard controls. -The 3D camera is (or can be) unrestricted in terms of rotations and translations. Both `cam3d!(scene)` and `cam3d_cad!(scene)` create this camera type. Unlike the 2D camera, settings and controls are stored in the `cam.attributes` field rather than in the struct directly, but can still be passed as keyword arguments. The general camera settings include +The behavior of the camera can be adjusted via keyword arguments or the fields +`settings` and `controls`. + +## Settings + +Settings include anything that isn't a mouse or keyboard button. -- `fov = 45f0` sets the "neutral" field of view, i.e. the fov corresponding to no zoom. This is irrelevant if the camera uses an orthographic projection. -- `near = automatic` sets the value of the near clip. By default this will be chosen based on the scenes bounding box. The final value is in `cam.near`. -- `far = automatic` sets the value of the far clip. By default this will be chosen based on the scenes bounding box. The final value is in `cam.far`. -- `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. - `projectiontype = Perspective` sets the type of the projection. Can be `Orthographic` or `Perspective`. +- `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. - `fixed_axis = false`: 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. Only applies to orthographic cameras. - `cad = false`: If true rotates the view around `lookat` when zooming off-center. -The camera view follows from the position of the camera `eyeposition`, the point which the camera focuses `lookat` and the up direction of the camera `upvector`. These can be accessed as `cam.eyeposition` etc and adjusted via `update_cam!(scene, cameracontrols(scene), eyeposition, lookat[, upvector = Vec3f(0, 0, 1)])`. They can also be passed as keyword arguments when the camera is constructed. +- `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. +- `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. +- `keyboard_zoomspeed = 1f0` sets the speed of keyboard based zooms. -The camera can be controlled by keyboard and mouse. The keyboard has the following available attributes +- `mouse_rotationspeed = 1f0` sets the speed of mouse rotations. +- `mouse_translationspeed = 0.5f0` sets the speed of mouse translations. +- `mouse_zoomspeed = 1f0` sets the speed of mouse zooming (mousewheel). + +- `update_rate = 1/30` sets the rate at which keyboard based camera updates are evaluated. +- `circular_rotation = (true, true, true)` enables circular rotations for (fixed x, fixed y, fixed z) rotation axis. (This means drawing a circle with your mouse around the center of the scene will result in a continuous rotation.) + +## Controls + +Controls include any kind of hotkey setting. - `up_key = Keyboard.r` sets the key for translations towards the top of the screen. - `down_key = Keyboard.f` sets the key for translations towards the bottom of the screen. @@ -50,8 +62,6 @@ The camera can be controlled by keyboard and mouse. The keyboard has the followi - `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (enlarge, via fov). - `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (shrink, via fov). -- `stretch_view_key = Keyboard.page_up` sets the key for moving `eyepostion` away from `lookat`. -- `contract_view_key = Keyboard.page_down` sets the key for moving `eyeposition` towards `lookat`. - `pan_left_key = Keyboard.j` sets the key for rotations around the screens vertical axis. - `pan_right_key = Keyboard.l` sets the key for rotations around the screens vertical axis. @@ -60,30 +70,32 @@ The camera can be controlled by keyboard and mouse. The keyboard has the followi - `roll_clockwise_key = Keyboard.e` sets the key for rotations of the screen. - `roll_counterclockwise_key = Keyboard.q` sets the key for rotations of the screen. -- `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. -- `keyboard_translationspeed = 0.5f0` sets the speed of keyboard based translations. -- `keyboard_zoomspeed = 1f0` sets the speed of keyboard based zooms. -- `update_rate = 1/30` sets the rate at which keyboard based camera updates are evaluated. - -and mouse interactions are controlled by +- `fix_x_key = Keyboard.x` sets the key for fixing translations and rotations to the (world/plot) x-axis. +- `fix_y_key = Keyboard.y` sets the key for fixing translations and rotations to the (world/plot) y-axis. +- `fix_z_key = Keyboard.z` sets the key for fixing translations and rotations to the (world/plot) z-axis. +- `reset = Keyboard.home` sets the key for fully resetting the camera. This equivalent to setting `lookat = Vec3f(0)`, `upvector = Vec3f(0, 0, 1)`, `eyeposition = Vec3f(3)` and then calling `center!(scene)`. - `translation_button = Mouse.right` sets the mouse button for drag-translations. (up/down/left/right) - `scroll_mod = true` sets an additional modifier button for scroll-based zoom. (true being neutral) - `rotation_button = Mouse.left` sets the mouse button for drag-rotations. (pan, tilt) -- `mouse_rotationspeed = 1f0` sets the speed of mouse rotations. -- `mouse_translationspeed = 0.5f0` sets the speed of mouse translations. -- `mouse_zoomspeed = 1f0` sets the speed of mouse zooming (mousewheel). -- `circular_rotation = (true, true, true)` enables circular rotations for (fixed x, fixed y, fixed z) rotation axis. (This means drawing a circle with your mouse around the center of the scene will result in a continuous rotation.) +## Other kwargs -There are also a few generally applicable controls: +Some keyword arguments are used to initialize fields. These include -- `fix_x_key = Keyboard.x` sets the key for fixing translations and rotations to the (world/plot) x-axis. -- `fix_y_key = Keyboard.y` sets the key for fixing translations and rotations to the (world/plot) y-axis. -- `fix_z_key = Keyboard.z` sets the key for fixing translations and rotations to the (world/plot) z-axis. -- `reset = Keyboard.home` sets the key for fully resetting the camera. This equivalent to setting `lookat = Vec3f(0)`, `upvector = Vec3f(0, 0, 1)`, `eyeposition = Vec3f(3)` and then calling `center!(scene)`. +- `eyeposition = Vec3f(3)`: The position of the camera. +- `lookat = Vec3f(0)`: The point the camera is focused on. +- `upvector = Vec3f(0, 0, 1)`: The world direction corresponding to the up direction of the screen. + +- `fov = 45.0` is the field of view. This is irrelevant if the camera uses an orthographic projection. +- `near = 0.1` sets the position of the near clip plane relative to `eyeposition - lookat`. Must be greater 0. Anything between the camera and the near clip plane is hidden. +- `far = 10.0` sets the position of the far clip plane relative to `eyeposition - lookat`. Anything further away than the far clip plane is hidden. + +Note that updating these observables in an active camera requires a call to `update_cam(scene)` +for them to be applied. For updating `eyeposition`, `lookat` and/or upvector +`update_cam!(scene, eyeposition, lookat, upvector = Vec3f(0,0,1))` is preferred. -You can also make adjustments to the camera position, rotation and zoom by calling relevant functions: +The camera position and orientation can also be adjusted via the functions - `translate_cam!(scene, v)` will translate the camera by the given world/plot space vector `v`. - `rotate_cam!(scene, angles)` will rotate the camera around its axes with the corresponding angles. The first angle will rotate around the cameras "right" that is the screens horizontal axis, the second around the up vector/vertical axis or `Vec3f(0, 0, +-1)` if `fixed_axis = true`, and the third will rotate around the view direction i.e. the axis out of the screen. The rotation respects the current `rotation_center` of the camera. From 4de5fa3b2e7760c693b3561b33c5c09d46688d2a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 13 Mar 2023 00:15:11 +0100 Subject: [PATCH 25/80] switch back rotation order to avoid change in upvector --- src/camera/camera3d.jl | 6 ++++-- test/events.jl | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index ff449803b9f..4c3ad8cd28d 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -537,8 +537,10 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) rotation = Quaternionf(0, 0, 0, 1) if !xor(fix_x, fix_y, fix_z) # if there are more or less than one restriction apply all rotations - rotation *= qrotation(x_axis, angles[1]) + # Note that the y rotation needs to happen first here so that + # fixed_axis = true actually keeps the the axis fixed. rotation *= qrotation(y_axis, angles[2]) + rotation *= qrotation(x_axis, angles[1]) rotation *= qrotation(z_axis, angles[3]) else # apply world space restrictions @@ -557,8 +559,8 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) ) else # restrict total quaternion rotation to one axis - rotation *= qrotation(x_axis, angles[1]) rotation *= qrotation(y_axis, angles[2]) + rotation *= qrotation(x_axis, angles[1]) rotation *= qrotation(z_axis, angles[3]) # the first three components are related to rotations around the x/y/z-axis rotation = Quaternionf(rotation.data .* (fix_x, fix_y, fix_z, 1)) diff --git a/test/events.jl b/test/events.jl index 657242025b8..464fa170627 100644 --- a/test/events.jl +++ b/test/events.jl @@ -218,15 +218,15 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right # 2) Outside scene, in drag e.mouseposition[] = (1000, 450) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(0.7128954, -4.1037745, 3.106576) - @test cc.upvector[] ≈ Vec3f(0.82216865, -0.17919835, 0.54030234) + @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491514) + @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) # 3) not in drag e.mousebutton[] = MouseButtonEvent(Mouse.left, Mouse.release) e.mouseposition[] = (400, 250) @test cc.lookat[] ≈ Vec3f(0) - @test cc.eyeposition[] ≈ Vec3f(0.7128954, -4.1037745, 3.106576) - @test cc.upvector[] ≈ Vec3f(0.82216865, -0.17919835, 0.54030234) + @test cc.eyeposition[] ≈ Vec3f(-2.8912058, -3.8524969, -1.9491514) + @test cc.upvector[] ≈ Vec3f(-0.5050875, -0.6730229, 0.5403024) From ae7f89adbd5551fa6d7f26fb87bfc021466229d7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 13 Mar 2023 00:34:04 +0100 Subject: [PATCH 26/80] fix docstring [skip ci] --- src/camera/camera3d.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 4c3ad8cd28d..577baeb96b4 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -34,7 +34,7 @@ Settings include anything that isn't a mouse or keyboard button. - `projectiontype = Perspective` sets the type of the projection. Can be `Orthographic` or `Perspective`. - `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. -- `fixed_axis = false`: If true panning uses the (world/plot) z-axis instead of the camera up direction. +- `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. Only applies to orthographic cameras. - `cad = false`: If true rotates the view around `lookat` when zooming off-center. From d31d905f1664296fda4d44ab2dc3a5278a123a24 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 13 Mar 2023 00:56:41 +0100 Subject: [PATCH 27/80] update docs --- src/camera/camera3d.jl | 44 ++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 577baeb96b4..80771175700 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -335,7 +335,7 @@ function on_pulse(scene, cam::Camera3D, timestep) if zooming zoom_step = (1f0 + keyboard_zoomspeed * timestep) ^ (zoom_out - zoom_in) - _zoom!(scene, cam, zoom_step, false) + _zoom!(scene, cam, zoom_step, false, false) end # if any are active, update matrices, else stop clock @@ -350,7 +350,7 @@ end function add_translation!(scene, cam::Camera3D) @extract cam.controls (translation_button, scroll_mod) - @extract cam.settings (mouse_translationspeed, mouse_zoomspeed, cad, projectiontype) + @extract cam.settings (mouse_translationspeed, mouse_zoomspeed, cad, projectiontype, zoom_shift_lookat) last_mousepos = RefValue(Vec2f(0, 0)) dragging = RefValue(false) @@ -400,7 +400,7 @@ function add_translation!(scene, cam::Camera3D) on(camera(scene), scene.events.scroll) do scroll if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) zoom_step = (1f0 + 0.1f0 * mouse_zoomspeed[]) ^ -scroll[2] - zoom!(scene, cam, zoom_step, cad[]) + _zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[]) return Consume(true) end return Consume(false) @@ -457,29 +457,53 @@ end # Simplified methods +""" + translate_cam!(scene, cam::Camera3D, v::Vec3) + +Translates the camera by the given vector in camera space, i.e. by `v[1]` to +the right, `v[2]` to the top and `v[3]` forward. + +Note that this method reacts to `fix_x_key` etc. If any of those keys are +pressed the translation will be restricted to act in these directions. +""" function translate_cam!(scene, cam::Camera3D, t::VecTypes) _translate_cam!(scene, cam, t) update_cam!(scene, cam) nothing end +""" + rotate_cam!(scene, cam::Camera3D, angles::Vec3) + +Rotates the camera by the given `angles` around the camera x- (left, right), +y- (up, down) and z-axis (in out). The rotation around the y axis is applied +first, then x, then y. + +Note that this method reacts to `fix_x_key` etc and `fixed_axis`. The former +restrict the rotation around a specific axis when a given key is pressed. The +latter keeps the camera y axis fixed as the data space z axis. +""" function rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) _rotate_cam!(scene, cam, angles, from_mouse) update_cam!(scene, cam) nothing end + +zoom!(scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false, false) """ - zoom!(scene, zoom_step) + zoom!(scene, cam::Camera3D, zoom_step[, cad = false, zoom_shift_lookat = false]) Zooms the camera in or out based on the multiplier `zoom_step`. A `zoom_step` of 1.0 is neutral, larger zooms out and lower zooms in. -Note that this method only applies to Camera3D. +If `cad = true` zooming will also apply a rotation based on how far the cursor +is from the center of the scene. If `zoom_shift_lookat = true` and +`projectiontype = Orthographic` zooming will keep the data under the cursor at +the same screen space position. """ -zoom!(scene, zoom_step) = zoom!(scene, cameracontrols(scene), zoom_step, false) -function zoom!(scene, cam, zoom_step, cad = false) - _zoom!(scene, cam, zoom_step, cad) +function zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false) + _zoom!(scene, cam, zoom_step, cad, zoom_shift_lookat) update_cam!(scene, cam) nothing end @@ -582,7 +606,7 @@ function _rotate_cam!(scene, cam::Camera3D, angles::VecTypes, from_mouse=false) end -function _zoom!(scene, cam::Camera3D, zoom_step, cad = false) +function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat = false) lookat = cam.lookat[] eyepos = cam.eyeposition[] viewdir = lookat - eyepos # -z @@ -598,7 +622,7 @@ function _zoom!(scene, cam::Camera3D, zoom_step, cad = false) shift *= 0.1 * sign(1 - zoom_step) * norm(viewdir) cam.eyeposition[] = lookat - zoom_step * viewdir + shift - elseif cam.settings.projectiontype[] == Makie.Orthographic && cam.settings.zoom_shift_lookat[] + elseif cam.settings.projectiontype[] == Makie.Orthographic && zoom_shift_lookat[] # keep data under cursor u_z = normalize(viewdir) u_x = normalize(cross(u_z, cam.upvector[])) From 8185a966409afba76bdfb23e2ccaeb27c91911a8 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 13 Mar 2023 01:09:17 +0100 Subject: [PATCH 28/80] update tests --- ReferenceTests/src/tests/examples3d.jl | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 6225f1dd8b0..6d9338aa5c0 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -29,19 +29,11 @@ end meshes = map(colormesh, rectangles) fig, ax, meshplot = mesh(merge(meshes)) scene = ax.scene - center!(scene) cam = cameracontrols(scene) - dir = widths(data_limits(scene)) ./ 2. - dir_scaled = Vec3f( - dir[1] * scene.transformation.scale[][1], - 0.0, - dir[3] * scene.transformation.scale[][2], - ) + cam.settings[:projectiontype][] = Makie.Orthographic cam.upvector[] = (0.0, 0.0, 1.0) - cam.lookat[] = minimum(data_limits(scene)) + dir_scaled - cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 6.3, cam.lookat[][3]) - cam.attributes[:projectiontype][] = Makie.Orthographic - cam.zoom_mult[] = 0.61f0 + cam.lookat[] = Vec3f(0.595, 2.5, 0.5) + cam.eyeposition[] = (cam.lookat[][1], cam.lookat[][2] + 0.61, cam.lookat[][3]) update_cam!(scene, cam) fig end @@ -556,7 +548,7 @@ end end end cam = cameracontrols(ax.scene) - cam.attributes.fov[] = 22f0 + cam.fov[] = 22f0 update_cam!(ax.scene, cam, Vec3f(0.625, 0, 3.5), Vec3f(0.625, 0, 0), Vec3f(0, 1, 0)) fig end From 3c22c4748eeca917e4bb7c7a2d5fff180ab033ee Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 15 Mar 2023 15:41:27 +0100 Subject: [PATCH 29/80] fix zoom and switch back to more extreme near/far --- src/camera/camera3d.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 80771175700..13e1b8e02f9 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -170,8 +170,8 @@ function Camera3D(scene::Scene; kwargs...) # Semi-Internal - projection matrix get(overwrites, :fov, Observable(45.0)), - get(overwrites, :near, Observable(0.1)), - get(overwrites, :far, Observable(10.0)), + get(overwrites, :near, Observable(0.01)), + get(overwrites, :far, Observable(100.0)), ) disconnect!(camera(scene)) @@ -400,7 +400,7 @@ function add_translation!(scene, cam::Camera3D) on(camera(scene), scene.events.scroll) do scroll if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) zoom_step = (1f0 + 0.1f0 * mouse_zoomspeed[]) ^ -scroll[2] - _zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[]) + zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[]) return Consume(true) end return Consume(false) From 6786f96178c32eee49a92c85973a0b30148e90c6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 15 Mar 2023 17:53:18 +0100 Subject: [PATCH 30/80] tweak default zoom some more --- src/camera/camera3d.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 13e1b8e02f9..6cb687e9f2c 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -677,14 +677,14 @@ function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) old_dir = normalize(cam.eyeposition[] .- cam.lookat[]) if cam.settings.projectiontype[] == Makie.Perspective - dist = 0.5 * norm(width) * (1.0 + 0.6 / tand(0.5 * cam.fov[])) + dist = 0.5 * norm(width) / tand(0.5 * cam.fov[]) else - dist = 0.6 * norm(width) + dist = 0.5 * norm(width) end - cam.lookat[] = center + cam.lookat[] = center cam.eyeposition[] = cam.lookat[] .+ dist * old_dir - cam.upvector[] = Vec3f(0, 0, 1) # Should we reset this? + cam.upvector[] = Vec3f(0, 0, 1) # Should we reset this? update_cam!(scene, cam) From 52742953975a23b5801f14c8ac61bcb54fe06f40 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 16 Mar 2023 18:46:30 +0100 Subject: [PATCH 31/80] Add update_cam! with angles --- src/camera/camera3d.jl | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 6cb687e9f2c..120e26158d9 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -692,7 +692,7 @@ function update_cam!(scene::Scene, cam::Camera3D, area3d::Rect) end # Update camera position via camera Position & Orientation -function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = camera.upvector[]) +function update_cam!(scene::Scene, camera::Camera3D, eyeposition::VecTypes, lookat::VecTypes, up::VecTypes = camera.upvector[]) camera.lookat[] = Vec3f(lookat) camera.eyeposition[] = Vec3f(eyeposition) camera.upvector[] = Vec3f(up) @@ -700,6 +700,28 @@ function update_cam!(scene::Scene, camera::Camera3D, eyeposition, lookat, up = c return end +update_cam!(scene::Scene, args::Real...) = update_cam!(scene, cameracontrols(scene), args...) + +""" + update_cam!(scene, cam::Camera3D, ϕ, θ[, radius]) + +Set the camera position based on two angles `0 ≤ ϕ ≤ 2π` and `-pi/2 ≤ θ ≤ pi/2` +and an optional radius around the current `cam.lookat[]`. +""" +function update_cam!( + scene::Scene, camera::Camera3D, phi::Real, theta::Real, + radius::Real = norm(camera.eyeposition[] - camera.lookat[]) + ) + st, ct = sincos(theta) + sp, cp = sincos(phi) + v = Vec3f(ct * cp, ct * sp, st) + u = Vec3f(-st * cp, -st * sp, ct) + camera.eyeposition[] = camera.lookat[] .+ radius * v + camera.upvector[] = u + update_cam!(scene, camera) + return +end + function show_cam(scene) cam = cameracontrols(scene) From c890d34efedb5f5c5066e4fa071b2ed3420d6278 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 19 Mar 2023 15:09:00 +0100 Subject: [PATCH 32/80] update NEWS --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 91da2178af0..329aefb8131 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # News +## next breaking + +- Made some adjustments to the 3D camera (switched back from fov to location based zoom, split hotkeys from other options & minor QoL changes) [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) + ## master - Added the `stephist` plotting function. [#2408](https://github.com/JuliaPlots/Makie.jl/pull/2408). From d6639fca62c4e6ce1148489a73f8d03a90697598 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 19 Mar 2023 15:28:48 +0100 Subject: [PATCH 33/80] bring back perspective zoom_shift_lookat --- src/camera/camera3d.jl | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 120e26158d9..33ff068c3d7 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -35,7 +35,7 @@ Settings include anything that isn't a mouse or keyboard button. - `projectiontype = Perspective` sets the type of the projection. Can be `Orthographic` or `Perspective`. - `rotation_center = :lookat` sets the default center for camera rotations. Currently allows `:lookat` or `:eyeposition`. - `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. Only applies to orthographic cameras. +- `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. - `keyboard_rotationspeed = 1f0` sets the speed of keyboard based rotations. @@ -622,18 +622,27 @@ function _zoom!(scene, cam::Camera3D, zoom_step, cad = false, zoom_shift_lookat shift *= 0.1 * sign(1 - zoom_step) * norm(viewdir) cam.eyeposition[] = lookat - zoom_step * viewdir + shift - elseif cam.settings.projectiontype[] == Makie.Orthographic && zoom_shift_lookat[] + elseif zoom_shift_lookat # keep data under cursor u_z = normalize(viewdir) 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] - shift = (1 - zoom_step) * norm(viewdir) * (rel_pos[1] * u_x + rel_pos[2] * u_y) + shift = (1 - zoom_step) * (rel_pos[1] * u_x + rel_pos[2] * u_y) + + if cam.settings.projectiontype[] == Makie.Orthographic + scale = norm(viewdir) + else + # With perspective projection depth scales shift, but there is no way + # to tell which depth the user may want to keep in view. So we just + # assume it's the same depth as "lookat". + scale = norm(viewdir) * tand(0.5 * cam.fov[]) + end - cam.lookat[] = lookat + shift - cam.eyeposition[] = lookat - zoom_step * viewdir + shift + cam.lookat[] = lookat + scale * shift + cam.eyeposition[] = lookat - zoom_step * viewdir + scale * shift else # just zoom in/out cam.eyeposition[] = lookat - zoom_step * viewdir From becc8cd707e42b3a4b9299fdc1bf266ac20f202c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 19 Mar 2023 15:49:28 +0100 Subject: [PATCH 34/80] add center in orbital update_cam! --- src/camera/camera3d.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 33ff068c3d7..4d18d70edbc 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -719,13 +719,15 @@ and an optional radius around the current `cam.lookat[]`. """ function update_cam!( scene::Scene, camera::Camera3D, phi::Real, theta::Real, - radius::Real = norm(camera.eyeposition[] - camera.lookat[]) + radius::Real = norm(camera.eyeposition[] - camera.lookat[]), + center = camera.lookat[] ) st, ct = sincos(theta) sp, cp = sincos(phi) v = Vec3f(ct * cp, ct * sp, st) u = Vec3f(-st * cp, -st * sp, ct) - camera.eyeposition[] = camera.lookat[] .+ radius * v + camera.lookat[] = center + camera.eyeposition[] = center .+ radius * v camera.upvector[] = u update_cam!(scene, camera) return From f264360dad9a68b59c73055df538fb3971b51f62 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 19 Mar 2023 16:04:24 +0100 Subject: [PATCH 35/80] bring back fov controls --- src/camera/camera3d.jl | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 4d18d70edbc..a0a8aceec84 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -60,8 +60,10 @@ Controls include any kind of hotkey setting. - `forward_key = Keyboard.w` sets the key for translations into the screen. - `backward_key = Keyboard.s` sets the key for translations out of the screen. -- `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (enlarge, via fov). -- `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (shrink, via fov). +- `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (translate eyeposition towards lookat). +- `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (translate eyeposition away from lookat). +- `increase_fov_key = Keyboard.page_up` sets the key for increasing the fov. +- `decrease_fov_key = Keyboard.page_down` sets the key for increasing the fov. - `pan_left_key = Keyboard.j` sets the key for rotations around the screens vertical axis. - `pan_right_key = Keyboard.l` sets the key for rotations around the screens vertical axis. @@ -116,6 +118,8 @@ function Camera3D(scene::Scene; kwargs...) # Zooms zoom_in_key = Keyboard.u, zoom_out_key = Keyboard.o, + increase_fov_key = Keyboard.page_up, + decrease_fov_key = Keyboard.page_down, # Rotations pan_left_key = Keyboard.j, pan_right_key = Keyboard.l, @@ -191,11 +195,11 @@ function Camera3D(scene::Scene; kwargs...) keynames = ( :up_key, :down_key, :left_key, :right_key, :forward_key, :backward_key, - :zoom_in_key, :zoom_out_key, + :zoom_in_key, :zoom_out_key, :increase_fov_key, :decrease_fov_key, :pan_left_key, :pan_right_key, :tilt_up_key, :tilt_down_key, :roll_clockwise_key, :roll_counterclockwise_key ) - + # Start ticking if relevant keys are pressed on(camera(scene), events(scene).keyboardbutton) do event if event.action in (Keyboard.press, Keyboard.repeat) && cam.pulser[] == -1.0 && @@ -276,7 +280,7 @@ function on_pulse(scene, cam::Camera3D, timestep) @extractvalue cam.controls ( right_key, left_key, up_key, down_key, backward_key, forward_key, tilt_up_key, tilt_down_key, pan_left_key, pan_right_key, roll_counterclockwise_key, roll_clockwise_key, - zoom_out_key, zoom_in_key + zoom_out_key, zoom_in_key, increase_fov_key, decrease_fov_key ) @extractvalue cam.settings ( keyboard_translationspeed, keyboard_rotationspeed, keyboard_zoomspeed, projectiontype @@ -338,8 +342,18 @@ function on_pulse(scene, cam::Camera3D, timestep) _zoom!(scene, cam, zoom_step, false, false) end + # fov + fov_inc = ispressed(scene, increase_fov_key) + fov_dec = ispressed(scene, decrease_fov_key) + fov_adjustment = fov_inc || fov_dec + + if fov_adjustment + step = (1 + keyboard_zoomspeed * timestep) ^ (fov_inc - fov_dec) + cam.fov[] = clamp(cam.fov[] * step, 0.1, 179) + end + # if any are active, update matrices, else stop clock - if translating || rotating || zooming + if translating || rotating || zooming || fov_adjustment update_cam!(scene, cam) return true else From 87e7254f2f2dcf9c57ee893d1ff0ab3783cef3e7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 19 Mar 2023 16:28:45 +0100 Subject: [PATCH 36/80] deprecate old_cam [skip ci] --- src/camera/old_camera3d.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/camera/old_camera3d.jl b/src/camera/old_camera3d.jl index 2d5f424cccf..e3167262327 100644 --- a/src/camera/old_camera3d.jl +++ b/src/camera/old_camera3d.jl @@ -96,7 +96,12 @@ An alias to [`old_cam3d_turntable!`](@ref). Creates a 3D camera for `scene`, which rotates around the plot's axis. """ -const old_cam3d! = old_cam3d_turntable! +old_cam3d!(scene::Scene; kwargs...) = old_cam3d_turntable!(scene; kwargs...) + +@deprecate old_cam3d! cam3d! +@deprecate old_cam3d_turntable! cam3d! +@deprecate old_cam3d_cad! cam3d_cad! + function projection_switch( wh::Rect2, From 6594406726f49974b7712605e899b593e381aa5a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 28 Mar 2023 17:20:28 +0200 Subject: [PATCH 37/80] fix whitespace? --- src/camera/camera3d.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index a0a8aceec84..41652b28ab8 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -9,12 +9,12 @@ struct Camera3D <: AbstractCamera3D pulser::Observable{Float64} selected::Observable{Bool} - # view matrix + # view matrix eyeposition::Observable{Vec3f} lookat::Observable{Vec3f} upvector::Observable{Vec3f} - # perspective projection matrix + # perspective projection matrix fov::Observable{Float32} near::Observable{Float32} far::Observable{Float32} From 43e64284d9ac57868129bc6ede601bd441db1264 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 28 Mar 2023 17:21:09 +0200 Subject: [PATCH 38/80] cleanup news --- NEWS.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0daffc5b8cd..d7f5de08910 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,11 +1,9 @@ # News -## next breaking +## master - Made some adjustments to the 3D camera (switched back from fov to location based zoom, split hotkeys from other options & minor QoL changes) [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) -## master - ## v0.19.3 - Added the `stephist` plotting function [#2408](https://github.com/JuliaPlots/Makie.jl/pull/2408). From 4f52bafd6b807013af8c4ceeafc56ea7d780eeca Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 12:19:46 +0200 Subject: [PATCH 39/80] squash merge jw/gl_linux_hidpi --- .github/workflows/glmakie.yaml | 2 +- GLMakie/Project.toml | 2 +- GLMakie/assets/shader/distance_shape.frag | 12 +- GLMakie/assets/shader/lines.frag | 4 - GLMakie/assets/shader/lines.geom | 55 ++++----- GLMakie/assets/shader/sprites.geom | 3 +- GLMakie/src/drawing_primitives.jl | 1 + GLMakie/src/events.jl | 114 ++++++++----------- GLMakie/src/glwindow.jl | 31 ++++-- GLMakie/src/picking.jl | 16 +-- GLMakie/src/postprocessing.jl | 5 +- GLMakie/src/rendering.jl | 20 ++-- GLMakie/src/screen.jl | 103 ++++++++++++----- GLMakie/test/runtests.jl | 2 +- GLMakie/test/unit_tests.jl | 130 ++++++++++++++++++++++ NEWS.md | 4 + docs/documentation/backends/glmakie.md | 44 ++++++++ src/theming.jl | 2 + 18 files changed, 377 insertions(+), 173 deletions(-) diff --git a/.github/workflows/glmakie.yaml b/.github/workflows/glmakie.yaml index 0d8b24bfe79..0fb1ca3eea9 100644 --- a/.github/workflows/glmakie.yaml +++ b/.github/workflows/glmakie.yaml @@ -44,7 +44,7 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v1 - - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev + - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev xsettingsd x11-xserver-utils - name: Install Julia dependencies shell: julia --project=monorepo {0} run: | diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index 67c405882df..ad100023a24 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -27,7 +27,7 @@ Colors = "0.11, 0.12" FileIO = "1.6" FixedPointNumbers = "0.7, 0.8" FreeTypeAbstraction = "0.10" -GLFW = "3" +GLFW = "3.3" GeometryBasics = "0.4.1" Makie = "=0.19.3" MeshIO = "0.4" diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag index df3e66e064c..18c750e5a41 100644 --- a/GLMakie/assets/shader/distance_shape.frag +++ b/GLMakie/assets/shader/distance_shape.frag @@ -25,6 +25,7 @@ uniform float stroke_width; uniform float glow_width; uniform int shape; // shape is a uniform for now. Making them a in && using them for control flow is expected to kill performance uniform vec2 resolution; +uniform float px_per_unit; uniform bool transparent_picking; flat in float f_viewport_from_u_scale; @@ -97,7 +98,9 @@ void stroke(vec4 strokecolor, float signed_distance, float width, inout vec4 col void glow(vec4 glowcolor, float signed_distance, float inside, inout vec4 color){ if (glow_width > 0.0){ - float outside = (abs(signed_distance)-stroke_width)/glow_width; + 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; color = mix(vec4(glowcolor.rgb, glowcolor.a*alpha), color, inside); } @@ -145,13 +148,14 @@ void main(){ // See notes in geometry shader where f_viewport_from_u_scale is computed. signed_distance *= f_viewport_from_u_scale; - float inside_start = max(-stroke_width, 0.0); + 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); - stroke(f_stroke_color, signed_distance, -stroke_width, final_color); - glow(f_glow_color, signed_distance, aastep(-stroke_width, signed_distance), final_color); + 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/lines.frag b/GLMakie/assets/shader/lines.frag index e3a11c4f7a9..63378a70816 100644 --- a/GLMakie/assets/shader/lines.frag +++ b/GLMakie/assets/shader/lines.frag @@ -56,10 +56,6 @@ vec2 get_sd(Nothing _, vec2 uv){ return uv; } -float ifelse(bool condition, float true_val, float false_val){ - return float(condition) * (true_val - false_val) + false_val; -} - void main(){ vec4 color = vec4(f_color.rgb, 0.0); vec2 xy = get_sd(pattern, f_uv); diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index 0e0ebf1c47b..0307c396565 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -41,16 +41,6 @@ vec2 screen_space(vec4 vertex) return vec2(vertex.xy / vertex.w) * resolution; } -// should be noticably faster than branching if true_val and false_val are -// easy to calculate -float ifelse(bool condition, float true_val, float false_val){ - return float(condition) * (true_val - false_val) + false_val; -} -float ifelse(float condition, float true_val, float false_val){ - return condition * (true_val - false_val) + false_val; -} - - //////////////////////////////////////////////////////////////////////////////// /// Emit Vertex Methods //////////////////////////////////////////////////////////////////////////////// @@ -158,12 +148,14 @@ void draw_patterned_line(bool isvalid[4]) // update left side temp = ceil((edge1 - right) * inv_pl) + left * inv_pl; - start_width = ifelse(temp < start, right - left, start_width); + if (temp < start) + start_width = right - left; start = min(start, temp); // update right side temp = floor((edge2 - left) * inv_pl) + right * inv_pl; - stop_width = ifelse(temp > stop, right - left, stop_width); + if (temp > stop) + stop_width = right - left; stop = max(stop, temp); } // Technically start and stop should be offset by another @@ -297,11 +289,10 @@ void draw_patterned_line(bool isvalid[4]) // If the line starts with this segment or the center of the "on" // section of the pattern is in this segment, we draw it, else // we skip past the first "on" section. - start = ifelse( - !isvalid[0] || (start > (g_lastlen[1] - start_width) * px2uv), - start - AA_THICKNESS * px2uv, - start + (start_width + 0.5 * AA_THICKNESS) * inv_pl - ); + if (!isvalid[0] || (start > (g_lastlen[1] - start_width) * px2uv)) + start = start - AA_THICKNESS * px2uv; + else + start = start + (start_width + 0.5 * AA_THICKNESS) * inv_pl; p1 += (2 * start * pattern_length - g_lastlen[1]) * v1; } @@ -327,11 +318,10 @@ void draw_patterned_line(bool isvalid[4]) } else { miter_b = n1; length_b = thickness_aa2; - stop = ifelse( - isvalid[3] && (stop > (g_lastlen[2] + stop_width) * px2uv), - stop - (stop_width + 0.5 * AA_THICKNESS) * inv_pl, - stop + AA_THICKNESS * px2uv - ); + if (isvalid[3] && (stop > (g_lastlen[2] + stop_width) * px2uv)) + stop = stop - (stop_width + 0.5 * AA_THICKNESS) * inv_pl; + else + stop = stop + AA_THICKNESS * px2uv; p2 += (2 * stop * pattern_length - g_lastlen[2]) * v1; } @@ -341,8 +331,10 @@ void draw_patterned_line(bool isvalid[4]) // If this segment starts or ends a line we force anti-aliasing to // happen at the respective edge. - f_uv_minmax.x = ifelse(isvalid[0], f_uv_minmax.x, g_lastlen[1] * px2uv); - f_uv_minmax.y = ifelse(isvalid[3], f_uv_minmax.y, g_lastlen[2] * px2uv); + if (!isvalid[0]) + f_uv_minmax.x = g_lastlen[1] * px2uv; + if (!isvalid[3]) + f_uv_minmax.y = g_lastlen[2] * px2uv; // generate rectangle for this segment emit_vertex(p1 + miter_a, vec2(start + dot(v1, miter_a) * px2uv, -thickness_aa1), 1); @@ -467,16 +459,11 @@ void draw_solid_line(bool isvalid[4]) // Here we treat this by adding an artificial AA boundary at the line start // and end. Note that always doing this will introduce AA where lines should // smoothly connect. - f_uv_minmax.x = ifelse( - segment_length < abs(length_a * dot(miter_a, v1)), - (u1 - g_thickness[1] * abs(dot(miter_a, v1) / dot(miter_a, n1))) * px2uv, - f_uv_minmax.x - ); - f_uv_minmax.y = ifelse( - segment_length < abs(length_b * dot(miter_b, v1)), - (u2 + g_thickness[2] * abs(dot(miter_b, v1) / dot(miter_b, n1))) * px2uv, - f_uv_minmax.y - ); + if (segment_length < abs(length_a * dot(miter_a, v1))) + f_uv_minmax.x = (u1 - g_thickness[1] * abs(dot(miter_a, v1) / dot(miter_a, n1))) * px2uv; + + if (segment_length < abs(length_b * dot(miter_b, v1))) + f_uv_minmax.y = (u2 + g_thickness[2] * abs(dot(miter_b, v1) / dot(miter_b, n1))) * px2uv; // To treat line starts and ends we elongate the line in the respective // direction and enforce an AA border at the original start/end position diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index 850b700bd20..cae60a31faf 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -38,6 +38,7 @@ uniform float stroke_width; uniform float glow_width; uniform int shape; // for RECTANGLE hack below uniform vec2 resolution; +uniform float px_per_unit; uniform float depth_shift; in int g_primitive_index[]; @@ -138,7 +139,7 @@ void main(void) 0.0, 1.0/vclip.w, 0.0, 0.0, 0.0, 0.0, 1.0/vclip.w, 0.0, -vclip.xyz/(vclip.w*vclip.w), 0.0); - mat2 dxyv_dxys = diagm(0.5*resolution) * mat2(d_ndc_d_clip*trans); + mat2 dxyv_dxys = diagm(0.5*px_per_unit*resolution) * mat2(d_ndc_d_clip*trans); // Now, our buffer size is expressed in viewport pixels but we get back to // the sprite coordinate system using the scale factor of the // transformation (for isotropic transformations). For anisotropic diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 5adba7a77aa..1daab1f9514 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -107,6 +107,7 @@ function cached_robj!(robj_func, screen, scene, x::AbstractPlot) gl_attributes[:ambient] = ambientlight.color end gl_attributes[:track_updates] = screen.config.render_on_demand + gl_attributes[:px_per_unit] = screen.px_per_unit robj = robj_func(gl_attributes) diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 88bfb76b39a..70cd305ae7c 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -37,50 +37,50 @@ function Makie.disconnect!(window::GLFW.Window, ::typeof(window_open)) GLFW.SetWindowCloseCallback(window, nothing) end -function window_position(window::GLFW.Window) - xy = GLFW.GetWindowPos(window) - (xy.x, xy.y) -end +function Makie.window_area(scene::Scene, screen::Screen) + disconnect!(screen, window_area) -struct WindowAreaUpdater - window::GLFW.Window - dpi::Observable{Float64} - area::Observable{GeometryBasics.HyperRectangle{2, Int64}} -end + # TODO: Figure out which monitor the window is on and react to DPI changes + monitor = GLFW.GetPrimaryMonitor() + props = MonitorProperties(monitor) + scene.events.window_dpi[] = minimum(props.dpi) + + function windowsizecb(window, width::Cint, height::Cint) + area = scene.events.window_area + sf = screen.scalefactor[] -function (x::WindowAreaUpdater)(::Nothing) - ShaderAbstractions.switch_context!(x.window) - rect = x.area[] - # TODO put back window position, but right now it makes more trouble than it helps# - # x, y = GLFW.GetWindowPos(window) - # if minimum(rect) != Vec(x, y) - # event[] = Recti(x, y, framebuffer_size(window)) - # end - w, h = GLFW.GetFramebufferSize(x.window) - if Vec(w, h) != widths(rect) - monitor = GLFW.GetPrimaryMonitor() - props = MonitorProperties(monitor) - # dpi of a monitor should be the same in x y direction. - # if not, minimum seems to be a fair default - x.dpi[] = minimum(props.dpi) - x.area[] = Recti(minimum(rect), w, h) + ShaderAbstractions.switch_context!(window) + winscale = sf / (@static Sys.isapple() ? scale_factor(window) : 1) + winw, winh = round.(Int, (width, height) ./ winscale) + if Vec(winw, winh) != widths(area[]) + area[] = Recti(minimum(area[]), winw, winh) + end + return end - return -end + # TODO put back window position, but right now it makes more trouble than it helps + #function windowposcb(window, x::Cint, y::Cint) + # area = scene.events.window_area + # ShaderAbstractions.switch_context!(window) + # winscale = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(window) : 1) + # xs, ys = round.(Int, (x, y) ./ winscale) + # if Vec(xs, ys) != minimum(area[]) + # area[] = Recti(xs, ys, widths(area[])) + # end + # return + #end -function Makie.window_area(scene::Scene, screen::Screen) - disconnect!(screen, window_area) - - updater = WindowAreaUpdater( - to_native(screen), scene.events.window_dpi, scene.events.window_area - ) - on(updater, screen.render_tick) + window = to_native(screen) + GLFW.SetWindowSizeCallback(window, (win, w, h) -> windowsizecb(win, w, h)) + #GLFW.SetWindowPosCallback(window, (win, x, y) -> windowposcb(win, x, y)) + windowsizecb(window, Cint.(window_size(window))...) return end function Makie.disconnect!(screen::Screen, ::typeof(window_area)) - filter!(p -> !isa(p[2], WindowAreaUpdater), screen.render_tick.listeners) + window = to_native(screen) + #GLFW.SetWindowPosCallback(window, nothing) + GLFW.SetWindowSizeCallback(window, nothing) return end function Makie.disconnect!(::GLFW.Window, ::typeof(window_area)) @@ -168,44 +168,28 @@ function Makie.disconnect!(window::GLFW.Window, ::typeof(unicode_input)) GLFW.SetCharCallback(window, nothing) end -# TODO memoise? Or to bug ridden for the small performance gain? -function retina_scaling_factor(w, fb) - (w[1] == 0 || w[2] == 0) && return (1.0, 1.0) - fb ./ w -end - -# TODO both of these methods are slow! -# ~90µs, ~80µs -# This is too slow for events that may happen 100x per frame -function framebuffer_size(window::GLFW.Window) - wh = GLFW.GetFramebufferSize(window) - (wh.width, wh.height) -end -function window_size(window::GLFW.Window) - wh = GLFW.GetWindowSize(window) - (wh.width, wh.height) -end -function retina_scaling_factor(window::GLFW.Window) - w, fb = window_size(window), framebuffer_size(window) - retina_scaling_factor(w, fb) -end - -function correct_mouse(window::GLFW.Window, w, h) - ws, fb = window_size(window), framebuffer_size(window) - s = retina_scaling_factor(ws, fb) - (w * s[1], fb[2] - (h * s[2])) +function correct_mouse(screen::Screen, w, h) + nw = to_native(screen) + sf = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(nw) : 1) + _, winh = window_size(nw) + @static if Sys.isapple() + return w, (winh / sf) - h + else + return w / sf, (winh - h) / sf + end end struct MousePositionUpdater - window::GLFW.Window + screen::Screen mouseposition::Observable{Tuple{Float64, Float64}} hasfocus::Observable{Bool} end function (p::MousePositionUpdater)(::Nothing) !p.hasfocus[] && return - x, y = GLFW.GetCursorPos(p.window) - pos = correct_mouse(p.window, x, y) + nw = to_native(p.screen) + x, y = GLFW.GetCursorPos(nw) + pos = correct_mouse(p.screen, x, y) if pos != p.mouseposition[] @print_error p.mouseposition[] = pos # notify!(e.mouseposition) @@ -222,7 +206,7 @@ which is not in scene coordinates, with the upper left window corner being 0 function Makie.mouse_position(scene::Scene, screen::Screen) disconnect!(screen, mouse_position) updater = MousePositionUpdater( - to_native(screen), scene.events.mouseposition, scene.events.hasfocus + screen, scene.events.mouseposition, scene.events.hasfocus ) on(updater, screen.render_tick) return diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index d643f609c5b..3ccf5ca1ef0 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -124,15 +124,13 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) ) end -function Base.resize!(fb::GLFramebuffer, window_size) - ws = Int.((window_size[1], window_size[2])) - if ws != size(fb) && all(x-> x > 0, window_size) - for (name, buffer) in fb.buffers - resize_nocopy!(buffer, ws) - end - fb.resolution[] = ws +function Base.resize!(fb::GLFramebuffer, w::Int, h::Int) + (w > 0 && h > 0 && (w, h) != size(fb)) || return + for (name, buffer) in fb.buffers + resize_nocopy!(buffer, (w, h)) end - nothing + fb.resolution[] = (w, h) + return nothing end @@ -188,10 +186,21 @@ function destroy!(nw::GLFW.Window) was_current && ShaderAbstractions.switch_context!() end -function windowsize(nw::GLFW.Window) +function window_size(nw::GLFW.Window) + was_destroyed(nw) && return (0, 0) + return Tuple(GLFW.GetWindowSize(nw)) +end +function window_position(nw::GLFW.Window) was_destroyed(nw) && return (0, 0) - size = GLFW.GetFramebufferSize(nw) - return (size.width, size.height) + return Tuple(GLFW.GetWindowPos(window)) +end +function framebuffer_size(nw::GLFW.Window) + was_destroyed(nw) && return (0, 0) + return Tuple(GLFW.GetFramebufferSize(nw)) +end +function scale_factor(nw::GLFW.Window) + was_destroyed(nw) && return 1f0 + return minimum(GLFW.GetWindowContentScale(nw)) end function Base.isopen(window::GLFW.Window) diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index d83c4d6f9d2..0a7b74346e7 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -6,16 +6,16 @@ function pick_native(screen::Screen, rect::Rect2i) isopen(screen) || return Matrix{SelectionID{Int}}(undef, 0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) glReadBuffer(GL_COLOR_ATTACHMENT1) rx, ry = minimum(rect) rw, rh = widths(rect) - w, h = window_size - sid = zeros(SelectionID{UInt32}, widths(rect)...) + w, h = size(screen.root_scene) if rx > 0 && ry > 0 && rx + rw <= w && ry + rh <= h + rx, ry, rw, rh = round.(Int, screen.px_per_unit[] .* (rx, ry, rw, rh)) + sid = zeros(SelectionID{UInt32}, rw, rh) glReadPixels(rx, ry, rw, rh, buff.format, buff.pixeltype, sid) return sid else @@ -26,15 +26,15 @@ end function pick_native(screen::Screen, xy::Vec{2, Float64}) isopen(screen) || return SelectionID{Int}(0, 0) ShaderAbstractions.switch_context!(screen.glscreen) - sid = Base.RefValue{SelectionID{UInt32}}() - window_size = size(screen) fb = screen.framebuffer buff = fb.buffers[:objectid] glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) glReadBuffer(GL_COLOR_ATTACHMENT1) x, y = floor.(Int, xy) - w, h = window_size + w, h = size(screen.root_scene) if x > 0 && y > 0 && x <= w && y <= h + x, y = round.(Int, screen.px_per_unit[] .* (x, y)) + sid = Base.RefValue{SelectionID{UInt32}}() glReadPixels(x, y, 1, 1, buff.format, buff.pixeltype, sid) return convert(SelectionID{Int}, sid[]) end @@ -65,7 +65,7 @@ end # Skips one set of allocations function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(screen) + w, h = size(scene) ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) x0, y0 = max.(1, floor.(Int, xy .- range)) @@ -95,7 +95,7 @@ end # Skips some allocations function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(screen) + w, h = size(scene) if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) return Tuple{AbstractPlot, Int}[] end diff --git a/GLMakie/src/postprocessing.jl b/GLMakie/src/postprocessing.jl index 3c295d158a6..fa55afd4bfd 100644 --- a/GLMakie/src/postprocessing.jl +++ b/GLMakie/src/postprocessing.jl @@ -285,14 +285,11 @@ function to_screen_postprocessor(framebuffer, shader_cache, screen_fb_id = nothi pass.postrenderfunction = () -> draw_fullscreen(pass.vertexarray.id) full_render = screen -> begin - fb = screen.framebuffer - w, h = size(fb) - # transfer everything to the screen default_id = isnothing(screen_fb_id) ? 0 : screen_fb_id[] # GLFW uses 0, Gtk uses a value that we have to probe at the beginning of rendering glBindFramebuffer(GL_FRAMEBUFFER, default_id) - glViewport(0, 0, w, h) + glViewport(0, 0, framebuffer_size(screen.glscreen)...) glClear(GL_COLOR_BUFFER_BIT) GLAbstraction.render(pass) # copy postprocess end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index b5f9c6e86ba..23268bb45c5 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,14 +1,14 @@ - -function setup!(screen) +function setup!(screen::Screen) glEnable(GL_SCISSOR_TEST) - if isopen(screen) - glScissor(0, 0, size(screen)...) + if isopen(screen) && !isnothing(screen.root_scene) + sf = screen.px_per_unit[] + glScissor(0, 0, round.(Int, size(screen.root_scene) .* sf)...) glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) for (id, scene) in screen.screens if scene.visible[] a = pixelarea(scene)[] - rt = (minimum(a)..., widths(a)...) + rt = (round.(Int, sf .* minimum(a))..., round.(Int, sf .* widths(a))...) glViewport(rt...) if scene.clear c = scene.backgroundcolor[] @@ -43,11 +43,10 @@ function render_frame(screen::Screen; resize_buffers=true) # render order here may introduce artifacts because of that. fb = screen.framebuffer - if resize_buffers - wh = Int.(framebuffer_size(nw)) - resize!(fb, wh) + if resize_buffers && !isnothing(screen.root_scene) + sf = screen.px_per_unit[] + resize!(fb, round.(Int, sf .* size(screen.root_scene))...) end - w, h = size(fb) # prepare stencil (for sub-scenes) glBindFramebuffer(GL_FRAMEBUFFER, fb.id) @@ -119,8 +118,9 @@ function GLAbstraction.render(filter_elem_func, screen::Screen) found, scene = id2scene(screen, screenid) found || continue scene.visible[] || continue + sf = screen.px_per_unit[] a = pixelarea(scene)[] - glViewport(minimum(a)..., widths(a)...) + glViewport(round.(Int, sf .* minimum(a))..., round.(Int, sf .* widths(a))...) render(elem) end catch e diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index d39dde37d0a..736373f118a 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -8,8 +8,7 @@ function renderloop end """ ## Renderloop -* `renderloop = GLMakie.renderloop`: sets a function `renderloop(::GLMakie.Screen)` which starts a renderloop for the screen. - +* `renderloop = GLMakie.renderloop`: Sets a function `renderloop(::GLMakie.Screen)` which starts a renderloop for the screen. !!! warning The keyword arguments below are not effective if `renderloop` isn't set to `GLMakie.renderloop`, unless implemented in a custom renderloop function: @@ -18,6 +17,7 @@ function renderloop end * `vsync = false`: Whether to enable vsync for the window. * `render_on_demand = true`: If `true`, the scene will only be rendered if something has changed in it. * `framerate = 30.0`: Sets the currently rendered frames per second. +* `px_per_unit = automatic`: Sets the ratio between the number of rendered pixels and the `Makie` resolution. It defaults to the value of `scalefactor` but may be any positive real number. ## GLFW window attributes * `float = false`: Whether the window should float above other windows. @@ -28,6 +28,7 @@ function renderloop end * `debugging = false`: If `true`, starts the GLFW.Window/OpenGL context with debug output. * `monitor::Union{Nothing, GLFW.Monitor} = nothing`: Sets the monitor on which the window should be opened. If set to `nothing`, GLFW will decide which monitor to use. * `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 * `oit = false`: Whether to enable order independent transparency for the window. @@ -43,6 +44,7 @@ mutable struct ScreenConfig vsync::Bool render_on_demand::Bool framerate::Float64 + px_per_unit::Union{Nothing, Float32} # GLFW window attributes float::Bool @@ -53,6 +55,7 @@ mutable struct ScreenConfig debugging::Bool monitor::Union{Nothing, GLFW.Monitor} visible::Bool + scalefactor::Union{Nothing, Float32} # Postprocessor oit::Bool @@ -67,6 +70,7 @@ mutable struct ScreenConfig vsync::Bool, render_on_demand::Bool, framerate::Number, + px_per_unit::Union{Makie.Automatic, Number}, # GLFW window attributes float::Bool, focus_on_show::Bool, @@ -76,6 +80,7 @@ mutable struct ScreenConfig debugging::Bool, monitor::Union{Nothing, GLFW.Monitor}, visible::Bool, + scalefactor::Union{Makie.Automatic, Number}, # Preprocessor oit::Bool, @@ -89,6 +94,7 @@ mutable struct ScreenConfig vsync, render_on_demand, framerate, + px_per_unit isa Makie.Automatic ? nothing : Float32(px_per_unit), # GLFW window attributes float, focus_on_show, @@ -98,6 +104,8 @@ mutable struct ScreenConfig debugging, monitor, visible, + scalefactor isa Makie.Automatic ? nothing : Float32(scalefactor), + # Preproccessor # Preprocessor oit, fxaa, @@ -147,6 +155,7 @@ mutable struct Screen{GLWindow} <: MakieScreen config::Union{Nothing, ScreenConfig} stop_renderloop::Bool rendertask::Union{Task, Nothing} + px_per_unit::Observable{Float32} screen2scene::Dict{WeakRef, ScreenID} screens::Vector{ScreenArea} @@ -157,6 +166,7 @@ mutable struct Screen{GLWindow} <: MakieScreen framecache::Matrix{RGB{N0f8}} render_tick::Observable{Nothing} window_open::Observable{Bool} + scalefactor::Observable{Float32} root_scene::Union{Scene, Nothing} reuse::Bool @@ -185,10 +195,10 @@ mutable struct Screen{GLWindow} <: MakieScreen screen = new{GLWindow}( glscreen, shader_cache, framebuffer, config, stop_renderloop, rendertask, - screen2scene, + Observable(0f0), screen2scene, screens, renderlist, postprocessors, cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(nothing), - Observable(true), nothing, reuse, true, false + Observable(true), Observable(0f0), nothing, reuse, true, false ) push!(ALL_SCREENS, screen) # track all created screens return screen @@ -213,6 +223,8 @@ function empty_screen(debugging::Bool; reuse=true) (GLFW.STENCIL_BITS, 0), (GLFW.AUX_BUFFERS, 0), + + (GLFW.SCALE_TO_MONITOR, true), ] resolution = (10, 10) window = try @@ -261,7 +273,9 @@ function empty_screen(debugging::Bool; reuse=true) Dict{UInt32, AbstractPlot}(), reuse, ) - GLFW.SetWindowRefreshCallback(window, window -> refreshwindowcb(window, screen)) + GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) + GLFW.SetWindowContentScaleCallback(window, scalechangecb(screen)) + return screen end @@ -276,6 +290,7 @@ function reopen!(screen::Screen) end @assert isempty(screen.window_open.listeners) screen.window_open[] = true + on(scalechangeobs(screen), screen.scalefactor) @assert isopen(screen) return screen end @@ -307,8 +322,6 @@ function singleton_screen(debugging::Bool) return reopen!(screen) end -const GLFW_FOCUS_ON_SHOW = 0x0002000C - function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig, scene::Scene, args...) apply_config!(screen, config) end @@ -317,7 +330,7 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B @debug("Applying screen config! to existing screen") glw = screen.glscreen ShaderAbstractions.switch_context!(glw) - GLFW.SetWindowAttrib(glw, GLFW_FOCUS_ON_SHOW, config.focus_on_show) + GLFW.SetWindowAttrib(glw, GLFW.FOCUS_ON_SHOW, config.focus_on_show) GLFW.SetWindowAttrib(glw, GLFW.DECORATED, config.decorated) GLFW.SetWindowAttrib(glw, GLFW.FLOATING, config.float) GLFW.SetWindowTitle(glw, config.title) @@ -325,6 +338,8 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B if !isnothing(config.monitor) GLFW.SetWindowMonitor(glw, config.monitor) end + screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : scale_factor(glw) + screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] function replace_processor!(postprocessor, idx) fb = screen.framebuffer @@ -361,10 +376,10 @@ function Screen(; # Screen config is managed by the current active theme, so managed by Makie config = Makie.merge_screen_config(ScreenConfig, screen_config) screen = screen_from_pool(config.debugging) + apply_config!(screen, config; start_renderloop=start_renderloop) if !isnothing(resolution) resize!(screen, resolution...) end - apply_config!(screen, config; start_renderloop=start_renderloop) return screen end @@ -563,7 +578,10 @@ function destroy!(screen::Screen) # otherwise, during rendertask clean up we may run into a destroyed window wait(screen) screen.rendertask = nothing - destroy!(screen.glscreen) + window = screen.glscreen + GLFW.SetWindowRefreshCallback(window, nothing) + GLFW.SetWindowContentScaleCallback(window, nothing) + destroy!(window) # Since those are sets, we can just delete them from there, even if they weren't in there (e.g. reuse=false) delete!(SCREEN_REUSE_POOL, screen) delete!(ALL_SCREENS, screen) @@ -587,6 +605,8 @@ function Base.close(screen::Screen; reuse=true) screen.window_open[] = false end empty!(screen) + Observables.clear(screen.px_per_unit) + Observables.clear(screen.scalefactor) if reuse && screen.reuse @debug("reusing screen!") push!(SCREEN_REUSE_POOL, screen) @@ -614,24 +634,30 @@ function closeall() return end -function resize_native!(window::GLFW.Window, resolution...) - if isopen(window) - ShaderAbstractions.switch_context!(window) - oldsize = windowsize(window) - retina_scale = retina_scaling_factor(window) - w, h = resolution ./ retina_scale - if oldsize == (w, h) - return - end - GLFW.SetWindowSize(window, round(Int, w), round(Int, h)) +function Base.resize!(screen::Screen, w::Int, h::Int) + window = to_native(screen) + (w > 0 && h > 0 && isopen(window)) || return nothing + + # Resize the window which appears on the user desktop (if necessary). + # + # On OSX with a Retina display, the window size is given in logical dimensions and + # is automatically scaled by the OS. To support arbitrary scale factors, we must account + # for the native scale factor when calculating the effective scaling to apply. + # + # On Linux and Windows, scale from the logical size to the pixel size. + ShaderAbstractions.switch_context!(window) + winscale = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(window) : 1) + winw, winh = round.(Int, winscale .* (w, h)) + if window_size(window) != (winw, winh) + GLFW.SetWindowSize(window, winw, winh) end -end -function Base.resize!(screen::Screen, w, h) - nw = to_native(screen) - resize_native!(nw, w, h) - fb = screen.framebuffer - resize!(fb, (w, h)) + # Then resize the underlying rendering framebuffers as well, which can be scaled + # independently of the window scale factor. + fbscale = screen.px_per_unit[] + fbw, fbh = round.(Int, fbscale .* (w, h)) + resize!(screen.framebuffer, fbw, fbh) + return nothing end function fast_color_data!(dest::Array{RGB{N0f8}, 2}, source::Texture{T, 2}) where T @@ -679,8 +705,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! # GLFW.PollEvents() - # keep current buffer size to allows larger-than-window renders - render_frame(screen, resize_buffers=false) # let it render + render_frame(screen, resize_buffers=true) # let it render glFinish() # block until opengl is done rendering if size(ctex) != size(screen.framecache) screen.framecache = Matrix{RGB{N0f8}}(undef, size(ctex)) @@ -804,12 +829,32 @@ function set_framerate!(screen::Screen, fps=30) screen.config.framerate = fps end -function refreshwindowcb(window, screen) +function refreshwindowcb(screen, window) screen.render_tick[] = nothing render_frame(screen) GLFW.SwapBuffers(window) return end +refreshwindowcb(screen) = window -> refreshwindowcb(screen, window) + +function scalechangecb(screen, window, xscale, yscale) + sf = min(xscale, yscale) + if isnothing(screen.config.px_per_unit) && screen.scalefactor[] == screen.px_per_unit[] + screen.px_per_unit[] = sf + end + screen.scalefactor[] = sf + return +end +scalechangecb(screen) = (window, xscale, yscale) -> scalechangecb(screen, window, xscale, yscale) + +function scalechangeobs(screen, _) + if !isnothing(screen.root_scene) + resize!(screen, size(screen.root_scene)...) + end + return nothing +end +scalechangeobs(screen) = scalefactor -> scalechangeobs(screen, scalefactor) + # TODO add render_tick event to scene events function vsynced_renderloop(screen) diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 8df6f3b28cf..e7fb58131ba 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -16,7 +16,7 @@ reference_tests_dir = normpath(joinpath(dirname(pathof(Makie)), "..", "Reference Pkg.develop(PackageSpec(path = reference_tests_dir)) using ReferenceTests -GLMakie.activate!(framerate=1.0) +GLMakie.activate!(framerate=1.0, scalefactor=1.0) @testset "mimes" begin Makie.inline!(true) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index c8d71e1ff5f..89e52d98396 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -243,6 +243,8 @@ end @test isempty(screen.window_open.listeners) @test isempty(screen.render_tick.listeners) + @test isempty(screen.px_per_unit.listeners) + @test isempty(screen.scalefactor.listeners) @test screen.root_scene === nothing @test screen.rendertask === nothing @@ -255,3 +257,131 @@ end # now every screen should be gone @test isempty(GLMakie.SCREEN_REUSE_POOL) end + +@testset "HiDPI displays" begin + import FileIO: @format_str, File, load + GLMakie.closeall() + + W, H = 400, 400 + N = 51 + x = collect(range(0.0, 2π, length=N)) + y = sin.(x) + fig, ax, pl = scatter(x, y, figure = (; resolution = (W, H))); + hidedecorations!(ax) + + # On OSX, the native window size has an underlying scale factor that we need to account + # for when interpreting native window sizes with respect to the desired figure size + # and desired scaling factor. + function scaled(screen::GLMakie.Screen, dims::Tuple{Vararg{Int}}) + sf = screen.scalefactor[] / (Sys.isapple() ? GLMakie.scale_factor(screen.glscreen) : 1) + return round.(Int, dims .* sf) + end + + screen = display(GLMakie.Screen(visible = false, scalefactor = 2), fig) + @test screen.scalefactor[] === 2f0 + @test screen.px_per_unit[] === 2f0 # inherited from scale factor + @test size(screen.framebuffer) == (2W, 2H) + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + # check that picking works through the resized GL buffers + GLMakie.Makie.colorbuffer(screen) # force render + # - point pick + point_px = project_sp(ax.scene, Point2f(x[end÷2], y[end÷2])) + elem, idx = pick(ax.scene, point_px) + @test elem === pl + @test idx == length(x) ÷ 2 + # - area pick + bottom_px = project_sp(ax.scene, Point2f(π, -1)) + right_px = project_sp(ax.scene, Point2f(2π, 0)) + quadrant = Rect2i(round.(bottom_px)..., round.(right_px - bottom_px)...) + picks = pick(ax.scene, quadrant) + points = Set(Int(p[2]) for p in picks if p[1] isa Scatter) + @test points == Set(((N+1)÷2):N) + + # render at lower resolution + screen = display(GLMakie.Screen(visible = false, scalefactor = 2, px_per_unit = 1), fig) + @test screen.scalefactor[] === 2f0 + @test screen.px_per_unit[] === 1f0 + @test size(screen.framebuffer) == (W, H) + + # decrease the scale factor after-the-fact + screen.scalefactor[] = 1 + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + # save images of different resolutions + mktemp() do path, io + close(io) + file = File{format"PNG"}(path) + + # save at current size + @test screen.px_per_unit[] == 1 + save(file, fig) + img = load(file) + @test size(img) == (W, H) + + # save with a different resolution + save(file, fig, px_per_unit = 2) + img = load(file) + @test size(img) == (2W, 2H) + # writing to file should not effect the visible figure + @test_broken screen.px_per_unit[] == 1 + end + + # make sure there isn't a race between changing the scale factor and window_area updater + # see https://github.com/MakieOrg/Makie.jl/pull/2544#issuecomment-1416861800 + screen = display(GLMakie.Screen(visible = false, scalefactor = 2, framerate = 60), fig) + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + on(screen.scalefactor) do sf + sleep(0.5) + end + screen.scalefactor[] = 1 + @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) + + if Sys.islinux() + # Test that GLMakie is correctly getting the default scale factor from X11 in a + # HiDPI environment. + + checkcmd = `which xrdb` & `which xsettingsd` + checkcmd = pipeline(ignorestatus(checkcmd), stdout = devnull, stderr = devnull) + hasxrdb = success(run(checkcmd)) + + # Only continue if running within an Xvfb environment where the setting is + # empty by default. Overriding during a user's session could be problematic + # (i.e. if running interactively rather than in CI). + inxvfb = hasxrdb ? isempty(readchomp(`xrdb -query`)) : false + + if hasxrdb && inxvfb + # GLFW looks for Xft.dpi resource setting. Spawn a temporary xsettingsd daemon + # to be the X resource manager + xsettingsd = run(pipeline(`xsettingsd -c /dev/null`), wait = false) + try + # Then set the DPI to 192, i.e. 2 times the default of 96dpi + run(pipeline(`echo "Xft.dpi: 192"`, `xrdb -merge`)) + + # Print out the automatically-determined scale factor from the GLScreen + jlscript = raw""" + using GLMakie + fig, ax, pl = scatter(1:2, 3:4) + screen = display(GLMakie.Screen(visible = false), fig) + print(Int(screen.scalefactor[])) + """ + cmd = ``` + $(Base.julia_cmd()) + --project=$(Base.active_project()) + --eval $jlscript + ``` + scalefactor = readchomp(cmd) + @test scalefactor == "2" + finally + # cleanup: kill the daemon before continuing with more tests + kill(xsettingsd) + end + else + @test_broken hasxrdb && inxvfb + end + else + @test_broken Sys.islinux() + end + + GLMakie.closeall() +end diff --git a/NEWS.md b/NEWS.md index c366c8d1384..00d9b43fc24 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,10 @@ ## master +- GLMakie has gained support for HiDPI (aka Retina) screens. + This also enables saving images with higher resolution than screen pixel dimensions. + [#2544](https://github.com/MakieOrg/Makie.jl/pull/2544) + ## v0.19.3 - Added the `stephist` plotting function [#2408](https://github.com/JuliaPlots/Makie.jl/pull/2408). diff --git a/docs/documentation/backends/glmakie.md b/docs/documentation/backends/glmakie.md index 353113bf44a..e15e78f4eca 100644 --- a/docs/documentation/backends/glmakie.md +++ b/docs/documentation/backends/glmakie.md @@ -15,6 +15,50 @@ println("~~~") ``` \textoutput{docs} +#### Window Scaling + +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 +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)) +# ... +display(fig, scalefactor = 1.5) +``` + +If the scale factor is not changed from its default automatic configuration, the window +will be resized to maintain its apparent size when moved across displays with different +scaling factors on Windows and OSX. +(Independent scaling factors are not supported by X11, and at this time the underlying +GLFW library is not compiled with Wayland support.) + +#### Resolution Scaling + +Related to the window scaling factor, the mapping from figure sizes and positions to pixels +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)) +# ... +display(fig, px_per_unit = 2) +``` + +The resolution scale factor may also be changed when saving pngs: +```julia +save("hires.png", fig, px_per_unit = 2) # 1600 × 1200 px png +save("lores.png", fig, px_per_unit = 0.5) # 400 × 300 px png +``` +If a script may run in interactive environments where the native screen DPI can vary, +you may want to explicitly set `px_per_unit = 1` when saving figures to ensure consistency +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)`. diff --git a/src/theming.jl b/src/theming.jl index 1cbbbe39d38..0098e0eea66 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -85,6 +85,7 @@ const minimal_default = Attributes( vsync = false, render_on_demand = true, framerate = 30.0, + px_per_unit = automatic, # GLFW window attributes float = false, @@ -95,6 +96,7 @@ const minimal_default = Attributes( debugging = false, monitor = nothing, visible = true, + scalefactor = automatic, # Postproccessor oit = true, From 2b330934066303dab38b70ee9c324170d51c680a Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 12:20:24 +0200 Subject: [PATCH 40/80] squash merge jk/html-png-display --- CairoMakie/src/display.jl | 14 ++++++++ CairoMakie/src/screen.jl | 17 +++++---- CairoMakie/test/runtests.jl | 4 +-- GLMakie/src/screen.jl | 1 + ReferenceTests/src/database.jl | 2 +- docs/documentation/figure.md | 59 +++++++++++++++++++++++++++++-- docs/documentation/figure_size.md | 51 -------------------------- src/theming.jl | 2 +- 8 files changed, 85 insertions(+), 65 deletions(-) delete mode 100644 docs/documentation/figure_size.md diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index d5cf249942b..4ad7cab8182 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -113,10 +113,24 @@ function Makie.backend_show(screen::Screen{IMAGE}, io::IO, ::MIME"image/png", sc return screen end +function Makie.backend_show(screen::Screen{IMAGE}, io::IO, ::Union{MIME"text/html",MIME"application/vnd.webio.application+html",MIME"application/prs.juno.plotpane+html",MIME"juliavscode/html"}, scene::Scene) + w, h = widths(scene.px_area[]) + cairo_draw(screen, scene) + png_io = IOBuffer() + Cairo.write_to_png(screen.surface, png_io) + b64 = Base64.base64encode(String(take!(png_io))) + print(io, "") + return screen +end + # Disabling mimes and showable const DISABLED_MIMES = Set{String}() const SUPPORTED_MIMES = Set([ + "text/html", + "application/vnd.webio.application+html", + "application/prs.juno.plotpane+html", + "juliavscode/html", "image/svg+xml", "application/pdf", "application/postscript", diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index fca5bea1048..4489078c8c7 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -1,6 +1,6 @@ using Base.Docs: doc -@enum RenderType SVG IMAGE PDF EPS +@enum RenderType SVG IMAGE PDF EPS HTML Base.convert(::Type{RenderType}, ::MIME{SYM}) where SYM = mime_to_rendertype(SYM) function Base.convert(::Type{RenderType}, type::String) @@ -12,6 +12,8 @@ function Base.convert(::Type{RenderType}, type::String) return PDF elseif type == "eps" return EPS + elseif type in ("html", "text/html", "application/vnd.webio.application+html", "application/prs.juno.plotpane+html", "juliavscode/html") + return HTML else error("Unsupported cairo render type: $type") end @@ -22,6 +24,7 @@ function to_mime(type::RenderType) type == SVG && return MIME("image/svg+xml") type == PDF && return MIME("application/pdf") type == EPS && return MIME("application/postscript") + type == HTML && return MIME("text/html") return MIME("image/png") end @@ -35,6 +38,8 @@ function mime_to_rendertype(mime::Symbol)::RenderType return PDF elseif mime == Symbol("application/postscript") return EPS + elseif mime in (Symbol("text/html"), Symbol("text/html"), Symbol("application/vnd.webio.application+html"), Symbol("application/prs.juno.plotpane+html"), Symbol("juliavscode/html")) + return HTML else error("Unsupported mime: $mime") end @@ -55,7 +60,7 @@ function surface_from_output_type(type::RenderType, io, w, h) return Cairo.CairoPDFSurface(io, w, h) elseif type === EPS return Cairo.CairoEPSSurface(io, w, h) - elseif type === IMAGE + elseif type === IMAGE || type === HTML img = fill(ARGB32(0, 0, 0, 0), w, h) return Cairo.CairoImageSurface(img) else @@ -76,8 +81,8 @@ end to_cairo_antialias(aa::Int) = aa """ -* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). -* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). +* `px_per_unit = 2.0`: see [figure docs](https://docs.makie.org/stable/documentation/figure/). +* `pt_per_unit = 0.75`: see [figure docs](https://docs.makie.org/stable/documentation/figure/). * `antialias::Union{Symbol, Int} = :best`: antialias modus Cairo uses to draw. Applicable options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]`. * `visible::Bool`: if true, a browser/image viewer will open to display rendered output. """ @@ -120,9 +125,7 @@ function activate!(; inline=LAST_INLINE[], type="png", screen_config...) # So, if we want to prefer the png mime, we disable the mimes that are usually higher up in the stack. disable_mime!("svg", "pdf") elseif type == "svg" - # SVG is usually pretty high up the priority, so we can just enable all mimes - # If we implement html display for CairoMakie, we might need to disable that. - disable_mime!() + disable_mime!("text/html", "application/vnd.webio.application+html", "application/prs.juno.plotpane+html", "juliavscode/html") else enable_only_mime!(type) end diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index d7ae59760ff..02849b04f2d 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -64,7 +64,7 @@ include(joinpath(@__DIR__, "rasterization_tests.jl")) fig = scatter(1:4, figure=(; resolution=(800, 800))) save("test.pdf", fig) save("test.png", fig) - @test size(load("test.png")) == (800, 800) + @test size(load("test.png")) == (1600, 1600) rm("test.pdf") rm("test.png") end @@ -193,7 +193,7 @@ excludes = Set([ functions = [:volume, :volume!, :uv_mesh] @testset "refimages" begin - CairoMakie.activate!(type = "png") + CairoMakie.activate!(type = "png", px_per_unit = 1) ReferenceTests.mark_broken_tests(excludes, functions=functions) recorded_files, recording_dir = @include_reference_tests "refimages.jl" missing_images, scores = ReferenceTests.record_comparison(recording_dir) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 736373f118a..c79c9b6b122 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -450,6 +450,7 @@ function Makie.insertplots!(screen::Screen, scene::Scene) get!(screen.screen2scene, WeakRef(scene)) do id = length(screen.screens) + 1 push!(screen.screens, (id, scene)) + on(_ -> screen.requires_update = true, scene.visible) return id end for elem in scene.plots diff --git a/ReferenceTests/src/database.jl b/ReferenceTests/src/database.jl index 7db2c8eafb7..0866743c36e 100644 --- a/ReferenceTests/src/database.jl +++ b/ReferenceTests/src/database.jl @@ -37,7 +37,7 @@ macro reference_test(name, code) error("title must be unique. Duplicate title: $(title)") end println("running $(lpad(COUNTER[] += 1, 3)): $($title)") - Makie.set_theme!(resolution=(500, 500)) + Makie.set_theme!(resolution=(500, 500), CairoMakie = (; px_per_unit = 1)) ReferenceTests.RNG.seed_rng!() result = let $(esc(code)) diff --git a/docs/documentation/figure.md b/docs/documentation/figure.md index 75cb51516b9..d9da33b6ba9 100644 --- a/docs/documentation/figure.md +++ b/docs/documentation/figure.md @@ -3,7 +3,7 @@ The `Figure` object contains a top-level `Scene` and a `GridLayout`, as well as a list of blocks that have been placed into it, like `Axis`, `Colorbar`, `Slider`, `Legend`, etc. -## Creating a `Figure` +## 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`. @@ -34,9 +34,9 @@ You can pass arguments to the created figure in a dict-like object to the specia scatter(rand(100, 2), figure = (resolution = (600, 400),)) ``` -## Placing blocks into a `Figure` +## Placing Blocks into a Figure -All blocks take their parent figure as the first argument, then you can place them in the figure layout via indexing syntax. +All Blocks take their parent figure as the first argument, then you can place them in the figure layout via indexing syntax. ```julia f = Figure() @@ -166,3 +166,56 @@ ax = f[1, 1] = Axis(f) contents(f[1, 1]) == [ax] content(f[1, 1]) == ax ``` + +## Figure size + +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. + +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. + +## Matching figure and font sizes to documents + +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. + +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. + +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. + +### Example + +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). + +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). + +Equivalently, the font size we need to match 12pt is `12 / 0.75 = 16`. + +Therefore, we can create our figure with `Figure(resolution = (480, 384), fontsize = 16)` and save with `save("figure.pdf", fig)`. + +Let's say we now decide that our figure is too large in vector format, because it has a million scatter points, so we want to switch to bitmap format. + +We keep our figure with its resolution and font size as it is. +The question is now only, how high should our dpi be. +With CairoMakie's default of `px_per_unit = 2`, we would get a pixel size of 960 x 768 for our image, if we divide that by 5 x 4 inches we get a dpi of 192. + +Let's say this is not sharp enough for our purposes and we want to bump to 600 dpi. +The necessary pixel size of the image is 3000 x 2400. +With our figure size of 480 x 386 device-independent pixels, that gives a `px_per_unit` value of 6.25 to reach 600 dpi. +Note that we do not have to change anything about the font or other content sizes in the figure, we just scale up the render size. +We only need to run `save("figure.png", fig, px_per_unit = 6.25)` and take care to insert the image with the correct size of 5 x 4 inches, as image files usually don't store what physical size they are intended to be. + +!!! note + If you keep the intended physical size of an image constant and increase the dpi by increasing `px_per_unit`, the size of text and other content relative to the figure will stay constant. + However, if you instead try to increase the dpi by increasing the Figure size itself, the relative size of text and other content will shrink when viewed at the same physical size. + The first option is usually much more convenient, as it keeps the look and layout of the overall figure exactly the same, just with higher resolution. \ No newline at end of file diff --git a/docs/documentation/figure_size.md b/docs/documentation/figure_size.md deleted file mode 100644 index 66f0b4d2c20..00000000000 --- a/docs/documentation/figure_size.md +++ /dev/null @@ -1,51 +0,0 @@ -# Exporting a Figure with physical dimensions - -Makie currently uses a unitless approach for specifying Figure resolution, font sizes, line widths, etc. The dimensions of the final result depend on which export format you use, and what settings you specify when saving. Unitless means that `resolution = (800, 600)` doesn't mean pixels, or mm, or cm. Before saving or displaying, these are just numbers. - -GLMakie and WGLMakie can only export bitmaps (png files). CairoMakie can export both bitmaps and vector graphics (svg and pdf). These two file types have fundamentally different concepts of size. - -## Bitmaps - -Bitmaps always have a fixed resolution in pixels. Pixels are not a physical dimension. Monitor pixels have a physical size, and printer dots have one as well, but not the actual image. An image only has a physical size if you decide for a mapping from pixels to physical dimensions. That is usually the `dpi` value (dots per inch). - -This value comes from printing and tells you how many printer dots fit into one inch. If we say that one printer dot corresponds to one pixel, then we can use `dpi` to convert from pixels to inch. (Pixels are not actually dots, but this interpretation is widely used. The actual metric to go from pixels to inches is `ppi` or pixels per inch.) - -So if you need a plot at 4 x 3 inches and 400 dpi, you could set the figure size like this: - -```! -size_in_inches = (4, 3) -dpi = 400 -size_in_pixels = size_in_inches .* dpi -``` - -This means, if you export a bitmap, it doesn't have a physical size per se, but you can choose one later by deciding at what dpi you want to print the image. If you place a bitmap into a LaTeX document, for example, you can choose the size yourself, but just have to take care that the resulting dpi are high enough for your purposes, and that the document doesn't look blurry when printed. - -When you save a `Figure` as a bitmap with GLMakie or WGLMakie, the unitless resolution can be interpreted as pixel size. If you save a bitmap with CairoMakie, you additionally have the option to use a scaling factor that decides the mapping from unitless dimensions to pixels. This is done with the `px_per_unit` keyword argument. - -```julia -f = Figure(resolution = (800, 600)) -# in GLMakie or WGLMakie -save("figure.png", f) # output size = 800 x 600 pixels -# in CairoMakie -save("figure.png", f) # output size = 800 x 600 pixels -save("figure.png", f, px_per_unit = 2) # output size = 1600 x 1200 pixels -``` - -## Vector graphics - -Vector graphics don't have a resolution like bitmap images. They are collections of mathematical descriptions of lines and curves, and these can be arbitrarily scaled up and down. Vector graphics do have physical dimensions in that their content size is usually specified in `pt` which has a direct mapping to inch by convention, as 1 inch is equivalent to 72 pt. - -When you export vector graphics with CairoMakie, you can control the mapping from unitless size to size in pt with the `pt_per_unit` keyword argument. This is by default set to `0.75`. The reason for this is that in web contexts, by convention 1 px is equivalent to 0.75 pt. So if you use Pluto or Jupyter notebooks and display a figure once as a bitmap and once as a vector graphic, they will have the same apparent size. - -If you want to save a figure for a publication, you usually want to fit font sizes to the rest of the document, and adjust the size to what the journal expects. - -Font sizes are usually given in pt, figure sizes in inches. - -So if you need a 4 x 3 inches figure and your font size is 12 pt, you should set up and save your figure like this: - -```julia -size_inches = (4, 3) -size_pt = 72 .* size_inches -f = Figure(resolution = size_pt, fontsize = 12) -save("figure.pdf", f, pt_per_unit = 1) -``` \ No newline at end of file diff --git a/src/theming.jl b/src/theming.jl index 0098e0eea66..98488fb1782 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -71,7 +71,7 @@ const minimal_default = Attributes( inspectable = true, CairoMakie = Attributes( - px_per_unit = 1.0, + px_per_unit = 2.0, pt_per_unit = 0.75, antialias = :best, visible = true, From 918a5c953220dd2ec500cac9200df9070de8e937 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 13:18:02 +0200 Subject: [PATCH 41/80] new figure size and related theming adjustments --- src/makielayout/blocks/menu.jl | 2 +- src/makielayout/blocks/textbox.jl | 2 +- src/makielayout/types.jl | 22 +++++++++++----------- src/theming.jl | 12 ++++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index d114ccb022a..74def19a2c7 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -330,7 +330,7 @@ function initialize_block!(m::Menu; default = 1) end dropdown_arrow = scatter!( blockscene, symbol_pos; - marker=lift(iso -> iso ? '▴' : '▾', blockscene, m.is_open), + marker=lift(iso -> iso ? :utriangle : :dtriangle, blockscene, m.is_open), markersize = m.dropdown_arrow_size, color = m.dropdown_arrow_color, strokecolor = :transparent, diff --git a/src/makielayout/blocks/textbox.jl b/src/makielayout/blocks/textbox.jl index 761ae53cd28..f679c7a9222 100644 --- a/src/makielayout/blocks/textbox.jl +++ b/src/makielayout/blocks/textbox.jl @@ -99,7 +99,7 @@ function initialize_block!(tbox::Textbox) end end - cursor = linesegments!(scene, cursorpoints, color = tbox.cursorcolor, linewidth = 2, inspectable = false) + cursor = linesegments!(scene, cursorpoints, color = tbox.cursorcolor, linewidth = 1, inspectable = false) tbox.cursoranimtask = nothing diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 40c9790fff2..0e81dc01578 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -292,9 +292,9 @@ end "The horizontal and vertical alignment of the yticklabels." yticklabelalign::Union{Makie.Automatic, Tuple{Symbol, Symbol}} = Makie.automatic "The size of the xtick marks." - xticksize::Float64 = 6f0 + xticksize::Float64 = 5f0 "The size of the ytick marks." - yticksize::Float64 = 6f0 + yticksize::Float64 = 5f0 "Controls if the xtick marks are visible." xticksvisible::Bool = true "Controls if the ytick marks are visible." @@ -440,7 +440,7 @@ end "The alignment of x minor ticks on the axis spine" xminortickalign::Float64 = 0f0 "The tick size of x minor ticks" - xminorticksize::Float64 = 4f0 + xminorticksize::Float64 = 3f0 "The tick width of x minor ticks" xminortickwidth::Float64 = 1f0 "The tick color of x minor ticks" @@ -452,7 +452,7 @@ end "The alignment of y minor ticks on the axis spine" yminortickalign::Float64 = 0f0 "The tick size of y minor ticks" - yminorticksize::Float64 = 4f0 + yminorticksize::Float64 = 3f0 "The tick width of y minor ticks" yminortickwidth::Float64 = 1f0 "The tick color of y minor ticks" @@ -698,7 +698,7 @@ end "The current value of the slider. Don't set this manually, use the function `set_close_to!`." value = 0 "The width of the slider line" - linewidth::Float32 = 15 + linewidth::Float32 = 10 "The color of the slider when the mouse hovers over it." color_active_dimmed::RGBAf = COLOR_ACCENT_DIMMED[] "The color of the slider when the mouse clicks and drags the slider." @@ -785,7 +785,7 @@ end "The vertical alignment of the button in its suggested boundingbox" valign = :center "The extra space added to the sides of the button label's boundingbox." - padding = (10f0, 10f0, 10f0, 10f0) + padding = (8f0, 8f0, 8f0, 8f0) "The font size of the button label." fontsize = @inherit(:fontsize, 16f0) "The text of the button label." @@ -898,13 +898,13 @@ end "Color of the dropdown arrow" dropdown_arrow_color = (:black, 0.2) "Size of the dropdown arrow" - dropdown_arrow_size = 20 + dropdown_arrow_size = 10 "The list of options selectable in the menu. This can be any iterable of a mixture of strings and containers with one string and one other value. If an entry is just a string, that string is both label and selection. If an entry is a container with one string and one other value, the string is the label and the other value is the selection." options = ["no options"] "Font size of the cell texts" fontsize = @inherit(:fontsize, 16f0) "Padding of entry texts" - textpadding = (10, 10, 10, 10) + textpadding = (8, 10, 8, 8) "Color of entry texts" textcolor = :black "The opening direction of the menu (:up or :down)" @@ -1130,13 +1130,13 @@ end "Color of the box border when focused and invalid." bordercolor_focused_invalid = RGBf(1, 0, 0) "Width of the box border." - borderwidth = 2f0 + borderwidth = 1f0 "Padding of the text against the box." - textpadding = (10, 10, 10, 10) + textpadding = (8, 8, 8, 8) "If the textbox is focused and receives text input." focused = false "Corner radius of text box." - cornerradius = 8 + cornerradius = 5 "Corner segments of one rounded corner." cornersegments = 20 "Validator that is called with validate_textbox(string, validator) to determine if the current string is valid. Can by default be a RegEx that needs to match the complete string, or a function taking a string as input and returning a Bool. If the validator is a type T (for example Float64), validation will be `tryparse(string, T)`." diff --git a/src/theming.jl b/src/theming.jl index 98488fb1782..5e58303a0be 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -32,16 +32,16 @@ const minimal_default = Attributes( italic = "TeX Gyre Heros Makie Italic", bold_italic = "TeX Gyre Heros Makie Bold Italic", ), - fontsize = 16, + fontsize = 14, textcolor = :black, padding = Vec3f(0.05), - figure_padding = 16, - rowgap = 24, - colgap = 24, + figure_padding = 12, + rowgap = 18, + colgap = 18, backgroundcolor = :white, colormap = :viridis, marker = :circle, - markersize = 12, + markersize = 9, markercolor = :black, markerstrokecolor = :black, markerstrokewidth = 0, @@ -51,7 +51,7 @@ const minimal_default = Attributes( patchcolor = RGBAf(0, 0, 0, 0.6), patchstrokecolor = :black, patchstrokewidth = 0, - resolution = (800, 600), # 4/3 aspect ratio + resolution = (600, 450), # 4/3 aspect ratio visible = true, axis = Attributes(), axis3d = Attributes(), From 2807baf04cfd7b333cbf175455f4e234ac7e620d Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 14:34:47 +0200 Subject: [PATCH 42/80] move html backend show out of cairomakie --- CairoMakie/src/display.jl | 10 ---------- src/display.jl | 9 +++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index 4ad7cab8182..ed087315a9b 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -113,16 +113,6 @@ function Makie.backend_show(screen::Screen{IMAGE}, io::IO, ::MIME"image/png", sc return screen end -function Makie.backend_show(screen::Screen{IMAGE}, io::IO, ::Union{MIME"text/html",MIME"application/vnd.webio.application+html",MIME"application/prs.juno.plotpane+html",MIME"juliavscode/html"}, scene::Scene) - w, h = widths(scene.px_area[]) - cairo_draw(screen, scene) - png_io = IOBuffer() - Cairo.write_to_png(screen.surface, png_io) - b64 = Base64.base64encode(String(take!(png_io))) - print(io, "") - return screen -end - # Disabling mimes and showable const DISABLED_MIMES = Set{String}() diff --git a/src/display.jl b/src/display.jl index ab6650f2144..542c1cd0933 100644 --- a/src/display.jl +++ b/src/display.jl @@ -381,3 +381,12 @@ function backend_show(screen::MakieScreen, io::IO, m::MIME"image/jpeg", scene::S FileIO.save(FileIO.Stream{FileIO.format"JPEG"}(Makie.raw_io(io)), img) return end + +function backend_show(screen::MakieScreen, io::IO, ::Union{MIME"text/html",MIME"application/vnd.webio.application+html",MIME"application/prs.juno.plotpane+html",MIME"juliavscode/html"}, scene::Scene) + w, h = widths(scene.px_area[]) + png_io = IOBuffer() + backend_show(screen, png_io, MIME"image/png"(), scene) + b64 = Base64.base64encode(String(take!(png_io))) + print(io, "") + return +end \ No newline at end of file From 74d8ed8b7e791e55e6669aadda28229ca76ba376 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 14:53:41 +0200 Subject: [PATCH 43/80] move WEB_MIMES constant from WGLMakie into Makie --- CairoMakie/src/display.jl | 5 +---- WGLMakie/src/display.jl | 10 ++-------- WGLMakie/test/runtests.jl | 2 +- src/display.jl | 14 ++++++++++++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index ed087315a9b..71c0958ea1e 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -117,10 +117,7 @@ end const DISABLED_MIMES = Set{String}() const SUPPORTED_MIMES = Set([ - "text/html", - "application/vnd.webio.application+html", - "application/prs.juno.plotpane+html", - "juliavscode/html", + Makie.WEB_MIMES..., "image/svg+xml", "application/pdf", "application/postscript", diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 794572150d8..e6bbf8f817d 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -35,12 +35,6 @@ function JSServe.jsrender(session::Session, fig::Makie.FigureLike) return JSServe.jsrender(session, Makie.get_scene(fig)) end -const WEB_MIMES = ( - MIME"text/html", - MIME"application/vnd.webio.application+html", - MIME"application/prs.juno.plotpane+html", - MIME"juliavscode/html") - """ * `framerate = 30`: Set framerate (frames per second) to a higher number for smoother animations, or to a lower to use less resources. """ @@ -86,7 +80,7 @@ function mark_as_displayed!(screen::Screen, scene::Scene) return end -for M in WEB_MIMES +for M in Makie.WEB_MIMES @eval begin function Makie.backend_show(screen::Screen, io::IO, m::$M, scene::Scene) inline_display = App() do session::Session @@ -108,7 +102,7 @@ for M in WEB_MIMES end function Makie.backend_showable(::Type{Screen}, ::T) where {T<:MIME} - return T in WEB_MIMES + return T in Makie.WEB_MIMES end # TODO implement diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 9a6c5716a3d..c3be46262fc 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -12,7 +12,7 @@ using ReferenceTests @testset "mimes" begin Makie.inline!(true) f, ax, pl = scatter(1:4) - @testset for mime in WGLMakie.WEB_MIMES + @testset for mime in Makie.WEB_MIMES @test showable(mime(), f) end # I guess we explicitely don't say we can show those since it's highly Inefficient compared to html diff --git a/src/display.jl b/src/display.jl index 542c1cd0933..5db9a75692f 100644 --- a/src/display.jl +++ b/src/display.jl @@ -156,9 +156,19 @@ end Base.showable(mime::MIME, fig::FigureLike) = _backend_showable(mime) -# need to define this to resolve ambiguoity issue +# need to define this to resolve ambiguity issue Base.showable(mime::MIME"application/json", fig::FigureLike) = _backend_showable(mime) +const WEB_MIMES = ( + MIME"text/html", + MIME"application/vnd.webio.application+html", + MIME"application/prs.juno.plotpane+html", + MIME"juliavscode/html") + +# because we have a default way to display pngs in html contexts, we can say that +# if png is showable, so are the html types +Base.showable(mime::Union{WEB_MIMES...}, fig::FigureLike) = showable(MIME"image/png"(), fig) + backend_showable(@nospecialize(screen), @nospecialize(mime)) = false # fallback show when no backend is selected @@ -382,7 +392,7 @@ function backend_show(screen::MakieScreen, io::IO, m::MIME"image/jpeg", scene::S return end -function backend_show(screen::MakieScreen, io::IO, ::Union{MIME"text/html",MIME"application/vnd.webio.application+html",MIME"application/prs.juno.plotpane+html",MIME"juliavscode/html"}, scene::Scene) +function backend_show(screen::MakieScreen, io::IO, ::Union{WEB_MIMES...}, scene::Scene) w, h = widths(scene.px_area[]) png_io = IOBuffer() backend_show(screen, png_io, MIME"image/png"(), scene) From effdd00481c4c214ee4dc718672bf47d7184c036 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 15:58:06 +0200 Subject: [PATCH 44/80] use strings again --- CairoMakie/src/display.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index 71c0958ea1e..ed087315a9b 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -117,7 +117,10 @@ end const DISABLED_MIMES = Set{String}() const SUPPORTED_MIMES = Set([ - Makie.WEB_MIMES..., + "text/html", + "application/vnd.webio.application+html", + "application/prs.juno.plotpane+html", + "juliavscode/html", "image/svg+xml", "application/pdf", "application/postscript", From 4195e293782f366dfce134d5010db538a810a06a Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 15:58:12 +0200 Subject: [PATCH 45/80] fix test --- test/boundingboxes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/boundingboxes.jl b/test/boundingboxes.jl index 266548725d8..0fdb0f86e0f 100644 --- a/test/boundingboxes.jl +++ b/test/boundingboxes.jl @@ -53,6 +53,6 @@ ax = Axis(fig[1, 1]) p = text!(ax, Point2f(10), text = "test", fontsize = 20) bb = boundingbox(p) - @test bb.origin ≈ Point3f(340, 341, 0) + @test bb.origin ≈ Point3f(351, 353, 0) @test bb.widths ≈ Vec3f(32.24, 23.3, 0) end \ No newline at end of file From ef3859edae90d14a2e882c4ac35b8c76ceab2859 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Fri, 31 Mar 2023 16:44:22 +0200 Subject: [PATCH 46/80] smaller colorbar ticks --- src/makielayout/types.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 0e81dc01578..3c5d11e9aea 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -516,7 +516,7 @@ end "The color of the tick labels." ticklabelcolor = @inherit(:textcolor, :black) "The size of the tick marks." - ticksize = 6f0 + ticksize = 5f0 "Controls if the tick marks are visible." ticksvisible = true "The ticks." @@ -592,7 +592,7 @@ end "The alignment of minor ticks on the axis spine" minortickalign = 0f0 "The tick size of minor ticks" - minorticksize = 4f0 + minorticksize = 3f0 "The tick width of minor ticks" minortickwidth = 1f0 "The tick color of minor ticks" From 625bf0695f970db7ed07c84ac6acc45add12ab94 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sat, 1 Apr 2023 13:37:10 +0200 Subject: [PATCH 47/80] adjust more theming gaps --- src/makielayout/types.jl | 2 +- src/theming.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 3c5d11e9aea..8882e450f88 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -983,7 +983,7 @@ const EntryGroup = Tuple{Optional{<:AbstractString}, Vector{LegendEntry}} "The vertical alignment of the entry labels." labelvalign = :center "The additional space between the legend content and the border." - padding = (10f0, 10f0, 8f0, 8f0) + 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." diff --git a/src/theming.jl b/src/theming.jl index 5e58303a0be..c71489dde0b 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -35,7 +35,7 @@ const minimal_default = Attributes( fontsize = 14, textcolor = :black, padding = Vec3f(0.05), - figure_padding = 12, + figure_padding = 14, rowgap = 18, colgap = 18, backgroundcolor = :white, From f9edbe39f1bcab8eb6a0b26d1903a41a0b2ec5f8 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sat, 1 Apr 2023 13:38:16 +0200 Subject: [PATCH 48/80] padding adjust --- src/theming.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theming.jl b/src/theming.jl index c71489dde0b..a12d06f67ec 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -35,7 +35,7 @@ const minimal_default = Attributes( fontsize = 14, textcolor = :black, padding = Vec3f(0.05), - figure_padding = 14, + figure_padding = 16, rowgap = 18, colgap = 18, backgroundcolor = :white, From 37a5eb167df4d8c60c7fe81fed7353b272113b67 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sat, 1 Apr 2023 19:44:07 +0200 Subject: [PATCH 49/80] fix size of docs example --- docs/examples/blocks/axis3.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/examples/blocks/axis3.md b/docs/examples/blocks/axis3.md index 504aa7f2ace..0ea53d4e3b7 100644 --- a/docs/examples/blocks/axis3.md +++ b/docs/examples/blocks/axis3.md @@ -124,15 +124,16 @@ using GLMakie GLMakie.activate!() # hide -f = Figure(resolution = (1200, 800), fontsize = 14) +f = Figure() xs = LinRange(0, 10, 100) ys = LinRange(0, 10, 100) zs = [cos(x) * sin(y) for x in xs, y in ys] for (i, perspectiveness) in enumerate(LinRange(0, 1, 6)) - Axis3(f[fldmod1(i, 3)...], perspectiveness = perspectiveness, - title = "$perspectiveness") + ax = Axis3(f[fldmod1(i, 3)...], perspectiveness = perspectiveness, + title = "perspectiveness = $perspectiveness", protrusions = (0, 0, 0, 20)) + hidedecorations!(ax) surface!(xs, ys, zs) end From b9daec841aa2bb517d3ce61d1020a8a3982a7db9 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sat, 1 Apr 2023 19:56:35 +0200 Subject: [PATCH 50/80] make toggle smaller --- src/makielayout/types.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 8882e450f88..bcbb310381a 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -834,9 +834,9 @@ end "The vertical alignment of the toggle in its suggested bounding box." valign = :center "The width of the toggle." - width = 60 + width = 32 "The height of the toggle." - height = 28 + height = 18 "Controls if the parent layout can adjust to this element's width" tellwidth = true "Controls if the parent layout can adjust to this element's height" From a25dfcc980eded941912854f17c72f0f40c87e36 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sat, 1 Apr 2023 20:00:54 +0200 Subject: [PATCH 51/80] make colorbar thinner --- src/makielayout/types.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index bcbb310381a..8be703fe9e5 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -602,7 +602,7 @@ end "The axis scale" scale = identity "The width or height of the colorbar, depending on if it's vertical or horizontal, unless overridden by `width` / `height`" - size = 16 + size = 14 end end From f2897c78ae3fa4105bff7aff6368a4efb944bc8f Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sat, 1 Apr 2023 20:09:16 +0200 Subject: [PATCH 52/80] smaller colorbar --- src/makielayout/types.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 8be703fe9e5..4b71f6558c1 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -602,7 +602,7 @@ end "The axis scale" scale = identity "The width or height of the colorbar, depending on if it's vertical or horizontal, unless overridden by `width` / `height`" - size = 14 + size = 12 end end From 232af7cb39002d4529b8bb3a55faf420669cf0c0 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sun, 2 Apr 2023 12:01:00 +0200 Subject: [PATCH 53/80] adjust legend doc example --- docs/examples/blocks/legend.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/examples/blocks/legend.md b/docs/examples/blocks/legend.md index 9a451de590a..a485111ebd0 100644 --- a/docs/examples/blocks/legend.md +++ b/docs/examples/blocks/legend.md @@ -279,15 +279,9 @@ using CairoMakie f = Figure() -Axis(f[1, 1]) - markersizes = [5, 10, 15, 20] colors = [:red, :green, :blue, :orange] -for ms in markersizes, color in colors - scatter!(randn(5, 2), markersize = ms, color = color) -end - group_size = [MarkerElement(marker = :circle, color = :black, strokecolor = :transparent, markersize = ms) for ms in markersizes] @@ -298,23 +292,28 @@ group_color = [PolyElement(color = color, strokecolor = :transparent) legends = [Legend(f, [group_size, group_color], [string.(markersizes), string.(colors)], - ["Size", "Color"]) for _ in 1:6] + ["Size", "Color"], tellheight = true) for _ in 1:4] -f[1, 2:4] = legends[1:3] -f[2:4, 2] = legends[4:6] +f[1, 1:2] = legends[1:2] +f[2, :] = legends[3] +f[3, :] = legends[4] -for l in legends[4:6] +for l in legends[3:4] l.orientation = :horizontal l.tellheight = true l.tellwidth = false end legends[2].titleposition = :left -legends[5].titleposition = :left +legends[4].titleposition = :left + +legends[1].nbanks = 2 +legends[4].nbanks = 2 -legends[3].nbanks = 2 -legends[5].nbanks = 2 -legends[6].nbanks = 2 +Label(f[1, 1, Left()], "titleposition = :top\norientation = :vertical\nnbanks = 2", font = :italic, padding = (0, 10, 0, 0)) +Label(f[1, 2, Right()], "titleposition = :left\norientation = :vertical\nnbanks = 1", font = :italic, padding = (10, 0, 0, 0)) +Label(f[2, 1:2, Top()], "titleposition = :top, orientation = :horizontal\nnbanks = 1", font = :italic) +Label(f[3, 1:2, Top()], "titleposition = :left, orientation = :horizontal\nnbanks = 2", font = :italic) f ``` From 91055d8f64c7c80fd7f5cadbc4155f7ce4f0d98b Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sun, 2 Apr 2023 12:04:38 +0200 Subject: [PATCH 54/80] fix intervalslider and example --- docs/examples/blocks/intervalslider.md | 2 +- src/makielayout/blocks/intervalslider.jl | 4 ++-- src/makielayout/types.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/blocks/intervalslider.md b/docs/examples/blocks/intervalslider.md index 79bcf559227..a3592fc9bd4 100644 --- a/docs/examples/blocks/intervalslider.md +++ b/docs/examples/blocks/intervalslider.md @@ -51,7 +51,7 @@ colors = lift(rs_h.interval, rs_v.interval) do h_int, v_int end end -scatter!(points, color = colors, colormap = [:black, :orange], strokewidth = 0) +scatter!(points, color = colors, colormap = [:gray90, :dodgerblue], strokewidth = 0) f ``` diff --git a/src/makielayout/blocks/intervalslider.jl b/src/makielayout/blocks/intervalslider.jl index d5e850ac059..01c456ca210 100644 --- a/src/makielayout/blocks/intervalslider.jl +++ b/src/makielayout/blocks/intervalslider.jl @@ -88,7 +88,7 @@ function initialize_block!(isl::IntervalSlider) end endbuttons = scatter!(blockscene, endpoints, color = endbuttoncolors, - markersize = isl.linewidth, strokewidth = 0, inspectable = false) + markersize = isl.linewidth, strokewidth = 0, inspectable = false, marker = Circle) linesegs = linesegments!(blockscene, linepoints, color = linecolors, linewidth = isl.linewidth, inspectable = false) @@ -107,7 +107,7 @@ function initialize_block!(isl::IntervalSlider) end buttonsizes = @lift($(isl.linewidth) .* $button_magnifications) buttons = scatter!(blockscene, middlepoints, color = isl.color_active, strokewidth = 0, - markersize = buttonsizes, inspectable = false) + markersize = buttonsizes, inspectable = false, marker = Circle) mouseevents = addmouseevents!(blockscene, isl.layoutobservables.computedbbox) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 4b71f6558c1..5bd478c065c 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -762,7 +762,7 @@ end "The current interval of the slider. Don't set this manually, use the function `set_close_to!`." interval = (0, 0) "The width of the slider line" - linewidth::Float64 = 15.0 + linewidth::Float64 = 10.0 "The color of the slider when the mouse hovers over it." color_active_dimmed::RGBAf = COLOR_ACCENT_DIMMED[] "The color of the slider when the mouse clicks and drags the slider." From b33c2229a3120b31b10a18da021269e628192695 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sun, 2 Apr 2023 12:31:47 +0200 Subject: [PATCH 55/80] more docs fixes, axis legend padding --- docs/examples/blocks/axis.md | 2 +- docs/examples/blocks/label.md | 14 ++++++++------ src/makielayout/blocks/legend.jl | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/examples/blocks/axis.md b/docs/examples/blocks/axis.md index bc9ac3765ca..25009b32e74 100644 --- a/docs/examples/blocks/axis.md +++ b/docs/examples/blocks/axis.md @@ -644,7 +644,7 @@ using CairoMakie CairoMakie.activate!() # hide -f = Figure(resolution = (800, 700)) +f = Figure(resolution = (600, 500)) lines(f[1, 1], -100:0.1:100, axis = ( yscale = Makie.pseudolog10, diff --git a/docs/examples/blocks/label.md b/docs/examples/blocks/label.md index cd32e01925c..26e48020158 100644 --- a/docs/examples/blocks/label.md +++ b/docs/examples/blocks/label.md @@ -19,7 +19,7 @@ fig[1:2, 1:3] = [Axis(fig) for _ in 1:6] supertitle = Label(fig[0, :], "Six plots", fontsize = 30) -sideinfo = Label(fig[2:3, 0], "This text is vertical", rotation = pi/2) +sideinfo = Label(fig[1:2, 0], "This text is vertical", rotation = pi/2) fig ``` @@ -35,19 +35,21 @@ CairoMakie.activate!() # hide f = Figure() Label(f[1, 1], - "Left Justified\nMultiline\nLabel\nLineheight 0.9", + "Multiline label\nwith\njustification = :left\nand\nlineheight = 0.9", justification = :left, lineheight = 0.9 ) Label(f[1, 2], - "Center Justified\nMultiline\nLabel\nLineheight 1.1", + "Multiline label\nwith\njustification = :center\nand\nlineheight = 1.1", justification = :center, - lineheight = 1.1 + lineheight = 1.1, + color = :dodgerblue, ) Label(f[1, 3], - "Right Justified\nMultiline\nLabel\nLineheight 1.3", + "Multiline label\nwith\njustification = :right\nand\nlineheight = 1.3", justification = :right, - lineheight = 1.3 + lineheight = 1.3, + color = :firebrick ) f diff --git a/src/makielayout/blocks/legend.jl b/src/makielayout/blocks/legend.jl index 22864932e6a..a733e117ebb 100644 --- a/src/makielayout/blocks/legend.jl +++ b/src/makielayout/blocks/legend.jl @@ -605,7 +605,7 @@ to one occurrence. function axislegend(ax, args...; position = :rt, kwargs...) Legend(ax.parent, args...; bbox = ax.scene.px_area, - margin = (10, 10, 10, 10), + margin = (6, 6, 6, 6), legend_position_to_aligns(position)..., kwargs...) end From 79fad8a3cc150643985d36f587aba1297553ef43 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sun, 2 Apr 2023 12:36:09 +0200 Subject: [PATCH 56/80] improve faq example --- docs/documentation/faq.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/documentation/faq.md b/docs/documentation/faq.md index 261d1fcc68d..e94a9fede36 100644 --- a/docs/documentation/faq.md +++ b/docs/documentation/faq.md @@ -118,13 +118,12 @@ f ### Columns or rows are shrunk to the size of Text or another element Columns or rows that have size `Auto(true)` try to determine the width or height of all -single-spanned elements that are placed in them, and if any elements report their -size the row or column will shrink to the maximum reported size. This is so smaller +single-spanned elements that are placed in them, and if any elements "tell" the layout their own height or width, +the row or column will shrink to the maximum reported size. This is so smaller elements with a known size take as little space as needed. But if there is other content in the row that should take more space, you can give the offending element -the attribute `tellheight = false` or `tellwidth = false`. This way, its own size -can be determined automatically, but -it doesn't report it to the row or column of the layout. Alternatively, you can set the size +the attribute `tellheight = false` or `tellwidth = false`. This way, its own height +or width doesn't influence the automatic sizing of the layout. Alternatively, you can set the size of that row or column to `Auto(false)` (or any other value than `Auto(true)`). \begin{examplefigure}{svg = true} @@ -135,8 +134,8 @@ f = Figure() Axis(f[1, 1], title = "Shrunk") Axis(f[2, 1], title = "Expanded") -Label(f[1, 2], "tellheight = true", tellheight = true) -Label(f[2, 2], "tellheight = false", tellheight = false) +Label(f[1, 2], "This Label has the setting\ntellheight = true\ntherefore the row it is in has\nadjusted to match its height.", tellheight = true) +Label(f[2, 2], "This Label has the setting\ntellheight = false.\nThe row it is in can use\nall the remaining space.", tellheight = false) f ``` From 093af51d6ae9f2f645f25f1615d79efc2427d9c9 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Wed, 5 Apr 2023 14:46:43 +0200 Subject: [PATCH 57/80] fix test value --- test/boundingboxes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/boundingboxes.jl b/test/boundingboxes.jl index 0fdb0f86e0f..a366d07991a 100644 --- a/test/boundingboxes.jl +++ b/test/boundingboxes.jl @@ -53,6 +53,6 @@ ax = Axis(fig[1, 1]) p = text!(ax, Point2f(10), text = "test", fontsize = 20) bb = boundingbox(p) - @test bb.origin ≈ Point3f(351, 353, 0) + @test bb.origin ≈ Point3f(343.0, 345.0, 0) @test bb.widths ≈ Vec3f(32.24, 23.3, 0) end \ No newline at end of file From 0aa4b02f9d6323ac7dd28309a6cb3406057e512b Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Wed, 5 Apr 2023 14:53:49 +0200 Subject: [PATCH 58/80] bump all versions --- CairoMakie/Project.toml | 4 ++-- GLMakie/Project.toml | 4 ++-- Project.toml | 2 +- RPRMakie/Project.toml | 4 ++-- WGLMakie/Project.toml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index cd1d754124f..16d71c5e61c 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -1,7 +1,7 @@ name = "CairoMakie" uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" author = ["Simon Danisch "] -version = "0.10.3" +version = "0.11.0" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" @@ -23,7 +23,7 @@ FFTW = "1" FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.1" -Makie = "=0.19.3" +Makie = "=0.20.0" SnoopPrecompile = "1.0" julia = "1.3" diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index ad100023a24..3966ac59a0c 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -1,6 +1,6 @@ name = "GLMakie" uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" -version = "0.8.3" +version = "0.9.0" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -29,7 +29,7 @@ FixedPointNumbers = "0.7, 0.8" FreeTypeAbstraction = "0.10" GLFW = "3.3" GeometryBasics = "0.4.1" -Makie = "=0.19.3" +Makie = "=0.20.0" MeshIO = "0.4" ModernGL = "1" Observables = "0.5.1" diff --git a/Project.toml b/Project.toml index 24952e0add9..42a5ed2c8c9 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Makie" uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" authors = ["Simon Danisch", "Julius Krumbiegel"] -version = "0.19.3" +version = "0.20.0" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 2efa90304f7..5323951cf90 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -1,7 +1,7 @@ name = "RPRMakie" uuid = "22d9f318-5e34-4b44-b769-6e3734a732a6" authors = ["Simon Danisch"] -version = "0.5.3" +version = "0.6.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -17,7 +17,7 @@ julia = "1.3" Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.1" -Makie = "=0.19.3" +Makie = "=0.20.0" RadeonProRender = "0.2.15" [extras] diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index 94a672ac963..04a000ec738 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -1,7 +1,7 @@ name = "WGLMakie" uuid = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" authors = ["SimonDanisch "] -version = "0.8.7" +version = "0.9.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -26,7 +26,7 @@ FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.1" Hyperscript = "0.0.3, 0.0.4" JSServe = "2.2" -Makie = "=0.19.3" +Makie = "=0.20.0" Observables = "0.5.1" RelocatableFolders = "0.1, 0.2, 0.3, 1.0" ShaderAbstractions = "0.3" From 2e64e8bd61b6ca50078c2937318d910da1988039 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 5 Apr 2023 18:32:06 +0200 Subject: [PATCH 59/80] fix wglmakie --- CairoMakie/src/display.jl | 5 +---- GLMakie/src/display.jl | 2 +- src/display.jl | 5 +---- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index ed087315a9b..71c0958ea1e 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -117,10 +117,7 @@ end const DISABLED_MIMES = Set{String}() const SUPPORTED_MIMES = Set([ - "text/html", - "application/vnd.webio.application+html", - "application/prs.juno.plotpane+html", - "juliavscode/html", + Makie.WEB_MIMES..., "image/svg+xml", "application/pdf", "application/postscript", diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl index c8d0aef1f72..5135fa1f1b5 100644 --- a/GLMakie/src/display.jl +++ b/GLMakie/src/display.jl @@ -10,4 +10,4 @@ function Base.display(screen::Screen, scene::Scene; connect=true) return screen end -Makie.backend_showable(::Type{Screen}, ::Union{MIME"image/jpeg", MIME"image/png"}) = true +Makie.backend_showable(::Type{Screen}, ::Union{MIME"image/jpeg", MIME"image/png", Makie.WEB_MIMES...}) = true diff --git a/src/display.jl b/src/display.jl index 5db9a75692f..ae240c1b861 100644 --- a/src/display.jl +++ b/src/display.jl @@ -165,9 +165,6 @@ const WEB_MIMES = ( MIME"application/prs.juno.plotpane+html", MIME"juliavscode/html") -# because we have a default way to display pngs in html contexts, we can say that -# if png is showable, so are the html types -Base.showable(mime::Union{WEB_MIMES...}, fig::FigureLike) = showable(MIME"image/png"(), fig) backend_showable(@nospecialize(screen), @nospecialize(mime)) = false @@ -399,4 +396,4 @@ function backend_show(screen::MakieScreen, io::IO, ::Union{WEB_MIMES...}, scene: b64 = Base64.base64encode(String(take!(png_io))) print(io, "") return -end \ No newline at end of file +end From 3f8fe437063a5eb3d30e75e258bcbf889c151952 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Sat, 8 Apr 2023 08:23:02 +0200 Subject: [PATCH 60/80] stringify web mimes for set --- CairoMakie/src/display.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index 71c0958ea1e..c93eb760dbc 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -117,7 +117,7 @@ end const DISABLED_MIMES = Set{String}() const SUPPORTED_MIMES = Set([ - Makie.WEB_MIMES..., + map(x->string(x()), Makie.WEB_MIMES)..., "image/svg+xml", "application/pdf", "application/postscript", From 78ee1e5e62b4943d603d7fda413d09f3123a734b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Apr 2023 21:52:04 +0200 Subject: [PATCH 61/80] improve default controls/hotkeys --- src/camera/camera3d.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 41652b28ab8..3f1832c2c26 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -118,8 +118,8 @@ function Camera3D(scene::Scene; kwargs...) # Zooms zoom_in_key = Keyboard.u, zoom_out_key = Keyboard.o, - increase_fov_key = Keyboard.page_up, - decrease_fov_key = Keyboard.page_down, + increase_fov_key = Keyboard.b, + decrease_fov_key = Keyboard.n, # Rotations pan_left_key = Keyboard.j, pan_right_key = Keyboard.l, @@ -135,7 +135,7 @@ function Camera3D(scene::Scene; kwargs...) fix_x_key = Keyboard.x, fix_y_key = Keyboard.y, fix_z_key = Keyboard.z, - reset = Keyboard.home + reset = Keyboard.left_control & Mouse.left ) replace!(controls, :Camera3D, scene, overwrites) @@ -240,7 +240,7 @@ function Camera3D(scene::Scene; kwargs...) # reset on(camera(scene), events(scene).keyboardbutton) do event - if cam.selected[] && event.key == controls[:reset][] && event.action == Keyboard.release + if cam.selected[] && ispressed(scene, controls[:reset][]) # center keeps the rotation of the camera so we reset that here # might make sense to keep user set lookat, upvector, eyeposition # around somewhere for this? From a7e819b79efcd069c9feacb35e129d2b11c0ca8b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 22 Apr 2023 00:54:00 +0200 Subject: [PATCH 62/80] add on-click refocus of camera --- src/Makie.jl | 1 + src/camera/camera3d.jl | 131 ++++++++------- src/interaction/events.jl | 43 +++-- src/interaction/inspector.jl | 125 +------------- src/interaction/position_on_plot.jl | 248 ++++++++++++++++++++++++++++ 5 files changed, 349 insertions(+), 199 deletions(-) create mode 100644 src/interaction/position_on_plot.jl diff --git a/src/Makie.jl b/src/Makie.jl index 86131652646..c41dc79b7bf 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -174,6 +174,7 @@ include("stats/hexbin.jl") # Interactiveness include("interaction/events.jl") include("interaction/interactive_api.jl") +include("interaction/position_on_plot.jl") include("interaction/inspector.jl") # documentation and help functions diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 3f1832c2c26..de514ba7e65 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -131,6 +131,7 @@ function Camera3D(scene::Scene; kwargs...) translation_button = Mouse.right, rotation_button = Mouse.left, scroll_mod = true, + reposition_button = Keyboard.left_alt & Mouse.left, # Shared controls fix_x_key = Keyboard.x, fix_y_key = Keyboard.y, @@ -222,8 +223,7 @@ function Camera3D(scene::Scene; kwargs...) end # Mouse controls - add_translation!(scene, cam) - add_rotation!(scene, cam) + add_mouse_controls!(scene, cam) # add camera controls to scene cameracontrols!(scene, cam) @@ -362,12 +362,17 @@ function on_pulse(scene, cam::Camera3D, timestep) end -function add_translation!(scene, cam::Camera3D) - @extract cam.controls (translation_button, scroll_mod) - @extract cam.settings (mouse_translationspeed, mouse_zoomspeed, cad, projectiontype, zoom_shift_lookat) +function add_mouse_controls!(scene, cam::Camera3D) + @extract cam.controls (translation_button, rotation_button, reposition_button, scroll_mod) + @extract cam.settings ( + mouse_translationspeed, mouse_rotationspeed, mouse_zoomspeed, + cad, projectiontype, zoom_shift_lookat + ) last_mousepos = RefValue(Vec2f(0, 0)) - dragging = RefValue(false) + dragging = RefValue((false, false)) # rotation, translation + + e = events(scene) function compute_diff(delta) if projectiontype[] == Perspective @@ -381,37 +386,80 @@ function add_translation!(scene, cam::Camera3D) end # drag start/stop - on(camera(scene), scene.events.mousebutton) do event - if ispressed(scene, translation_button[]) - if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] + on(camera(scene), e.mousebutton) do event + # Drag start translation/rotation + if event.action == Mouse.press && is_mouseinside(scene) + if ispressed(scene, translation_button[]) last_mousepos[] = mouseposition_px(scene) - dragging[] = true + dragging[] = (false, true) + return Consume(true) + elseif ispressed(scene, rotation_button[]) + last_mousepos[] = mouseposition_px(scene) + dragging[] = (true, false) return Consume(true) end - elseif event.action == Mouse.release && dragging[] - mousepos = mouseposition_px(scene) - diff = compute_diff(last_mousepos[] .- mousepos) - last_mousepos[] = mousepos - dragging[] = false - translate_cam!(scene, cam, mouse_translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) - return Consume(true) + # drag stop & repostion + elseif event.action == Mouse.release + consume = false + + # Drag stop translation/rotation + if dragging[][1] + mousepos = mouseposition_px(scene) + diff = compute_diff(last_mousepos[] .- mousepos) + last_mousepos[] = mousepos + dragging[] = (false, false) + translate_cam!(scene, cam, mouse_translationspeed[] .* Vec3f(diff[1], diff[2], 0f0)) + consume = true + elseif dragging[][2] + mousepos = mouseposition_px(scene) + dragging[] = (false, false) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) + mp = (last_mousepos[] .- mousepos) .* 0.01f0 .* rot_scaling + last_mousepos[] = mousepos + rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) + consume = true + end + + # reposition + if ispressed(scene, reposition_button[], event.button) && is_mouseinside(scene) + p = get_position(scene) + if p !== Point3f(NaN) + # 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) + end + consume = true + end + + return Consume(consume) end + return Consume(false) end # in drag - on(camera(scene), scene.events.mouseposition) do mp - if dragging[] && ispressed(scene, translation_button[]) + on(camera(scene), e.mouseposition) do mp + if dragging[][2] && ispressed(scene, translation_button[]) mousepos = screen_relative(scene, mp) diff = compute_diff(last_mousepos[] .- mousepos) last_mousepos[] = mousepos translate_cam!(scene, cam, mouse_translationspeed[] * Vec3f(diff[1], diff[2], 0f0)) return Consume(true) + elseif dragging[][1] && ispressed(scene, rotation_button[]) + mousepos = screen_relative(scene, mp) + rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) + mp = (last_mousepos[] .- mousepos) * 0.01f0 * rot_scaling + last_mousepos[] = mousepos + rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) + return Consume(true) end return Consume(false) end - on(camera(scene), scene.events.scroll) do scroll + #zoom + on(camera(scene), e.scroll) do scroll if is_mouseinside(scene) && ispressed(scene, scroll_mod[]) zoom_step = (1f0 + 0.1f0 * mouse_zoomspeed[]) ^ -scroll[2] zoom!(scene, cam, zoom_step, cad[], zoom_shift_lookat[]) @@ -419,49 +467,8 @@ function add_translation!(scene, cam::Camera3D) end return Consume(false) end -end - -function add_rotation!(scene, cam::Camera3D) - @extract cam.controls (rotation_button, ) - @extract cam.settings (mouse_rotationspeed, ) - - last_mousepos = RefValue(Vec2f(0, 0)) - dragging = RefValue(false) - e = events(scene) - # drag start/stop - on(camera(scene), e.mousebutton) do event - if ispressed(scene, rotation_button[]) - if event.action == Mouse.press && is_mouseinside(scene) && !dragging[] - last_mousepos[] = mouseposition_px(scene) - dragging[] = true - return Consume(true) - end - elseif event.action == Mouse.release && dragging[] - mousepos = mouseposition_px(scene) - dragging[] = false - rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) - mp = (last_mousepos[] .- mousepos) .* 0.01f0 .* rot_scaling - last_mousepos[] = mousepos - rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) - return Consume(true) - end - return Consume(false) - end - - # in drag - on(camera(scene), e.mouseposition) do mp - if dragging[] && ispressed(scene, rotation_button[]) - mousepos = screen_relative(scene, mp) - rot_scaling = mouse_rotationspeed[] * (e.window_dpi[] * 0.005) - mp = (last_mousepos[] .- mousepos) * 0.01f0 * rot_scaling - last_mousepos[] = mousepos - rotate_cam!(scene, cam, Vec3f(-mp[2], mp[1], 0f0), true) - return Consume(true) - end - return Consume(false) - end end @@ -756,4 +763,4 @@ function show_cam(scene) println("cam.upvector[] = ", round.(cam.upvector[], digits=2)) println("cam.fov[] = ", round.(cam.fov[], digits=2)) return -end +end \ No newline at end of file diff --git a/src/interaction/events.jl b/src/interaction/events.jl index 9fedf07127f..7b1ed7bf04d 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -224,10 +224,10 @@ create_sets(s::Set) = [Set{Union{Keyboard.Button, Mouse.Button}}(s)] # ispressed and logic evaluation """ - ispressed(parent, result::Bool) - ispressed(parent, button::Union{Mouse.Button, Keyboard.Button) - ispressed(parent, collection::Union{Set, Vector, Tuple}) - ispressed(parent, op::BooleanOperator) +ispressed(parent, result::Bool[, waspressed = nothing]) +ispressed(parent, button::Union{Mouse.Button, Keyboard.Button[, waspressed = nothing]) + ispressed(parent, collection::Union{Set, Vector, Tuple}[, waspressed = nothing]) + ispressed(parent, op::BooleanOperator[, waspressed = nothing]) This function checks if a button or combination of buttons is pressed. @@ -251,25 +251,32 @@ 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. -See also: [`And`](@ref), [`Or`](@ref), [`Not`](@ref), [`Exclusively`](@ref), +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 +pass `event.button` so that a key combination including that button still evaluates +as true. + +See also: [`waspressed`](@ref) [`And`](@ref), [`Or`](@ref), [`Not`](@ref), [`Exclusively`](@ref), [`&`](@ref), [`|`](@ref), [`!`](@ref) """ -ispressed(events::Events, mb::Mouse.Button) = mb in events.mousebuttonstate -ispressed(events::Events, key::Keyboard.Button) = key in events.keyboardstate -ispressed(parent, result::Bool) = result +ispressed(events::Events, mb::Mouse.Button, waspressed = nothing) = mb in events.mousebuttonstate || mb == waspressed +ispressed(events::Events, key::Keyboard.Button, waspressed = nothing) = key in events.keyboardstate || key == waspressed +ispressed(parent, result::Bool, waspressed = nothing) = result -ispressed(parent, mb::Mouse.Button) = ispressed(events(parent), mb) -ispressed(parent, key::Keyboard.Button) = ispressed(events(parent), key) +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) = ispressed(parent, op.left) && ispressed(parent, op.right) -ispressed(parent, op::Or) = ispressed(parent, op.left) || ispressed(parent, op.right) -ispressed(parent, op::Not) = !ispressed(parent, op.x) -ispressed(parent, op::Exclusively) = ispressed(events(parent), op) -ispressed(e::Events, op::Exclusively) = op.x == union(e.keyboardstate, e.mousebuttonstate) +ispressed(parent, op::And, waspressed = nothing) = ispressed(parent, op.left, waspressed) && ispressed(parent, op.right, waspressed) +ispressed(parent, op::Or, waspressed = nothing) = ispressed(parent, op.left, waspressed) || ispressed(parent, op.right, waspressed) +ispressed(parent, op::Not, waspressed = nothing) = !ispressed(parent, op.x, waspressed) +ispressed(parent, op::Exclusively, waspressed = nothing) = ispressed(events(parent), op, waspressed) +ispressed(e::Events, op::Exclusively, waspressed::Union{Mouse.Button, Keyboard.Button}) = op.x == union(e.keyboardstate, e.mousebuttonstate, waspressed) +ispressed(e::Events, op::Exclusively, waspressed = nothing) = op.x == union(e.keyboardstate, e.mousebuttonstate) # collections -ispressed(parent, set::Set) = all(x -> ispressed(parent, x), set) -ispressed(parent, set::Vector) = all(x -> ispressed(parent, x), set) -ispressed(parent, set::Tuple) = all(x -> ispressed(parent, x), set) +ispressed(parent, set::Set, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) +ispressed(parent, set::Vector, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) +ispressed(parent, set::Tuple, waspressed = nothing) = all(x -> ispressed(parent, x, waspressed), set) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 414e182f7fd..4fb53c82807 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -73,53 +73,6 @@ function closest_point_on_line(A::Point2f, B::Point2f, P::Point2f) A .+ AB * dot(AP, AB) / dot(AB, AB) end -function view_ray(scene) - inv_projview = inv(camera(scene).projectionview[]) - view_ray(inv_projview, events(scene).mouseposition[], pixelarea(scene)[]) -end -function view_ray(inv_view_proj, mpos, area::Rect2) - # This figures out the camera view direction from the projectionview matrix (?) - # and computes a ray from a near and a far point. - # Based on ComputeCameraRay from ImGuizmo - mp = 2f0 .* (mpos .- minimum(area)) ./ widths(area) .- 1f0 - v = inv_view_proj * Vec4f(0, 0, -10, 1) - reversed = v[3] < v[4] - near = reversed ? 1f0 - 1e-6 : 0f0 - far = reversed ? 0f0 : 1f0 - 1e-6 - - origin = inv_view_proj * Vec4f(mp[1], mp[2], near, 1f0) - origin = origin[Vec(1, 2, 3)] ./ origin[4] - - p = inv_view_proj * Vec4f(mp[1], mp[2], far, 1f0) - p = p[Vec(1, 2, 3)] ./ p[4] - - dir = normalize(p .- origin) - return origin, dir -end - - -# These work in 2D and 3D -function closest_point_on_line(A, B, origin, dir) - closest_point_on_line( - to_ndim(Point3f, A, 0), - to_ndim(Point3f, B, 0), - to_ndim(Point3f, origin, 0), - to_ndim(Vec3f, dir, 0) - ) -end -function closest_point_on_line(A::Point3f, B::Point3f, origin::Point3f, dir::Vec3f) - # See: - # https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection - AB_norm = norm(B .- A) - u_AB = (B .- A) / AB_norm - u_dir = normalize(dir) - u_perp = normalize(cross(u_dir, u_AB)) - # e_RD, e_perp defines a plane with normal n - n = normalize(cross(u_dir, u_perp)) - t = dot(origin .- A, n) / dot(u_AB, n) - A .+ clamp(t, 0.0, AB_norm) * u_AB -end - function point_in_triangle(A::Point2, B::Point2, C::Point2, P::Point2, ϵ = 1e-6) # adjusted from ray_triangle_intersection AO = A .- P @@ -132,39 +85,6 @@ function point_in_triangle(A::Point2, B::Point2, C::Point2, P::Point2, ϵ = 1e-6 return (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) end -function ray_triangle_intersection(A, B, C, origin, dir, ϵ = 1e-6) - # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html - AO = A .- origin - BO = B .- origin - CO = C .- origin - A1 = 0.5 * dot(cross(BO, CO), dir) - A2 = 0.5 * dot(cross(CO, AO), dir) - A3 = 0.5 * dot(cross(AO, BO), dir) - - e = 1e-3 - if (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) - Point3f((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) - else - Point3f(NaN) - end -end - - -### Surface positions -######################################## - -surface_x(xs::ClosedInterval, i, j, N) = minimum(xs) + (maximum(xs) - minimum(xs)) * (i-1) / (N-1) -surface_x(xs, i, j, N) = xs[i] -surface_x(xs::AbstractMatrix, i, j, N) = xs[i, j] - -surface_y(ys::ClosedInterval, i, j, N) = minimum(ys) + (maximum(ys) - minimum(ys)) * (j-1) / (N-1) -surface_y(ys, i, j, N) = ys[j] -surface_y(ys::AbstractMatrix, i, j, N) = ys[i, j] - -function surface_pos(xs, ys, zs, i, j) - N, M = size(zs) - Point3f(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) -end ### Mapping mesh vertex indices to Vector{Polygon} index @@ -586,9 +506,7 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i scene = parent_scene(plot) # cast ray from cursor into screen, find closest point to line - p0, p1 = plot[1][][idx-1:idx] - origin, dir = view_ray(scene) - pos = closest_point_on_line(p0, p1, origin, dir) + pos = get_position(plot, idx) proj_pos = shift_project(scene, plot, to_ndim(Point3f, pos, 0)) update_tooltip_alignment!(inspector, proj_pos) @@ -602,7 +520,7 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i if haskey(plot, :inspector_label) tt.text[] = plot[:inspector_label][](plot, idx, typeof(p0)(pos)) else - tt.text[] = position2string(typeof(p0)(pos)) + tt.text[] = position2string(eltypetype(plot[1][])(pos)) end tt.visible[] = true a.indicator_visible[] && (a.indicator_visible[] = false) @@ -668,38 +586,7 @@ function show_data(inspector::DataInspector, plot::Surface, idx) proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) - xs = plot[1][] - ys = plot[2][] - zs = plot[3][] - w, h = size(zs) - _i = mod1(idx, w); _j = div(idx-1, w) - - # This isn't the most accurate so we include some neighboring faces - origin, dir = view_ray(scene) - pos = Point3f(NaN) - for i in _i-1:_i+1, j in _j-1:_j+1 - (1 <= i <= w) && (1 <= j < h) || continue - - if i - 1 > 0 - pos = ray_triangle_intersection( - surface_pos(xs, ys, zs, i, j), - surface_pos(xs, ys, zs, i-1, j), - surface_pos(xs, ys, zs, i, j+1), - origin, dir - ) - end - - if i + 1 <= w && isnan(pos) - pos = ray_triangle_intersection( - surface_pos(xs, ys, zs, i, j), - surface_pos(xs, ys, zs, i, j+1), - surface_pos(xs, ys, zs, i+1, j+1), - origin, dir - ) - end - - isnan(pos) || break - end + pos = get_position(plot, idx) if !isnan(pos) tt[1][] = proj_pos @@ -1047,11 +934,11 @@ function show_data(inspector::DataInspector, plot::VolumeSlices, idx, child::Hea Point3f(T * Point4f(qs[2], ps[1], 0, 1)) ] - origin, dir = view_ray(scene) + ray = ray_at_cursor(scene) pos = Point3f(NaN) - pos = ray_triangle_intersection(vs[1], vs[2], vs[3], origin, dir) + pos = ray_triangle_intersection(vs[1], vs[2], vs[3], ray) if isnan(pos) - pos = ray_triangle_intersection(vs[3], vs[4], vs[1], origin, dir) + pos = ray_triangle_intersection(vs[3], vs[4], vs[1], ray) end if !isnan(pos) diff --git a/src/interaction/position_on_plot.jl b/src/interaction/position_on_plot.jl new file mode 100644 index 00000000000..1dd54508994 --- /dev/null +++ b/src/interaction/position_on_plot.jl @@ -0,0 +1,248 @@ +struct Ray + origin::Point3f + direction::Vec3f +end + +""" + ray_at_cursor(scenelike) + +Returns a Ray into the scene starting at the current cursor position. +""" +ray_at_cursor(x) = ray_at_cursor(get_scene(x)) +function ray_at_cursor(scene::Scene) + return ray_at_cursor(scene, cameracontrols(scene)) +end + +function ray_at_cursor(scene::Scene, cam::Camera3D) + lookat = cam.lookat[] + eyepos = cam.eyeposition[] + viewdir = lookat - eyepos + + 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[]) + aspect = px_width / px_height + rel_pos = 2 .* mouseposition_px(scene) ./ (px_width, px_height) .- 1 + + if cam.settings.projectiontype[] === Perspective + dir = (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) * tand(0.5 * cam.fov[]) + u_z + return Ray(cam.eyeposition[], normalize(dir)) + else + # Orthographic has consistent direction, but not starting point + origin = norm(viewdir) * (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) + return Ray(origin, normalize(viewdir)) + end +end + +function ray_at_cursor(scene::Scene, ::Camera2D) + @info "TODO verify" + rel_pos = mouseposition_px(scene) ./ widths(scene.px_area[]) + origin = minimum(cam.area[]) .+ rel_pos .* widths(cam.area[]) + return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) +end + +function ray_at_cursor(scene::Scene, ::PixelCamera) + @info "TODO verify" + return Ray(to_ndim(Point3f, mouseposition_px(scene), 10_000f0), Vec3f(0,0,-1)) +end + +function ray_at_cursor(scene::Scene, ::RelativeCamera) + @info "TODO verify" + origin = mouseposition_px(scene) ./ widths(scene.px_area[]) + return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) +end + +ray_at_cursor(scene::Scene, cam) = _ray_at_cursor(scene, cam) +function _ray_at_cursor(scene::Scene, cam = scene.camera_controls) + inv_view_proj = inv(camera(scene).projectionview[]) + mpos = events(scene).mouseposition[] + area = pixelarea(scene)[] + + # This figures out the camera view direction from the projectionview matrix + # and computes a ray from a near and a far point. + # Based on ComputeCameraRay from ImGuizmo + mp = 2f0 .* (mpos .- minimum(area)) ./ widths(area) .- 1f0 + v = inv_view_proj * Vec4f(0, 0, -10, 1) + reversed = v[3] < v[4] + near = reversed ? 1f0 - 1e-6 : 0f0 + far = reversed ? 0f0 : 1f0 - 1e-6 + + origin = inv_view_proj * Vec4f(mp[1], mp[2], near, 1f0) + origin = origin[Vec(1, 2, 3)] ./ origin[4] + + p = inv_view_proj * Vec4f(mp[1], mp[2], far, 1f0) + p = p[Vec(1, 2, 3)] ./ p[4] + + dir = normalize(p .- origin) + + return Ray(origin, dir) +end + + +############################################## + + +# These work in 2D and 3D +function closest_point_on_line(A::VecTypes, B::VecTypes, ray::Ray) + return closest_point_on_line(to_ndim(Point3f, A, 0), to_ndim(Point3f, B, 0), ray) +end +function closest_point_on_line(A::Point3f, B::Point3f, ray::Ray) + # See: + # https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection + AB_norm = norm(B .- A) + u_AB = (B .- A) / AB_norm + u_perp = normalize(cross(ray.direction, u_AB)) + # e_RD, e_perp defines a plane with normal n + n = normalize(cross(ray.direction, u_perp)) + t = dot(ray.origin .- A, n) / dot(u_AB, n) + return A .+ clamp(t, 0.0, AB_norm) * u_AB +end + + +function ray_triangle_intersection(A::VecTypes{3}, B::VecTypes{3}, C::VecTypes{3}, ray::Ray, ϵ = 1e-6) + # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html + AO = A .- ray.origin + BO = B .- ray.origin + CO = C .- ray.origin + A1 = 0.5 * dot(cross(BO, CO), ray.direction) + A2 = 0.5 * dot(cross(CO, AO), ray.direction) + A3 = 0.5 * dot(cross(AO, BO), ray.direction) + + if (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) + return Point3f((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) + else + return Point3f(NaN) + end +end + +function ray_rect_intersection(rect::Rect2f, ray::Ray) + possible_hit = ray.origin - ray.origin[3] / ray.direction[3] * ray.direction + min = minimum(rect); max = maximum(rect) + if all(min <= possible_hit[Vec(1,2)] <= max) + return possible_hit + end + return Point3f(NaN) +end + + +function ray_rect_intersection(rect::Rect3f, ray::Ray) + mins = (minimum(rect) - ray.origin) ./ ray.direction + maxs = (maximum(rect) - ray.origin) ./ ray.direction + x, y, z = min.(mins, maxs) + possible_hit = max(x, y, z) + if possible_hit < minimum(max.(mins, maxs)) + return ray.origin + possible_hit * ray.direction + end + return Point3f(NaN) +end + +### Surface positions +######################################## + +surface_x(xs::ClosedInterval, i, j, N) = minimum(xs) + (maximum(xs) - minimum(xs)) * (i-1) / (N-1) +surface_x(xs, i, j, N) = xs[i] +surface_x(xs::AbstractMatrix, i, j, N) = xs[i, j] + +surface_y(ys::ClosedInterval, i, j, N) = minimum(ys) + (maximum(ys) - minimum(ys)) * (j-1) / (N-1) +surface_y(ys, i, j, N) = ys[j] +surface_y(ys::AbstractMatrix, i, j, N) = ys[i, j] + +function surface_pos(xs, ys, zs, i, j) + N, M = size(zs) + return Point3f(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) +end + + +################################################# + + +""" + get_position(scene) = get_position(pick(scene)) + get_position(plot, index) + +Given the result of `pick(...)` this function returns a relevant position +for the given input. If `plot = nothing` (i.e pick did not find a plot) +the function will return `Point3f(NaN)`. + +For most plot types the returned position is interpolated to match up with the +cursor position exactly. Exceptions: +- `scatter` and `meshscatter` return the position of the clicked marker/mesh +- `text` is excluded, always returning `Point3f(NaN)` +- `volume` returns a relevant position on its bounding box +""" +get_position(scene::Scene) = get_position(pick(scene)...) +get_position(plot::Union{Scatter, MeshScatter}, idx) = plot[1][][idx] + +function get_position(plot::Union{Lines, LineSegments}, idx) + p0, p1 = plot[1][][idx-1:idx] + return closest_point_on_line(p0, p1, ray_at_cursor(parent_scene(plot))) +end + +function get_position(plot::Union{Heatmap, Image}, idx) + p0, p1 = Point2f.(extrema(plot.x[]), extrema(plot.y[])) + return ray_rect_intersection(Rect2f(p0, p1 - p0), ray_at_cursor(parent_scene(plot))) +end + +function get_position(plot::Mesh, idx) + positions = coordinates(plot.mesh[]) + ray = ray_at_cursor(parent_scene(plot)) + + for f in faces(plot.mesh[]) + if idx in f + p1, p2, p3 = positions[f] + pos = ray_triangle_intersection(p1, p2, p3, ray) + if pos !== Point3f(NaN) + return pos + end + end + end + + return Point3f(NaN) +end + +function get_position(plot::Surface, idx) + xs = plot[1][] + ys = plot[2][] + zs = plot[3][] + w, h = size(zs) + _i = mod1(idx, w); _j = div(idx-1, w) + + # This isn't the most accurate so we include some neighboring faces + ray = ray_at_cursor(parent_scene(plot)) + pos = Point3f(NaN) + for i in _i-1:_i+1, j in _j-1:_j+1 + (1 <= i <= w) && (1 <= j < h) || continue + + if i - 1 > 0 + pos = ray_triangle_intersection( + surface_pos(xs, ys, zs, i, j), + surface_pos(xs, ys, zs, i-1, j), + surface_pos(xs, ys, zs, i, j+1), + ray + ) + end + + if i + 1 <= w && isnan(pos) + pos = ray_triangle_intersection( + surface_pos(xs, ys, zs, i, j), + surface_pos(xs, ys, zs, i, j+1), + surface_pos(xs, ys, zs, i+1, j+1), + ray + ) + end + + isnan(pos) || break + end + + return pos +end + +function get_position(plot::Volume, idx) + min, max = Point3f.(extrema(plot.x[]), extrema(plot.y[]), extrema(plot.z[])) + return ray_rect_intersection(Rect3f(min, max .- min), ray_at_cursor(parent_scene(plot))) +end + +get_position(plot::Text, idx) = Point3f(NaN) +get_position(plot::Nothing, idx) = Point3f(NaN) \ No newline at end of file From d51ed11bbb35a2f7618a7e6bb49b39d47e674237 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Apr 2023 10:45:40 +0200 Subject: [PATCH 63/80] DataInspector fixes - fix error on range access for lines - fix a typo - fix blinking in image - fix error in arrows --- src/interaction/inspector.jl | 8 +++++--- src/interaction/position_on_plot.jl | 2 ++ src/utilities/utilities.jl | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 4fb53c82807..7f2b523a0ab 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -520,7 +520,7 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i if haskey(plot, :inspector_label) tt.text[] = plot[:inspector_label][](plot, idx, typeof(p0)(pos)) else - tt.text[] = position2string(eltypetype(plot[1][])(pos)) + tt.text[] = position2string(eltype(plot[1][])(pos)) end tt.visible[] = true a.indicator_visible[] && (a.indicator_visible[] = false) @@ -661,7 +661,9 @@ function show_imagelike(inspector, plot, name, edge_based) scene, position, color = a._color, visible = a.indicator_visible, inspectable = false, - marker=:rect, markersize = map(r -> 3r, a.range), + # 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), strokecolor = a.indicator_color, strokewidth = a.indicator_linewidth ) @@ -829,7 +831,7 @@ function show_data(inspector::DataInspector, plot::Arrows, idx, ::LineSegments) return show_data(inspector, plot, div(idx+1, 2), nothing) end function show_data(inspector::DataInspector, plot::Arrows, idx, source) - a = inspector.plot.attributes + a = inspector.attributes tt = inspector.plot pos = plot[1][][idx] mpos = Point2f(mouseposition_px(inspector.root)) diff --git a/src/interaction/position_on_plot.jl b/src/interaction/position_on_plot.jl index 1dd54508994..c7db2d291a2 100644 --- a/src/interaction/position_on_plot.jl +++ b/src/interaction/position_on_plot.jl @@ -55,6 +55,8 @@ function ray_at_cursor(scene::Scene, ::RelativeCamera) end ray_at_cursor(scene::Scene, cam) = _ray_at_cursor(scene, cam) + +# This method should always work function _ray_at_cursor(scene::Scene, cam = scene.camera_controls) inv_view_proj = inv(camera(scene).projectionview[]) mpos = events(scene).mouseposition[] diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index acbec1bc086..70b2b36c729 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -390,5 +390,6 @@ function extract_keys(attributes, keys) end # Scalar - Vector getindex -sv_getindex(v::Vector, i::Integer) = v[i] -sv_getindex(x, i::Integer) = x +sv_getindex(v::AbstractVector, i::Integer) = v[i] +sv_getindex(x, ::Integer) = x +sv_getindex(x::VecTypes, ::Integer) = x From fdc3666e35f420d47f00984aa608e9824d80daa1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Apr 2023 13:27:07 +0200 Subject: [PATCH 64/80] fix wrong tooltip in volumeslices --- src/interaction/inspector.jl | 39 ++++++++--------------------- src/interaction/position_on_plot.jl | 7 ++++++ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 7f2b523a0ab..0dd7a1ec521 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -924,45 +924,28 @@ function show_data(inspector::DataInspector, plot::VolumeSlices, idx, child::Hea proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) - qs = extrema(child[1][]) - ps = extrema(child[2][]) + a0, a1 = extrema(child[1][]) + b0, b1 = extrema(child[2][]) data = child[3][] T = child.transformation.model[] - vs = [ # clockwise - Point3f(T * Point4f(qs[1], ps[1], 0, 1)), - Point3f(T * Point4f(qs[1], ps[2], 0, 1)), - Point3f(T * Point4f(qs[2], ps[2], 0, 1)), - Point3f(T * Point4f(qs[2], ps[1], 0, 1)) - ] - - ray = ray_at_cursor(scene) - pos = Point3f(NaN) - pos = ray_triangle_intersection(vs[1], vs[2], vs[3], ray) - if isnan(pos) - pos = ray_triangle_intersection(vs[3], vs[4], vs[1], ray) - end + # Transform the Ray rather than the Rect here to avoid using a Rect3f + rect = Rect2f(a0, b0, a1, b1) + ray = transform(inv(T), ray_at_cursor(scene)) + p = ray_rect_intersection(rect, ray) # in heatmap space (with z = normal of heatmap) - if !isnan(pos) - child_idx = findfirst(isequal(child), plot.plots) - if child_idx == 2 - x = pos[2]; y = pos[3] - elseif child_idx == 3 - x = pos[1]; y = pos[3] - else - x = pos[1]; y = pos[2] - end - i = clamp(round(Int, (x - qs[1]) / (qs[2] - qs[1]) * size(data, 1) + 0.5), 1, size(data, 1)) - j = clamp(round(Int, (y - ps[1]) / (ps[2] - ps[1]) * size(data, 2) + 0.5), 1, size(data, 2)) + if !isnan(p) + i = clamp(round(Int, (p[1] - a0) / (a1 - a0) * size(data, 1) + 0.5), 1, size(data, 1)) + j = clamp(round(Int, (p[2] - b0) / (b1 - b0) * size(data, 2) + 0.5), 1, size(data, 2)) val = data[i, j] tt[1][] = proj_pos if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, (i, j), pos) + tt.text[] = plot[:inspector_label][](plot, (i, j), p) else tt.text[] = @sprintf( "x: %0.6f\ny: %0.6f\nz: %0.6f\n%0.6f0", - pos[1], pos[2], pos[3], val + p[1], p[2], p[3], val ) end tt.visible[] = true diff --git a/src/interaction/position_on_plot.jl b/src/interaction/position_on_plot.jl index c7db2d291a2..e0eb414c879 100644 --- a/src/interaction/position_on_plot.jl +++ b/src/interaction/position_on_plot.jl @@ -83,6 +83,13 @@ function _ray_at_cursor(scene::Scene, cam = scene.camera_controls) end +function transform(M::Mat4f, ray::Ray) + p4d = M * to_ndim(Point4f, ray.origin, 1f0) + dir = normalize(M[Vec(1,2,3), Vec(1,2,3)] * ray.direction) + return Ray(p4d[Vec(1,2,3)] / p4d[4], dir) +end + + ############################################## From 6945b97a3ff0bd98b547bd2d30dcf2e9a63405cb Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Apr 2023 18:23:42 +0200 Subject: [PATCH 65/80] fix line indicator in band --- src/interaction/inspector.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 0dd7a1ec521..a56b086c818 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -136,20 +136,20 @@ function point_in_quad_parameter( # Our initial guess is that P is in the center of the quad (in terms of AB and DC) f = 0.5 - - for i in 0:iterations + AB = B - A + DC = C - D + for _ in 0:iterations # vector between top and bottom point of the current line dir = (D + f * (C - D)) - (A + f * (B - A)) - DC = C - D - AB = B - A # solves P + _ * dir = A + f1 * (B - A) (intersection point of ray & line) f1, _ = inv(Mat2f(AB..., dir...)) * (P - A) f2, _ = inv(Mat2f(DC..., dir...)) * (P - D) # next fraction estimate should be between f1 and f2 # adding 2f to this helps avoid jumping between low and high values + old_f = f f = 0.25 * (2f + f1 + f2) - if abs(f2 - f1) < epsilon + if abs(old_f - f) < epsilon return f end end @@ -986,7 +986,8 @@ function show_data(inspector::DataInspector, plot::Band, ::Integer, ::Mesh) if a.enable_indicators[] model = plot.model[] - if inspector.selection != plot || isempty(inspector.temp_plots) + # Why does this sometimes create 2+ plots + if inspector.selection != plot || (length(inspector.temp_plots) != 1) clear_temporary_plots!(inspector, plot) p = lines!( scene, [P1, P2], model = model, From 1053b3a7ae4978727ad874a48fa3f31774f88e6a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Apr 2023 18:47:33 +0200 Subject: [PATCH 66/80] update NEWS --- NEWS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 540c6824083..f278095d360 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,8 @@ ## master -- Made some adjustments to the 3D camera (switched back from fov to location based zoom, split hotkeys from other options & minor QoL changes) [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) +- Improvements to 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) +- Fixed some errors/problems with DataInspector [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) ## v0.19.4 From 10aff3691c3bc78e04dfc233bad97430aab7d799 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 23 Apr 2023 21:10:30 +0200 Subject: [PATCH 67/80] update docs --- docs/documentation/cameras.md | 16 ++++++++++++---- docs/documentation/events.md | 2 ++ src/camera/camera2d.jl | 20 ++++++++++++++++---- src/camera/camera3d.jl | 19 ++++++++++++++++--- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/docs/documentation/cameras.md b/docs/documentation/cameras.md index 236764434cc..ea7a3e8238f 100644 --- a/docs/documentation/cameras.md +++ b/docs/documentation/cameras.md @@ -1,13 +1,15 @@ # Cameras -A `Camera` is simply a viewport through which the Scene is visualized. `Makie` offers 2D and 3D projections, and 2D plots can be projected in 3D! +A `Camera` is simply a viewport through which the Scene is visualized. `Makie` offers 2D and 3D projections, and 2D plots can be projected in 3D! -To specify the camera you want to use for your Scene, you can set the `camera` attribute. Currently, we offer four types of camera: +To specify the camera you want to use for your Scene, you can set the `camera` attribute. Currently, we offer the following cameras/constructors \apilink{campixel!} +\apilink{cam_relative!} \apilink{cam2d!} -`cam3d!` -`cam3d_cad!` +\apilink{Camera3D} +\apilink{cam3d!} +\apilink{cam3d_cad!} which will mutate the camera of the Scene into the specified type. @@ -15,6 +17,10 @@ which will mutate the camera of the Scene into the specified type. The pixel camera (\apilink{campixel!(scene)}) projects the scene in pixel space, i.e. each integer step in the displayed data will correspond to one pixel. There are no controls for this camera. The clipping limits are set to `(-10_000, 10_000)`. +## Relative Camera + +The relative camera (\apilink{cam_relative!(scene)}) projects the scene into a 0..1 by 0..1 space. There are no controls for this camera. The clipping limits are set to `(-10_000, 10_000)`. + ## 2D Camera The 2D camera (\apilink{cam2d!(scene)}) uses an orthographic projection with a fixed rotation and aspect ratio. You can set the following attributes via keyword arguments in `cam2d!` or by accessing the camera struct `cam = cameracontrols(scene)`: @@ -30,6 +36,8 @@ Note that this camera is not used by `Axis`. It is used, by default, for 2D `LSc {{doc Camera3D}} +`cam3d!` and `cam3d_cad!` but create a `Camera3D` with some specific options. + ## Example - Visualizing the cameras view box ```julia diff --git a/docs/documentation/events.md b/docs/documentation/events.md index 593fead36c3..077cf680bd0 100644 --- a/docs/documentation/events.md +++ b/docs/documentation/events.md @@ -376,6 +376,8 @@ Furthermore you can wrap any of the above in `Exclusively` to discard matches wh - `hotkey = Keyboard.left_control & Keyboard.a` is equivalent to `(Keyboard.left_control, Keyboard.a)` - `hotkey = (Keyboard.left_control | Keyboard.right_control) & Keyboard.a` allows either left or right control with a. +Note that the way we used `ispressed` above, the condition will be true for "press" and "repeat" events. You can further restrict to one or the other by checking `event.action`. If you wish to react to a "release" event, you will need to pass `event.key`/`event.button` as a third argument to `ispressed(fig, hotkey, event.key)`. This will tell `ispressed` to assume the key or button is pressed if it is part of the hotkey. + ## Interactive Widgets Makie has a couple of useful interactive widgets like sliders, buttons and menus, which you can learn about in the \myreflink{Blocks} section. diff --git a/src/camera/camera2d.jl b/src/camera/camera2d.jl index ff276348d94..2e02a43a472 100644 --- a/src/camera/camera2d.jl +++ b/src/camera/camera2d.jl @@ -11,14 +11,23 @@ end """ cam2d!(scene::SceneLike, kwargs...) -Creates a 2D camera for the given Scene. +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 + +- `zoomspeed = 0.1f0` sets the zoom speed. +- `zoombutton = true` sets a button (combination) which needs to be pressed to enable zooming. By default no button needs to be pressed. +- `panbutton = Mouse.right` sets the button used to translate the camera. This must include a mouse button. +- `selectionbutton = (Keyboard.space, Mouse.left)` sets the button used for rectangle selection. This must include a mouse button. """ function cam2d!(scene::SceneLike; kw_args...) cam_attributes = merged_get!(:cam2d, scene, Attributes(kw_args)) do Attributes( area = Observable(Rectf(0, 0, 1, 1)), zoomspeed = 0.10f0, - zoombutton = nothing, + zoombutton = true, panbutton = Mouse.right, selectionbutton = (Keyboard.space, Mouse.left), padding = 0.001, @@ -318,7 +327,9 @@ end """ campixel!(scene; nearclip=-1000f0, farclip=1000f0) -Creates a pixel-level camera for the `Scene`. No controls! +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. """ function campixel!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) disconnect!(camera(scene)) @@ -338,7 +349,8 @@ struct RelativeCamera <: AbstractCamera end """ cam_relative!(scene) -Creates a pixel-level camera for the `Scene`. No controls! +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) projection = orthographicprojection(0f0, 1f0, 0f0, 1f0, nearclip, farclip) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index de514ba7e65..c3612ef7320 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -62,8 +62,8 @@ Controls include any kind of hotkey setting. - `zoom_in_key = Keyboard.u` sets the key for zooming into the scene (translate eyeposition towards lookat). - `zoom_out_key = Keyboard.o` sets the key for zooming out of the scene (translate eyeposition away from lookat). -- `increase_fov_key = Keyboard.page_up` sets the key for increasing the fov. -- `decrease_fov_key = Keyboard.page_down` sets the key for increasing the fov. +- `increase_fov_key = Keyboard.b` sets the key for increasing the fov. +- `decrease_fov_key = Keyboard.n` sets the key for decreasing the fov. - `pan_left_key = Keyboard.j` sets the key for rotations around the screens vertical axis. - `pan_right_key = Keyboard.l` sets the key for rotations around the screens vertical axis. @@ -75,7 +75,8 @@ Controls include any kind of hotkey setting. - `fix_x_key = Keyboard.x` sets the key for fixing translations and rotations to the (world/plot) x-axis. - `fix_y_key = Keyboard.y` sets the key for fixing translations and rotations to the (world/plot) y-axis. - `fix_z_key = Keyboard.z` sets the key for fixing translations and rotations to the (world/plot) z-axis. -- `reset = Keyboard.home` sets the key for fully resetting the camera. This equivalent to setting `lookat = Vec3f(0)`, `upvector = Vec3f(0, 0, 1)`, `eyeposition = Vec3f(3)` and then calling `center!(scene)`. +- `reset = Keyboard.left_control & Mouse.left` sets the key for resetting the camera. This equivalent to calling `center!(scene)`. +- `reposition_button = Keyboard.left_alt & Mouse.left` sets the key for focusing the camera on a plot object. - `translation_button = Mouse.right` sets the mouse button for drag-translations. (up/down/left/right) - `scroll_mod = true` sets an additional modifier button for scroll-based zoom. (true being neutral) @@ -254,9 +255,21 @@ function Camera3D(scene::Scene; kwargs...) end # These imitate the old camera +""" + cam3d!(scene[; kwargs...]) + +Creates a `Camera3D` with `zoom_shift_lookat = true` and `fixed_axis = true`. +For more information, see [`Camera3D``](@ref) +""" cam3d!(scene; zoom_shift_lookat = true, fixed_axis = true, kwargs...) = Camera3D(scene, zoom_shift_lookat = zoom_shift_lookat, fixed_axis = fixed_axis; kwargs...) +""" + cam3d_cad!(scene[; kwargs...]) + +Creates a `Camera3D` with `cad = true`, `zoom_shift_lookat = false` and +`fixed_axis = false`. For more information, see [`Camera3D``](@ref) +""" cam3d_cad!(scene; cad = true, zoom_shift_lookat = false, fixed_axis = false, kwargs...) = Camera3D(scene, cad = cad, zoom_shift_lookat = zoom_shift_lookat, fixed_axis = fixed_axis; kwargs...) From 64f7730d5b072704b54761547bdeeb8c2bed8ee3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 5 May 2023 15:32:02 +0200 Subject: [PATCH 68/80] consider transforms when centering on plot object --- src/camera/camera3d.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index c3612ef7320..04a5eb33ce6 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -435,8 +435,16 @@ function add_mouse_controls!(scene, cam::Camera3D) # reposition if ispressed(scene, reposition_button[], event.button) && is_mouseinside(scene) - p = get_position(scene) + plt, idx = pick(scene) + p = get_position(plt, idx) + if p !== Point3f(NaN) + # transform data -> world + p = apply_transform(transform_func_obs(plt), p, get(plt, :space, :data)) + model = to_value(get(plt, :model, plt.transformation.model)) + p4d = model * to_ndim(Point4f, to_ndim(Point3f, p, 0), 1) + p = p4d[Vec(1,2,3)] / p4d[4] + # if translation/rotation happens with on-click reposition, # try uncommenting this # dragging[] = (false, false) From fc4fcd5bf6b17adeb6c2767e19b01c4a2b98bce0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 12 Jun 2023 13:45:01 +0200 Subject: [PATCH 69/80] fix type error in cam2d! --- src/camera/camera2d.jl | 4 ++-- src/interaction/events.jl | 2 -- src/types.jl | 10 ++++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/camera/camera2d.jl b/src/camera/camera2d.jl index 2e02a43a472..c5d0110744a 100644 --- a/src/camera/camera2d.jl +++ b/src/camera/camera2d.jl @@ -1,8 +1,8 @@ struct Camera2D <: AbstractCamera area::Observable{Rect2f} zoomspeed::Observable{Float32} - zoombutton::Observable{ButtonTypes} - panbutton::Observable{Union{ButtonTypes, Vector{ButtonTypes}}} + zoombutton::Observable{IsPressedInputType} + panbutton::Observable{IsPressedInputType} padding::Observable{Float32} last_area::Observable{Vec{2, Int}} update_limits::Observable{Bool} diff --git a/src/interaction/events.jl b/src/interaction/events.jl index 7b1ed7bf04d..7d599680730 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -75,8 +75,6 @@ function onpick end ################################################################################ -abstract type BooleanOperator end - """ And(left, right[, rest...]) diff --git a/src/types.jl b/src/types.jl index c28e74df6a8..7c741d71e09 100644 --- a/src/types.jl +++ b/src/types.jl @@ -378,3 +378,13 @@ end # The color type we ideally use for most color attributes const RGBColors = Union{RGBAf, Vector{RGBAf}, Vector{Float32}} + + +abstract type BooleanOperator end + +""" + IsPressedInputType + +Union containing possible input types for `ispressed`. +""" +const IsPressedInputType = Union{Bool, BooleanOperator, Mouse.Button, Keyboard.Button, Set, Vector, Tuple} \ No newline at end of file From a26e6a1dd5c462755335f61ad852d493c6490c44 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 12 Jun 2023 13:45:21 +0200 Subject: [PATCH 70/80] check other ray_at_cursor methods --- src/interaction/position_on_plot.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/interaction/position_on_plot.jl b/src/interaction/position_on_plot.jl index e0eb414c879..f22df4885fd 100644 --- a/src/interaction/position_on_plot.jl +++ b/src/interaction/position_on_plot.jl @@ -36,20 +36,17 @@ function ray_at_cursor(scene::Scene, cam::Camera3D) end end -function ray_at_cursor(scene::Scene, ::Camera2D) - @info "TODO verify" +function ray_at_cursor(scene::Scene, cam::Camera2D) rel_pos = mouseposition_px(scene) ./ widths(scene.px_area[]) origin = minimum(cam.area[]) .+ rel_pos .* widths(cam.area[]) return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) end function ray_at_cursor(scene::Scene, ::PixelCamera) - @info "TODO verify" return Ray(to_ndim(Point3f, mouseposition_px(scene), 10_000f0), Vec3f(0,0,-1)) end function ray_at_cursor(scene::Scene, ::RelativeCamera) - @info "TODO verify" origin = mouseposition_px(scene) ./ widths(scene.px_area[]) return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) end From ac64b9d04d62d9b0ceed68393b6cb695b2926b5c Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 3 Jul 2023 16:20:39 +0200 Subject: [PATCH 71/80] Update Project.toml --- CairoMakie/Project.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index 785703803ed..bd2a3657972 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -23,8 +23,8 @@ FFTW = "1" FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.1" -Makie = "=0.20.0" -SnoopPrecompile = "1.0" +Makie = "=0.19.6" +PrecompileTools = "1.0" julia = "1.3" [extras] @@ -33,3 +33,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Pkg"] + From d89aac0fd1ba61754ef3d02fd26596b5fd6edd37 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Jul 2023 18:24:22 +0200 Subject: [PATCH 72/80] Merge branch 'master' into ff/camera --- CairoMakie/src/screen.jl | 4 +- GLMakie/assets/shader/distance_shape.frag | 43 +- GLMakie/assets/shader/sprites.geom | 4 +- GLMakie/src/glshaders/particles.jl | 20 +- GLMakie/src/glwindow.jl | 33 +- NEWS.md | 31 +- Project.toml | 4 +- RPRMakie/Project.toml | 2 +- RPRMakie/examples/bars.jl | 4 +- RPRMakie/examples/lego.jl | 3 +- RPRMakie/examples/lines.jl | 23 +- RPRMakie/examples/material_x.jl | 25 ++ RPRMakie/examples/materials.jl | 2 +- RPRMakie/examples/opengl_interop.jl | 7 +- RPRMakie/examples/volume.jl | 14 + RPRMakie/src/RPRMakie.jl | 2 +- RPRMakie/src/lines.jl | 8 +- RPRMakie/src/meshes.jl | 10 +- RPRMakie/src/scene.jl | 1 + RPRMakie/src/volume.jl | 48 ++- ReferenceTests/Project.toml | 1 + ReferenceTests/src/ReferenceTests.jl | 1 + ReferenceTests/src/tests/examples2d.jl | 67 +++ ReferenceTests/src/tests/primitives.jl | 21 + ReferenceTests/src/tests/refimages.jl | 1 + WGLMakie/assets/particles.frag | 16 +- WGLMakie/assets/particles.vert | 2 +- WGLMakie/src/particles.jl | 1 + WGLMakie/test/runtests.jl | 3 +- docs/Project.toml | 1 + docs/documentation/events.md | 2 +- docs/examples/plotting_functions/scatter.md | 2 +- .../plotting_functions/tricontourf.md | 103 +++++ src/Makie.jl | 4 +- src/basic_recipes/tricontourf.jl | 62 ++- src/bezier.jl | 30 +- src/camera/camera3d.jl | 11 +- src/interaction/inspector.jl | 209 ++++----- src/interaction/position_on_plot.jl | 254 ----------- src/interaction/ray_casting.jl | 397 ++++++++++++++++++ src/interfaces.jl | 2 +- src/layouting/transformation.jl | 30 +- src/makielayout/blocks/slider.jl | 4 + src/utilities/texture_atlas.jl | 7 +- test/ray_casting.jl | 163 +++++++ test/runtests.jl | 1 + 46 files changed, 1195 insertions(+), 488 deletions(-) create mode 100644 RPRMakie/examples/material_x.jl create mode 100644 RPRMakie/examples/volume.jl delete mode 100644 src/interaction/position_on_plot.jl create mode 100644 src/interaction/ray_casting.jl create mode 100644 test/ray_casting.jl diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index 67117ddde9f..ed8cd516697 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -76,8 +76,8 @@ end to_cairo_antialias(aa::Int) = aa """ -* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). -* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html). +* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/stable/documentation/figure_size/). +* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/stable/documentation/figure_size/). * `antialias::Union{Symbol, Int} = :best`: antialias modus Cairo uses to draw. Applicable options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]`. * `visible::Bool`: if true, a browser/image viewer will open to display rendered output. """ diff --git a/GLMakie/assets/shader/distance_shape.frag b/GLMakie/assets/shader/distance_shape.frag index 3e16d4a4e70..1221978a1be 100644 --- a/GLMakie/assets/shader/distance_shape.frag +++ b/GLMakie/assets/shader/distance_shape.frag @@ -14,6 +14,7 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d #define ROUNDED_RECTANGLE 2 #define DISTANCEFIELD 3 #define TRIANGLE 4 +#define ELLIPSE 5 #define M_SQRT_2 1.4142135 @@ -37,6 +38,7 @@ flat in uvec2 f_id; flat in int f_primitive_index; in vec2 f_uv; // f_uv.{x,y} are in the interval [-a, 1+a] flat in vec4 f_uv_texture_bbox; +flat in vec2 f_sprite_scale; // These versions of aastep assume that `dist` is a signed distance function // which has been scaled to be in units of pixels. @@ -65,15 +67,46 @@ float triangle(vec2 P){ return -max(r1,r2); } float circle(vec2 uv){ - return 0.5-length(uv-vec2(0.5)); + return 0.5 - length(uv - vec2(0.5)); } float rectangle(vec2 uv){ - vec2 d = max(-uv, uv-vec2(1)); + vec2 s = f_sprite_scale / min(f_sprite_scale.x, f_sprite_scale.y); + vec2 d = s * max(-uv, uv-vec2(1)); return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))); } float rounded_rectangle(vec2 uv, vec2 tl, vec2 br){ - vec2 d = max(tl-uv, uv-br); - return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y)))-tl.x); + vec2 s = f_sprite_scale / min(f_sprite_scale.x, f_sprite_scale.y); + vec2 d = s * max(tl-uv, uv-br); + return -((length(max(vec2(0.0), d)) + min(0.0, max(d.x, d.y))) - s.x * tl.x); +} +// See https://iquilezles.org/articles/ellipsedist/ +float ellipse(vec2 uv, vec2 scale) +{ + // to central coordinates, use symmetry (quarter ellipse, 0 <= p <= wh) + vec2 wh = scale / min(scale.x, scale.y); + vec2 p = wh * abs(uv - vec2(0.5)); + wh = wh * 0.5; + + // initial value + vec2 q = wh * (p - wh); + vec2 cs = normalize( (q.x1.0) ? -d : d; } void fill(vec4 fillcolor, Nothing image, vec2 uv, float infill, inout vec4 color){ @@ -141,6 +174,8 @@ void main(){ signed_distance = rectangle(f_uv); else if(shape == TRIANGLE) signed_distance = triangle(f_uv); + else if(shape == ELLIPSE) + signed_distance = ellipse(f_uv, f_sprite_scale); // See notes in geometry shader where f_viewport_from_u_scale is computed. signed_distance *= f_viewport_from_u_scale; diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index 47da9a6657c..d34d131a187 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -60,6 +60,7 @@ flat out vec4 f_glow_color; flat out uvec2 f_id; out vec2 f_uv; flat out vec4 f_uv_texture_bbox; +flat out vec2 f_sprite_scale; uniform mat4 projection, view, model; @@ -88,6 +89,7 @@ void emit_vertex(vec4 vertex, vec2 uv) f_stroke_color = g_stroke_color[0]; f_glow_color = g_glow_color[0]; f_id = g_id[0]; + f_sprite_scale = g_offset_width[0].zw; EmitVertex(); } @@ -157,7 +159,7 @@ void main(void) // any calculation based on them will not be a distance function.) // * For sampled distance fields, we need to consistently choose the *x* // for the scaling in get_distancefield_scale(). - float sprite_from_u_scale = abs(o_w.z); + float sprite_from_u_scale = min(abs(o_w.z), abs(o_w.w)); f_viewport_from_u_scale = viewport_from_sprite_scale * sprite_from_u_scale; f_distancefield_scale = get_distancefield_scale(distancefield); diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 7b8bd663363..9bc4d3b4fae 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -33,6 +33,14 @@ struct PointSizeRender end (x::PointSizeRender)() = glPointSize(to_pointsize(x.size[])) +# For switching between ellipse method and faster circle method in shader +is_all_equal_scale(o::Observable) = is_all_equal_scale(o[]) +is_all_equal_scale(::Real) = true +is_all_equal_scale(::Vector{Real}) = true +is_all_equal_scale(v::Vec2f) = v[1] == v[2] # could use ≈ too +is_all_equal_scale(vs::Vector{Vec2f}) = all(is_all_equal_scale, vs) + + @nospecialize @@ -164,7 +172,7 @@ function draw_scatter(screen, (marker, position), data) rot = get!(data, :rotation, Vec4f(0, 0, 0, 1)) rot = vec2quaternion(rot) delete!(data, :rotation) - + @gen_defaults! data begin shape = Cint(0) position = position => GLBuffer @@ -174,6 +182,16 @@ function draw_scatter(screen, (marker, position), data) image = nothing => Texture end + data[:shape] = map( + convert(Observable{Int}, pop!(data, :shape)), data[:scale] + ) do shape, scale + if shape == 0 && !is_all_equal_scale(scale) + return Cint(5) # scaled CIRCLE -> ELLIPSE + else + return shape + end + end + @gen_defaults! data begin quad_offset = Vec2f(0) => GLBuffer intensity = nothing => GLBuffer diff --git a/GLMakie/src/glwindow.jl b/GLMakie/src/glwindow.jl index d643f609c5b..df0836b308b 100644 --- a/GLMakie/src/glwindow.jl +++ b/GLMakie/src/glwindow.jl @@ -56,6 +56,36 @@ function attach_colorbuffer!(fb::GLFramebuffer, key::Symbol, t::Texture{T, 2}) w return next_color_id end +function enum_to_error(s) + s == GL_FRAMEBUFFER_COMPLETE && return + s == GL_FRAMEBUFFER_UNDEFINED && + error("GL_FRAMEBUFFER_UNDEFINED: The specified framebuffer is the default read or draw framebuffer, but the default framebuffer does not exist.") + s == GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT && + error("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: At least one of the framebuffer attachment points is incomplete.") + s == GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT && + error("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: The framebuffer does not have at least one image attached to it.") + s == GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER && + error("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: The value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for any color attachment point(s) specified by GL_DRAW_BUFFERi.") + s == GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER && + error("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: GL_READ_BUFFER is not GL_NONE and the value of GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE is GL_NONE for the color attachment point specified by GL_READ_BUFFER.") + s == GL_FRAMEBUFFER_UNSUPPORTED && + error("GL_FRAMEBUFFER_UNSUPPORTED: The combination of internal formats of the attached images violates a driver implementation-dependent set of restrictions. Check your OpenGL driver!") + s == GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE && + error("GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: The value of GL_RENDERBUFFER_SAMPLES is not the same for all attached renderbuffers; +if the value of GL_TEXTURE_SAMPLES is not the same for all attached textures; or, if the attached images consist of a mix of renderbuffers and textures, + the value of GL_RENDERBUFFER_SAMPLES does not match the value of GL_TEXTURE_SAMPLES. + GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE is also returned if the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not consistent across all attached textures; + or, if the attached images include a mix of renderbuffers and textures, the value of GL_TEXTURE_FIXED_SAMPLE_LOCATIONS is not set to GL_TRUE for all attached textures.") + s == GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS && + error("GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: Any framebuffer attachment is layered, and any populated attachment is not layered, or if all populated color attachments are not from textures of the same target.") + return error("Unknown framebuffer completion error code: $s") +end + +function check_framebuffer() + status = glCheckFramebufferStatus(GL_FRAMEBUFFER) + return enum_to_error(status) +end + function GLFramebuffer(fb_size::NTuple{2, Int}) # Create framebuffer frambuffer_id = glGenFramebuffers() @@ -92,8 +122,7 @@ function GLFramebuffer(fb_size::NTuple{2, Int}) attach_framebuffer(depth_buffer, GL_DEPTH_ATTACHMENT) attach_framebuffer(depth_buffer, GL_STENCIL_ATTACHMENT) - status = glCheckFramebufferStatus(GL_FRAMEBUFFER) - @assert status == GL_FRAMEBUFFER_COMPLETE + check_framebuffer() fb_size_node = Observable(fb_size) diff --git a/NEWS.md b/NEWS.md index a3d81f4cad8..336e814a081 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,29 +2,32 @@ ## master -- Improvements to 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) -- Fixed some errors/problems with DataInspector [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) -- Scale errorbar whiskers and bracket correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012) -- Update bracket when the screen is resized or transformations change [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012) +- Improved 3D camera handling, hotkeys and functionality [#2746](https://github.com/MakieOrg/Makie.jl/pull/2746) +- Fixed DataInspector interaction with transformations [#3002](https://github.com/MakieOrg/Makie.jl/pull/3002) +- Fix incomplete stroke with some Bezier markers in CairoMakie and blurry strokes in GLMakie [#2961](https://github.com/MakieOrg/Makie.jl/pull/2961) +- Added the ability to use custom triangulations from DelaunayTriangulation.jl [#2896](https://github.com/MakieOrg/Makie.jl/pull/2896). +- Adjusted scaling of scatter/text stroke, glow and anti-aliasing width under non-uniform 2D scaling (Vec2f markersize/fontsize) in GLMakie [#2950](https://github.com/MakieOrg/Makie.jl/pull/2950). +- Scaled `errorbar` whiskers and `bracket` correctly with transformations [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). +- Updated `bracket` when the screen is resized or transformations change [#3012](https://github.com/MakieOrg/Makie.jl/pull/3012). ## v0.19.6 -- Fix broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). -- Fix WGLMakie JS popup [#2976](https://github.com/MakieOrg/Makie.jl/pull/2976). -- Fix legendelements when children have no elements [#2982](https://github.com/MakieOrg/Makie.jl/pull/2982). -- Bump compat for StatsBase to 0.34 [#2915](https://github.com/MakieOrg/Makie.jl/pull/2915). +- Fixed broken AA for lines with strongly varying linewidth [#2953](https://github.com/MakieOrg/Makie.jl/pull/2953). +- Fixed WGLMakie JS popup [#2976](https://github.com/MakieOrg/Makie.jl/pull/2976). +- Fixed `legendelements` when children have no elements [#2982](https://github.com/MakieOrg/Makie.jl/pull/2982). +- Bumped compat for StatsBase to 0.34 [#2915](https://github.com/MakieOrg/Makie.jl/pull/2915). - Improved thread safety [#2840](https://github.com/MakieOrg/Makie.jl/pull/2840). ## v0.19.5 -- Add `loop` option for GIF outputs when recording videos with `record` [#2891](https://github.com/MakieOrg/Makie.jl/pull/2891). -- More fixes for line rendering in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). +- Added `loop` option for GIF outputs when recording videos with `record` [#2891](https://github.com/MakieOrg/Makie.jl/pull/2891). +- Fixed line rendering issues in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). - Fixed incorrect line alpha in dense lines in GLMakie [#2843](https://github.com/MakieOrg/Makie.jl/pull/2843). -- Change `scene.clear` to an observable and make changes in `Scene` Observables trigger renders in GLMakie [#2929](https://github.com/MakieOrg/Makie.jl/pull/2929). +- Changed `scene.clear` to an observable and made changes in `Scene` Observables trigger renders in GLMakie [#2929](https://github.com/MakieOrg/Makie.jl/pull/2929). - Added contour labels [#2496](https://github.com/MakieOrg/Makie.jl/pull/2496). -- Allow rich text to be used in Legends [#2902](https://github.com/MakieOrg/Makie.jl/pull/2902). -- More support for zero length Geometries [#2917](https://github.com/MakieOrg/Makie.jl/pull/2917). -- Make CairoMakie drawing for polygons with holes order independent [#2918](https://github.com/MakieOrg/Makie.jl/pull/2918). +- Allowed rich text to be used in Legends [#2902](https://github.com/MakieOrg/Makie.jl/pull/2902). +- Added more support for zero length Geometries [#2917](https://github.com/MakieOrg/Makie.jl/pull/2917). +- Made CairoMakie drawing for polygons with holes order independent [#2918](https://github.com/MakieOrg/Makie.jl/pull/2918). - Fixes for `Makie.inline!()`, allowing now for `Makie.inline!(automatic)` (default), which is better at automatically opening a window/ inlining a plot into plotpane when needed [#2919](https://github.com/MakieOrg/Makie.jl/pull/2919) [#2937](https://github.com/MakieOrg/Makie.jl/pull/2937). - Block/Axis doc improvements [#2940](https://github.com/MakieOrg/Makie.jl/pull/2940) [#2932](https://github.com/MakieOrg/Makie.jl/pull/2932) [#2894](https://github.com/MakieOrg/Makie.jl/pull/2894). diff --git a/Project.toml b/Project.toml index 89a3bb6f529..0153527c67d 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" +DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" @@ -34,7 +35,6 @@ MakieCore = "20f20a25-4f0e-4fdf-b5d1-57303727442b" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Match = "7eb4fadd-790c-5f42-8a69-bfa0b872bfbf" MathTeXEngine = "0a4f8689-d25c-4efe-a92b-7142dfc1aa53" -MiniQhull = "978d7f02-9e05-4691-894f-ae31a51d76ca" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Packing = "19eb6ba3-879d-56ad-ad62-d5c202156566" @@ -64,6 +64,7 @@ ColorSchemes = "3.5" ColorTypes = "0.8, 0.9, 0.10, 0.11" Colors = "0.9, 0.10, 0.11, 0.12" Contour = "0.5, 0.6" +DelaunayTriangulation = "0.6.2, 0.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" FFMPEG = "0.2, 0.3, 0.4" @@ -83,7 +84,6 @@ MacroTools = "0.5" MakieCore = "=0.6.3" Match = "1.1" MathTeXEngine = "0.5" -MiniQhull = "0.4" Observables = "0.5.3" OffsetArrays = "1" Packing = "0.5" diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 46151afff69..e0aea02aeb2 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -18,7 +18,7 @@ Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.1" Makie = "=0.19.6" -RadeonProRender = "0.2.15" +RadeonProRender = "0.3.0" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/RPRMakie/examples/bars.jl b/RPRMakie/examples/bars.jl index 61514ecc546..a6be9abe0e6 100644 --- a/RPRMakie/examples/bars.jl +++ b/RPRMakie/examples/bars.jl @@ -2,7 +2,7 @@ using GeometryBasics, RPRMakie using Colors, FileIO, ImageShow using Colors: N0f8 -RPRMakie.activate!(plugin=RPR.Northstar) +RPRMakie.activate!(plugin=RPR.Northstar, resource=RPR.GPU0) fig = Figure(; resolution=(800, 600), fontsize=26) radiance = 10000 lights = [EnvironmentLight(0.5, load(RPR.assetpath("studio026.exr"))), @@ -25,4 +25,4 @@ cam.eyeposition[] = Float32[5, 22, 12] cam.lookat[] = Float32[5, 5, -0.5] cam.upvector[] = Float32[0.0, 0.0, 1.0] cam.fov[] = 14.0 -ax.scene +@time display(ax.scene) diff --git a/RPRMakie/examples/lego.jl b/RPRMakie/examples/lego.jl index b5655a0d681..cb364038271 100644 --- a/RPRMakie/examples/lego.jl +++ b/RPRMakie/examples/lego.jl @@ -88,8 +88,7 @@ nsteps = length(angles); #Number of animation steps translations = LinRange(0, total_translation, nsteps) s -Makie.record(s, "lego_walk.mp4", zip(translations, angles)) do (translation, angle) - +@time Makie.record(s, "lego_walk.mp4", zip(translations, angles)) do (translation, angle) # Rotate right arm + hand for name in ["arm_left", "arm_right", "leg_left", "leg_right"] rotate!(figure[name], rotation_axes[name], angle) diff --git a/RPRMakie/examples/lines.jl b/RPRMakie/examples/lines.jl index 4b6a103fd44..b3f2624e7f9 100644 --- a/RPRMakie/examples/lines.jl +++ b/RPRMakie/examples/lines.jl @@ -2,19 +2,32 @@ using GeometryBasics, RPRMakie using Colors, FileIO using Colors: N0f8 +function box!(ax, size) + orig = Vec3f(-2, -2, 0) + mesh!(ax, Rect3f(orig, Vec3f(size, size, 0.1)); color=:white, + material=(reflection_color=Vec4f(1), reflection_weight=10f0)) + mesh!(ax, Rect3f(orig, Vec3f(0.1, size, size)); color=:white) + mesh!(ax, Rect3f(orig, Vec3f(size, 0.1, size)); color=:white) + return +end + begin - RPRMakie.activate!(plugin=RPR.Tahoe) fig = Figure(; resolution=(1000, 1000)) - ax = LScene(fig[1, 1]) + 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) points = Point3f[] for i in 4:10 n = i + 1 y = LinRange(0, i, n) y2 = (y ./ 2) .- 2 xyz = Point3f.((i - 5) ./ 2, y2, sin.(y) .+ 1) - lines!(ax, xyz; linewidth=5, color=:red) + lp = lines!(ax, xyz; linewidth=10, color=:white) append!(points, xyz) end - meshscatter!(ax, points, color=:green) - ax.scene + mat = (; emission_color=:red, emission_weight=Vec3f(5.0f0)) + meshscatter!(ax, points; material=mat) + box!(ax, 5) + RPRMakie.activate!(plugin = RPR.Northstar, iterations = 500, resource = RPR.GPU0) + ax.scene |> display end diff --git a/RPRMakie/examples/material_x.jl b/RPRMakie/examples/material_x.jl new file mode 100644 index 00000000000..415f87c7e7a --- /dev/null +++ b/RPRMakie/examples/material_x.jl @@ -0,0 +1,25 @@ +# download material from: https://matlib.gpuopen.com/main/materials/all?material=8686536a-8041-445b-97f1-f249f4c3b0af +using RPRMakie, ImageShow + +material = "Pinwheel_Pattern_Marble_Tiles_4k_16b" # folder you downloaded & extracted + +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)) + 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 + marble_tiles = RPR.Matx(matsys, joinpath(material, "Pinwheel_Pattern_Marble_Tiles.mtlx")) + + mesh!(ax, load(Makie.assetpath("matball_floor.obj")); color=:white) + matball!(ax, marble_tiles; color=nothing) + cam = cameracontrols(ax.scene) + cam.eyeposition[] = Vec3f(0.0, -2, 1) + cam.lookat[] = Vec3f(0) + cam.upvector[] = Float32[0.0, -0.01, 1.0] + update_cam!(ax.scene, cam) + # TODO, material doesn't show up? + colorbuffer(screen) +end diff --git a/RPRMakie/examples/materials.jl b/RPRMakie/examples/materials.jl index 3e5e7639dda..ccdcb2209d7 100644 --- a/RPRMakie/examples/materials.jl +++ b/RPRMakie/examples/materials.jl @@ -8,7 +8,7 @@ img = begin PointLight(Vec3f(10), RGBf(radiance, radiance, radiance * 1.1))] fig = Figure(; resolution=(1500, 700)) ax = LScene(fig[1, 1]; show_axis=false, scenekw=(lights=lights,)) - screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar) + screen = RPRMakie.Screen(ax.scene; plugin=RPR.Northstar, iterations=1000) matsys = screen.matsys emissive = RPR.EmissiveMaterial(matsys) diff --git a/RPRMakie/examples/opengl_interop.jl b/RPRMakie/examples/opengl_interop.jl index e43fe2df392..04e7c2ccd10 100644 --- a/RPRMakie/examples/opengl_interop.jl +++ b/RPRMakie/examples/opengl_interop.jl @@ -77,9 +77,10 @@ cam.lookat[] = Vec3f(0, 0, -1) cam.upvector[] = Vec3f(0, 0, 1) cam.fov[] = 30 -display(fig) - -context, task = RPRMakie.replace_scene_rpr!(ax.scene, screen; refresh=refresh) +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); # Change light parameters interactively begin diff --git a/RPRMakie/examples/volume.jl b/RPRMakie/examples/volume.jl new file mode 100644 index 00000000000..7c4a86ddb63 --- /dev/null +++ b/RPRMakie/examples/volume.jl @@ -0,0 +1,14 @@ +using RPRMakie +using Makie, NIfTI, FileIO +using GLMakie +r = LinRange(-1, 1, 100) +cube = [(x .^ 2 + y .^ 2 + z .^ 2) for x = r, y = r, z = r] + +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)) +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 57e415bfae3..74bf5fc7d4d 100644 --- a/RPRMakie/src/RPRMakie.jl +++ b/RPRMakie/src/RPRMakie.jl @@ -30,7 +30,7 @@ function ScreenConfig(iterations::Int, max_recursion::Int, render_resource, rend iterations, max_recursion, Int32(render_resource isa Makie.Automatic ? RPR.RPR_CREATION_FLAGS_ENABLE_GPU0 : render_resource), - render_plugin isa Makie.Automatic ? RPR.Tahoe : render_plugin + render_plugin isa Makie.Automatic ? RPR.Northstar : render_plugin ) end diff --git a/RPRMakie/src/lines.jl b/RPRMakie/src/lines.jl index 467ce80572e..a6c91665dd4 100644 --- a/RPRMakie/src/lines.jl +++ b/RPRMakie/src/lines.jl @@ -24,8 +24,8 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Lines) indices = line2segments(points) radius = [plot.linewidth[] / 1000] curve = RPR.Curve(context, points, indices, radius, [Vec2f(0.0)], [length(indices) ÷ 4]) - material = RPR.MaterialNode(matsys, RPR.RPR_MATERIAL_NODE_DIFFUSE) - set!(material, RPR.RPR_MATERIAL_INPUT_COLOR, to_color(plot.color[])) + material = extract_material(matsys, plot) + material.color = to_color(plot.color[]) set!(curve, material) return curve end @@ -57,7 +57,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.LineSegments) curve = RPR.Curve(context, points, indices, radius, Vec2f.(0.0, LinRange(0, 1, nsegments)), fill(1, nsegments)) - material = RPR.DiffuseMaterial(matsys) + material = extract_material(matsys, plot) color = to_color(plot.color[]) function set_color!(colorvec) @@ -78,6 +78,6 @@ function to_rpr_object(context, matsys, scene, plot::Makie.LineSegments) else material.color = to_color(color) end - set!(curve, material.node) + set!(curve, material) return curve end diff --git a/RPRMakie/src/meshes.jl b/RPRMakie/src/meshes.jl index 4e31520ed6f..c94f3611a97 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -68,15 +68,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) instances = [marker] n_instances = length(positions) RPR.rprShapeSetObjectID(marker, 0) - material = if haskey(plot, :material) - if plot.material isa Attributes - RPR.Material(matsys, Dict(map(((k,v),)-> k => to_value(v), plot.material))) - else - plot.material[] - end - else - RPR.DiffuseMaterial(matsys) - end + material = extract_material(matsys, plot) set!(marker, material) for i in 1:(n_instances-1) inst = RPR.Shape(context, marker) diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index 6a5902bfea8..07e2d491961 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -5,6 +5,7 @@ function update_rpr_camera!(oldvals, camera, cam_controls, cam) c = cam_controls l, u, p, fov = c.lookat[], c.upvector[], c.eyeposition[], c.fov[] far, near, res = c.far[], c.near[], cam.resolution[] + fov = 45f0 # The current camera ignores fov updates new_vals = (; l, u, p, fov, far, near, res) new_vals == oldvals && return oldvals wd = norm(l - p) diff --git a/RPRMakie/src/volume.jl b/RPRMakie/src/volume.jl index d3766f82a09..197c56ed1a0 100644 --- a/RPRMakie/src/volume.jl +++ b/RPRMakie/src/volume.jl @@ -1,29 +1,37 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Volume) volume = plot.volume[] - xyz = plot.x[], plot.y[], plot.z[] - mini_maxi = extrema.(xyz) - mini = first.(mini_maxi) - maxi = last.(mini_maxi) + cube = RPR.VolumeCube(context) - vol_cube = RadeonProRender.Shape(context, Rect3f(mini, maxi .- mini)) - color_lookup = to_colormap(plot.colormap[]) - density_lookup = [Vec3f(plot.absorption[])] + function update_cube(m, xyz...) + mi = minimum.(xyz) + maxi = maximum.(xyz) + w = maxi .- mi + m2 = Mat4f(w[1], 0, 0, 0, 0, w[2], 0, 0, 0, 0, w[3], 0, mi[1], mi[2], mi[3], 1) + mat = convert(Mat4f, m) * m2 + transform!(cube, mat) + return + end + onany(update_cube, plot.model, plot.x, plot.y, plot.z) + update_cube(plot.model[], plot.x[], plot.y[], plot.z[]) mini, maxi = extrema(volume) - grid = RPR.VoxelGrid(context, (volume .- mini) ./ (maxi - mini)) - rpr_vol = RPR.HeteroVolume(context) + vol_normed = (volume .- mini) ./ (maxi - mini) + grid = RPR.VoxelGrid(context, vol_normed) + gridsampler = RPR.GridSamplerMaterial(matsys) + gridsampler.data = grid - RPR.set_albedo_grid!(rpr_vol, grid) - RPR.set_albedo_lookup!(rpr_vol, color_lookup) + color_sampler = RPR.ImageTextureMaterial(matsys) + color_sampler.data = RPR.Image(context, reverse(to_colormap(plot.colormap[]))) + gridsampler2 = RPR.GridSamplerMaterial(matsys) + color_sampler.uv = gridsampler - RPR.set_density_grid!(rpr_vol, grid) - RPR.set_density_lookup!(rpr_vol, density_lookup) + volmat = RPR.VolumeMaterial(matsys) + on(plot.absorption; update=true) do absorption + return volmat.density = Vec4f(absorption, 0.0, 0.0, 0.0) + end + volmat.densitygrid = gridsampler + volmat.color = color_sampler + set!(cube, volmat) - mat = RPR.TransparentMaterial(matsys) - mat.color = Vec4f(1) - - set!(vol_cube, rpr_vol) - set!(vol_cube, mat) - - return [vol_cube, rpr_vol] + return [cube] end diff --git a/ReferenceTests/Project.toml b/ReferenceTests/Project.toml index 39ba457065e..7ba378ebc8e 100644 --- a/ReferenceTests/Project.toml +++ b/ReferenceTests/Project.toml @@ -7,6 +7,7 @@ version = "0.1.0" CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" diff --git a/ReferenceTests/src/ReferenceTests.jl b/ReferenceTests/src/ReferenceTests.jl index fc7cadedc13..fe844401fa1 100644 --- a/ReferenceTests/src/ReferenceTests.jl +++ b/ReferenceTests/src/ReferenceTests.jl @@ -27,6 +27,7 @@ using Colors using LaTeXStrings using GeometryBasics using DelimitedFiles +using DelaunayTriangulation basedir(files...) = normpath(joinpath(@__DIR__, "..", files...)) loadasset(files...) = FileIO.load(assetpath(files...)) diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 902329173bf..836977ecbde 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -685,6 +685,73 @@ end f end +@reference_test "tricontourf with boundary nodes" begin + n = 20 + angles = range(0, 2pi, length = n+1)[1:end-1] + x = [cos.(angles); 2 .* cos.(angles .+ pi/n)] + y = [sin.(angles); 2 .* sin.(angles .+ pi/n)] + z = (x .- 0.5).^2 + (y .- 0.5).^2 .+ 0.5.* RNG.randn.() + + inner = [n:-1:1; n] # clockwise inner + outer = [(n+1):(2n); n+1] # counter-clockwise outer + boundary_nodes = [[outer], [inner]] + tri = DelaunayTriangulation.triangulate([x'; y'], boundary_nodes = boundary_nodes) + f, ax, _ = tricontourf(tri, z) + scatter!(x, y, color = z, strokewidth = 1, strokecolor = :black) + f +end + +@reference_test "tricontourf with boundary nodes and edges" begin + curve_1 = [ + [(0.0, 0.0), (5.0, 0.0), (10.0, 0.0), (15.0, 0.0), (20.0, 0.0), (25.0, 0.0)], + [(25.0, 0.0), (25.0, 5.0), (25.0, 10.0), (25.0, 15.0), (25.0, 20.0), (25.0, 25.0)], + [(25.0, 25.0), (20.0, 25.0), (15.0, 25.0), (10.0, 25.0), (5.0, 25.0), (0.0, 25.0)], + [(0.0, 25.0), (0.0, 20.0), (0.0, 15.0), (0.0, 10.0), (0.0, 5.0), (0.0, 0.0)] + ] + curve_2 = [ + [(4.0, 6.0), (4.0, 14.0), (4.0, 20.0), (18.0, 20.0), (20.0, 20.0)], + [(20.0, 20.0), (20.0, 16.0), (20.0, 12.0), (20.0, 8.0), (20.0, 4.0)], + [(20.0, 4.0), (16.0, 4.0), (12.0, 4.0), (8.0, 4.0), (4.0, 4.0), (4.0, 6.0)] + ] + curve_3 = [ + [(12.906, 10.912), (16.0, 12.0), (16.16, 14.46), (16.29, 17.06), + (13.13, 16.86), (8.92, 16.4), (8.8, 10.9), (12.906, 10.912)] + ] + curves = [curve_1, curve_2, curve_3] + points = [ + (3.0, 23.0), (9.0, 24.0), (9.2, 22.0), (14.8, 22.8), (16.0, 22.0), + (23.0, 23.0), (22.6, 19.0), (23.8, 17.8), (22.0, 14.0), (22.0, 11.0), + (24.0, 6.0), (23.0, 2.0), (19.0, 1.0), (16.0, 3.0), (10.0, 1.0), (11.0, 3.0), + (6.0, 2.0), (6.2, 3.0), (2.0, 3.0), (2.6, 6.2), (2.0, 8.0), (2.0, 11.0), + (5.0, 12.0), (2.0, 17.0), (3.0, 19.0), (6.0, 18.0), (6.5, 14.5), + (13.0, 19.0), (13.0, 12.0), (16.0, 8.0), (9.8, 8.0), (7.5, 6.0), + (12.0, 13.0), (19.0, 15.0) + ] + boundary_nodes, points = convert_boundary_points_to_indices(curves; existing_points=points) + edges = Set(((1, 19), (19, 12), (46, 4), (45, 12))) + + tri = triangulate(points; boundary_nodes = boundary_nodes, edges = edges, check_arguments = false) + z = [(x - 1) * (y + 1) for (x, y) in each_point(tri)] + f, ax, _ = tricontourf(tri, z, levels = 30) + f +end + +@reference_test "tricontourf with provided triangulation" begin + θ = [LinRange(0, 2π * (1 - 1/19), 20); 0] + xy = Vector{Vector{Vector{NTuple{2,Float64}}}}() + cx = [0.0, 3.0] + for i in 1:2 + push!(xy, [[(cx[i] + cos(θ), sin(θ)) for θ in θ]]) + push!(xy, [[(cx[i] + 0.5cos(θ), 0.5sin(θ)) for θ in reverse(θ)]]) + end + boundary_nodes, points = convert_boundary_points_to_indices(xy) + tri = triangulate(points; boundary_nodes=boundary_nodes, check_arguments=false) + z = [(x - 3/2)^2 + y^2 for (x, y) in each_point(tri)] + + f, ax, tr = tricontourf(tri, z, colormap = :matter) + f +end + @reference_test "contour labels 2D" begin paraboloid = (x, y) -> 10(x^2 + y^2) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 211f350862b..4327bf8ab7e 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -253,6 +253,27 @@ end f end +@reference_test "BezierPath marker stroke" begin + f = Figure(resolution = (800, 800)) + ax = Axis(f[1, 1]) + + # Same as above + markers = [ + :rect, :circle, :cross, :x, :utriangle, :rtriangle, :dtriangle, :ltriangle, :pentagon, + :hexagon, :octagon, :star4, :star5, :star6, :star8, :vline, :hline, 'x', 'X' + ] + + for (i, marker) in enumerate(markers) + scatter!( + Point2f.(1:5, i), marker = marker, + markersize = range(10, 30, length = 5), color = :orange, + strokewidth = 2, strokecolor = :black + ) + end + + f +end + @reference_test "complex_bezier_markers" begin f = Figure(resolution = (800, 800)) diff --git a/ReferenceTests/src/tests/refimages.jl b/ReferenceTests/src/tests/refimages.jl index 74faac9c3a3..73ce26f4a42 100644 --- a/ReferenceTests/src/tests/refimages.jl +++ b/ReferenceTests/src/tests/refimages.jl @@ -10,6 +10,7 @@ using ReferenceTests.LaTeXStrings using ReferenceTests.DelimitedFiles using ReferenceTests.Test using ReferenceTests.Colors: RGB, N0f8 +using ReferenceTests.DelaunayTriangulation using Makie: Record, volume @testset "primitives" begin diff --git a/WGLMakie/assets/particles.frag b/WGLMakie/assets/particles.frag index 69c7e040868..5615fb356da 100644 --- a/WGLMakie/assets/particles.frag +++ b/WGLMakie/assets/particles.frag @@ -32,11 +32,17 @@ vec4 pack_int(uint id, uint index) { } void main() { - vec3 L = normalize(frag_lightdir); - vec3 N = normalize(frag_normal); - vec3 light1 = blinnphong(N, frag_position, L, frag_color.rgb); - vec3 light2 = blinnphong(N, frag_position, -L, frag_color.rgb); - vec3 color = get_ambient() * frag_color.rgb + light1 + get_backlight() * light2; + vec3 L, N, light1, light2, color; + if (get_shading()) { + L = normalize(frag_lightdir); + 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; + } else { + color = frag_color.rgb; + } + if (picking) { if (frag_color.a > 0.1) { diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert index fefb693af48..f2785d2aed4 100644 --- a/WGLMakie/assets/particles.vert +++ b/WGLMakie/assets/particles.vert @@ -30,7 +30,7 @@ flat out uint frag_instance_id; 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() * get_position(); + 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); diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index d12ad0c9fce..702a16d3df3 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -92,6 +92,7 @@ function create_shader(scene::Scene, plot::MeshScatter) # 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 return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), instance, VertexArray(; per_instance...), uniform_dict) diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 667b8d19bc1..20b317aecaa 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -54,7 +54,8 @@ excludes = Set([ "scatter with stroke", "scatter with glow", "lines and linestyles", - "Textured meshscatter" # not yet implemented + "Textured meshscatter", # not yet implemented + "BezierPath marker stroke", # not yet implemented ]) Makie.inline!(Makie.automatic) diff --git a/docs/Project.toml b/docs/Project.toml index e7d571969bd..d05280df457 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -6,6 +6,7 @@ Chain = "8be319e6-bccf-4806-a6f7-6fae938471bc" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +DelaunayTriangulation = "927a84f5-c5f4-47a5-9785-b46e178433df" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/documentation/events.md b/docs/documentation/events.md index 077cf680bd0..99ff8e4fc22 100644 --- a/docs/documentation/events.md +++ b/docs/documentation/events.md @@ -225,7 +225,7 @@ scene ## Point Picking -Makie provides a function `pick(x[, position = events(x).mouseposition[]])` to get the plot displayed at a certain position with `x` being a `Figure`, `Axis`, `FigureAxisPlot` or `Scene`. This is currently a **GLMakie** only feature. The function returns a primitive plot and an index. The primitive plots are the base plots drawable in backends: +Makie provides a function `pick(x[, position = events(x).mouseposition[]])` to get the plot displayed at a certain position with `x` being a `Figure`, `Axis`, `FigureAxisPlot` or `Scene`. The function returns a primitive plot and an index. The primitive plots are the base plots drawable in backends: - scatter - text diff --git a/docs/examples/plotting_functions/scatter.md b/docs/examples/plotting_functions/scatter.md index 31fc226540f..947e1c06c81 100644 --- a/docs/examples/plotting_functions/scatter.md +++ b/docs/examples/plotting_functions/scatter.md @@ -243,7 +243,7 @@ scatter(1:5, #### Construction from svg path strings You can also create a bezier path from an [svg path specification string](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands). -You can automatically resize the path and flip the y-axis (svgs usually have a coordinate system where y increases downwards) with the keywords `fit` and `yflip`. +You can automatically resize the path and flip the y- and x-axes (svgs usually have a coordinate system where y increases downwards) with the keywords `fit`, `yflip`, and `xflip`. By default, the bounding box for the fitted path is a square of width 1 centered on zero. You can pass a different bounding `Rect` with the `bbox` keyword argument. By default, the aspect of the path is left intact, and if it's not matching the new bounding box, the path is centered so it fits inside. diff --git a/docs/examples/plotting_functions/tricontourf.md b/docs/examples/plotting_functions/tricontourf.md index 3adfadd50df..abc18e24168 100644 --- a/docs/examples/plotting_functions/tricontourf.md +++ b/docs/examples/plotting_functions/tricontourf.md @@ -44,6 +44,8 @@ f #### Triangulation modes +Manual triangulations can be passed as a 3xN matrix of integers, where each column of three integers specifies the indices of the corners of one triangle in the vector of points. + \begin{examplefigure}{svg = true} ```julia using CairoMakie @@ -74,6 +76,107 @@ f ``` \end{examplefigure} +By default, `tricontourf` performs unconstrained triangulations. +Greater control over the triangulation, such as allowing for enforced boundaries, can be achieved by using [DelaunayTriangulation.jl](https://github.com/DanielVandH/DelaunayTriangulation.jl) and passing the resulting triangulation as the first argument of `tricontourf`. +For example, the above annulus can also be plotted as follows: + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +using DelaunayTriangulation +CairoMakie.activate!() # hide +using Random + +Random.seed!(123) + +n = 20 +angles = range(0, 2pi, length = n+1)[1:end-1] +x = [cos.(angles); 2 .* cos.(angles .+ pi/n)] +y = [sin.(angles); 2 .* sin.(angles .+ pi/n)] +z = (x .- 0.5).^2 + (y .- 0.5).^2 .+ 0.5.*randn.() + +inner = [n:-1:1; n] # clockwise inner +outer = [(n+1):(2n); n+1] # counter-clockwise outer +boundary_nodes = [[outer], [inner]] +points = [x'; y'] +tri = triangulate(points; boundary_nodes = boundary_nodes) +f, ax, _ = tricontourf(tri, z; + axis = (; aspect = 1, title = "Constrained triangulation\nvia DelaunayTriangulation.jl")) +scatter!(x, y, color = z, strokewidth = 1, strokecolor = :black) +f +``` +\end{examplefigure} + +Boundary nodes make it possible to support more complicated regions, possibly with holes, than is possible by only providing points themselves. + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +using DelaunayTriangulation +CairoMakie.activate!() # hide + +## Start by defining the boundaries, and then convert to the appropriate interface +curve_1 = [ + [(0.0, 0.0), (5.0, 0.0), (10.0, 0.0), (15.0, 0.0), (20.0, 0.0), (25.0, 0.0)], + [(25.0, 0.0), (25.0, 5.0), (25.0, 10.0), (25.0, 15.0), (25.0, 20.0), (25.0, 25.0)], + [(25.0, 25.0), (20.0, 25.0), (15.0, 25.0), (10.0, 25.0), (5.0, 25.0), (0.0, 25.0)], + [(0.0, 25.0), (0.0, 20.0), (0.0, 15.0), (0.0, 10.0), (0.0, 5.0), (0.0, 0.0)] +] # outer-most boundary: counter-clockwise +curve_2 = [ + [(4.0, 6.0), (4.0, 14.0), (4.0, 20.0), (18.0, 20.0), (20.0, 20.0)], + [(20.0, 20.0), (20.0, 16.0), (20.0, 12.0), (20.0, 8.0), (20.0, 4.0)], + [(20.0, 4.0), (16.0, 4.0), (12.0, 4.0), (8.0, 4.0), (4.0, 4.0), (4.0, 6.0)] +] # inner boundary: clockwise +curve_3 = [ + [(12.906, 10.912), (16.0, 12.0), (16.16, 14.46), (16.29, 17.06), + (13.13, 16.86), (8.92, 16.4), (8.8, 10.9), (12.906, 10.912)] +] # this is inside curve_2, so it's counter-clockwise +curves = [curve_1, curve_2, curve_3] +points = [ + (3.0, 23.0), (9.0, 24.0), (9.2, 22.0), (14.8, 22.8), (16.0, 22.0), + (23.0, 23.0), (22.6, 19.0), (23.8, 17.8), (22.0, 14.0), (22.0, 11.0), + (24.0, 6.0), (23.0, 2.0), (19.0, 1.0), (16.0, 3.0), (10.0, 1.0), (11.0, 3.0), + (6.0, 2.0), (6.2, 3.0), (2.0, 3.0), (2.6, 6.2), (2.0, 8.0), (2.0, 11.0), + (5.0, 12.0), (2.0, 17.0), (3.0, 19.0), (6.0, 18.0), (6.5, 14.5), + (13.0, 19.0), (13.0, 12.0), (16.0, 8.0), (9.8, 8.0), (7.5, 6.0), + (12.0, 13.0), (19.0, 15.0) +] +boundary_nodes, points = convert_boundary_points_to_indices(curves; existing_points=points) +edges = Set(((1, 19), (19, 12), (46, 4), (45, 12))) + +## Extract the x, y +tri = triangulate(points; boundary_nodes = boundary_nodes, edges = edges, check_arguments = false) +z = [(x - 1) * (y + 1) for (x, y) in each_point(tri)] # note that each_point preserves the index order +f, ax, _ = tricontourf(tri, z, levels = 30; axis = (; aspect = 1)) +f +``` +\end{examplefigure} + +\begin{examplefigure}{svg = true} +```julia +using CairoMakie +using DelaunayTriangulation +CairoMakie.activate!() # hide + +using Random +Random.seed!(1234) + +θ = [LinRange(0, 2π * (1 - 1/19), 20); 0] +xy = Vector{Vector{Vector{NTuple{2,Float64}}}}() +cx = [0.0, 3.0] +for i in 1:2 + push!(xy, [[(cx[i] + cos(θ), sin(θ)) for θ in θ]]) + push!(xy, [[(cx[i] + 0.5cos(θ), 0.5sin(θ)) for θ in reverse(θ)]]) +end +boundary_nodes, points = convert_boundary_points_to_indices(xy) +tri = triangulate(points; boundary_nodes=boundary_nodes, check_arguments=false) +z = [(x - 3/2)^2 + y^2 for (x, y) in each_point(tri)] # note that each_point preserves the index order + +f, ax, tr = tricontourf(tri, z, colormap = :matter) +f +``` +\end{examplefigure} + #### Relative mode Sometimes it's beneficial to drop one part of the range of values, usually towards the outer boundary. diff --git a/src/Makie.jl b/src/Makie.jl index 0c819c9ba18..cb3e0f56d59 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -49,7 +49,7 @@ import ImageIO import FileIO import SparseArrays import TriplotBase -import MiniQhull +import DelaunayTriangulation as DelTri import Setfield import REPL import MacroTools @@ -176,7 +176,7 @@ include("stats/hexbin.jl") # Interactiveness include("interaction/events.jl") include("interaction/interactive_api.jl") -include("interaction/position_on_plot.jl") +include("interaction/ray_casting.jl") include("interaction/inspector.jl") # documentation and help functions diff --git a/src/basic_recipes/tricontourf.jl b/src/basic_recipes/tricontourf.jl index 9b98a876cbb..b4f3808a6b9 100644 --- a/src/basic_recipes/tricontourf.jl +++ b/src/basic_recipes/tricontourf.jl @@ -1,10 +1,12 @@ struct DelaunayTriangulation end """ + tricontourf(triangles::Triangulation, zs; kwargs...) tricontourf(xs, ys, zs; kwargs...) -Plots a filled tricontour of the height information in `zs` at horizontal positions `xs` -and vertical positions `ys`. +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. ## Attributes @@ -14,7 +16,7 @@ and vertical positions `ys`. - `mode = :normal` sets the way in which a vector of levels is interpreted, if it's set to `:relative`, each number is interpreted as a fraction between the minimum and maximum values of `zs`. For example, `levels = 0.1:0.1:1.0` would exclude the lower 10% of data. - `extendlow = nothing`. This sets the color of an optional additional band from `minimum(zs)` to the lowest value in `levels`. If it's `:auto`, the lower end of the colormap is picked and the remaining colors are shifted accordingly. If it's any color representation, this color is used. If it's `nothing`, no band is added. - `extendhigh = nothing`. This sets the color of an optional additional band from the highest value of `levels` to `maximum(zs)`. If it's `:auto`, the high end of the colormap is picked and the remaining colors are shifted accordingly. If it's any color representation, this color is used. If it's `nothing`, no band is added. -- `triangulation = DelaunayTriangulation()`. The mode with which the points in `xs` and `ys` are triangulated. Passing `DelaunayTriangulation()` performs a delaunay triangulation. You can also pass a preexisting triangulation as an `AbstractMatrix{<:Int}` with size (3, n), where each column specifies the vertex indices of one triangle. +- `triangulation = DelaunayTriangulation()`. The mode with which the points in `xs` and `ys` are triangulated. Passing `DelaunayTriangulation()` performs a Delaunay triangulation. You can also pass a preexisting triangulation as an `AbstractMatrix{<:Int}` with size (3, n), where each column specifies the vertex indices of one triangle, or as a `Triangulation` from DelaunayTriangulation.jl. ### Generic @@ -41,12 +43,33 @@ $(ATTRIBUTES) nan_color = :transparent, inspectable = theme(scene, :inspectable), transparency = false, - triangulation = DelaunayTriangulation() + triangulation = DelaunayTriangulation(), + edges = nothing, ) end -function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}) - map(x -> elconvert(Float32, x), (x, y, z)) +function Makie.used_attributes(::Type{<:Tricontourf}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}) + return (:triangulation,) +end + +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' + end + tri = DelTri.Triangulation(points) + triangles = DelTri.get_triangles(tri) + for τ in DelTri.each_solid_triangle(triangulation) + DelTri.add_triangle!(triangles, τ) + end + end + return (tri, z) end function compute_contourf_colormap(levels, cmap, elow, ehigh) @@ -90,8 +113,8 @@ function compute_highcolor(eh, cmap) end end -function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractVector{<:Real},<:AbstractVector{<:Real}}}) - xs, ys, zs = c[1:3] +function Makie.plot!(c::Tricontourf{<:Tuple{<:DelTri.Triangulation, <:AbstractVector{<:Real}}}) + tri, zs = c[1:2] c.attributes[:_computed_levels] = lift(c, zs, c.levels, c.mode) do zs, levels, mode return _get_isoband_levels(Val(mode), levels, vec(zs)) @@ -117,7 +140,7 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV polys = Observable(PolyType[]) colors = Observable(Float64[]) - function calculate_polys(xs, ys, zs, levels::Vector{Float32}, is_extended_low, is_extended_high, triangulation) + function calculate_polys(triangulation, zs, levels::Vector{Float32}, is_extended_low, is_extended_high) empty!(polys[]) empty!(colors[]) @@ -131,7 +154,10 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV lows = levels[1:end-1] highs = levels[2:end] - trianglelist = compute_triangulation(triangulation, xs, ys) + xs = [DelTri.getx(p) for p in DelTri.each_point(triangulation)] # each_point preserves indices + ys = [DelTri.gety(p) for p in DelTri.each_point(triangulation)] + + trianglelist = compute_triangulation(triangulation) filledcontours = filled_tricontours(xs, ys, zs, trianglelist, levels) levelcenters = (highs .+ lows) ./ 2 @@ -154,10 +180,10 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV return end - onany(calculate_polys, c, xs, ys, zs, c._computed_levels, is_extended_low, is_extended_high, c.triangulation) + onany(calculate_polys, c, tri, zs, c._computed_levels, is_extended_low, is_extended_high) # onany doesn't get called without a push, so we call # it on a first run! - calculate_polys(xs[], ys[], zs[], c._computed_levels[], is_extended_low[], is_extended_high[], c.triangulation[]) + calculate_polys(tri[], zs[], c._computed_levels[], is_extended_low[], is_extended_high[]) poly!(c, polys, @@ -175,16 +201,8 @@ function Makie.plot!(c::Tricontourf{<:Tuple{<:AbstractVector{<:Real},<:AbstractV ) end -function compute_triangulation(::DelaunayTriangulation, xs, ys) - vertices = [xs'; ys'] - return MiniQhull.delaunay(vertices) -end - -function compute_triangulation(triangulation::AbstractMatrix{<:Int}, xs, ys) - if size(triangulation, 1) != 3 - throw(ArgumentError("Triangulation matrix must be of size (3, n) but is of size $(size(triangulation)).")) - end - triangulation +function compute_triangulation(tri) + return [T[j] for T in DelTri.each_solid_triangle(tri), j in 1:3]' end # FIXME: TriplotBase augments levels so here the implementation is just repeated without that step diff --git a/src/bezier.jl b/src/bezier.jl index 924f0d675bc..d9074dcb7dd 100644 --- a/src/bezier.jl +++ b/src/bezier.jl @@ -201,7 +201,8 @@ function bezier_ngon(n, radius, angle) for a in range(0, 2pi, length = n+1)[1:end-1]] BezierPath([ MoveTo(points[1]); - LineTo.(points[2:end]) + LineTo.(points[2:end]); + ClosePath() ]) end @@ -212,7 +213,8 @@ function bezier_star(n, inner_radius, outer_radius, angle) for (i, a) in enumerate(range(0, 2pi, length = 2n+1)[1:end-1])] BezierPath([ MoveTo(points[1]); - LineTo.(points[2:end]) + LineTo.(points[2:end]); + ClosePath() ]) end @@ -240,12 +242,15 @@ function BezierPath(poly::Polygon) return BezierPath(commands) end -function BezierPath(svg::AbstractString; fit = false, bbox = nothing, flipy = false, keep_aspect = true) +function BezierPath(svg::AbstractString; fit = false, bbox = nothing, flipy = false, flipx = false, keep_aspect = true) commands = parse_bezier_commands(svg) p = BezierPath(commands) if flipy p = scale(p, Vec(1, -1)) end + if flipx + p = scale(p, Vec(-1, 1)) + end if fit if bbox === nothing p = fit_to_bbox(p, Rect2f((-0.5, -0.5), (1.0, 1.0)), keep_aspect = keep_aspect) @@ -490,24 +495,23 @@ function render_path(path, bitmap_size_px = 256) # in the outline, 1 unit = 1/64px scale_factor = bitmap_size_px * 64 - # we transform the path into the unit square and we can - # scale and translate this to a 4096x4096 grid, which is 64px x 64px - # when rendered to bitmap + # 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 + # 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) - path_unit_square = fit_to_unit_square(path_replaced, false) + aspect = widths(bbox(path)) / maximum(widths(bbox(path))) + path_unit_rect = fit_to_bbox(path_replaced, Rect2f(Point2f(0), aspect)) - path_transformed = Makie.scale( - path_unit_square, - scale_factor, - ) + path_transformed = Makie.scale(path_unit_rect, scale_factor) outline_ref = make_outline(path_transformed) - w = bitmap_size_px - h = bitmap_size_px + # Adjust bitmap size to match path aspect + w = ceil(Int, bitmap_size_px * aspect[1]) + h = ceil(Int, bitmap_size_px * aspect[2]) pitch = w * 1 # 8 bit gray pixelbuffer = zeros(UInt8, h * pitch) bitmap_ref = Ref{FT_Bitmap}() diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 04a5eb33ce6..0e64b458a17 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -206,7 +206,6 @@ function Camera3D(scene::Scene; kwargs...) on(camera(scene), events(scene).keyboardbutton) do event if event.action in (Keyboard.press, Keyboard.repeat) && cam.pulser[] == -1.0 && cam.selected[] && any(key -> ispressed(scene, controls[key][]), keynames) - cam.pulser[] = time() return Consume(true) end @@ -435,16 +434,8 @@ function add_mouse_controls!(scene, cam::Camera3D) # reposition if ispressed(scene, reposition_button[], event.button) && is_mouseinside(scene) - plt, idx = pick(scene) - p = get_position(plt, idx) - + _, _, p = ray_assisted_pick(scene) if p !== Point3f(NaN) - # transform data -> world - p = apply_transform(transform_func_obs(plt), p, get(plt, :space, :data)) - model = to_value(get(plt, :model, plt.transformation.model)) - p4d = model * to_ndim(Point4f, to_ndim(Point3f, p, 0), 1) - p = p4d[Vec(1,2,3)] / p4d[4] - # if translation/rotation happens with on-click reposition, # try uncommenting this # dragging[] = (false, false) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index a56b086c818..541c5fa66c5 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -161,11 +161,13 @@ end ## Shifted projection ######################################## -function shift_project(scene, plot, pos) +@deprecate shift_project(scene, plot, pos) shift_project(scene, pos) false + +function shift_project(scene, pos) project( camera(scene).projectionview[], Vec2f(widths(pixelarea(scene)[])), - apply_transform(transform_func(plot), pos, to_value(get(plot, :space, :data))) + pos ) .+ Vec2f(origin(pixelarea(scene)[])) end @@ -337,8 +339,8 @@ function show_data_recursion(inspector, plot, idx) show_data(inspector, plot, idx) end - if processed - inspector.selection = plot + if processed && inspector.selection != plot + clear_temporary_plots!(inspector, plot) end return processed @@ -359,8 +361,8 @@ function show_data_recursion(inspector, plot::AbstractPlot, idx, source) show_data(inspector, plot, idx, source) end - if processed - inspector.selection = plot + if processed && inspector.selection != plot + clear_temporary_plots!(inspector, plot) end return processed @@ -420,13 +422,14 @@ function show_data(inspector::DataInspector, plot::Scatter, idx) tt = inspector.plot scene = parent_scene(plot) - proj_pos = shift_project(scene, plot, to_ndim(Point3f, plot[1][][idx], 0)) + pos = position_on_plot(plot, idx) + proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, plot[1][][idx]) + tt.text[] = plot[:inspector_label][](plot, idx, pos) else - tt.text[] = position2string(plot[1][][idx]) + tt.text[] = position2string(pos) end tt.offset[] = ifelse( a.apply_tooltip_offset[], @@ -446,11 +449,9 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) scene = parent_scene(plot) if a.enable_indicators[] - T = transformationmatrix( - plot[1][][idx], - _to_scale(plot.markersize[], idx), - _to_rotation(plot.rotations[], idx) - ) + translation = apply_transform_and_model(plot, plot[1][][idx]) + rotation = _to_rotation(plot.rotations[], idx) + scale = _to_scale(plot.markersize[], idx) if inspector.selection != plot clear_temporary_plots!(inspector, plot) @@ -465,9 +466,12 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) bbox = Rect{3, Float32}(convert_attribute( plot.marker[], Key{:marker}(), Key{Makie.plotkey(plot)}() )) + T = Transformation( + identity; translation = translation, rotation = rotation, scale = scale + ) p = wireframe!( - scene, bbox, model = T, color = a.indicator_color, + scene, bbox, transformation = T, color = a.indicator_color, linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false ) @@ -478,21 +482,21 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) elseif !isempty(inspector.temp_plots) p = inspector.temp_plots[1] - p.model[] = T - + transform!(p, translation = translation, scale = scale, rotation = rotation) end a.indicator_visible[] = true end - proj_pos = shift_project(scene, plot, to_ndim(Point3f, plot[1][][idx], 0)) + pos = position_on_plot(plot, idx) + proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, plot[1][][idx]) + tt.text[] = plot[:inspector_label][](plot, idx, pos) else - tt.text[] = position2string(plot[1][][idx]) + tt.text[] = position2string(pos) end tt.visible[] = true @@ -506,9 +510,8 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i scene = parent_scene(plot) # cast ray from cursor into screen, find closest point to line - pos = get_position(plot, idx) - - proj_pos = shift_project(scene, plot, to_ndim(Point3f, pos, 0)) + pos = position_on_plot(plot, idx) + proj_pos = shift_project(scene, pos) update_tooltip_alignment!(inspector, proj_pos) tt.offset[] = ifelse( @@ -518,7 +521,7 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i ) if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, typeof(p0)(pos)) + tt.text[] = plot[:inspector_label][](plot, idx, eltype(plot[1][])(pos)) else tt.text[] = position2string(eltype(plot[1][])(pos)) end @@ -534,7 +537,14 @@ function show_data(inspector::DataInspector, plot::Mesh, idx) tt = inspector.plot scene = parent_scene(plot) - bbox = boundingbox(plot) + # Manual boundingbox including transfunc + bbox = let + points = point_iterator(plot) + trans_func = transform_func(plot) + model = plot.model[] + iter = iterate_transformed(points, model, to_value(get(plot, :space, :data)), trans_func) + limits_from_transformed_points(iter) + end proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) @@ -550,7 +560,8 @@ 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 ) @@ -581,12 +592,11 @@ end function show_data(inspector::DataInspector, plot::Surface, idx) a = inspector.attributes tt = inspector.plot - scene = parent_scene(plot) proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) - pos = get_position(plot, idx) + pos = position_on_plot(plot, idx) if !isnan(pos) tt[1][] = proj_pos @@ -617,20 +627,22 @@ function show_imagelike(inspector, plot, name, edge_based) a = inspector.attributes tt = inspector.plot scene = parent_scene(plot) - mpos = mouseposition(scene) - if plot.interpolate[] - i, j, z = _interpolated_getindex(plot[1][], plot[2][], plot[3][], mpos) - x, y = mpos - else - i, j, z = _pixelated_getindex(plot[1][], plot[2][], plot[3][], mpos, edge_based) - x = i; y = j + pos = position_on_plot(plot, -1, apply_transform = false)[Vec(1, 2)] # index irrelevant + + # Not on image/heatmap + if isnan(pos) + a.indicator_visible[] = false + tt.visible[] = false + return true end - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, (i, j), Point3f(mpos[1], mpos[2], z)) + if plot.interpolate[] + i, j, z = _interpolated_getindex(plot[1][], plot[2][], plot[3][], pos) + x, y = pos else - tt.text[] = color2text(name, x, y, z) + i, j, z = _pixelated_getindex(plot[1][], plot[2][], plot[3][], pos, edge_based) + x = i; y = j end # in case we hover over NaN values @@ -640,6 +652,12 @@ function show_imagelike(inspector, plot, name, edge_based) return true end + if haskey(plot, :inspector_label) + tt.text[] = plot[:inspector_label][](plot, (i, j), Point3f(pos[1], pos[2], z)) + else + tt.text[] = color2text(name, x, y, z) + end + a._color[] = if z isa AbstractFloat interpolated_getindex( to_colormap(plot.colormap[]), z, @@ -649,43 +667,44 @@ function show_imagelike(inspector, plot, name, edge_based) z end - position = to_ndim(Point3f, mpos, 0) proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) if a.enable_indicators[] if plot.interpolate[] - if inspector.selection != plot + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + !(inspector.temp_plots[1] isa Scatter) clear_temporary_plots!(inspector, plot) p = scatter!( - scene, position, color = a._color, + scene, pos, color = a._color, visible = a.indicator_visible, - inspectable = false, + inspectable = false, model = plot.model, # 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), strokecolor = a.indicator_color, - strokewidth = a.indicator_linewidth + strokewidth = a.indicator_linewidth, + depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[]-1)) push!(inspector.temp_plots, p) - elseif !isempty(inspector.temp_plots) + else p = inspector.temp_plots[1] - p[1].val[1] = position + p[1].val[1] = pos notify(p[1]) end else bbox = _pixelated_image_bbox(plot[1][], plot[2][], plot[3][], i, j, edge_based) - if inspector.selection != plot + if inspector.selection != plot || (length(inspector.temp_plots) != 1) || + !(inspector.temp_plots[1] isa Wireframe) clear_temporary_plots!(inspector, plot) p = wireframe!( - scene, bbox, color = a.indicator_color, + scene, bbox, color = a.indicator_color, model = plot.model, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, - visible = a.indicator_visible, inspectable = false + visible = a.indicator_visible, inspectable = false, + depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[]-1)) push!(inspector.temp_plots, p) - elseif !isempty(inspector.temp_plots) + else p = inspector.temp_plots[1] p[1][] = bbox end @@ -791,8 +810,8 @@ function show_data(inspector::DataInspector, plot::BarPlot, idx) tt = inspector.plot scene = parent_scene(plot) - pos = plot[1][][idx] - proj_pos = shift_project(scene, plot, to_ndim(Point3f, pos, 0)) + pos = apply_transform_and_model(plot, plot[1][][idx]) + proj_pos = shift_project(scene, to_ndim(Point3f, pos, 0)) update_tooltip_alignment!(inspector, proj_pos) if a.enable_indicators[] @@ -833,7 +852,8 @@ end function show_data(inspector::DataInspector, plot::Arrows, idx, source) a = inspector.attributes tt = inspector.plot - pos = plot[1][][idx] + pos = apply_transform_and_model(plot, plot[1][][idx]) + mpos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, mpos) @@ -842,7 +862,7 @@ function show_data(inspector::DataInspector, plot::Arrows, idx, source) tt[1][] = mpos if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, mpos) + tt.text[] = plot[:inspector_label][](plot, idx, pos) else tt.text[] = "Position:\n $p\nDirection:\n $v" end @@ -856,7 +876,7 @@ end # backend handle picking colors from a colormap function show_data(inspector::DataInspector, plot::Contourf, idx, source::Mesh) tt = inspector.plot - idx = show_poly(inspector, plot.plots[1], idx, source) + idx = show_poly(inspector, plot, plot.plots[1], idx, source) level = plot.plots[1].color[][idx] mpos = Point2f(mouseposition_px(inspector.root)) @@ -882,14 +902,13 @@ end # return true # end -function show_poly(inspector, plot, idx, source) +function show_poly(inspector, plot, poly, idx, source) a = inspector.attributes - idx = vertexindex2poly(plot[1][], idx) + idx = vertexindex2poly(poly[1][], idx) if a.enable_indicators[] - - line_collection = copy(convert_arguments(PointBased(), plot[1][][idx].exterior)[1]) - for int in plot[1][][idx].interiors + line_collection = copy(convert_arguments(PointBased(), poly[1][][idx].exterior)[1]) + for int in poly[1][][idx].interiors push!(line_collection, Point2f(NaN)) append!(line_collection, convert_arguments(PointBased(), int)[1]) end @@ -899,11 +918,11 @@ function show_poly(inspector, plot, 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 + visible = a.indicator_visible, inspectable = false, depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[]-1)) push!(inspector.temp_plots, p) elseif !isempty(inspector.temp_plots) @@ -919,51 +938,46 @@ end function show_data(inspector::DataInspector, plot::VolumeSlices, idx, child::Heatmap) a = inspector.attributes tt = inspector.plot - scene = parent_scene(plot) - proj_pos = Point2f(mouseposition_px(inspector.root)) - update_tooltip_alignment!(inspector, proj_pos) + pos = position_on_plot(child, -1, apply_transform = false)[Vec(1, 2)] # index irrelevant - a0, a1 = extrema(child[1][]) - b0, b1 = extrema(child[2][]) - data = child[3][] - T = child.transformation.model[] + # Not on heatmap + if isnan(pos) + a.indicator_visible[] && (a.indicator_visible[] = false) + tt.visible[] = false + return true + end - # Transform the Ray rather than the Rect here to avoid using a Rect3f - rect = Rect2f(a0, b0, a1, b1) - ray = transform(inv(T), ray_at_cursor(scene)) - p = ray_rect_intersection(rect, ray) # in heatmap space (with z = normal of heatmap) + i, j, val = _pixelated_getindex(child[1][], child[2][], child[3][], pos, true) - if !isnan(p) - i = clamp(round(Int, (p[1] - a0) / (a1 - a0) * size(data, 1) + 0.5), 1, size(data, 1)) - j = clamp(round(Int, (p[2] - b0) / (b1 - b0) * size(data, 2) + 0.5), 1, size(data, 2)) - val = data[i, j] + 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) - tt[1][] = proj_pos - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, (i, j), p) - else - tt.text[] = @sprintf( - "x: %0.6f\ny: %0.6f\nz: %0.6f\n%0.6f0", - p[1], p[2], p[3], val - ) - end - tt.visible[] = true + if haskey(plot, :inspector_label) + tt.text[] = plot[:inspector_label][](plot, (i, j), world_pos) else - tt.visible[] = false + tt.text[] = @sprintf( + "x: %0.6f\ny: %0.6f\nz: %0.6f\n%0.6f0", + world_pos[1], world_pos[2], world_pos[3], val + ) end + + tt.visible[] = true a.indicator_visible[] && (a.indicator_visible[] = false) return true end -function show_data(inspector::DataInspector, plot::Band, ::Integer, ::Mesh) +function show_data(inspector::DataInspector, plot::Band, idx::Integer, mesh::Mesh) scene = parent_scene(plot) tt = inspector.plot a = inspector.attributes - pos = Point2f(mouseposition(scene)) + pos = Point2f(position_on_plot(mesh, idx, apply_transform = false)) #Point2f(mouseposition(scene)) ps1 = plot.converted[1][] ps2 = plot.converted[2][] @@ -984,23 +998,20 @@ function show_data(inspector::DataInspector, plot::Band, ::Integer, ::Mesh) # Draw the line if a.enable_indicators[] - model = plot.model[] - # Why does this sometimes create 2+ plots if inspector.selection != plot || (length(inspector.temp_plots) != 1) clear_temporary_plots!(inspector, plot) p = lines!( - scene, [P1, P2], model = model, + scene, [P1, P2], transformation = Transformation(plot.transformation), color = a.indicator_color, strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, - visible = a.indicator_visible, inspectable = false + visible = a.indicator_visible, inspectable = false, + depth_shift = -1f-3 ) - translate!(p, Vec3f(0, 0, a.depth[])) push!(inspector.temp_plots, p) elseif !isempty(inspector.temp_plots) p = inspector.temp_plots[1] p[1][] = [P1, P2] - p.model[] = model end a.indicator_visible[] = true @@ -1012,6 +1023,8 @@ function show_data(inspector::DataInspector, plot::Band, ::Integer, ::Mesh) if haskey(plot, :inspector_label) tt.text[] = plot[:inspector_label][](plot, right, (P1, P2)) else + P1 = apply_transform_and_model(mesh, P1, Point2f) + P2 = apply_transform_and_model(mesh, P2, Point2f) tt.text[] = @sprintf("(%0.3f, %0.3f) .. (%0.3f, %0.3f)", P1[1], P1[2], P2[1], P2[2]) end tt.visible[] = true diff --git a/src/interaction/position_on_plot.jl b/src/interaction/position_on_plot.jl deleted file mode 100644 index f22df4885fd..00000000000 --- a/src/interaction/position_on_plot.jl +++ /dev/null @@ -1,254 +0,0 @@ -struct Ray - origin::Point3f - direction::Vec3f -end - -""" - ray_at_cursor(scenelike) - -Returns a Ray into the scene starting at the current cursor position. -""" -ray_at_cursor(x) = ray_at_cursor(get_scene(x)) -function ray_at_cursor(scene::Scene) - return ray_at_cursor(scene, cameracontrols(scene)) -end - -function ray_at_cursor(scene::Scene, cam::Camera3D) - lookat = cam.lookat[] - eyepos = cam.eyeposition[] - viewdir = lookat - eyepos - - 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[]) - aspect = px_width / px_height - rel_pos = 2 .* mouseposition_px(scene) ./ (px_width, px_height) .- 1 - - if cam.settings.projectiontype[] === Perspective - dir = (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) * tand(0.5 * cam.fov[]) + u_z - return Ray(cam.eyeposition[], normalize(dir)) - else - # Orthographic has consistent direction, but not starting point - origin = norm(viewdir) * (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) - return Ray(origin, normalize(viewdir)) - end -end - -function ray_at_cursor(scene::Scene, cam::Camera2D) - rel_pos = mouseposition_px(scene) ./ widths(scene.px_area[]) - origin = minimum(cam.area[]) .+ rel_pos .* widths(cam.area[]) - return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) -end - -function ray_at_cursor(scene::Scene, ::PixelCamera) - return Ray(to_ndim(Point3f, mouseposition_px(scene), 10_000f0), Vec3f(0,0,-1)) -end - -function ray_at_cursor(scene::Scene, ::RelativeCamera) - origin = mouseposition_px(scene) ./ widths(scene.px_area[]) - return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) -end - -ray_at_cursor(scene::Scene, cam) = _ray_at_cursor(scene, cam) - -# This method should always work -function _ray_at_cursor(scene::Scene, cam = scene.camera_controls) - inv_view_proj = inv(camera(scene).projectionview[]) - mpos = events(scene).mouseposition[] - area = pixelarea(scene)[] - - # This figures out the camera view direction from the projectionview matrix - # and computes a ray from a near and a far point. - # Based on ComputeCameraRay from ImGuizmo - mp = 2f0 .* (mpos .- minimum(area)) ./ widths(area) .- 1f0 - v = inv_view_proj * Vec4f(0, 0, -10, 1) - reversed = v[3] < v[4] - near = reversed ? 1f0 - 1e-6 : 0f0 - far = reversed ? 0f0 : 1f0 - 1e-6 - - origin = inv_view_proj * Vec4f(mp[1], mp[2], near, 1f0) - origin = origin[Vec(1, 2, 3)] ./ origin[4] - - p = inv_view_proj * Vec4f(mp[1], mp[2], far, 1f0) - p = p[Vec(1, 2, 3)] ./ p[4] - - dir = normalize(p .- origin) - - return Ray(origin, dir) -end - - -function transform(M::Mat4f, ray::Ray) - p4d = M * to_ndim(Point4f, ray.origin, 1f0) - dir = normalize(M[Vec(1,2,3), Vec(1,2,3)] * ray.direction) - return Ray(p4d[Vec(1,2,3)] / p4d[4], dir) -end - - -############################################## - - -# These work in 2D and 3D -function closest_point_on_line(A::VecTypes, B::VecTypes, ray::Ray) - return closest_point_on_line(to_ndim(Point3f, A, 0), to_ndim(Point3f, B, 0), ray) -end -function closest_point_on_line(A::Point3f, B::Point3f, ray::Ray) - # See: - # https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection - AB_norm = norm(B .- A) - u_AB = (B .- A) / AB_norm - u_perp = normalize(cross(ray.direction, u_AB)) - # e_RD, e_perp defines a plane with normal n - n = normalize(cross(ray.direction, u_perp)) - t = dot(ray.origin .- A, n) / dot(u_AB, n) - return A .+ clamp(t, 0.0, AB_norm) * u_AB -end - - -function ray_triangle_intersection(A::VecTypes{3}, B::VecTypes{3}, C::VecTypes{3}, ray::Ray, ϵ = 1e-6) - # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html - AO = A .- ray.origin - BO = B .- ray.origin - CO = C .- ray.origin - A1 = 0.5 * dot(cross(BO, CO), ray.direction) - A2 = 0.5 * dot(cross(CO, AO), ray.direction) - A3 = 0.5 * dot(cross(AO, BO), ray.direction) - - if (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) - return Point3f((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) - else - return Point3f(NaN) - end -end - -function ray_rect_intersection(rect::Rect2f, ray::Ray) - possible_hit = ray.origin - ray.origin[3] / ray.direction[3] * ray.direction - min = minimum(rect); max = maximum(rect) - if all(min <= possible_hit[Vec(1,2)] <= max) - return possible_hit - end - return Point3f(NaN) -end - - -function ray_rect_intersection(rect::Rect3f, ray::Ray) - mins = (minimum(rect) - ray.origin) ./ ray.direction - maxs = (maximum(rect) - ray.origin) ./ ray.direction - x, y, z = min.(mins, maxs) - possible_hit = max(x, y, z) - if possible_hit < minimum(max.(mins, maxs)) - return ray.origin + possible_hit * ray.direction - end - return Point3f(NaN) -end - -### Surface positions -######################################## - -surface_x(xs::ClosedInterval, i, j, N) = minimum(xs) + (maximum(xs) - minimum(xs)) * (i-1) / (N-1) -surface_x(xs, i, j, N) = xs[i] -surface_x(xs::AbstractMatrix, i, j, N) = xs[i, j] - -surface_y(ys::ClosedInterval, i, j, N) = minimum(ys) + (maximum(ys) - minimum(ys)) * (j-1) / (N-1) -surface_y(ys, i, j, N) = ys[j] -surface_y(ys::AbstractMatrix, i, j, N) = ys[i, j] - -function surface_pos(xs, ys, zs, i, j) - N, M = size(zs) - return Point3f(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) -end - - -################################################# - - -""" - get_position(scene) = get_position(pick(scene)) - get_position(plot, index) - -Given the result of `pick(...)` this function returns a relevant position -for the given input. If `plot = nothing` (i.e pick did not find a plot) -the function will return `Point3f(NaN)`. - -For most plot types the returned position is interpolated to match up with the -cursor position exactly. Exceptions: -- `scatter` and `meshscatter` return the position of the clicked marker/mesh -- `text` is excluded, always returning `Point3f(NaN)` -- `volume` returns a relevant position on its bounding box -""" -get_position(scene::Scene) = get_position(pick(scene)...) -get_position(plot::Union{Scatter, MeshScatter}, idx) = plot[1][][idx] - -function get_position(plot::Union{Lines, LineSegments}, idx) - p0, p1 = plot[1][][idx-1:idx] - return closest_point_on_line(p0, p1, ray_at_cursor(parent_scene(plot))) -end - -function get_position(plot::Union{Heatmap, Image}, idx) - p0, p1 = Point2f.(extrema(plot.x[]), extrema(plot.y[])) - return ray_rect_intersection(Rect2f(p0, p1 - p0), ray_at_cursor(parent_scene(plot))) -end - -function get_position(plot::Mesh, idx) - positions = coordinates(plot.mesh[]) - ray = ray_at_cursor(parent_scene(plot)) - - for f in faces(plot.mesh[]) - if idx in f - p1, p2, p3 = positions[f] - pos = ray_triangle_intersection(p1, p2, p3, ray) - if pos !== Point3f(NaN) - return pos - end - end - end - - return Point3f(NaN) -end - -function get_position(plot::Surface, idx) - xs = plot[1][] - ys = plot[2][] - zs = plot[3][] - w, h = size(zs) - _i = mod1(idx, w); _j = div(idx-1, w) - - # This isn't the most accurate so we include some neighboring faces - ray = ray_at_cursor(parent_scene(plot)) - pos = Point3f(NaN) - for i in _i-1:_i+1, j in _j-1:_j+1 - (1 <= i <= w) && (1 <= j < h) || continue - - if i - 1 > 0 - pos = ray_triangle_intersection( - surface_pos(xs, ys, zs, i, j), - surface_pos(xs, ys, zs, i-1, j), - surface_pos(xs, ys, zs, i, j+1), - ray - ) - end - - if i + 1 <= w && isnan(pos) - pos = ray_triangle_intersection( - surface_pos(xs, ys, zs, i, j), - surface_pos(xs, ys, zs, i, j+1), - surface_pos(xs, ys, zs, i+1, j+1), - ray - ) - end - - isnan(pos) || break - end - - return pos -end - -function get_position(plot::Volume, idx) - min, max = Point3f.(extrema(plot.x[]), extrema(plot.y[]), extrema(plot.z[])) - return ray_rect_intersection(Rect3f(min, max .- min), ray_at_cursor(parent_scene(plot))) -end - -get_position(plot::Text, idx) = Point3f(NaN) -get_position(plot::Nothing, idx) = Point3f(NaN) \ No newline at end of file diff --git a/src/interaction/ray_casting.jl b/src/interaction/ray_casting.jl new file mode 100644 index 00000000000..c3d97bcfede --- /dev/null +++ b/src/interaction/ray_casting.jl @@ -0,0 +1,397 @@ +################################################################################ +### Ray Generation +################################################################################ + +struct Ray + origin::Point3f + direction::Vec3f +end + +""" + ray_at_cursor(fig/ax/scene) + +Returns a Ray into the scene starting at the current cursor position. +""" +ray_at_cursor(x) = ray_at_cursor(get_scene(x)) +function ray_at_cursor(scene::Scene) + return Ray(scene, mouseposition_px(scene)) +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 +when calling `mouseposition_px(scene)`. +""" +Ray(scene::Scene, xy::VecTypes{2}) = Ray(scene, cameracontrols(scene), xy) + + +function Ray(scene::Scene, cam::Camera3D, xy::VecTypes{2}) + lookat = cam.lookat[] + eyepos = cam.eyeposition[] + viewdir = lookat - eyepos + + 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[]) + aspect = px_width / px_height + rel_pos = 2 .* xy ./ (px_width, px_height) .- 1 + + if cam.settings.projectiontype[] === Perspective + dir = (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) * tand(0.5 * cam.fov[]) + u_z + return Ray(cam.eyeposition[], normalize(dir)) + else + # Orthographic has consistent direction, but not starting point + origin = norm(viewdir) * (rel_pos[1] * aspect * u_x + rel_pos[2] * u_y) + return Ray(origin, normalize(viewdir)) + end +end + +function Ray(scene::Scene, cam::Camera2D, xy::VecTypes{2}) + rel_pos = xy ./ widths(scene.px_area[]) + pv = scene.camera.projectionview[] + m = Vec2f(pv[1, 1], pv[2, 2]) + b = Vec2f(pv[1, 4], pv[2, 4]) + origin = (2 * rel_pos .- 1 - b) ./ m + return Ray(to_ndim(Point3f, origin, 10_000f0), Vec3f(0,0,-1)) +end + +function Ray(::Scene, ::PixelCamera, xy::VecTypes{2}) + return Ray(to_ndim(Point3f, xy, 10_000f0), Vec3f(0,0,-1)) +end + +function Ray(scene::Scene, ::RelativeCamera, xy::VecTypes{2}) + origin = xy ./ widths(scene.px_area[]) + 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 +function ray_from_projectionview(scene::Scene, xy::VecTypes{2}) + inv_view_proj = inv(camera(scene).projectionview[]) + area = pixelarea(scene)[] + + # This figures out the camera view direction from the projectionview matrix + # and computes a ray from a near and a far point. + # Based on ComputeCameraRay from ImGuizmo + mp = 2f0 .* xy ./ widths(area) .- 1f0 + v = inv_view_proj * Vec4f(0, 0, -10, 1) + reversed = v[3] < v[4] + near = reversed ? 1f0 - 1e-6 : 0f0 + far = reversed ? 0f0 : 1f0 - 1e-6 + + origin = inv_view_proj * Vec4f(mp[1], mp[2], near, 1f0) + origin = origin[Vec(1, 2, 3)] ./ origin[4] + + p = inv_view_proj * Vec4f(mp[1], mp[2], far, 1f0) + p = p[Vec(1, 2, 3)] ./ p[4] + + dir = normalize(p .- origin) + + return Ray(origin, dir) +end + + +function transform(M::Mat4f, ray::Ray) + p4d = M * to_ndim(Point4f, ray.origin, 1f0) + dir = normalize(M[Vec(1,2,3), Vec(1,2,3)] * ray.direction) + return Ray(p4d[Vec(1,2,3)] / p4d[4], dir) +end + + +################################################################################ +### Ray - object intersections +################################################################################ + + +# These work in 2D and 3D +function closest_point_on_line(A::VecTypes, B::VecTypes, ray::Ray) + return closest_point_on_line(to_ndim(Point3f, A, 0), to_ndim(Point3f, B, 0), ray) +end +function closest_point_on_line(A::Point3f, B::Point3f, ray::Ray) + # See: + # https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection + AB_norm = norm(B .- A) + u_AB = (B .- A) / AB_norm + u_perp = normalize(cross(ray.direction, u_AB)) + # e_RD, e_perp defines a plane with normal n + n = normalize(cross(ray.direction, u_perp)) + t = dot(ray.origin .- A, n) / dot(u_AB, n) + return A .+ clamp(t, 0.0, AB_norm) * u_AB +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), + ray, ϵ + ) +end + +function ray_triangle_intersection(A::VecTypes{3}, B::VecTypes{3}, C::VecTypes{3}, ray::Ray, ϵ = 1e-6) + # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html + # Alternative: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm + AO = A .- ray.origin + BO = B .- ray.origin + CO = C .- ray.origin + A1 = 0.5 * dot(cross(BO, CO), ray.direction) + A2 = 0.5 * dot(cross(CO, AO), ray.direction) + A3 = 0.5 * dot(cross(AO, BO), ray.direction) + + # all positive or all negative + if (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) + return Point3f((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) + else + return Point3f(NaN) + end +end + +function ray_rect_intersection(rect::Rect2f, ray::Ray) + possible_hit = ray.origin - ray.origin[3] / ray.direction[3] * ray.direction + min = minimum(rect); max = maximum(rect) + if all(min <= possible_hit[Vec(1,2)] <= max) + return possible_hit + end + return Point3f(NaN) +end + + +function ray_rect_intersection(rect::Rect3f, ray::Ray) + mins = (minimum(rect) - ray.origin) ./ ray.direction + maxs = (maximum(rect) - ray.origin) ./ ray.direction + x, y, z = min.(mins, maxs) + possible_hit = max(x, y, z) + if possible_hit < minimum(max.(mins, maxs)) + return ray.origin + possible_hit * ray.direction + end + return Point3f(NaN) +end + +function is_point_on_ray(p::Point3f, ray::Ray) + diff = ray.origin - p + return abs(dot(diff, ray.direction)) ≈ abs(norm(diff)) +end + + +################################################################################ +### Ray casting (positions from ray-plot intersections) +################################################################################ + + +""" + ray_assisted_pick(fig/ax/scene[, xy = events(fig/ax/scene).mouseposition[], apply_transform = true]) + +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))[])) +position = position_on_plot(plot, idx, ray, apply_transform = true) +``` +See [`position_on_plot`](@ref) for more information. +""" +function ray_assisted_pick(obj, xy = events(obj).mouseposition[]; apply_transform = true) + 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)[])) + pos = position_on_plot(plot, idx, ray, apply_transform = apply_transform) + return (plot, idx, pos) +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 +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)[]))) +``` +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 +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 +differs: +- `scatter` and `meshscatter` return the position of the picked marker/mesh +- `text` is excluded, always returning `Point3f(NaN)` +- `volume` calculates the ray - rect intersection for its bounding box +- `lines` and `linesegments` return the closest point on the line to the ray +- `mesh` and `surface` check for ray-triangle intersections for every triangle containing the picked vertex +- `image` and `heatmap` check for ray-rect intersection +""" +function position_on_plot(plot::AbstractPlot, idx::Integer; apply_transform = true) + return position_on_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) + else + return pos + end +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 + p4d = inv(plot.model[]) * to_ndim(Point4f, pos, 1f0) + p3d = p4d[Vec(1, 2, 3)] / p4d[4] + itf = inverse_transform(transform_func(plot)) + return Makie.apply_transform(itf, p3d, get(plot, :space, :data)) + end +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 + # 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)) + p0, p1 = map(Point2f.(extrema(plot.x[]), extrema(plot.y[]))) do p + return Makie.apply_transform(transform_func(plot), p, space) + 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] + else + pos = Makie.apply_transform(inverse_transform(transform_func(plot)), pos, space) + return to_ndim(Point3f, pos, 0) + end +end + +function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) + positions = coordinates(plot.mesh[]) + ray = transform(inv(plot.model[]), ray) + tf = transform_func(plot) + space = to_value(get(plot, :space, :data)) + + for f in faces(plot.mesh[]) + if idx in f + p1, p2, p3 = positions[f] + p1, p2, p3 = Makie.apply_transform.(tf, (p1, p2, p3), space) + pos = ray_triangle_intersection(p1, p2, p3, ray) + if pos !== Point3f(NaN) + if apply_transform + p4d = plot.model[] * to_ndim(Point4f, pos, 1) + return Point3f(p4d) / p4d[4] + else + return Makie.apply_transform(inverse_transform(tf), pos, space) + end + end + end + end + + @debug "Did not find intersection for index = $idx when casting a ray on mesh." + + return Point3f(NaN) +end + +# Handling indexing into different surface input types +surface_x(xs::ClosedInterval, i, j, N) = minimum(xs) + (maximum(xs) - minimum(xs)) * (i-1) / (N-1) +surface_x(xs, i, j, N) = xs[i] +surface_x(xs::AbstractMatrix, i, j, N) = xs[i, j] + +surface_y(ys::ClosedInterval, i, j, N) = minimum(ys) + (maximum(ys) - minimum(ys)) * (j-1) / (N-1) +surface_y(ys, i, j, N) = ys[j] +surface_y(ys::AbstractMatrix, i, j, N) = ys[i, j] + +function surface_pos(xs, ys, zs, i, j) + N, M = size(zs) + return Point3f(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) +end + +function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) + xs = plot[1][] + ys = plot[2][] + zs = plot[3][] + w, h = size(zs) + _i = mod1(idx, w); _j = div(idx-1, w) + + 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 + (1 <= i <= w) && (1 <= j < h) || continue + + if i - 1 > 0 + # transforms only apply to x and y coordinates of surfaces + A = surface_pos(xs, ys, zs, i, j) + B = surface_pos(xs, ys, zs, i-1, j) + C = surface_pos(xs, ys, zs, i, j+1) + A, B, C = map((A, B, C)) do p + xy = Makie.apply_transform(tf, Point2f(p), space) + Point3f(xy[1], xy[2], p[3]) + end + pos = ray_triangle_intersection(A, B, C, ray) + end + + if i + 1 <= w && isnan(pos) + A = surface_pos(xs, ys, zs, i, j) + B = surface_pos(xs, ys, zs, i, j+1) + C = surface_pos(xs, ys, zs, i+1, j+1) + A, B, C = map((A, B, C)) do p + xy = Makie.apply_transform(tf, Point2f(p), space) + Point3f(xy[1], xy[2], p[3]) + end + pos = ray_triangle_intersection(A, B, C, ray) + end + + isnan(pos) || break + end + + if apply_transform + p4d = plot.model[] * to_ndim(Point4f, pos, 1) + return p4d[Vec(1, 2, 3)] / p4d[4] + else + xy = Makie.apply_transform(inverse_transform(tf), Point2f(pos), space) + return Point3f(xy[1], xy[2], pos[3]) + end +end + +function position_on_plot(plot::Volume, idx, ray::Ray; apply_transform = true) + min, max = Point3f.(extrema(plot.x[]), extrema(plot.y[]), extrema(plot.z[])) + + if apply_transform + min = apply_transform_and_model(plot, min) + max = apply_transform_and_model(plot, max) + return ray_rect_intersection(Rect3f(min, max .- min), ray) + else + min = Makie.apply_transform(transform_func(plot), min, get(plot, :space, :data)) + max = Makie.apply_transform(transform_func(plot), max, get(plot, :space, :data)) + ray = transform(inv(plot.model[]), ray) + pos = ray_rect_intersection(Rect3f(min, max .- min), ray) + return Makie.apply_transform(inverse_transform(plot), pos, get(plot, :space, :data)) + end +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 diff --git a/src/interfaces.jl b/src/interfaces.jl index bc159b80cd0..f08358613bf 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -24,7 +24,7 @@ function color_and_colormap!(plot, intensity = plot[:color]) get!(plot, :nan_color, RGBAf(0,0,0,0)) if intensity[] isa Number plot[:colorrange][] isa Automatic && - error("Cannot determine a colorrange automatically for single number color value $intens. Pass an explicit colorrange.") + error("Cannot determine a colorrange automatically for single number color value $intensity. Pass an explicit colorrange.") args = @converted_attribute plot (colorrange, lowclip, highclip, nan_color) plot[:color] = lift(numbers_to_colors, plot, intensity, colormap, args...) delete!(plot, :colorrange) diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index 9454f8b50fb..6ddc7cd53d0 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -205,6 +205,34 @@ transformation(x::Attributes) = x.transformation[] transform_func(x) = transform_func_obs(x)[] transform_func_obs(x) = transformation(x).transform_func +""" + apply_transform_and_model(plot, pos, output_type = Point3f) + apply_transform_and_model(model, transfrom_func, pos, output_type = Point3f) + + +Applies the transform function and model matrix (i.e. transformations from +`translate!`, `rotate!` and `scale!`) to the given input +""" +function apply_transform_and_model(plot::AbstractPlot, pos, output_type = Point3f) + return apply_transform_and_model( + plot.model[], transform_func(plot), pos, + to_value(get(plot, :space, :data)), + output_type + ) +end +function apply_transform_and_model(model::Mat4f, f, pos::VecTypes, space = :data, output_type = Point3f) + transformed = apply_transform(f, pos, space) + p4d = to_ndim(Point4f, to_ndim(Point3f, transformed, 0), 1) + p4d = model * p4d + p4d = p4d ./ p4d[4] + return to_ndim(output_type, p4d, NaN) +end +function apply_transform_and_model(model::Mat4f, f, positions::Vector, space = :data, output_type = Point3f) + return map(positions) do pos + apply_transform_and_model(model, f, pos, space, output_type) + end +end + """ apply_transform(f, data, space) Apply the data transform func to the data if the space matches one @@ -268,7 +296,7 @@ function apply_transform(f::PointTrans{N1}, point::Point{N2}) where {N1, N2} end function apply_transform(f, data::AbstractArray) - map(point-> apply_transform(f, point), data) + map(point -> apply_transform(f, point), data) end function apply_transform(f::Tuple{Any, Any}, point::VecTypes{2}) diff --git a/src/makielayout/blocks/slider.jl b/src/makielayout/blocks/slider.jl index 51941b1be6a..28ebc809db4 100644 --- a/src/makielayout/blocks/slider.jl +++ b/src/makielayout/blocks/slider.jl @@ -191,7 +191,11 @@ function closest_index_inexact(sliderrange, value) end """ + set_close_to!(slider, value) -> closest_value + Set the `slider` to the value in the slider's range that is closest to `value` and return this value. +This function should be used to set a slider to a value programmatically, rather than +mutating its value observable directly, which doesn't update the slider visually. """ function set_close_to!(slider::Slider, value) closest = closest_index(slider.range[], value) diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index 7dfb92c9c04..bc413a87273 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -70,7 +70,7 @@ function Base.show(io::IO, atlas::TextureAtlas) println(io, " font_render_callback: ", length(atlas.font_render_callback)) end -const SERIALIZATION_FORMAT_VERSION = "v1" +const SERIALIZATION_FORMAT_VERSION = "v2" # basically a singleton for the textureatlas function get_cache_path(resolution::Int, pix_per_glyph::Int) @@ -486,8 +486,7 @@ end function marker_scale_factor(atlas::TextureAtlas, path::BezierPath) # padded_width = (unpadded_target_width + unpadded_target_width * pad_per_unit) - path_width = widths(Makie.bbox(path)) - return (1f0 .+ bezierpath_pad_scale_factor(atlas, path)) .* path_width + return (1f0 .+ bezierpath_pad_scale_factor(atlas, path)) .* widths(Makie.bbox(path)) end function rescale_marker(atlas::TextureAtlas, pathmarker::BezierPath, font, markersize) @@ -512,7 +511,7 @@ end function offset_bezierpath(atlas::TextureAtlas, bp::BezierPath, markersize::Vec2, markeroffset::Vec2) bb = bbox(bp) - pad_offset = (origin(bb) .- 0.5f0 .* bezierpath_pad_scale_factor(atlas, bp) .* widths(bb)) + pad_offset = origin(bb) .- 0.5f0 .* bezierpath_pad_scale_factor(atlas, bp) .* widths(bb) return markersize .* pad_offset end diff --git a/test/ray_casting.jl b/test/ray_casting.jl new file mode 100644 index 00000000000..ec88508ba18 --- /dev/null +++ b/test/ray_casting.jl @@ -0,0 +1,163 @@ +@testset "Ray Casting" begin + @testset "View Rays" begin + scene = Scene() + xy = 0.5 * widths(pixelarea(scene)[]) + + orthographic_cam3d!(x) = cam3d!(x, perspectiveprojection = Makie.Orthographic) + + for set_cam! in (cam2d!, cam_relative!, campixel!, cam3d!, orthographic_cam3d!) + @testset "$set_cam!" begin + set_cam!(scene) + ray = Makie.Ray(scene, xy) + ref_ray = Makie.ray_from_projectionview(scene, xy) + # Direction matches and is normalized + @test ref_ray.direction ≈ ray.direction + @test norm(ray.direction) ≈ 1f0 + # origins are on the same ray + @test Makie.is_point_on_ray(ray.origin, ref_ray) + end + end + end + + + # 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) + ray = Makie.Ray(transformed + direction, normalize(direction)) + + @test Makie.is_point_on_ray(transformed, ray) + transformed_ray = Makie.transform(inv(model), ray) + @test Makie.is_point_on_ray(point, transformed_ray) + + + @testset "Intersections" begin + p = rand(Point3f) + v = rand(Vec3f) + ray = Makie.Ray(p + 10v, normalize(v)) + + # ray - line + w = cross(v, rand(Vec3f)) + A = p - 5w + B = p + 5w + result = Makie.closest_point_on_line(A, B, ray) + @test result ≈ p + + # ray - triangle + w2 = cross(v, w) + A = p - 5w - 5w2 + B = p + 5w + C = p + 5w2 + result = Makie.ray_triangle_intersection(A, B, C, ray) + @test result ≈ p + + # ray - rect3 + rect = Rect(Vec(A), 10w + 10w2 + 10v) + result = Makie.ray_rect_intersection(rect, ray) + @test Makie.is_point_on_ray(result, ray) + + # ray - rect2 + p2 = Point2f(ray.origin - ray.origin[3] / ray.direction[3] * ray.direction) + w = rand(Vec2f) + rect = Rect2f(p2 - 5w, 10w) + result = Makie.ray_rect_intersection(rect, ray) + @test result ≈ Point3f(p2..., 0) + end + + + # 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + 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)) + p = volume!(scene, rand(10, 10, 10)) + cam3d!(scene) + center!(scene) + ray = Makie.Ray(scene, (16.0, 306.0)) + pos = Makie.position_on_plot(p, 0, ray) + @test pos ≈ Point3f(10.0, 0.18444633, 9.989262) + end + + # For recreating the above: + #= + # Scene setup from tests: + scene = Scene(resolution = (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 + mp = events(scene).mouseposition[] + _p, idx = pick(scene, mp, 10) + pos[] = Makie.position_on_plot(p, idx) + println(_p == p) + println("ray = Makie.Ray(scene, $mp)") + println("pos = Makie.position_on_plot(p, $idx, ray)") + println("@test pos ≈ Point3f(", pos[][1], ", ", pos[][2], ", ", pos[][3], ")") + end + end + + # Optional - show selected positon + # This may change the camera, so don't use it for test values + # scatter!(scene, pos) + + scene + =# +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 5d46b19a92e..1e2e257d552 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -32,4 +32,5 @@ using Makie: volume include("events.jl") include("text.jl") include("boundingboxes.jl") + include("ray_casting.jl") end From 001542e247d579be6e9aa8c69e00edf203fe2992 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Jul 2023 18:25:09 +0200 Subject: [PATCH 73/80] avoid centering on unrelated plots --- src/camera/camera3d.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 0e64b458a17..f715ac9127b 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -434,8 +434,8 @@ function add_mouse_controls!(scene, cam::Camera3D) # reposition if ispressed(scene, reposition_button[], event.button) && is_mouseinside(scene) - _, _, p = ray_assisted_pick(scene) - if p !== Point3f(NaN) + plt, _, p = ray_assisted_pick(scene) + if p !== Point3f(NaN) && 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) From 98c109664149a822500cc65cd59129b0a76bb890 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 4 Jul 2023 21:26:00 +0200 Subject: [PATCH 74/80] fix test errors --- GLMakie/test/glmakie_refimages.jl | 2 ++ test/ray_casting.jl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 5a386d5d0e8..991be84f8a0 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -65,6 +65,7 @@ end markersize=size, axis = (; scenekw = (;limits=Rect3f(Point3(0), Point3(1)))) ) + update_cam!(ax.scene, Point3f(2.224431, 2.224431, 2.128731), Point3f(0.5957, 0.5957, 0.50000006)) Record(fig, [10, 5, 100, 60, 177]) do i makenew[] = i end @@ -82,6 +83,7 @@ end end end fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0) + update_cam!(ax.scene, Point3f(45.383663, 45.38298, 43.136826), Point3f(12.246061, 12.245379, 9.999225)) screen = display(GLMakie.Screen(;renderloop=(screen) -> nothing, start_renderloop=false), fig.scene) buff = RNG.rand(Point3f, 10^4) .* 20f0; update_loop(meshplot, buff, screen) diff --git a/test/ray_casting.jl b/test/ray_casting.jl index ec88508ba18..135dd82d95d 100644 --- a/test/ray_casting.jl +++ b/test/ray_casting.jl @@ -131,7 +131,7 @@ center!(scene) ray = Makie.Ray(scene, (16.0, 306.0)) pos = Makie.position_on_plot(p, 0, ray) - @test pos ≈ Point3f(10.0, 0.18444633, 9.989262) + @test pos ≈ Point3f(10.0, 0.08616829, 9.989262) end # For recreating the above: From 9b7ed17e48f371b54fb9c1f18a48bcdda249197b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 12 Jul 2023 14:25:39 +0200 Subject: [PATCH 75/80] fix Makie version --- CairoMakie/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index bd2a3657972..14337fe18d6 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -23,7 +23,7 @@ FFTW = "1" FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.1" -Makie = "=0.19.6" +Makie = "=0.20.0" PrecompileTools = "1.0" julia = "1.3" From 9c7f2533cec1559b1aa3605e79bc8b0d146ee046 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 12 Jul 2023 14:46:01 +0200 Subject: [PATCH 76/80] fix ubuntu test error --- GLMakie/test/unit_tests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 89e52d98396..a9b89ba0a6a 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -306,6 +306,7 @@ end # decrease the scale factor after-the-fact screen.scalefactor[] = 1 + yield() @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) # save images of different resolutions From e804e2131da1c935499ac19156ef8b6a074231ec Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 12 Jul 2023 15:26:34 +0200 Subject: [PATCH 77/80] remove yield --- GLMakie/test/unit_tests.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index a9b89ba0a6a..89e52d98396 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -306,7 +306,6 @@ end # decrease the scale factor after-the-fact screen.scalefactor[] = 1 - yield() @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) # save images of different resolutions From ea78111d8af6327485daeef35df393cf45e1902d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 12 Jul 2023 15:49:13 +0200 Subject: [PATCH 78/80] try colorbuffer() to fix test error --- GLMakie/test/unit_tests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 89e52d98396..ba7096941b1 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -306,6 +306,7 @@ end # decrease the scale factor after-the-fact screen.scalefactor[] = 1 + GLMakie.Makie.colorbuffer(screen) @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) # save images of different resolutions From 9d340028307c7a95859af342851cab06513918a7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 12 Jul 2023 17:04:24 +0200 Subject: [PATCH 79/80] try fixing line AA scaling --- GLMakie/assets/shader/line_segment.vert | 3 ++- GLMakie/assets/shader/lines.vert | 3 ++- GLMakie/src/drawing_primitives.jl | 21 ++++++++++++++++++--- GLMakie/src/glshaders/lines.jl | 10 +++++----- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/GLMakie/assets/shader/line_segment.vert b/GLMakie/assets/shader/line_segment.vert index 7e42313e132..50c17b9b05c 100644 --- a/GLMakie/assets/shader/line_segment.vert +++ b/GLMakie/assets/shader/line_segment.vert @@ -16,6 +16,7 @@ in float lastlen; uniform mat4 projectionview, model; uniform uint objectid; uniform float depth_shift; +uniform float px_per_unit; out uvec2 g_id; out vec4 g_color; @@ -42,7 +43,7 @@ void main() int index = gl_VertexID; g_id = uvec2(objectid, index+1); g_color = to_color(color, color_map, color_norm, index); - g_thickness = thickness; + g_thickness = px_per_unit * thickness; gl_Position = projectionview * model * to_vec4(vertex); gl_Position.z += gl_Position.w * depth_shift; } diff --git a/GLMakie/assets/shader/lines.vert b/GLMakie/assets/shader/lines.vert index b5f648c8bb2..d789fdbd04f 100644 --- a/GLMakie/assets/shader/lines.vert +++ b/GLMakie/assets/shader/lines.vert @@ -26,6 +26,7 @@ vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_ uniform mat4 projectionview, model; uniform uint objectid; uniform int total_length; +uniform float px_per_unit; out uvec2 g_id; out vec4 g_color; @@ -50,7 +51,7 @@ void main() int index = gl_VertexID; g_id = uvec2(objectid, index+1); g_valid_vertex = get_valid_vertex(valid_vertex); - g_thickness = thickness; + g_thickness = px_per_unit * thickness; g_color = _color(color, intensity, color_map, color_norm, index, total_length); #ifdef FAST_PATH diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 31dd51f8910..99c52abbc58 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -276,9 +276,15 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) positions = handle_view(x[1], data) space = get!(gl_attributes, :space, :data) # needs to happen before connect_camera! call connect_camera!(x, data, scene.camera) - transform_func = transform_func_obs(x) + # Tweak things for px_per_unit + resolution = pop!(data, :resolution) + px_per_unit = data[:px_per_unit] + data[:resolution] = map((ppu, res) -> ppu .* res, px_per_unit, resolution) + + transform_func = transform_func_obs(x) ls = to_value(linestyle) + if isnothing(ls) data[:pattern] = ls data[:fast] = true @@ -286,7 +292,9 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::Lines)) positions = apply_transform(transform_func, positions, space) else linewidth = gl_attributes[:thickness] - data[:pattern] = map((ls, lw) -> ls .* _mean(lw), linestyle, linewidth) + data[:pattern] = map(linestyle, linewidth, px_per_unit) do ls, lw, ppu + ppu * _mean(lw) .* ls + end data[:fast] = false pvm = map(*, data[:projectionview], data[:model]) @@ -311,13 +319,16 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::LineSegments return cached_robj!(screen, scene, x) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) + px_per_unit = data[:px_per_unit] ls = to_value(linestyle) if isnothing(ls) data[:pattern] = nothing data[:fast] = true else linewidth = gl_attributes[:thickness] - data[:pattern] = ls .* _mean(to_value(linewidth)) + data[:pattern] = map(linestyle, linewidth, px_per_unit) do ls, lw, ppu + ppu * _mean(lw) .* ls + end data[:fast] = false end space = get(gl_attributes, :space, :data) # needs to happen before connect_camera! call @@ -332,6 +343,10 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(x::LineSegments end connect_camera!(x, data, scene.camera) + # Tweak things for px_per_unit + resolution = pop!(data, :resolution) + data[:resolution] = map((ppu, res) -> ppu .* res, px_per_unit, resolution) + return draw_linesegments(screen, positions, data) end end diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 1fd0b61912d..f43b537a6d1 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -145,13 +145,13 @@ function draw_linesegments(screen, positions::VectorTypes{T}, data::Dict) where gl_primitive = GL_LINES pattern_length = 1f0 end - if !isa(pattern, Texture) && pattern !== nothing - if !isa(pattern, Vector) - error("Pattern needs to be a Vector of floats") + if !isa(pattern, Texture) && to_value(pattern) !== nothing + if !isa(to_value(pattern), Vector) + error("Pattern needs to be a Vector of floats. Found: $(typeof(pattern))") end - tex = GLAbstraction.Texture(ticks(pattern, 100), x_repeat = :repeat) + tex = GLAbstraction.Texture(map(pt -> ticks(pt, 100), pattern), x_repeat = :repeat) data[:pattern] = tex - data[:pattern_length] = Float32((last(pattern) - first(pattern))) + data[:pattern_length] = map(pt -> Float32(last(pt) - first(pt)), pattern) end return assemble_shader(data) end From cd71e69484234f4bfd5c801fa55af7336a515b69 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Mon, 31 Jul 2023 18:25:20 +0200 Subject: [PATCH 80/80] fix incorrect merge --- GLMakie/assets/shader/util.vert | 65 ++++++++++----------------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index 8a34d01fff5..be3bdadfcc1 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -123,25 +123,25 @@ uniform vec4 lowclip; uniform vec4 nan_color; vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange) { -float cmin = colorrange.x; -float cmax = colorrange.y; -if (value <= cmax && value >= cmin) { - // in value range, continue! -} else if (value < cmin) { -return lowclip; -} else if (value > cmax) { -return highclip; -} else { - // isnan CAN be broken (of course) -.- - // so if outside value range and not smaller/bigger min/max we assume NaN -return nan_color; -} -float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); - // 1/0 corresponds to the corner of the colormap, so to properly interpolate - // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). -float stepsize = 1.0 / float(textureSize(color_map, 0)); -i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; -return texture(color_map, i01); + float cmin = colorrange.x; + float cmax = colorrange.y; + if (value <= cmax && value >= cmin) { + // in value range, continue! + } else if (value < cmin) { + return lowclip; + } else if (value > cmax) { + return highclip; + } else { + // isnan CAN be broken (of course) -.- + // so if outside value range and not smaller/bigger min/max we assume NaN + return nan_color; + } + float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); + // 1/0 corresponds to the corner of the colormap, so to properly interpolate + // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). + float stepsize = 1.0 / float(textureSize(color_map, 0)); + i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; + return texture(color_map, i01); } @@ -271,30 +271,3 @@ void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection, vec3 l o_camdir = normalize(-view_pos).xyz; o_view_pos = view_pos.xyz / view_pos.w; } - -uniform vec4 highclip; -uniform vec4 lowclip; -uniform vec4 nan_color; - -vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange) { - float cmin = colorrange.x; - float cmax = colorrange.y; - if (value <= cmax && value >= cmin) { - // in value range, continue! - } else if (value < cmin) { - return lowclip; - } else if (value > cmax) { - return highclip; - } else { - // isnan CAN be broken (of course) -.- - // so if outside value range and not smaller/bigger min/max we assume NaN - return nan_color; - } - float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); - // 1/0 corresponds to the corner of the colormap, so to properly interpolate - // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). - float stepsize = 1.0 / float(textureSize(color_map, 0)); - i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; - return texture(color_map, i01); - -}