From 5c7aafad5b6f1411976498b3f64f6ca00fbfeed2 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 16 Dec 2024 15:57:48 +0530 Subject: [PATCH 1/3] Fix CairoMakie not being able to plot exotic point arrays in lines (#4668) * Make type annotation on `points` more abstract * Implement the function barrier approach * Fix typo * Add a test to the CairoMakie tests * Add a changelog entry * Remove type annotation to enable 2D->3D * mention lines in changelog --------- Co-authored-by: Frederic Freyer --- CHANGELOG.md | 1 + CairoMakie/src/utils.jl | 15 ++++++++++++--- CairoMakie/test/runtests.jl | 11 +++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdfe0a97768..da6ca7d4c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Added `transform_marker` attribute to meshscatter and changed the default behavior to not transform marker/mesh vertices [#4606](https://github.com/MakieOrg/Makie.jl/pull/4606) - Fixed some issues with meshscatter not correctly transforming with transform functions and float32 rescaling [#4606](https://github.com/MakieOrg/Makie.jl/pull/4606) - Fixed `poly` pipeline for 3D and/or Float64 polygons that begin from an empty vector [#4615](https://github.com/MakieOrg/Makie.jl/pull/4615). +- Fixed an issue where `reinterpret`ed arrays of line points were not handled correctly in CairoMakie [#4668](https://github.com/MakieOrg/Makie.jl/pull/4668). ## [0.21.18] - 2024-12-12 diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index bc99af64138..766d9c2a2c1 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -193,7 +193,18 @@ end -function project_line_points(scene, plot::T, positions, colors, linewidths) where {T <: Union{Lines, LineSegments}} +function project_line_points(scene, plot::T, positions::AbstractArray{<: Makie.VecTypes{N, FT}}, colors, linewidths) where {T <: Union{Lines, LineSegments}, N, FT <: Real} + + # Standard transform from input space to clip space + # Note that this is type unstable, so there is a function barrier in place. + space = (plot.space[])::Symbol + points = Makie.apply_transform(transform_func(plot), positions, space) + + return project_transformed_line_points(scene, plot, points, colors, linewidths) +end + +function project_transformed_line_points(scene, plot::T, points::AbstractArray{<: Makie.VecTypes{N, FT}}, colors, linewidths) where {T <: Union{Lines, LineSegments}, N, FT <: Real} + # Note that here, `points` has already had `transform_func` applied. # If colors are defined per point they need to be interpolated like positions # at clip planes per_point_colors = colors isa AbstractArray @@ -201,8 +212,6 @@ function project_line_points(scene, plot::T, positions, colors, linewidths) wher space = (plot.space[])::Symbol model = (plot.model[])::Mat4d - # Standard transform from input space to clip space - points = Makie.apply_transform(transform_func(plot), positions, space)::typeof(positions) f32convert = Makie.f32_convert_matrix(scene.float32convert, space) transform = Makie.space_to_clip(scene.camera, space) * f32convert * model clip_points = map(points) do point diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index a506645e4e3..0813c6d337e 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -304,4 +304,15 @@ end ps, _, _ = CairoMakie.project_line_points(a.scene, ls, ls_points, nothing, nothing) @test length(ps) >= 6 # at least 6 points: [2,3,3,4,4,5] @test all(ref -> findfirst(p -> isapprox(p, ref, atol = 1e-4), ps) !== nothing, necessary_points) + + # Check that `reinterpret`ed arrays of points are handled correctly + # ref. https://github.com/MakieOrg/Makie.jl/issues/4661 + + data = reinterpret(Point2f, rand(Point2f, 10) .=> rand(Point2f, 10)) + + f, a, p = lines(data) + Makie.update_state_before_display!(f) + ps, _, _ = @test_nowarn CairoMakie.project_line_points(a.scene, p, data, nothing, nothing) + @test length(ps) == length(data) # this should never clip! + end \ No newline at end of file From f29e93a8c0c91994421ef8fdb91ff04137ed4598 Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Mon, 16 Dec 2024 14:46:31 +0100 Subject: [PATCH 2/3] Fix poly Circle & Rect stroke (#4664) * fix discontinuous Rect2 stroke and wireframe-like CIrcle stroke * extend poly refimg test * fix test (axis background poly now uses lines, so +2 shaders) * switch to heatmap to generate more shaders * Update CHANGELOG.md --- CHANGELOG.md | 1 + GLMakie/test/unit_tests.jl | 34 +++++++++++++------------- ReferenceTests/src/tests/examples2d.jl | 6 +++++ src/basic_recipes/poly.jl | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da6ca7d4c11..5aab35c4e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Added `transform_marker` attribute to meshscatter and changed the default behavior to not transform marker/mesh vertices [#4606](https://github.com/MakieOrg/Makie.jl/pull/4606) - Fixed some issues with meshscatter not correctly transforming with transform functions and float32 rescaling [#4606](https://github.com/MakieOrg/Makie.jl/pull/4606) - Fixed `poly` pipeline for 3D and/or Float64 polygons that begin from an empty vector [#4615](https://github.com/MakieOrg/Makie.jl/pull/4615). +- Fixed gaps in corners of `poly(Rect2(...))` stroke [#4664](https://github.com/MakieOrg/Makie.jl/pull/4664) - Fixed an issue where `reinterpret`ed arrays of line points were not handled correctly in CairoMakie [#4668](https://github.com/MakieOrg/Makie.jl/pull/4668). ## [0.21.18] - 2024-12-12 diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 2f7a380570c..c14d1ba7504 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -9,7 +9,7 @@ end @testset "shader cache" begin GLMakie.closeall() screen = display(GLMakie.Screen(visible = false), Figure()) - cache = screen.shader_cache + cache = screen.shader_cache; # Postprocessing shaders @test length(cache.shader_cache) == 5 @test length(cache.template_cache) == 5 @@ -17,33 +17,33 @@ end # Shaders for scatter + linesegments + poly etc (axis) display(screen, scatter(1:4)) - @test length(cache.shader_cache) == 16 - @test length(cache.template_cache) == 16 - @test length(cache.program_cache) == 10 + @test length(cache.shader_cache) == 18 + @test length(cache.template_cache) == 18 + @test length(cache.program_cache) == 11 # No new shaders should be added: display(screen, scatter(1:4)) - @test length(cache.shader_cache) == 16 - @test length(cache.template_cache) == 16 - @test length(cache.program_cache) == 10 + @test length(cache.shader_cache) == 18 + @test length(cache.template_cache) == 18 + @test length(cache.program_cache) == 11 # Same for linesegments display(screen, linesegments(1:4)) - @test length(cache.shader_cache) == 16 - @test length(cache.template_cache) == 16 - @test length(cache.program_cache) == 10 - - # Lines hasn't been compiled so one new program should be added - display(screen, lines(1:4)) @test length(cache.shader_cache) == 18 @test length(cache.template_cache) == 18 @test length(cache.program_cache) == 11 + # heatmap hasn't been compiled so one new program should be added + display(screen, heatmap([1,2,2.5,3], [1,2,2.5,3], rand(4,4))) + @test length(cache.shader_cache) == 20 + @test length(cache.template_cache) == 20 + @test length(cache.program_cache) == 12 + # For second time no new shaders should be added - display(screen, lines(1:4)) - @test length(cache.shader_cache) == 18 - @test length(cache.template_cache) == 18 - @test length(cache.program_cache) == 11 + display(screen, heatmap([1,2,2.5,3], [1,2,2.5,3], rand(4,4))) + @test length(cache.shader_cache) == 20 + @test length(cache.template_cache) == 20 + @test length(cache.program_cache) == 12 end @testset "unit tests" begin diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index de209fdb03b..a43731aa7ff 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -151,6 +151,12 @@ end linesegments!(ax, [Point2f(50 + i, 50 + i) => Point2f(i + 70, i + 70) for i = 1:100:400], linewidth=8, color=:purple ) + poly!(ax, [Polygon(decompose(Point2f, Rect2f(150, 0, 100, 100))), Polygon(decompose(Point2f, Circle(Point2f(350, 200), 50)))], + color=:gray, strokewidth=10, strokecolor=:red) + # single objects + poly!(ax, Circle(Point2f(50, 350), 50), color=:gray, strokewidth=10, strokecolor=:red) + poly!(ax, Rect2f(0, 150, 100, 100), color=:gray, strokewidth=10, strokecolor=:red) + poly!(ax, Polygon(decompose(Point2f, Rect2f(150, 300, 100, 100))), color=:gray, strokewidth=10, strokecolor=:red) fig end diff --git a/src/basic_recipes/poly.jl b/src/basic_recipes/poly.jl index 7d361df8df8..4f41e919c4c 100644 --- a/src/basic_recipes/poly.jl +++ b/src/basic_recipes/poly.jl @@ -146,7 +146,7 @@ function to_lines(polygon::AbstractVector{<: VecTypes}) return result end -function plot!(plot::Poly{<: Tuple{<: Union{Polygon, MultiPolygon, AbstractVector{<: PolyElements}}}}) +function plot!(plot::Poly{<: Tuple{<: Union{Polygon, MultiPolygon, Rect2, Circle, AbstractVector{<: PolyElements}}}}) geometries = plot[1] transform_func = plot.transformation.transform_func meshes = lift(poly_convert, plot, geometries, transform_func) From 711a3ac1e811229ac0c33de1ac3a3f1d25bd5fae Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Mon, 16 Dec 2024 15:57:39 +0100 Subject: [PATCH 3/3] add test for Billboard + transform_marker & fix CairoMakie scatter with markerspace = :data, rotation, transform_marker (#4663) * add test for Billboard + transform_marker * temp fix 3D char scatter (data space, transform_marker = true, ...) * fix other marker types + cleanup * expand test to cover all marker types (except FastPixel) * enable now working refimages * fix var name * fix test variations * remove now unused project_scale * fix precompile call syntax * fix image corruption on specific rotation * minor cleanup * update changelog [skip ci] --- CHANGELOG.md | 1 + CairoMakie/src/cairo-extension.jl | 8 ++ CairoMakie/src/precompiles.jl | 4 +- CairoMakie/src/primitives.jl | 166 +++++++++++-------------- CairoMakie/src/utils.jl | 50 +++++--- CairoMakie/test/runtests.jl | 2 - ReferenceTests/src/tests/primitives.jl | 42 +++++++ src/camera/projection_math.jl | 4 + 8 files changed, 167 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aab35c4e50..b62f2c7bead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Fixed `poly` pipeline for 3D and/or Float64 polygons that begin from an empty vector [#4615](https://github.com/MakieOrg/Makie.jl/pull/4615). - Fixed gaps in corners of `poly(Rect2(...))` stroke [#4664](https://github.com/MakieOrg/Makie.jl/pull/4664) - Fixed an issue where `reinterpret`ed arrays of line points were not handled correctly in CairoMakie [#4668](https://github.com/MakieOrg/Makie.jl/pull/4668). +- Fixed various issues with `markerspace = :data`, `transform_marker = true` and `rotation` for scatter in CairoMakie (incorrect marker transformations, ignored transformations, Cairo state corruption) [#4663](https://github.com/MakieOrg/Makie.jl/pull/4663) ## [0.21.18] - 2024-12-12 diff --git a/CairoMakie/src/cairo-extension.jl b/CairoMakie/src/cairo-extension.jl index 34e620f7e2c..4857f666c9b 100644 --- a/CairoMakie/src/cairo-extension.jl +++ b/CairoMakie/src/cairo-extension.jl @@ -28,6 +28,14 @@ function cairo_font_face_destroy(font_face) ) end +function cairo_transform(ctx, cairo_matrix) + ccall( + (:cairo_transform, Cairo.libcairo), + Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), + ctx.ptr, Ref(cairo_matrix) + ) +end + function set_ft_font(ctx, font) font_face = Base.@lock font.lock ccall( diff --git a/CairoMakie/src/precompiles.jl b/CairoMakie/src/precompiles.jl index 5f06373c6e9..d6cdbef3efa 100644 --- a/CairoMakie/src/precompiles.jl +++ b/CairoMakie/src/precompiles.jl @@ -18,7 +18,7 @@ end precompile(openurl, (String,)) precompile(draw_atomic_scatter, (Scene, Cairo.CairoContext, Tuple{typeof(identity),typeof(identity)}, Vector{ColorTypes.RGBA{Float32}}, Vec{2,Float32}, ColorTypes.RGBA{Float32}, - Float32, BezierPath, Vec{2,Float32}, Quaternionf, + Float32, BezierPath, Vec{3,Float32}, Quaternionf, Mat4f, Vector{Point{2,Float32}}, Mat4f, Makie.FreeTypeAbstraction.FTFont, Symbol, - Symbol)) + Symbol, Vector{Plane3f}, Bool)) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 2234ab7e5c0..306da4c21f8 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -325,44 +325,56 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Scat markerspace = primitive.markerspace[] space = primitive.space[] transfunc = Makie.transform_func(primitive) + billboard = primitive.rotation[] isa Billboard return draw_atomic_scatter(scene, ctx, transfunc, colors, markersize, strokecolor, strokewidth, marker, marker_offset, rotation, model, positions, size_model, font, markerspace, - space, clip_planes) + space, clip_planes, billboard) end function draw_atomic_scatter( scene, ctx, transfunc, colors, markersize, strokecolor, strokewidth, marker, marker_offset, rotation, model, positions, size_model, font, - markerspace, space, clip_planes + markerspace, space, clip_planes, billboard ) transformed = apply_transform(transfunc, positions, space) indices = unclipped_indices(to_model_space(model, clip_planes), transformed, space) - projected_positions = project_position(scene, space, transformed, indices, model) + transform = Makie.clip_to_space(scene.camera, markerspace) * + Makie.space_to_clip(scene.camera, space) * + Makie.f32_convert_matrix(scene.float32convert, space) * + model + model33 = size_model[Vec(1,2,3), Vec(1,2,3)] - Makie.broadcast_foreach_index(projected_positions, indices, colors, markersize, strokecolor, + Makie.broadcast_foreach_index(view(transformed, indices), indices, colors, markersize, strokecolor, strokewidth, marker, marker_offset, remove_billboard(rotation)) do pos, col, markersize, strokecolor, strokewidth, m, mo, rotation isnan(pos) && return isnan(rotation) && return # matches GLMakie + isnan(markersize) && return - scale = project_scale(scene, markerspace, markersize, size_model) - offset = project_scale(scene, markerspace, mo, size_model) + p4d = transform * to_ndim(Point4d, to_ndim(Point3d, pos, 0), 1) + o = p4d[Vec(1, 2, 3)] ./ p4d[4] .+ model33 * to_ndim(Vec3d, mo, 0) + proj_pos, mat, jl_mat = project_marker(scene, markerspace, o, + markersize, rotation, size_model, billboard) - Cairo.set_source_rgba(ctx, rgbatuple(col)...) + # mat and jl_mat are the same matrix, once as a CairoMatrix, once as a Mat2f + # They both describe an approximate basis transformation matrix from + # marker space to pixel space with scaling appropriate to markersize. + # Markers that can be drawn from points/vertices of shape (e.g. Rect) + # could be projected more accurately by projecting each point individually + # and then building the shape. + # Enclosed area of the marker must be at least 1 pixel? + (abs(det(jl_mat)) < 1) && return + + Cairo.set_source_rgba(ctx, rgbatuple(col)...) Cairo.save(ctx) - # Setting a markersize of 0.0 somehow seems to break Cairos global state? - # At least it stops drawing any marker afterwards - # TODO, maybe there's something wrong somewhere else? - if !(isnan(scale) || norm(scale) ≈ 0.0) - if m isa Char - draw_marker(ctx, m, best_font(m, font), pos, scale, strokecolor, strokewidth, offset, rotation) - else - draw_marker(ctx, m, pos, scale, strokecolor, strokewidth, offset, rotation) - end + if m isa Char + draw_marker(ctx, m, best_font(m, font), proj_pos, strokecolor, strokewidth, jl_mat, mat) + else + draw_marker(ctx, m, proj_pos, strokecolor, strokewidth, mat) end Cairo.restore(ctx) end @@ -370,32 +382,30 @@ function draw_atomic_scatter( return end -function draw_marker(ctx, marker::Char, font, pos, scale, strokecolor, strokewidth, marker_offset, rotation) +function draw_marker(ctx, marker::Char, font, pos, strokecolor, strokewidth, jl_mat, mat) cairoface = set_ft_font(ctx, font) + # The given pos includes the user position which corresponds to the center + # of the marker and the user marker_offset which may shift the position. + # At this point we still need to center the character we draw. For that we + # get the character boundingbox where (0,0) is the anchor point: charextent = Makie.FreeTypeAbstraction.get_extent(font, marker) inkbb = Makie.FreeTypeAbstraction.inkboundingbox(charextent) - # scale normalized bbox by font size - inkbb_scaled = Rect2f(origin(inkbb) .* scale, widths(inkbb) .* scale) + # And calculate an offset to the the center of the marker + centering_offset = Makie.origin(inkbb) .+ 0.5f0 .* widths(inkbb) + # which we then transform from marker space to screen space using the + # local coordinate transform derived by project_marker() + # (Need yflip because Cairo's y coordinates are reversed) + char_offset = Vec2f(jl_mat * ((1, -1) .* centering_offset)) - # flip y for the centering shift of the character because in Cairo y goes down - centering_offset = Vec2f(1, -1) .* (-origin(inkbb_scaled) .- 0.5f0 .* widths(inkbb_scaled)) - # this is the origin where we actually have to place the glyph so it can be centered - charorigin = pos .+ Vec2f(marker_offset[1], -marker_offset[2]) - old_matrix = get_font_matrix(ctx) - set_font_matrix(ctx, scale_matrix(scale...)) - - # First, we translate to the point where the - # marker is supposed to go. + # The offset is then applied to pos and the marker placement is set + charorigin = pos - char_offset Cairo.translate(ctx, charorigin[1], charorigin[2]) - # Then, we rotate the context by the - # appropriate amount, - Cairo.rotate(ctx, to_2d_rotation(rotation)) - # and apply a centering offset to account for - # the fact that text is shown from the (relative) - # bottom left corner. - Cairo.translate(ctx, centering_offset[1], centering_offset[2]) + + # The font matrix takes care of rotation, scaling and shearing of the marker + old_matrix = get_font_matrix(ctx) + set_font_matrix(ctx, mat) Cairo.move_to(ctx, 0, 0) Cairo.text_path(ctx, string(marker)) @@ -409,49 +419,41 @@ function draw_marker(ctx, marker::Char, font, pos, scale, strokecolor, strokewid cairo_font_face_destroy(cairoface) set_font_matrix(ctx, old_matrix) + return end -function draw_marker(ctx, ::Type{<: Circle}, pos, scale, strokecolor, strokewidth, marker_offset, rotation) - pos += Point2f(marker_offset[1], -marker_offset[2]) - - if scale[1] != scale[2] - old_matrix = Cairo.get_matrix(ctx) - Cairo.scale(ctx, scale[1], scale[2]) - Cairo.translate(ctx, pos[1]/scale[1], pos[2]/scale[2]) - Cairo.arc(ctx, 0, 0, 0.5, 0, 2*pi) - else - Cairo.arc(ctx, pos[1], pos[2], scale[1]/2, 0, 2*pi) - end - +function draw_marker(ctx, ::Type{<: Circle}, pos, strokecolor, strokewidth, mat) + # There are already active transforms so we can't Cairo.set_matrix() here + Cairo.translate(ctx, pos[1], pos[2]) + cairo_transform(ctx, mat) + Cairo.arc(ctx, 0, 0, 0.5, 0, 2*pi) Cairo.fill_preserve(ctx) - Cairo.set_line_width(ctx, Float64(strokewidth)) - sc = to_color(strokecolor) Cairo.set_source_rgba(ctx, rgbatuple(sc)...) Cairo.stroke(ctx) - scale[1] != scale[2] && Cairo.set_matrix(ctx, old_matrix) - nothing + return end -function draw_marker(ctx, ::Union{Makie.FastPixel,<:Type{<:Rect}}, pos, scale, strokecolor, strokewidth, - marker_offset, rotation) - - Cairo.translate(ctx, pos[1] + marker_offset[1], pos[2] - marker_offset[2]) - Cairo.rotate(ctx, to_2d_rotation(rotation)) - Cairo.rectangle(ctx, -0.5scale[1], -0.5scale[2], scale...) +function draw_marker(ctx, ::Union{Makie.FastPixel,<:Type{<:Rect}}, pos, strokecolor, strokewidth, mat) + # There are already active transforms so we can't Cairo.set_matrix() here + Cairo.translate(ctx, pos[1], pos[2]) + cairo_transform(ctx, mat) + Cairo.rectangle(ctx, -0.5, -0.5, 1, 1) Cairo.fill_preserve(ctx) Cairo.set_line_width(ctx, Float64(strokewidth)) sc = to_color(strokecolor) Cairo.set_source_rgba(ctx, rgbatuple(sc)...) Cairo.stroke(ctx) + return end -function draw_marker(ctx, beziermarker::BezierPath, pos, scale, strokecolor, strokewidth, marker_offset, rotation) +function draw_marker(ctx, beziermarker::BezierPath, pos, strokecolor, strokewidth, mat) Cairo.save(ctx) - Cairo.translate(ctx, pos[1] + marker_offset[1], pos[2] - marker_offset[2]) - Cairo.rotate(ctx, to_2d_rotation(rotation)) - Cairo.scale(ctx, scale[1], -scale[2]) # flip y for cairo + # There are already active transforms so we can't Cairo.set_matrix() here + Cairo.translate(ctx, pos[1], pos[2]) + cairo_transform(ctx, mat) + Cairo.scale(ctx, 1, -1) # maybe to transition BezierPath y to Cairo y? draw_path(ctx, beziermarker) Cairo.fill_preserve(ctx) sc = to_color(strokecolor) @@ -459,6 +461,7 @@ function draw_marker(ctx, beziermarker::BezierPath, pos, scale, strokecolor, str Cairo.set_line_width(ctx, Float64(strokewidth)) Cairo.stroke(ctx) Cairo.restore(ctx) + return end function draw_path(ctx, bp::BezierPath) @@ -495,9 +498,9 @@ function path_command(ctx, c::EllipticalArc) end -function draw_marker(ctx, marker::Matrix{T}, pos, scale, - strokecolor #= unused =#, strokewidth #= unused =#, - marker_offset, rotation) where T<:Colorant +function draw_marker(ctx, marker::Matrix{T}, pos, + strokecolor #= unused =#, strokewidth #= unused =#, + mat) where T<:Colorant # convert marker to Cairo compatible image data marker = permutedims(marker, (2,1)) @@ -505,14 +508,15 @@ function draw_marker(ctx, marker::Matrix{T}, pos, scale, w, h = size(marker) - Cairo.translate(ctx, pos[1] + marker_offset[1], pos[2] - marker_offset[2]) - Cairo.rotate(ctx, to_2d_rotation(rotation)) - Cairo.scale(ctx, scale[1] / w, scale[2] / h) + # There are already active transforms so we can't Cairo.set_matrix() here + Cairo.translate(ctx, pos[1], pos[2]) + cairo_transform(ctx, mat) + Cairo.scale(ctx, 1.0 / w, 1.0 / h) Cairo.set_source_surface(ctx, marker_surf, -w/2, -h/2) Cairo.paint(ctx) + return end - ################################################################################ # Text # ################################################################################ @@ -616,28 +620,8 @@ function draw_glyph_collection( return end - scale3 = scale isa Number ? Point3f(scale, scale, 0) : to_ndim(Point3f, scale, 0) - - # the CairoMatrix is found by transforming the right and up vector - # of the character into screen space and then subtracting the projected - # origin. The resulting vectors give the directions in which the character - # needs to be stretched in order to match the 3D projection - - xvec = rotation * (scale3[1] * Point3d(1, 0, 0)) - yvec = rotation * (scale3[2] * Point3d(0, -1, 0)) - - glyphpos = _project_position(scene, markerspace, gp3, id, true) - xproj = _project_position(scene, markerspace, gp3 + model33 * xvec, id, true) - yproj = _project_position(scene, markerspace, gp3 + model33 * yvec, id, true) - - xdiff = xproj - glyphpos - ydiff = yproj - glyphpos - - mat = Cairo.CairoMatrix( - xdiff[1], xdiff[2], - ydiff[1], ydiff[2], - 0, 0, - ) + scale2 = scale isa Number ? Vec2d(scale, scale) : scale + glyphpos, mat, _ = project_marker(scene, markerspace, gp3, scale2, rotation, model33, id) Cairo.save(ctx) set_font_matrix(ctx, mat) @@ -1282,7 +1266,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki scale = Makie.voxel_size(primitive) colors = Makie.voxel_colors(primitive) marker = GeometryBasics.expand_faceviews(normal_mesh(Rect3f(Point3f(-0.5), Vec3f(1)))) - + # transformation to world space transformed_pos = _transform_to_world(scene, primitive, pos) diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 766d9c2a2c1..d15f44813e3 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -92,22 +92,42 @@ function project_position(@nospecialize(scenelike), space, point, model, yflip:: project_position(scene, Makie.transform_func(scenelike), space, point, model, yflip) end -function project_scale(scene::Scene, space, s::Number, model = Mat4d(I)) - project_scale(scene, space, Vec2d(s), model) -end - -function project_scale(scene::Scene, space, s, model = Mat4d(I)) - p4d = model * to_ndim(Vec4d, s, 0) - if is_data_space(space) - @inbounds p = (scene.camera.projectionview[] * p4d)[Vec(1, 2)] - return p .* scene.camera.resolution[] .* 0.5 - elseif is_pixel_space(space) - return p4d[Vec(1, 2)] - elseif is_relative_space(space) - return p4d[Vec(1, 2)] .* scene.camera.resolution[] - else # clip - return p4d[Vec(1, 2)] .* scene.camera.resolution[] .* 0.5f0 +function project_marker(scene, markerspace, origin, scale, rotation, model, billboard = false) + scale3 = to_ndim(Vec2d, scale, first(scale)) + model33 = model[Vec(1,2,3), Vec(1,2,3)] + origin3 = to_ndim(Point3d, origin, 0) + return project_marker(scene, markerspace, origin3, scale3, rotation, model33, Mat4d(I), billboard) +end +function project_marker(scene, markerspace, origin::Point3, scale::Vec, rotation, model33::Mat3, id = Mat4d(I), billboard = false) + # the CairoMatrix is found by transforming the right and up vector + # of the marker into screen space and then subtracting the projected + # origin. The resulting vectors give the directions in which the character + # needs to be stretched in order to match the 3D projection + + xvec = rotation * (model33 * (scale[1] * Point3d(1, 0, 0))) + yvec = rotation * (model33 * (scale[2] * Point3d(0, -1, 0))) + + proj_pos = _project_position(scene, markerspace, origin, id, true) + + if billboard && Makie.is_data_space(markerspace) + p4d = scene.camera.view[] * to_ndim(Point4d, origin, 1) + xproj = _project_position(scene, :eye, p4d[Vec(1,2,3)] / p4d[4] + xvec, id, true) + yproj = _project_position(scene, :eye, p4d[Vec(1,2,3)] / p4d[4] + yvec, id, true) + else + xproj = _project_position(scene, markerspace, origin + xvec, id, true) + yproj = _project_position(scene, markerspace, origin + yvec, id, true) end + + xdiff = xproj - proj_pos + ydiff = yproj - proj_pos + + mat = Cairo.CairoMatrix( + xdiff[1], xdiff[2], + ydiff[1], ydiff[2], + 0, 0, + ) + + return proj_pos, mat, Mat2f(xdiff..., ydiff...) end function project_shape(@nospecialize(scenelike), space, rect::Rect, model) diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index 0813c6d337e..c621d82b9de 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -191,9 +191,7 @@ excludes = Set([ "Textured meshscatter", # not yet implemented "Voxel - texture mapping", # not yet implemented "Miter Joints for line rendering", # CairoMakie does not show overlap here - "Scatter with FastPixel", # almost works, but scatter + markerspace=:data seems broken for 3D "picking", # Not implemented - "scatter marker_offset 3D with rotation", # missing support for 3D scatter with markerspace = :data "MetaMesh (Sponza)", # makes little sense without per pixel depth order ]) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 360a4041c18..ddae6dc6b0c 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -225,6 +225,48 @@ end s end +function make_billboard_rotations_test_fig(marker) + r = Rect3f(Point3f(-0.5), Vec3f(1)) + ps = coordinates(r) + phis = collect(range(0, 2pi, length = 8)) + quats = Makie.to_rotation(phis) + + fig = Figure(size = (500, 800)) + for (k, transform_marker) in zip(0:1, [true, false]) + for (i, space, ms) in zip(1:2, [:data, :pixel], [1, 30]) + for (j, rot, lbl) in zip(1:3, [Billboard(phis), phis, quats], ["Billboard", "angles", "Quaternion"]) + Label(fig[i+2k, j][1,1], "$space | $lbl | $transform_marker", tellwidth = false) + a, p = scatter(fig[i+2k, j][2,1], ps, marker = marker, + strokewidth = 2, strokecolor = :black, color = :yellow, + markersize = ms, markerspace = space, rotation = rot, + transform_marker = transform_marker) + + Makie.rotate!(p, Vec3f(1, 0.5, 0.1), 1.0) + a.scene.camera_controls.settings[:center] = false + Makie.update_cam!(a.scene, r) + end + end + end + + fig +end + +@reference_test "scatter Billboard and transform_marker for Char markers" begin + make_billboard_rotations_test_fig('L') +end +@reference_test "scatter Billboard and transform_marker for Rect markers" begin + make_billboard_rotations_test_fig(Rect) +end +@reference_test "scatter Billboard and transform_marker for Circle markers" begin + make_billboard_rotations_test_fig(Circle) +end +@reference_test "scatter Billboard and transform_marker for BezierPath markers" begin + make_billboard_rotations_test_fig(:utriangle) +end +@reference_test "scatter Billboard and transform_marker for image markers" begin + make_billboard_rotations_test_fig(Makie.FileIO.load(Makie.assetpath("cow.png"))) +end + @reference_test "scatter with stroke" begin s = Scene(size = (350, 700), camera = campixel!) diff --git a/src/camera/projection_math.jl b/src/camera/projection_math.jl index f0c6f8b2f94..7fb24056f4c 100644 --- a/src/camera/projection_math.jl +++ b/src/camera/projection_math.jl @@ -416,6 +416,8 @@ end function space_to_clip(cam::Camera, space::Symbol, projectionview::Bool=true) if is_data_space(space) return projectionview ? cam.projectionview[] : cam.projection[] + elseif space == :eye + return cam.projection[] elseif is_pixel_space(space) return cam.pixel_space[] elseif is_relative_space(space) @@ -430,6 +432,8 @@ end function clip_to_space(cam::Camera, space::Symbol) if is_data_space(space) return inv(cam.projectionview[]) + elseif space == :eye + return inv(cam.projectionview[]) elseif is_pixel_space(space) w, h = cam.resolution[] return Mat4d(0.5w, 0, 0, 0, 0, 0.5h, 0, 0, 0, 0, -10_000, 0, 0.5w, 0.5h, 0, 1) # -10_000