diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ef892f55e..06436d6af65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Remove StableHashTraits in favor of calculating hashes directly with CRC32c [#3667](https://github.com/MakieOrg/Makie.jl/pull/3667). - **Breaking (sort of)** Added a new `@recipe` variant which allows documenting attributes directly where they are defined and validating that all attributes are known whenever a plot is created. This is not breaking in the sense that the API changes, but user code is likely to break because of misspelled attribute names etc. that have so far gone unnoticed. - Add axis converts, enabling unit/categorical support and more [#3226](https://github.com/MakieOrg/Makie.jl/pull/3226). +- **Breaking** Streamline `data_limits` and `boundingbox` [#3671](https://github.com/MakieOrg/Makie.jl/pull/3671) + - `data_limits` now only considers plot positions, completely ignoring transformations + - `boundingbox(::Text)` is deprecated in favor of `text_boundingbox(::Text)` + - `boundingbox` now always consider `transform_func` and `model` (except for Text for the time being) - **Breaking** Reworked line shaders in GLMakie and WGLMakie [#3558](https://github.com/MakieOrg/Makie.jl/pull/3558) - GLMakie: Removed support for per point linewidths - GLMakie: Adjusted dots (e.g. with `linestyle = :dot`) to bend across a joint @@ -17,6 +21,7 @@ - Both: Adjusted handling of thin lines which may result in different color intensities - Fixed an issue with lines being drawn in the wrong direction in 3D (with perspective projection) [#3651](https://github.com/MakieOrg/Makie.jl/pull/3651). + ## [0.20.8] - 2024-02-22 - Fixed excessive use of space with HTML image outputs [#3642](https://github.com/MakieOrg/Makie.jl/pull/3642). diff --git a/CairoMakie/src/CairoMakie.jl b/CairoMakie/src/CairoMakie.jl index 57b9940e216..04b1c4b6c7f 100644 --- a/CairoMakie/src/CairoMakie.jl +++ b/CairoMakie/src/CairoMakie.jl @@ -12,6 +12,7 @@ using Makie: to_value, to_colormap, extrema_nan using Makie.Observables using Makie: spaces, is_data_space, is_pixel_space, is_relative_space, is_clip_space using Makie: numbers_to_colors +using Makie: Mat3f, Mat4f, Mat3d, Mat4d # re-export Makie, including deprecated names for name in names(Makie, all=true) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 09499f2aa9b..45ba0779667 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -33,8 +33,10 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio # Standard transform from input space to clip space points = Makie.apply_transform(Makie.transform_func(primitive), positions, space) res = scene.camera.resolution[] - transform = Makie.space_to_clip(scene.camera, space) * model - clip_points = map(p -> transform * to_ndim(Vec4f, to_ndim(Vec3f, p, 0f0), 1f0), points) + + f32convert = Makie.f32_convert_matrix(scene.float32convert, space) + transform = Makie.space_to_clip(scene.camera, space) * model * f32convert + clip_points = map(p -> transform * to_ndim(Vec4d, to_ndim(Vec3d, p, 0), 1), points) # yflip and clip -> screen/pixel coords function clip2screen(res, p) @@ -374,7 +376,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Scat model = primitive.model[] positions = primitive[1][] isempty(positions) && return - size_model = transform_marker ? model : Mat4f(I) + size_model = transform_marker ? model : Mat4d(I) font = to_font(to_value(get(primitive, :font, Makie.defaultfont()))) @@ -630,7 +632,7 @@ function draw_glyph_collection( strokecolors = glyph_collection.strokecolors model = _deref(_model) - model33 = transform_marker ? model[Vec(1, 2, 3), Vec(1, 2, 3)] : Mat3f(I) + model33 = transform_marker ? model[Vec(1, 2, 3), Vec(1, 2, 3)] : Mat3d(I) id = Mat4f(I) glyph_pos = let @@ -639,7 +641,7 @@ function draw_glyph_collection( Makie.clip_to_space(scene.camera, markerspace) * Makie.space_to_clip(scene.camera, space) * - model * to_ndim(Point4f, to_ndim(Point3f, p, 0), 1) + model * to_ndim(Point4d, to_ndim(Point3d, p, 0), 1) end Cairo.save(ctx) @@ -674,8 +676,8 @@ function draw_glyph_collection( # 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] * Point3f(1, 0, 0)) - yvec = rotation * (scale3[2] * Point3f(0, -1, 0)) + 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) @@ -767,7 +769,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio else ys = regularly_spaced_array_to_range(ys) end - model = primitive.model[]::Mat4f + model = primitive.model[]::Mat4d interpolate = to_value(primitive.interpolate) # Debug attribute we can set to disable fastpath @@ -901,10 +903,10 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki end function draw_mesh2D(scene, screen, @nospecialize(plot), @nospecialize(mesh)) - vs = decompose(Point2f, mesh)::Vector{Point2f} + vs = decompose(Point2f, mesh)::Vector{Point2f} fs = decompose(GLTriangleFace, mesh)::Vector{GLTriangleFace} uv = decompose_uv(mesh)::Union{Nothing, Vector{Vec2f}} - model = plot.model[]::Mat4f + model = plot.model[]::Mat4d color = hasproperty(mesh, :color) ? to_color(mesh.color) : plot.calculated_colors[] cols = per_face_colors(color, nothing, fs, nothing, uv) space = to_value(get(plot, :space, :data))::Symbol @@ -971,7 +973,7 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f per_face_col = per_face_colors(color, matcap, meshfaces, meshnormals, meshuvs) - model = attributes.model[]::Mat4f + model = attributes.model[]::Mat4d space = to_value(get(attributes, :space, :data))::Symbol func = Makie.transform_func(attributes) @@ -1003,7 +1005,7 @@ function draw_mesh3D( i = Vec(1, 2, 3) normalmatrix = transpose(inv(model[i, i])) - local_model = rotation * Makie.scalematrix(Vec3f(scale)) + local_model = rotation * Makie.scalematrix(Vec3d(scale)) # pass transform_func as argument to function, so that we get a function barrier # and have `transform_func` be fully typed inside closure vs = broadcast(meshpoints, (transform_func,)) do v, f diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 9f87e61af9d..fbb50129574 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -8,10 +8,35 @@ function project_position(scene::Scene, transform_func::T, space, point, model:: _project_position(scene, space, point, model, yflip) end -function _project_position(scene::Scene, space, point, model, yflip::Bool) +# much faster than dot-ing `project_position` because it skips all the repeated mat * mat +function _project_position(scene::Scene, space, ps::Vector{<: VecTypes{N, T1}}, model, yflip::Bool) where {N, T1} + transform = let + f32convert = Makie.f32_convert_matrix(scene.float32convert, space) + M = Makie.space_to_clip(scene.camera, space) * model * f32convert + res = scene.camera.resolution[] + px_scale = Vec3d(0.5 * res[1], 0.5 * (yflip ? -res[2] : res[2]), 1) + px_offset = Vec3d(0.5 * res[1], 0.5 * res[2], 0) + M = Makie.transformationmatrix(px_offset, px_scale) * M + M[Vec(1,2,4), Vec(1,2,3,4)] # skip z, i.e. calculate (x, y, w) + end + + output = similar(ps, Point2f) + + @inbounds for i in eachindex(ps) + p4d = to_ndim(Point4d, to_ndim(Point3d, ps[i], 0), 1) + px_pos = transform * p4d + output[i] = px_pos[Vec(1, 2)] / px_pos[3] + end + + return output +end + +function _project_position(scene::Scene, space, point::VecTypes{N, T1}, model, yflip::Bool) where {N, T1} + T = promote_type(Float32, T1) # always Float, at least Float32 res = scene.camera.resolution[] - p4d = to_ndim(Vec4f, to_ndim(Vec3f, point, 0f0), 1f0) - clip = Makie.space_to_clip(scene.camera, space) * model * p4d + p4d = to_ndim(Vec4{T}, to_ndim(Vec3{T}, point, 0), 1) + f32convert = Makie.f32_convert_matrix(scene.float32convert, space) + clip = Makie.space_to_clip(scene.camera, space) * model * f32convert * p4d @inbounds begin # between -1 and 1 p = (clip ./ clip[4])[Vec(1, 2)] @@ -29,12 +54,12 @@ 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 = Mat4f(I)) - project_scale(scene, space, Vec2f(s), model) +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 = Mat4f(I)) - p4d = model * to_ndim(Vec4f, s, 0f0) +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 diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index eba19fca0b4..da6745ebfd6 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -154,7 +154,7 @@ function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space] get!(gl_attributes, :world_normalmatrix) do return lift(plot, gl_attributes[:model]) do m i = Vec(1, 2, 3) - return transpose(inv(m[i, i])) + return Mat3f(transpose(inv(m[i, i]))) end end @@ -162,7 +162,7 @@ function connect_camera!(plot, gl_attributes, cam, space = gl_attributes[:space] get!(gl_attributes, :view_normalmatrix) do return lift(plot, gl_attributes[:view], gl_attributes[:model]) do v, m i = Vec(1, 2, 3) - return transpose(inv(v[i, i] * m[i, i])) + return Mat3f(transpose(inv(v[i, i] * m[i, i]))) end end get!(gl_attributes, :projection) do @@ -221,6 +221,33 @@ function get_space(x) return haskey(x, :markerspace) ? x.markerspace : x.space end +# TODO consider mirroring f32convert to plot attributes +function apply_transform_and_f32_conversion( + scene::Scene, plot::AbstractPlot, data, + space::Observable = get(plot, :space, Observable(:data)) + ) + f32c_obs = isnothing(scene.float32convert) ? Observable(nothing) : scene.float32convert.scaling + return map(plot, f32c_obs, transform_func_obs(plot), data, space) do _, _tf, data, space + tf = space == :data ? _tf : identity + f32c = space in (:data, :transformed) ? scene.float32convert : nothing + # avoid intermediate array? + return [Makie.f32_convert(f32c, apply_transform(tf, x)) for x in data] + end +end + +# For Vector{<: Real} applying to x/y/z dimension +function apply_transform_and_f32_conversion( + scene::Scene, plot::AbstractPlot, data, dim::Integer, + space::Observable = get(plot, :space, Observable(:data)) + ) + f32c_obs = isnothing(scene.float32convert) ? Observable(nothing) : scene.float32convert.scaling + return map(plot, f32c_obs, transform_func_obs(plot), data, space) do _, _tf, data, space + tf = space == :data ? _tf : identity + f32c = space in (:data, :transformed) ? scene.float32convert : nothing + return [Makie.f32_convert(f32c, apply_transform(tf, x), dim) for x in data] + end +end + const EXCLUDE_KEYS = Set([:transformation, :tickranges, :ticklabels, :raw, :SSAO, :lightposition, :material, :axis_cycler, :inspector_label, :inspector_hover, :inspector_clear, :inspectable, @@ -382,7 +409,8 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Sca space = plot.space positions = handle_view(plot[1], gl_attributes) - positions = lift(apply_transform, plot, transform_func_obs(plot), positions, space) + positions = apply_transform_and_f32_conversion(scene, plot, positions) + # positions = lift(apply_transform, plot, transform_func_obs(plot), positions, space) if plot isa Scatter mspace = plot.markerspace @@ -444,20 +472,25 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Lines)) data = Dict{Symbol, Any}(gl_attributes) positions = handle_view(plot[1], data) - transform_func = transform_func_obs(plot) space = plot.space if isnothing(to_value(linestyle)) data[:pattern] = nothing data[:fast] = true - positions = lift(apply_transform, plot, transform_func, positions, space) + # positions = lift(apply_transform, plot, transform_func, positions, space) + positions = apply_transform_and_f32_conversion(scene, plot, positions) else data[:pattern] = linestyle data[:fast] = false pvm = lift(*, plot, data[:projectionview], data[:model]) - positions = lift(plot, transform_func, positions, space, pvm) do f, ps, space, pvm - transformed = apply_transform(f, ps, space) + transform_func = transform_func_obs(plot) + f32c = scene.float32convert + f32c_obs = f32c isa Makie.Float32Convert ? f32c.scaling : Observable(nothing) + positions = lift(plot, f32c_obs, transform_func, positions, + space, pvm) do _, f, ps, space, pvm + + transformed = Makie.f32_convert(f32c, apply_transform(f, ps, space), space) output = Vector{Point4f}(undef, length(transformed)) for i in eachindex(transformed) output[i] = pvm * to_ndim(Point4f, to_ndim(Point3f, transformed[i], 0f0), 1f0) @@ -480,7 +513,8 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::LineSegme data[:pattern] = pop!(data, :linestyle) positions = handle_view(plot[1], data) - positions = lift(apply_transform, plot, transform_func_obs(plot), positions, plot.space) + # positions = lift(apply_transform, plot, transform_func_obs(plot), positions, plot.space) + positions = apply_transform_and_f32_conversion(scene, plot, positions) if haskey(data, :intensity) data[:color] = pop!(data, :intensity) end @@ -489,11 +523,14 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::LineSegme end end +# TODO: Float32 convert function draw_atomic(screen::Screen, scene::Scene, plot::Text{<:Tuple{<:Union{<:Makie.GlyphCollection, <:AbstractVector{<:Makie.GlyphCollection}}}}) return cached_robj!(screen, scene, plot) do gl_attributes glyphcollection = plot[1] + f32c = scene.float32convert + f32c_obs = f32c isa Makie.Float32Convert ? f32c.scaling : Observable(nothing) transfunc = Makie.transform_func_obs(plot) pos = gl_attributes[:position] space = plot.space @@ -502,8 +539,8 @@ function draw_atomic(screen::Screen, scene::Scene, atlas = gl_texture_atlas() # calculate quad metrics - glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space) do pos, gc, offset, transfunc, space - return Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) + glyph_data = lift(plot, pos, glyphcollection, offset, f32c_obs, transfunc, space) do pos, gc, offset, _, transfunc, space + return Makie.text_quads(atlas, pos, to_value(gc), offset, f32c, transfunc, space) end # unpack values from the one signal: @@ -576,20 +613,22 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) t = Makie.transform_func_obs(plot) mat = plot[3] space = plot.space # needs to happen before connect_camera! call - xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space + f32c = scene.float32convert + f32c_obs = f32c isa Makie.Float32Convert ? f32c.scaling : Observable(nothing) + xypos = lift(plot, f32c_obs, t, plot[1], plot[2], space) do _, t, x, y, space x1d = xy_convert(x, size(mat[], 1)) y1d = xy_convert(y, size(mat[], 2)) # Only if transform doesn't do anything, we can stay linear in 1/2D if Makie.is_identity_transform(t) - return (x1d, y1d) + return (Makie.f32_convert(f32c, x1d, 1), Makie.f32_convert(f32c, y1d, 2)) else # If we do any transformation, we have to assume things aren't on the grid anymore # so x + y need to become matrices. map!(x1d, x1d) do x - return apply_transform(t, Point(x, 0), space)[1] + return Makie.f32_convert(f32c, apply_transform(t, Point(x, 0), space)[1], 1) end map!(y1d, y1d) do y - return apply_transform(t, Point(0, y), space)[2] + return Makie.f32_convert(f32c, apply_transform(t, Point(0, y), space)[2], 2) end return (x1d, y1d) end @@ -621,9 +660,9 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Image) xmin, xmax = extrema(x) ymin, ymax = extrema(y) rect = Rect2f(xmin, ymin, xmax - xmin, ymax - ymin) - return decompose(Point2f, rect) + return decompose(Point2d, rect) end - gl_attributes[:vertices] = lift(apply_transform, plot, transform_func_obs(plot), position, plot.space) + gl_attributes[:vertices] = apply_transform_and_f32_conversion(scene, plot, position) rect = Rect2f(0, 0, 1, 1) gl_attributes[:faces] = decompose(GLTriangleFace, rect) gl_attributes[:texturecoordinates] = map(decompose_uv(rect)) do uv @@ -679,9 +718,9 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= gl_attributes[:color] = nothing end - gl_attributes[:vertices] = lift(transfunc, mesh, space) do t, mesh, space - apply_transform(t, metafree(coordinates(mesh)), space) - end + # TODO: avoid intermediate observable + positions = map(m -> metafree(coordinates(m)), mesh) + gl_attributes[:vertices] = apply_transform_and_f32_conversion(Makie.parent_scene(plot), plot, positions) gl_attributes[:faces] = lift(x-> decompose(GLTriangleFace, x), mesh) if hasproperty(to_value(mesh), :uv) gl_attributes[:texturecoordinates] = lift(decompose_uv, mesh) @@ -734,18 +773,20 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) if all(T -> T <: Union{AbstractMatrix, AbstractVector}, types) t = Makie.transform_func_obs(plot) + f32c = scene.float32convert + f32c_obs = f32c isa Makie.Float32Convert ? f32c.scaling : Observable(nothing) mat = plot[3] - xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space + xypos = lift(plot, f32c_obs, t, plot[1], plot[2], space) do f32c_inner, t, x, y, space # Only if transform doesn't do anything, we can stay linear in 1/2D - if Makie.is_identity_transform(t) + if Makie.is_identity_transform(t) && isnothing(f32c_inner) return (x, y) else matrix = if x isa AbstractMatrix && y isa AbstractMatrix - apply_transform.((t,), Point.(x, y), space) + Makie.f32_convert(f32c, apply_transform.((t,), Point.(x, y), space), space) else # If we do any transformation, we have to assume things aren't on the grid anymore # so x + y need to become matrices. - [apply_transform(t, Point(x, y), space) for x in x, y in y] + [Makie.f32_convert(f32c, apply_transform(t, Point(x, y), space), space) for x in x, y in y] end return (first.(matrix), last.(matrix)) end diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 65cd58bae2d..aa8e226ef7a 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -59,7 +59,7 @@ end end end fig, ax, meshplot = meshscatter(RNG.rand(Point3f, 10^4) .* 20f0; color=:black) - screen = display(GLMakie.Screen(;renderloop=(screen) -> nothing, start_renderloop=false), fig.scene) + screen = display(GLMakie.Screen(;renderloop=(screen) -> nothing, start_renderloop=false, visible=false), fig.scene) buff = RNG.rand(Point3f, 10^4) .* 20f0; update_loop(meshplot, buff, screen) @test isnothing(screen.rendertask) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 726f4b90705..0793ce9fb9d 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -1,14 +1,14 @@ using GLMakie.Makie: getscreen function project_sp(scene, point) - point_px = Makie.project(scene, point) + point_px = Makie.project(scene, Makie.f32_convert(scene.float32convert, point)) offset = Point2f(minimum(viewport(scene)[])) return point_px .+ offset end @testset "shader cache" begin GLMakie.closeall() - screen = display(Figure()) + screen = display(GLMakie.Screen(visible = false), Figure()) cache = screen.shader_cache # Postprocessing shaders @test length(cache.shader_cache) == 5 @@ -140,7 +140,7 @@ end heatmap!(ax, rand(4, 4)) lines!(ax, 1:5, rand(5); linewidth=3) text!(ax, [Point2f(2)], text=["hi"]) - screen = display(fig) + screen = display(GLMakie.Screen(visible = false), fig) empty!(fig) @test screen in fig.scene.current_screens @test length(fig.scene.current_screens) == 1 @@ -179,7 +179,7 @@ end hmp = heatmap!(ax, rand(4, 4)) lp = lines!(ax, 1:5, rand(5); linewidth=3) tp = text!(ax, [Point2f(2)], text=["hi"]) - screen = display(fig) + screen = display(GLMakie.Screen(visible = false), fig) @test ax.scene.plots == [hmp, lp, tp] @@ -222,9 +222,9 @@ end fig = Figure() ax = Axis(fig[1,1]) # only happens with axis # lines!(ax, 1:5, rand(5); linewidth=5) # but doesn't need a plot - screen = display(fig) + screen = display(GLMakie.Screen(visible = false), fig) GLMakie.closeall() - display(fig) + display(GLMakie.Screen(visible = false), fig) @test true # test for no errors for now end @@ -232,9 +232,9 @@ end GLMakie.closeall() fig = Figure() ax = Axis(fig[1,1]) # only happens with axis - screen = display(fig) + screen = display(GLMakie.Screen(visible = false), fig) close(screen) - screen = display(fig) + screen = display(GLMakie.Screen(visible = false), fig) resize!(fig, 800,601) @test true # test for no errors for now # GLMakie.destroy!(screen) @@ -242,9 +242,9 @@ end end @testset "destroying singleton screen" begin - screen = display(scatter(1:4)) + screen = display(GLMakie.Screen(visible = false), scatter(1:4)) GLMakie.destroy!(screen) - screen = display(scatter(1:4)) + screen = display(GLMakie.Screen(visible = false), scatter(1:4)) @test isopen(screen) # shouldn't run into double closing a destroyed window GLMakie.destroy!(screen) end @@ -437,7 +437,7 @@ end @testset "image size changes" begin s = Scene() im = image!(s, 0..10, 0..10, zeros(RGBf, 10, 20)) - display(s) + display(GLMakie.Screen(visible = false), s) im[3][] = zeros(RGBf, 20, 10) # same length, different size im[3][] = zeros(RGBf, 15, 5) # smaller size im[3][] = zeros(RGBf, 25, 15) # larger size @@ -462,4 +462,4 @@ end @test robj.uniforms[:resolution][] == screen.px_per_unit[] * cam.resolution[] @test robj.uniforms[:projectionview][] == cam.projectionview[] -end \ No newline at end of file +end diff --git a/ReferenceTests/src/tests/dates.jl b/ReferenceTests/src/tests/dates.jl index 1b2a4ad3b8c..aeebfb36263 100644 --- a/ReferenceTests/src/tests/dates.jl +++ b/ReferenceTests/src/tests/dates.jl @@ -1,4 +1,4 @@ -using Makie.Unitful, Makie.Dates, Test +using Makie, Makie.Unitful, Makie.Dates, Test some_time = Time("11:11:55.914") date = Date("2021-10-27") diff --git a/ReferenceTests/src/tests/text.jl b/ReferenceTests/src/tests/text.jl index 04e4c30626a..04bc41886a7 100644 --- a/ReferenceTests/src/tests/text.jl +++ b/ReferenceTests/src/tests/text.jl @@ -90,7 +90,7 @@ end align = (halign, :center), justification = justification) - bb = boundingbox(t) + bb = Makie.text_boundingbox(t) wireframe!(scene, bb, color = (:red, 0.2)) end @@ -119,7 +119,7 @@ end markerspace = :data ) - wireframe!(scene, boundingbox(t1), color = (:blue, 0.3)) + wireframe!(scene, Makie.text_boundingbox(t1), color = (:blue, 0.3)) t2 = text!(scene, fill("makie", 4), @@ -130,7 +130,7 @@ end markerspace = :pixel ) - wireframe!(scene, boundingbox(t2), color = (:red, 0.3)) + wireframe!(scene, Makie.text_boundingbox(t2), color = (:red, 0.3)) scene end @@ -149,7 +149,7 @@ end markerspace = :data ) - wireframe!(scene, boundingbox(t), color = (:blue, 0.3)) + wireframe!(scene, Makie.text_boundingbox(t), color = (:blue, 0.3)) t2 = text!(scene, "makie", @@ -161,7 +161,7 @@ end ) # these boundingboxes should be invisible because they only enclose the anchor - wireframe!(scene, boundingbox(t2), color = (:red, 0.3)) + wireframe!(scene, Makie.text_boundingbox(t2), color = (:red, 0.3)) end scene @@ -186,12 +186,12 @@ end t1 = text!(scene, "Line1\nLine 2\n\nLine4", position = (200, 400), align = (:center, :center), markerspace = :data) - wireframe!(scene, boundingbox(t1), color = (:red, 0.3)) + wireframe!(scene, Makie.text_boundingbox(t1), color = (:red, 0.3)) t2 = text!(scene, "\nLine 2\nLine 3\n\n\nLine6\n\n", position = (400, 400), align = (:center, :center), markerspace = :data) - wireframe!(scene, boundingbox(t2), color = (:blue, 0.3)) + wireframe!(scene, Makie.text_boundingbox(t2), color = (:blue, 0.3)) scene end @@ -283,7 +283,7 @@ end position = Point2f(50, 50), rotation = 0.0, markerspace = :data) - wireframe!(s, boundingbox(t), color=:black) + wireframe!(s, Makie.text_boundingbox(t), color=:black) s end diff --git a/src/Makie.jl b/src/Makie.jl index 21991fae449..d53796cf89e 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -106,6 +106,37 @@ const RGBAf = RGBA{Float32} const RGBf = RGB{Float32} const NativeFont = FreeTypeAbstraction.FTFont +################################################################################ + +# TODO: remove after GeometryBasics#214 +const Point2d = Point2{Float64} +const Point3d = Point3{Float64} +const Point4d = Point4{Float64} +const Vec2d = Vec2{Float64} +const Vec3d = Vec3{Float64} +const Vec4d = Vec4{Float64} +const Rect2d = Rect2{Float64} +const Rect3d = Rect3{Float64} +const Rectd = Rect{N, Float64} where N +const Mat3d = Mat3{Float64} +const Mat4d = Mat4{Float64} +export Point2d, Point3d, Point4d, Vec2d, Vec3d, Vec4d, Rect2d, Rect3d + +# TODO: move to GeometryBasics? +function GeometryBasics.Rect3{T}(r::Rect2) where {T} # used in text boundingbox + return Rect3{T}(Vec3{T}(origin(r)..., zero(T)), Vec3{T}(widths(r)..., zero(T))) +end + +# TODO: patch GridLayoutBase, probably to use Float64 consistently? +function GridLayoutBase.BBox(left::T1, right::T2, bottom::T3, top::T4) where {T1 <: Real, T2 <: Real, T3 <: Real, T4 <: Real} + mini = (left, bottom) + maxi = (right, top) + T = promote_type(T1, T2, T3, T4, Float32) # Float32 to skip Int outputs + return Rect2{T}(mini, maxi .- mini) +end + +################################################################################ + const ASSETS_DIR = RelocatableFolders.@path joinpath(@__DIR__, "..", "assets") assetpath(files...) = normpath(joinpath(ASSETS_DIR, files...)) @@ -122,6 +153,7 @@ include("patterns.jl") include("utilities/utilities.jl") # need Makie.AbstractPattern include("lighting.jl") # Basic scene/plot/recipe interfaces + types +include("float32-scaling2.jl") include("scenes.jl") include("interfaces.jl") @@ -181,6 +213,8 @@ include("layouting/transformation.jl") include("layouting/data_limits.jl") include("layouting/layouting.jl") include("layouting/boundingbox.jl") +include("layouting/text_boundingbox.jl") +include("layouting/maybe_unused.jl") # Declaritive SpecApi include("specapi.jl") @@ -355,6 +389,7 @@ export resize_to_layout! include("makielayout/MakieLayout.jl") include("figureplotting.jl") +include("makielayout/blocks/float32-scaling.jl") include("makielayout/blocks/unitful-integration.jl") include("makielayout/blocks/categorical-integration.jl") include("makielayout/blocks/dates-integration.jl") diff --git a/src/basic_recipes/axis.jl b/src/basic_recipes/axis.jl index cc5d39e05a1..806f9f5286a 100644 --- a/src/basic_recipes/axis.jl +++ b/src/basic_recipes/axis.jl @@ -342,6 +342,6 @@ function plot!(axis::Axis3D) return axis end -function axis3d!(scene::Scene, lims = data_limits(scene, p -> isaxis(p) || not_in_data_space(p)); kw...) +function axis3d!(scene::Scene, lims = boundingbox(scene, p -> isaxis(p) || not_in_data_space(p)); kw...) axis3d!(scene, Attributes(), lims; ticks = (ranges = automatic, labels = automatic), kw...) end diff --git a/src/basic_recipes/bracket.jl b/src/basic_recipes/bracket.jl index 20262386af1..b5ccb0dd682 100644 --- a/src/basic_recipes/bracket.jl +++ b/src/basic_recipes/bracket.jl @@ -112,7 +112,8 @@ function Makie.plot!(pl::Bracket) pl end -data_limits(pl::Bracket) = mapreduce(ps -> Rect3f([ps...]), union, pl[1][]) +data_limits(pl::Bracket) = mapreduce(ps -> Rect3d([ps...]), union, pl[1][]) +boundingbox(pl::Bracket) = transform_bbox(pl, data_limits(pl)) bracket_bezierpath(style::Symbol, args...) = bracket_bezierpath(Val(style), args...) diff --git a/src/basic_recipes/contours.jl b/src/basic_recipes/contours.jl index b1406f89b98..f87c8ee94e3 100644 --- a/src/basic_recipes/contours.jl +++ b/src/basic_recipes/contours.jl @@ -336,11 +336,16 @@ function plot!(plot::T) where T <: Union{Contour, Contour3d} plot end -function point_iterator(x::Contour{<: Tuple{X, Y, Z}}) where {X, Y, Z} - axes = (x[1], x[2]) - extremata = map(extrema∘to_value, axes) - minpoint = Point2f(first.(extremata)...) - widths = last.(extremata) .- first.(extremata) - rect = Rect2f(minpoint, Vec2f(widths)) - return unique(decompose(Point, rect)) +function data_limits(plot::Contour{<: Tuple{X, Y, Z}}) where {X, Y, Z} + mini_maxi = extrema_nan.((plot[1][], plot[2][])) + mini = Vec3d(first.(mini_maxi)..., 0) + maxi = Vec3d(last.(mini_maxi)..., 0) + return Rect3d(mini, maxi .- mini) end +function boundingbox(plot::Contour{<: Tuple{X, Y, Z}}) where {X, Y, Z} + return transform_bbox(plot, data_limits(plot)) +end +# TODO: should this have a data_limits overload? +function boundingbox(plot::Contour3d) + return transform_bbox(plot, data_limits(plot)) +end \ No newline at end of file diff --git a/src/basic_recipes/datashader.jl b/src/basic_recipes/datashader.jl index 87179139c4b..fe605326453 100644 --- a/src/basic_recipes/datashader.jl +++ b/src/basic_recipes/datashader.jl @@ -469,6 +469,7 @@ function Makie.plot!(p::DataShader{<:Tuple{Dict{String, Vector{Point{2, Float32} end data_limits(p::DataShader) = p._boundingbox[] +boundingbox(p::DataShader) = transform_bbox(p, p._boundingbox[]) function convert_arguments(P::Type{<:Union{MeshScatter,Image,Surface,Contour,Contour3d}}, canvas::Canvas, operation=automatic, local_operation=identity) pixel = Aggregation.get_aggregation(canvas; operation=operation, local_operation=local_operation) diff --git a/src/basic_recipes/error_and_rangebars.jl b/src/basic_recipes/error_and_rangebars.jl index f3ac3d46e54..a18e273b1ea 100644 --- a/src/basic_recipes/error_and_rangebars.jl +++ b/src/basic_recipes/error_and_rangebars.jl @@ -279,6 +279,5 @@ function screen_to_plot(plot, p::VecTypes) end # ignore whiskers when determining data limits -function data_limits(bars::Union{Errorbars, Rangebars}) - data_limits(bars.plots[1]) -end +data_limits(bars::Union{Errorbars, Rangebars}) = data_limits(bars.plots[1]) +boundingbox(bars::Union{Errorbars, Rangebars}) = transform_bbox(bars, data_limits(bars)) diff --git a/src/basic_recipes/hvlines.jl b/src/basic_recipes/hvlines.jl index 7e95eb4457d..2ff135136da 100644 --- a/src/basic_recipes/hvlines.jl +++ b/src/basic_recipes/hvlines.jl @@ -88,7 +88,7 @@ function data_limits(p::HLines) itf = inverse_transform(p.transformation.transform_func[]) xmin, xmax = apply_transform.(itf[1], first.(extrema(limits))) ymin, ymax = extrema(p[1][]) - return Rect3f(Point3f(xmin, ymin, 0), Vec3f(xmax - xmin, ymax - ymin, 0)) + return Rect3d(Point3d(xmin, ymin, 0), Vec3d(xmax - xmin, ymax - ymin, 0)) end function data_limits(p::VLines) @@ -97,5 +97,7 @@ function data_limits(p::VLines) itf = inverse_transform(p.transformation.transform_func[]) xmin, xmax = extrema(p[1][]) ymin, ymax = apply_transform.(itf[2], getindex.(extrema(limits), 2)) - return Rect3f(Point3f(xmin, ymin, 0), Vec3f(xmax - xmin, ymax - ymin, 0)) -end \ No newline at end of file + return Rect3d(Point3d(xmin, ymin, 0), Vec3d(xmax - xmin, ymax - ymin, 0)) +end + +boundingbox(p::Union{HLines, VLines}) = transform_bbox(p, data_limits(p)) \ No newline at end of file diff --git a/src/basic_recipes/hvspan.jl b/src/basic_recipes/hvspan.jl index 51c391789fb..3761213d234 100644 --- a/src/basic_recipes/hvspan.jl +++ b/src/basic_recipes/hvspan.jl @@ -91,7 +91,7 @@ function data_limits(p::HSpan) xmin, xmax = apply_transform.(itf[1], first.(extrema(limits))) ymin = minimum(p[1][]) ymax = maximum(p[2][]) - return Rect3f(Point3f(xmin, ymin, 0), Vec3f(xmax - xmin, ymax - ymin, 0)) + return Rect3d(Point3d(xmin, ymin, 0), Vec3d(xmax - xmin, ymax - ymin, 0)) end function data_limits(p::VSpan) @@ -101,5 +101,7 @@ function data_limits(p::VSpan) xmin = minimum(p[1][]) xmax = maximum(p[2][]) ymin, ymax = apply_transform.(itf[2], getindex.(extrema(limits), 2)) - return Rect3f(Point3f(xmin, ymin, 0), Vec3f(xmax - xmin, ymax - ymin, 0)) -end \ No newline at end of file + return Rect3d(Point3d(xmin, ymin, 0), Vec3d(xmax - xmin, ymax - ymin, 0)) +end + +boundingbox(p::Union{HSpan, VSpan}) = transform_bbox(p, data_limits(p)) diff --git a/src/basic_recipes/tooltip.jl b/src/basic_recipes/tooltip.jl index 683e6ecd023..e7fd75de6ea 100644 --- a/src/basic_recipes/tooltip.jl +++ b/src/basic_recipes/tooltip.jl @@ -134,7 +134,7 @@ function plot!(p::Tooltip{<:Tuple{<:VecTypes}}) bbox = map( p, px_pos, p.text, text_align, text_offset, textpadding, p.align ) do p, s, _, o, pad, align - bb = boundingbox(tp) + to_ndim(Vec3f, o, 0) + bb = text_boundingbox(tp) + to_ndim(Vec3f, o, 0) l, r, b, t = pad return Rect3f(origin(bb) .- (l, b, 0), widths(bb) .+ (l+r, b+t, 0)) end diff --git a/src/basic_recipes/triplot.jl b/src/basic_recipes/triplot.jl index c746f6e101a..6739b3b139d 100644 --- a/src/basic_recipes/triplot.jl +++ b/src/basic_recipes/triplot.jl @@ -236,11 +236,11 @@ function data_limits(p::Triplot{<:Tuple{<:Vector{<:Point}}}) if transform_func(p) isa Polar # Because the Polar transform is handled explicitly we cannot rely # on the default data_limits. (data limits are pre transform) - iter = (to_ndim(Point3f, p, 0f0) for p in p.converted[1][]) - limits_from_transformed_points(iter) + return Rect3d(p.converted[1][]) else - # First component is either another Voronoiplot or a poly plot. Both + # First component is either another Triplot or a poly plot. Both # cases span the full limits of the plot - data_limits(p.plots[1]) + return data_limits(p.plots[1]) end -end \ No newline at end of file +end +boundingbox(p::Triplot{<:Tuple{<:Vector{<:Point}}}) = transform_bbox(p, data_limits(p)) \ No newline at end of file diff --git a/src/basic_recipes/voronoiplot.jl b/src/basic_recipes/voronoiplot.jl index 2d6963a2f6b..4bdcfa74337 100644 --- a/src/basic_recipes/voronoiplot.jl +++ b/src/basic_recipes/voronoiplot.jl @@ -155,18 +155,18 @@ function plot!(p::Voronoiplot{<:Tuple{<:Vector{<:Point{N}}}}) where {N} return voronoiplot!(p, attr, vorn) end -function data_limits(p::Voronoiplot{<:Tuple{<:Vector{<:Point{N}}}}) where {N} +function data_limits(p::Voronoiplot{<:Tuple{<:Vector{<:Point}}}) if transform_func(p) isa Polar # Because the Polar transform is handled explicitly we cannot rely # on the default data_limits. (data limits are pre transform) - iter = (to_ndim(Point3f, p, 0f0) for p in p.converted[1][]) - limits_from_transformed_points(iter) + return Rect3d(p.converted[1][]) else # First component is either another Voronoiplot or a poly plot. Both # cases span the full limits of the plot - data_limits(p.plots[1]) + return data_limits(p.plots[1]) end end +boundingbox(p::Voronoiplot{<:Tuple{<:Vector{<:Point}}}) = transform_bbox(p, data_limits(p)) function plot!(p::Voronoiplot{<:Tuple{<:DelTri.VoronoiTessellation}}) generators_2f = Observable(Point2f[]) diff --git a/src/camera/projection_math.jl b/src/camera/projection_math.jl index eada9918c2e..214c6fd6711 100644 --- a/src/camera/projection_math.jl +++ b/src/camera/projection_math.jl @@ -278,6 +278,16 @@ function to_world( to_world(zeros(Point{N, T}), prj_view_inv, cam_res) end +function project(matrix::Mat4{T1}, p::VT, dim4::Real = 1.0) where {N, T1 <: Real, T2 <: Real, VT <: VecTypes{N, T2}} + T = promote_type(T1, T2) # TODO: technically this should be the less accurate of T1 and T2? + p = to_ndim(Vec4{T}, to_ndim(Vec3{T}, p, 0.0), dim4) + p = matrix * p + to_ndim(VT, p, 0.0) +end + + +# TODO: these may need to consider float32 converts (depending on how we handle them) + function project(scene::Scene, point::T) where T<:StaticVector cam = scene.camera area = viewport(scene)[] @@ -291,29 +301,30 @@ function project(scene::Scene, point::T) where T<:StaticVector ) end -function project(matrix::Mat4f, p::T, dim4 = 1.0) where T <: VecTypes - p = to_ndim(Vec4f, to_ndim(Vec3f, p, 0.0), dim4) - p = matrix * p - to_ndim(T, p, 0.0) -end - -function project(proj_view::Mat4f, resolution::Vec2, point::Point) - p4d = to_ndim(Vec4f, to_ndim(Vec3f, point, 0f0), 1f0) +function project(proj_view::Mat4{T1}, resolution::Vec2, point::Point{N, T2}) where {N, T1, T2} + T = promote_type(T1, T2) + p4d = to_ndim(Vec4{T}, to_ndim(Vec3{T}, point, 0), 1) clip = proj_view * p4d + # at this point the visible range is strictly -1..1 so FLoat64 doesn't matter p = (clip ./ clip[4])[Vec(1, 2)] p = Vec2f(p[1], p[2]) return (((p .+ 1f0) ./ 2f0) .* (resolution .- 1f0)) .+ 1f0 end -function project_point2(mat4::Mat4, point2::Point2) - Point2f(mat4 * to_ndim(Point4f, to_ndim(Point3f, point2, 0), 1)) +function project_point2(mat4::Mat4{T1}, point2::Point2{T2}) where {T1, T2} + T = promote_type(T1, T2) + Point2{T2}(mat4 * to_ndim(Point4{T}, to_ndim(Point3{T}, point2, 0), 1)) end -function transform(model::Mat4, x::T) where T - x4d = to_ndim(Vec4f, x, 0.0) - to_ndim(T, model * x4d, 0.0) +function transform(model::Mat4{T1}, x::VT) where {T1, VT} + T = promote_type(T1, eltype(VT)) + # TODO: no w = 1? Is this meant to skip translations? + x4d = to_ndim(Vec4{T}, x, 0.0) + to_ndim(VT, model * x4d, 0.0) end +################################################################################ + # project between different coordinate systems/spaces function space_to_clip(cam::Camera, space::Symbol, projectionview::Bool=true) if is_data_space(space) @@ -360,11 +371,12 @@ function is_space_compatible(a::Union{Tuple, Vector}, b::Union{Tuple, Vector}) end is_space_compatible(a::Union{Tuple, Vector}, b::Symbol) = is_space_compatible(b, a) -function project(cam::Camera, input_space::Symbol, output_space::Symbol, pos) - input_space === output_space && return to_ndim(Point3f, pos, 0) +function project(cam::Camera, input_space::Symbol, output_space::Symbol, pos::VecTypes{N, T1}) where {N, T1} + T = promote_type(Float32, T1) # always float, maybe Float64 + input_space === output_space && return to_ndim(Point3{T}, pos, 0) clip_from_input = space_to_clip(cam, input_space) output_from_clip = clip_to_space(cam, output_space) - p4d = to_ndim(Point4f, to_ndim(Point3f, pos, 0), 1) + p4d = to_ndim(Point4{T}, to_ndim(Point3{T}, pos, 0), 1) transformed = output_from_clip * clip_from_input * p4d - return Point3f(transformed[Vec(1, 2, 3)] ./ transformed[4]) + return Point3{T}(transformed[Vec(1, 2, 3)] ./ transformed[4]) end diff --git a/src/conversions.jl b/src/conversions.jl index 18c4fd0df40..8b819163efd 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -27,9 +27,9 @@ end image::AbstractMatrix{<:Union{Float32,Colorant}} end -@convert_target struct PointBased{N} # We can use the traits as well for conversion targers +@convert_target struct PointBased{N, T} # We can use the traits as well for conversion targers # all position based traits get converted to a simple vector of points - positions::AbstractVector{Point{N,Float32}} + positions::AbstractVector{Point{N,T}} end @convert_target struct Mesh @@ -122,19 +122,19 @@ convert_single_argument(a::AbstractArray{<:Point{N}}) where {N} = a Wrap a single point or equivalent object in a single-element array. """ function convert_arguments(::PointBased, x::Real, y::Real) - ([Point2f(x, y)],) + ([Point2(x, y)],) end function convert_arguments(::PointBased, x::Real, y::Real, z::Real) - ([Point3f(x, y, z)],) + ([Point3(x, y, z)],) end -function convert_arguments(::PointBased, position::VecTypes{N, <: Number}) where N - ([convert(Point{N, Float32}, position)],) +function convert_arguments(::PointBased, position::VecTypes{N, T}) where {N, T <: Real} + ([convert(Point{N, T}, position)],) end -function convert_arguments(::PointBased, positions::AbstractVector{<: VecTypes{N, <: Number}}) where N - (elconvert(Point{N, Float32}, positions),) +function convert_arguments(::PointBased, positions::AbstractVector{<: VecTypes{N, T}}) where {N, T <: Real} + (elconvert(Point{N, T}, positions),) end function convert_arguments(::PointBased, positions::SubArray{<: VecTypes, 1}) @@ -147,7 +147,7 @@ Enables to use scatter like a surface plot with x::Vector, y::Vector, z::Matrix spanning z over the grid spanned by x y """ function convert_arguments(::PointBased, x::AbstractArray{<: Real}, y::AbstractVector{<: Real}, z::AbstractArray{<: Real}) - (vec(Point3f.(x, y', z)),) + (vec(Point3.(x, y', z)),) end function convert_arguments(p::PointBased, x::AbstractInterval, y::AbstractInterval, z::RealMatrix) @@ -156,7 +156,7 @@ end function convert_arguments(::PointBased, x::AbstractArray{<:Real}, y::RealMatrix, z::AbstractArray{<:Real}) - (vec(Point3f.(x, y, z)),) + (vec(Point3.(x, y, z)),) end """ @@ -166,8 +166,8 @@ Takes vectors `x`, `y`, and `z` and turns it into a vector of 3D points of the v from `x`, `y`, and `z`. `P` is the plot Type (it is optional). """ -convert_arguments(::PointBased, x::RealVector, y::RealVector, z::RealVector) = (Point3f.(x, y, z),) -convert_arguments(P::PointBased, x::RealVector, y::RealVector) = (Point2f.(x, y),) +convert_arguments(::PointBased, x::RealVector, y::RealVector, z::RealVector) = (Point3.(x, y, z),) +convert_arguments(P::PointBased, x::RealVector, y::RealVector) = (Point2.(x, y),) """ convert_arguments(P, x)::(Vector) @@ -210,13 +210,13 @@ Takes an input `Rect` `x` and decomposes it to points. `P` is the plot Type (it is optional). """ -function convert_arguments(P::PointBased, x::Rect2) +function convert_arguments(P::PointBased, x::Rect2{T}) where T # TODO fix the order of decompose - return convert_arguments(P, decompose(Point2f, x)[[1, 2, 4, 3]]) + return convert_arguments(P, decompose(Point2{promote_type(Float32, T)}, x)[[1, 2, 4, 3]]) end function convert_arguments(P::PointBased, mesh::AbstractMesh) - return convert_arguments(P, decompose(Point3f, mesh)) + return convert_arguments(P, decompose(Point3, mesh)) end function convert_arguments(PB::PointBased, linesegments::FaceView{<:Line, P}) where {P<:AbstractPoint} diff --git a/src/float32-scaling2.jl b/src/float32-scaling2.jl new file mode 100644 index 00000000000..f9bd61183ba --- /dev/null +++ b/src/float32-scaling2.jl @@ -0,0 +1,133 @@ +#= +New file to keep the old version around + +# Notes +Conversion chain: + +| from | using | to | change in data | +| ----------- | ----------------- | ------------ | -------------------------------- | +| input | convert_arguments | converted | structure normalization | +| converted | transform_func | transformed | apply generic transformation | +| transformed | float32scaling | f32scaled | Float32 convert + scaling | +| f32scaled | model | world space | placement in world | +| world space | view | view space | to camera coordinate system | +| view space | projection | clip space | to -1..1 space (+ projection) | +| clip space | (viewport) | screen space | to pixel units (OpenGL internal) | + +- model, view, projection should be FLoat32 on GPU, so: + - input, converted, transformed can be whatever type + - f32scaled needs to be Float32 + - float32scaling needs to make f32scaled, model, view, projection float save + - this requires translations and scaling to be extracted from at least projectionview +- when float32scaling applies convert_arguments should have been handled, so + splitting dimensions is extra work +- -1..1 = -1e0 .. 1e0 is equally far from -floatmax and +floatmax, and also + exponentially the same distance from floatmin and floatmax, so it should be + a good target range to avoid frequent updates +=# + +struct LinearScaling + scale::Vec{3, Float64} + offset::Vec{3, Float64} +end + +# muladd is no better than a * b + c etc +# Don't apply Float32 here so we can still work with full precision by calling these directly +@inline (ls::LinearScaling)(x::Real, dim::Integer) = ls.scale[dim] * x + ls.offset[dim] +@inline (ls::LinearScaling)(p::VecTypes{2}) = ls.scale[Vec(1, 2)] .* p + ls.offset[Vec(1, 2)] +@inline (ls::LinearScaling)(p::VecTypes{3}) = ls.scale .* p + ls.offset + +struct Float32Convert + scaling::Observable{LinearScaling} + resolution::Float32 +end + +function Float32Convert() + scaling = LinearScaling(Vec{3, Float64}(1.0), Vec{3, Float64}(0.0)) + return Float32Convert(Observable(scaling), 1e4) +end + +# transformed space limits +update_limits!(::Nothing, lims::Rect) = false +function update_limits!(c::Float32Convert, lims::Rect) + mini = to_ndim(Vec3d, minimum(lims), -1) + maxi = to_ndim(Vec3d, maximum(lims), +1) + return update_limits!(c, mini, maxi) +end +function update_limits!(c::Float32Convert, mini::VecTypes{3, Float64}, maxi::VecTypes{3, Float64}) + linscale = c.scaling[] + + low = linscale(mini) + high = linscale(maxi) + @assert all(low .<= high) # TODO: Axis probably does that + + delta = high - low + max_eps = eps(Float32) * max.(abs.(low), abs.(high)) + min_resolved = delta ./ max_eps + + # Could we have less than c.resolution floats in the given range? + needs_update = any(min_resolved .< c.resolution) + # Are we outside the range (floatmin, floatmax) that Float32 can resolve? + needs_update = needs_update || any(delta .< 1e-35) || any(delta .> 1e35) + + if needs_update + # Vec{N}(+1) = scale * maxi + offset + # Vec{N}(-1) = scale * mini + offset + scale = 2.0 ./ (maxi - mini) + offset = 1.0 .- scale * maxi + c.scaling[] = LinearScaling(scale, offset) + + return true + end + + return false +end + +@inline f32_convert(::Nothing, x::Real) = Float32(x) +@inline f32_convert(::Nothing, x::VecTypes{N}) where N = to_ndim(Point{N, Float32}, x, 0) +@inline f32_convert(::Nothing, x::AbstractArray) = f32_convert.(nothing, x) + +@inline function f32_convert(c::Float32Convert, p::VecTypes{N}) where N + # Point3f(::Point3i) doesn't work + return to_ndim(Point{N, Float32}, c.scaling[](p), 0) +end +@inline function f32_convert(c::Float32Convert, ps::AbstractArray{<: VecTypes{N}}) where N + return [to_ndim(Point{N, Float32}, c.scaling[](p), 0) for p in ps] +end + +@inline f32_convert(::Nothing, x::Real, dim::Integer) = Float32(x) +@inline f32_convert(::Nothing, x::VecTypes, dim::Integer) = Float32(x[dim]) +@inline f32_convert(::Nothing, x::AbstractArray, dim::Integer) = f32_convert.(nothing, x, dim) +@inline f32_convert(c::Float32Convert, x::Real, dim::Integer) = Float32(c.scaling[](x, dim)) +@inline function f32_convert(c::Float32Convert, xs::AbstractArray{<: Real}, dim::Integer) + return [Float32(c.scaling[](x, dim)) for x in xs] +end + +@inline function f32_convert(c::Float32Convert, r::Rect{N}) where {N} + mini = c.scaling[](minimum(r)) + maxi = c.scaling[](maximum(r)) + return Rect{N, Float32}(mini, maxi - mini) +end + + +@inline f32_convert(c::Nothing, data, ::Symbol) = f32_convert(c, data) +@inline function f32_convert(c::Float32Convert, data, space::Symbol) + return space in (:data, :transformed) ? f32_convert(c, data) : f32_convert(nothing, data) +end +@inline f32_convert(c::Nothing, data, dim::Integer, ::Symbol) = f32_convert(c, data, dim) +@inline function f32_convert(c::Float32Convert, data, dim::Integer, space::Symbol) + return space in (:data, :transformed) ? f32_convert(c, data, dim) : f32_convert(nothing, data, dim) +end + + +f32_convert_matrix(::Nothing, ::Symbol) = Mat4d(I) +function f32_convert_matrix(c::Float32Convert, space::Symbol) + if space in (:data, :transformed) # maybe :world? + linear = c.scaling[] + scale = to_ndim(Vec3d, linear.scale, 1) + translation = to_ndim(Vec3d, linear.offset, 0) + return transformationmatrix(translation, scale) + else + return Mat4d(I) + end +end \ No newline at end of file diff --git a/src/layouting/boundingbox.jl b/src/layouting/boundingbox.jl index 0a9226d16b1..fc81bc298a6 100644 --- a/src/layouting/boundingbox.jl +++ b/src/layouting/boundingbox.jl @@ -1,163 +1,110 @@ -function parent_transform(x) - p = parent(transformation(x)) - isnothing(p) ? Mat4f(I) : p.model[] -end -function boundingbox(x, exclude = (p)-> false) - return parent_transform(x) * data_limits(x, exclude) -end +################################################################################ +### boundingbox +################################################################################ -function project_widths(matrix, vec) - pr = project(matrix, vec) - zero = project(matrix, zeros(typeof(vec))) - return pr - zero -end - -function rotate_bbox(bb::Rect3f, rot) - points = decompose(Point3f, bb) - Rect3f(Ref(rot) .* points) -end -function gl_bboxes(gl::GlyphCollection) - scales = gl.scales.sv isa Vec2f ? (gl.scales.sv for _ in gl.extents) : gl.scales.sv - map(gl.glyphs, gl.extents, scales) do c, ext, scale - hi_bb = height_insensitive_boundingbox_with_advance(ext) - # TODO c != 0 filters out all non renderables, which is not always desired - Rect2f( - Makie.origin(hi_bb) * scale, - (c != 0) * widths(hi_bb) * scale - ) - end -end +""" + boundingbox(scenelike[, exclude = plot -> false]) -function height_insensitive_boundingbox(ext::GlyphExtent) - l = ext.ink_bounding_box.origin[1] - w = ext.ink_bounding_box.widths[1] - b = ext.descender - h = ext.ascender - return Rect2f((l, b), (w, h - b)) -end +Returns the combined world space bounding box of all plots collected under +`scenelike`. This include `plot.transformation`, i.e. the `transform_func` and +the `model` matrix. Plots with `exclude(plot) == true` are excluded. -function height_insensitive_boundingbox_with_advance(ext::GlyphExtent) - l = 0f0 - r = ext.hadvance - b = ext.descender - h = ext.ascender - return Rect2f((l, b), (r - l, h - b)) +See also: [`data_limits`](@ref) +""" +function boundingbox(scenelike, exclude = (p)-> false) + bb_ref = Base.RefValue(Rect3d()) + foreach_plot(scenelike) do plot + if !exclude(plot) + update_boundingbox!(bb_ref, future_boundingbox(plot)) + end + end + return bb_ref[] end -_inkboundingbox(ext::GlyphExtent) = ext.ink_bounding_box - -unchecked_boundingbox(glyphcollection::GlyphCollection, position::Point3f, rotation::Quaternion) = - unchecked_boundingbox(glyphcollection, rotation) + position - -function unchecked_boundingbox(glyphcollection::GlyphCollection, rotation::Quaternion) - isempty(glyphcollection.glyphs) && return Rect3f(Point3f(0), Vec3f(0)) +""" + boundingbox(plot::AbstractPlot) - glyphorigins = glyphcollection.origins - glyphbbs = gl_bboxes(glyphcollection) +Returns the world space bounding box of a plot. This include `plot.transformation`, +i.e. the `transform_func` and the `model` matrix. - bb = Rect3f() - for (charo, glyphbb) in zip(glyphorigins, glyphbbs) - charbb = rotate_bbox(Rect3f(glyphbb), rotation) + charo - if !isfinite_rect(bb) - bb = charbb - else - bb = union(bb, charbb) - end +See also: [`data_limits`](@ref) +""" +boundingbox(plot::AbstractPlot) = _boundingbox(plot) + +# TODO: This only exists to deprecate boundingbox(::Text) more smoothly. Once +# that is fully removed this should be boundingbox(plot). +function _boundingbox(plot::AbstractPlot) + # Assume primitive plot + if isempty(plot.plots) + return Rect3d(iterate_transformed(plot)) end - return bb -end -function unchecked_boundingbox(layouts::AbstractArray{<:GlyphCollection}, positions, rotations) - isempty(layouts) && return Rect3f((0, 0, 0), (0, 0, 0)) - - bb = Rect3f() - broadcast_foreach(layouts, positions, rotations) do layout, pos, rot - if !isfinite_rect(bb) - bb = boundingbox(layout, pos, rot) - else - bb = union(bb, boundingbox(layout, pos, rot)) - end + # Assume combined plot + bb_ref = Base.RefValue(future_boundingbox(plot.plots[1])) + for i in 2:length(plot.plots) + update_boundingbox!(bb_ref, future_boundingbox(plot.plots[i])) end - return bb -end -function boundingbox(x::Union{GlyphCollection,AbstractArray{<:GlyphCollection}}, args...) - bb = unchecked_boundingbox(x, args...) - isfinite_rect(bb) || error("Invalid text boundingbox") - bb + return bb_ref[] end - -function boundingbox(x::Text{<:Tuple{<:GlyphCollection}}) - if x.space[] == x.markerspace[] - pos = to_ndim(Point3f, x.position[], 0) +# Replace future_boundingbox with just boundingbox once boundingbox(::Text) is +# no longer in pixel space +@inline future_boundingbox(plot::AbstractPlot) = boundingbox(plot) +@inline future_boundingbox(plot::Text) = _boundingbox(plot) + +function _boundingbox(plot::Text) + if plot.space[] == plot.markerspace[] + return transform_bbox(plot, text_boundingbox(plot)) else - cam = parent_scene(x).camera - transformed = apply_transform(x.transformation.transform_func[], x.position[]) - pos = Makie.project(cam, x.space[], x.markerspace[], transformed) + return Rect3d(iterate_transformed(plot)) end - return boundingbox(x[1][], pos, to_rotation(x.rotation[])) end -function boundingbox(x::Text{<:Tuple{<:AbstractArray{<:GlyphCollection}}}) - if x.space[] == x.markerspace[] - pos = to_ndim.(Point3f, x.position[], 0) - else - cam = (parent_scene(x).camera,) - transformed = apply_transform(x.transformation.transform_func[], x.position[]) - pos = Makie.project.(cam, x.space[], x.markerspace[], transformed) - end - return boundingbox(x[1][], pos, to_rotation(x.rotation[])) +# for convenience +function transform_bbox(scenelike, lims::Rect) + return Rect3d(iterate_transformed(scenelike, point_iterator(lims))) end -function boundingbox(plot::Text) - bb = Rect3f() - for p in plot.plots - _bb = boundingbox(p) - if !isfinite_rect(bb) - bb = _bb - elseif isfinite_rect(_bb) - bb = union(bb, _bb) - end +# same as data_limits except using iterate_transformed +function boundingbox(plot::MeshScatter) + # TODO: avoid mesh generation here if possible + @get_attribute plot (marker, markersize, rotations) + marker_bb = Rect3d(marker) + positions = iterate_transformed(plot) + scales = markersize + # fast path for constant markersize + if scales isa VecTypes{3} && rotations isa Quaternion + bb = Rect3d(positions) + marker_bb = rotations * (marker_bb * scales) + return Rect3d(minimum(bb) + minimum(marker_bb), widths(bb) + widths(marker_bb)) + else + # TODO: optimize const scale, var rot and var scale, const rot + return limits_with_marker_transforms(positions, scales, rotations, marker_bb) end - return bb end -_is_latex_string(x::AbstractVector{<:LaTeXString}) = true -_is_latex_string(x::LaTeXString) = true -_is_latex_string(other) = false - -function text_bb(str, font, size) - rot = Quaternionf(0,0,0,1) - fonts = nothing # TODO: remove the arg if possible - layout = layout_text( - str, size, font, fonts, Vec2f(0), rot, 0.5, 1.0, - RGBAf(0, 0, 0, 0), RGBAf(0, 0, 0, 0), 0f0, 0f0) - return boundingbox(layout, Point3f(0), rot) -end -""" -Calculate an approximation of a tight rectangle around a 2D rectangle rotated by `angle` radians. -This is not perfect but works well enough. Check an A vs X to see the difference. -""" -function rotatedrect(rect::Rect{2, T}, angle)::Rect{2, T} where T - ox, oy = rect.origin - wx, wy = rect.widths - points = Mat{2, 4, T}( - ox, oy, - ox, oy+wy, - ox+wx, oy, - ox+wx, oy+wy - ) - mrot = Mat{2, 2, T}( - cos(angle), -sin(angle), - sin(angle), cos(angle) - ) - rotated = mrot * points - - rmins = minimum(rotated; dims=2) - rmaxs = maximum(rotated; dims=2) - - return Rect2(rmins..., (rmaxs .- rmins)...) + +################################################################################ +### transformed point iterator +################################################################################ + + +# TODO should Float32 conversions apply here? + +@inline iterate_transformed(plot) = iterate_transformed(plot, point_iterator(plot)) + +function iterate_transformed(plot, points::AbstractArray{<: VecTypes}) + return apply_transform_and_model(plot, points) end + +# TODO: Can this be deleted? +function iterate_transformed(plot, points::T) where T + @warn "iterate_transformed with $T" + t = transformation(plot) + model = model_transform(t) # will auto-promote if points if Float64 + trans_func = transform_func(t) + [to_ndim(Point3d, project(model, apply_transform(trans_func, point, space))) for point in points] +end \ No newline at end of file diff --git a/src/layouting/data_limits.jl b/src/layouting/data_limits.jl index 92d4e94a38b..dc86fd5a0d5 100644 --- a/src/layouting/data_limits.jl +++ b/src/layouting/data_limits.jl @@ -1,154 +1,193 @@ -_isfinite(x) = isfinite(x) -_isfinite(x::VecTypes) = all(isfinite, x) -isfinite_rect(x::Rect) = all(isfinite, x.origin) && all(isfinite, x.widths) -scalarmax(x::Union{Tuple, AbstractArray}, y::Union{Tuple, AbstractArray}) = max.(x, y) -scalarmax(x, y) = max(x, y) -scalarmin(x::Union{Tuple, AbstractArray}, y::Union{Tuple, AbstractArray}) = min.(x, y) -scalarmin(x, y) = min(x, y) +#= +Hierarchy: +- boundingbox falls back on points_iterator of primitives +- points_iterator falls back on data_limits +- data_limits uses points_iterator for a few specific primitive plots + +So overload both `data_limits` and `boundingbox`. You can use: +- `points_iterator(::Rect)` to decompose the Rect +- `_boundingbox(plot, ::Rect)` to transform the Rect using the plots transformations +=# + +################################################################################ +### data_limits +################################################################################ + +""" + data_limits(scenelike[, exclude = plot -> false]) + +Returns the combined data limits of all plots collected under `scenelike` for +which `exclude(plot) == false`. This is solely based on the positional data of +a plot and thus does not include any transformations. + +See also: [`boundingbox`](@ref) +""" +function data_limits(scenelike, exclude=(p)-> false) + bb_ref = Base.RefValue(Rect3d()) + foreach_plot(scenelike) do plot + if !exclude(plot) + update_boundingbox!(bb_ref, data_limits(plot)) + end + end + return bb_ref[] +end -extrema_nan(itr::Pair) = (itr[1], itr[2]) -extrema_nan(itr::ClosedInterval) = (minimum(itr), maximum(itr)) +""" + data_limits(plot::AbstractPlot) -function extrema_nan(itr) - vs = iterate(itr) - vs === nothing && return (NaN, NaN) - v, s = vs - vmin = vmax = v - # find first finite value - while vs !== nothing && !_isfinite(v) - v, s = vs - vmin = vmax = v - vs = iterate(itr, s) +Returns the bounding box of a plot based on just its position data. + +See also: [`boundingbox`](@ref) +""" +function data_limits(plot::AbstractPlot) + # Assume primitive plot + if isempty(plot.plots) + return Rect3d(point_iterator(plot)) end - while vs !== nothing - x, s = vs - vs = iterate(itr, s) - _isfinite(x) || continue - vmax = scalarmax(x, vmax) - vmin = scalarmin(x, vmin) + + # Assume combined plot + bb_ref = Base.RefValue(data_limits(plot.plots[1])) + for i in 2:length(plot.plots) + update_boundingbox!(bb_ref, data_limits(plot.plots[i])) end - return (vmin, vmax) + + return bb_ref[] end -function distinct_extrema_nan(x) - lo, hi = extrema_nan(x) - lo == hi ? (lo - 0.5f0, hi + 0.5f0) : (lo, hi) +# A few overloads for performance +function data_limits(plot::Surface) + mini_maxi = extrema_nan.((plot.x[], plot.y[], plot.z[])) + mini = first.(mini_maxi) + maxi = last.(mini_maxi) + return Rect3d(mini, maxi .- mini) end -function point_iterator(plot::Union{Scatter, MeshScatter, Lines, LineSegments}) - return plot.positions[] +function data_limits(plot::Union{Heatmap, Image}) + mini_maxi = extrema_nan.((plot.x[], plot.y[])) + mini = Vec3d(first.(mini_maxi)..., 0) + maxi = Vec3d(last.(mini_maxi)..., 0) + return Rect3d(mini, maxi .- mini) +end + +function data_limits(x::Volume) + axes = (x[1][], x[2][], x[3][]) + extremata = extrema.(axes) + return Rect3d(first.(extremata), last.(extremata) .- first.(extremata)) end -# TODO? -function data_limits(text::Text{<: Tuple{<: Union{GlyphCollection, AbstractVector{GlyphCollection}}}}) - if is_data_space(text.markerspace[]) - return boundingbox(text) +# We don't want pixel space line segments to be considered... +function data_limits(plot::Text) + if plot.space[] == plot.markerspace[] + return text_boundingbox(plot) else - if text.position[] isa VecTypes - return Rect3f(text.position[]) - else - # TODO: is this branch necessary? - return Rect3f(convert_arguments(PointBased(), text.position[])[1]) - end + return Rect3d(point_iterator(plot)) end end -function data_limits(text::Text) - return data_limits(text.plots[1]) +# includes markersize and rotation +function data_limits(plot::MeshScatter) + # TODO: avoid mesh generation here if possible + @get_attribute plot (marker, markersize, rotations) + marker_bb = Rect3d(marker) + positions = point_iterator(plot) + scales = markersize + # fast path for constant markersize + if scales isa VecTypes{3} && rotations isa Quaternion + bb = Rect3d(positions) + marker_bb = rotations * (marker_bb * scales) + return Rect3d(minimum(bb) + minimum(marker_bb), widths(bb) + widths(marker_bb)) + else + # TODO: optimize const scale, var rot and var scale, const rot + return limits_with_marker_transforms(positions, scales, rotations, marker_bb) + end end -point_iterator(mesh::GeometryBasics.Mesh) = decompose(Point, mesh) +# include bbox from scaled markers +function limits_with_marker_transforms(positions, scales, rotations, element_bbox) + isempty(positions) && return Rect3d() -function point_iterator(list::AbstractVector) - if length(list) == 1 - # save a copy! - return point_iterator(list[1]) - else - points = Point3f[] - for elem in list - for point in point_iterator(elem) - push!(points, to_ndim(Point3f, point, 0)) - end - end - return points + first_scale = attr_broadcast_getindex(scales, 1) + first_rot = attr_broadcast_getindex(rotations, 1) + full_bbox = Ref(first_rot * (element_bbox * first_scale) + first(positions)) + for (i, pos) in enumerate(positions) + scale, rot = attr_broadcast_getindex(scales, i), attr_broadcast_getindex(rotations, i) + transformed_bbox = rot * (element_bbox * scale) + pos + update_boundingbox!(full_bbox, transformed_bbox) end + + return full_bbox[] end -point_iterator(plot::Mesh) = point_iterator(plot.mesh[]) -function br_getindex(vector::AbstractVector, idx::CartesianIndex, dim::Int) - return vector[Tuple(idx)[dim]] -end +################################################################################ +### point_iterator +################################################################################ -function br_getindex(matrix::AbstractMatrix, idx::CartesianIndex, dim::Int) - return matrix[idx] -end -function point_iterator(plot::Union{Image, Heatmap, Surface}) - rect = data_limits(plot) - return unique(decompose(Point3f, rect)) +function point_iterator(plot::Union{Scatter, MeshScatter, Lines, LineSegments}) + return plot.positions[] end -function point_iterator(x::Volume) - axes = (x[1], x[2], x[3]) - extremata = map(extrema∘to_value, axes) - minpoint = Point3f(first.(extremata)...) - widths = last.(extremata) .- first.(extremata) - rect = Rect3f(minpoint, Vec3f(widths)) - return unique(decompose(Point, rect)) +point_iterator(plot::Text) = point_iterator(plot.plots[1]) +function point_iterator(plot::Text{<: Tuple{<: Union{GlyphCollection, AbstractVector{GlyphCollection}}}}) + return plot.position[] end -function foreach_plot(f, s::Scene) - foreach_plot(f, s.plots) - foreach(sub-> foreach_plot(f, sub), s.children) -end +point_iterator(mesh::GeometryBasics.Mesh) = decompose(Point, mesh) +point_iterator(plot::Mesh) = point_iterator(plot.mesh[]) -foreach_plot(f, s::Figure) = foreach_plot(f, s.scene) -foreach_plot(f, s::FigureAxisPlot) = foreach_plot(f, s.figure) -foreach_plot(f, list::AbstractVector) = foreach(f, list) -function foreach_plot(f, plot::Plot) - if isempty(plot.plots) - f(plot) - else - foreach_plot(f, plot.plots) - end -end +# Fallback for other primitive plots, used in boundingbox +point_iterator(plot::AbstractPlot) = point_iterator(data_limits(plot)) -function foreach_transformed(f, point_iterator, model, trans_func) - for point in point_iterator - point_t = apply_transform(trans_func, point) - point_m = project(model, point_t) - f(point_m) - end - return -end +# For generic usage +point_iterator(bbox::Rect) = unique(decompose(Point3d, bbox)) -function foreach_transformed(f, plot) - points = point_iterator(plot) - t = transformation(plot) - model = model_transform(t) - trans_func = t.transform_func[] - # use function barrier since trans_func is Any - foreach_transformed(f, points, model, identity) -end -function iterate_transformed(plot) - points = point_iterator(plot) - t = transformation(plot) - model = model_transform(t) - # TODO: without this, axes with log scales error. Why? - trans_func = identity # transform_func(t) - # trans_func = identity - iterate_transformed(points, model, to_value(get(plot, :space, :data)), trans_func) +################################################################################ +### Utilities +################################################################################ + + +isfinite_rect(x::Rect) = all(isfinite, x.origin) && all(isfinite, x.widths) +_isfinite(x) = isfinite(x) +_isfinite(x::VecTypes) = all(isfinite, x) +scalarmax(x::Union{Tuple, AbstractArray}, y::Union{Tuple, AbstractArray}) = max.(x, y) +scalarmax(x, y) = max(x, y) +scalarmin(x::Union{Tuple, AbstractArray}, y::Union{Tuple, AbstractArray}) = min.(x, y) +scalarmin(x, y) = min(x, y) + +extrema_nan(itr::Pair) = (itr[1], itr[2]) +extrema_nan(itr::ClosedInterval) = (minimum(itr), maximum(itr)) +function extrema_nan(itr) + vs = iterate(itr) + vs === nothing && return (NaN, NaN) + v, s = vs + vmin = vmax = v + # find first finite value + while vs !== nothing && !_isfinite(v) + v, s = vs + vmin = vmax = v + vs = iterate(itr, s) + end + while vs !== nothing + x, s = vs + vs = iterate(itr, s) + _isfinite(x) || continue + vmax = scalarmax(x, vmax) + vmin = scalarmin(x, vmin) + end + return (vmin, vmax) end -function iterate_transformed(points, model, space, trans_func) - (to_ndim(Point3f, project(model, apply_transform(trans_func, point, space)), 0f0) for point in points) +# used in colorsampler.jl, datashader.jl +function distinct_extrema_nan(x) + lo, hi = extrema_nan(x) + lo == hi ? (lo - 0.5f0, hi + 0.5f0) : (lo, hi) end function update_boundingbox!(bb_ref, point) if all(isfinite, point) - vec = to_ndim(Vec3f, point, 0.0) + vec = to_ndim(Vec3d, point, 0.0) bb_ref[] = update(bb_ref[], vec) end end @@ -166,22 +205,7 @@ function update_boundingbox!(bb_ref, bb::Rect) return end -# Default data_limits -function data_limits(plot::AbstractPlot) - # Assume primitive plot - if isempty(plot.plots) - return limits_from_transformed_points(iterate_transformed(plot)) - end - - # Assume Plot Plot - bb_ref = Base.RefValue(data_limits(plot.plots[1])) - for i in 2:length(plot.plots) - update_boundingbox!(bb_ref, data_limits(plot.plots[i])) - end - - return bb_ref[] -end - +# used in PolarAxis function _update_rect(rect::Rect{N, T}, point::VecTypes{N, T}) where {N, T} mi = minimum(rect) ma = maximum(rect) @@ -195,74 +219,15 @@ function _update_rect(rect::Rect{N, T}, point::VecTypes{N, T}) where {N, T} typeof(rect)(new_o, new_w) end -function limits_from_transformed_points(points_iterator) - isempty(points_iterator) && return Rect3f() - first, rest = Iterators.peel(points_iterator) - bb = foldl(_update_rect, rest, init = Rect3f(first, zero(first))) - return bb -end -# include bbox from scaled markers -function limits_from_transformed_points(positions, scales, rotations, element_bbox) - isempty(positions) && return Rect3f() - - first_scale = attr_broadcast_getindex(scales, 1) - first_rot = attr_broadcast_getindex(rotations, 1) - full_bbox = Ref(first_rot * (element_bbox * first_scale) + first(positions)) - for (i, pos) in enumerate(positions) - scale, rot = attr_broadcast_getindex(scales, i), attr_broadcast_getindex(rotations, i) - transformed_bbox = rot * (element_bbox * scale) + pos - update_boundingbox!(full_bbox, transformed_bbox) - end - - return full_bbox[] -end - -function data_limits(scenelike, exclude=(p)-> false) - bb_ref = Base.RefValue(Rect3f()) - foreach_plot(scenelike) do plot - if !exclude(plot) - update_boundingbox!(bb_ref, data_limits(plot)) - end - end - return bb_ref[] -end - -# A few overloads for performance -function data_limits(plot::Surface) - mini_maxi = extrema_nan.((plot.x[], plot.y[], plot.z[])) - mini = first.(mini_maxi) - maxi = last.(mini_maxi) - return Rect3f(mini, maxi .- mini) -end - -function data_limits(plot::Heatmap) - mini_maxi = extrema_nan.((plot.x[], plot.y[])) - mini = Vec3f(first.(mini_maxi)..., 0) - maxi = Vec3f(last.(mini_maxi)..., 0) - return Rect3f(mini, maxi .- mini) -end - -function data_limits(plot::Image) - mini_maxi = extrema_nan.((plot.x[], plot.y[])) - mini = Vec3f(first.(mini_maxi)..., 0) - maxi = Vec3f(last.(mini_maxi)..., 0) - return Rect3f(mini, maxi .- mini) -end - -function data_limits(plot::MeshScatter) - # TODO: avoid mesh generation here if possible - @get_attribute plot (marker, markersize, rotations) - marker_bb = Rect3f(marker) - positions = iterate_transformed(plot) - scales = markersize - # fast path for constant markersize - if scales isa VecTypes{3} && rotations isa Quaternion - bb = limits_from_transformed_points(positions) - marker_bb = rotations * (marker_bb * scales) - return Rect3f(minimum(bb) + minimum(marker_bb), widths(bb) + widths(marker_bb)) +foreach_plot(f, s::Scene) = foreach_plot(f, s.plots) +# foreach_plot(f, s::Figure) = foreach_plot(f, s.scene) +# foreach_plot(f, s::FigureAxisPlot) = foreach_plot(f, s.figure) +foreach_plot(f, list::AbstractVector) = foreach(f, list) +function foreach_plot(f, plot::Plot) + if isempty(plot.plots) + f(plot) else - # TODO: optimize const scale, var rot and var scale, const rot - return limits_from_transformed_points(positions, scales, rotations, marker_bb) + foreach_plot(f, plot.plots) end -end +end \ No newline at end of file diff --git a/src/layouting/layouting.jl b/src/layouting/layouting.jl index 4765bbc7cad..93f2bf9b42e 100644 --- a/src/layouting/layouting.jl +++ b/src/layouting/layouting.jl @@ -268,8 +268,8 @@ _offset_to_vec(o::Vector) = to_ndim.(Vec3f, o, 0) Base.getindex(x::ScalarOrVector, i) = x.sv isa Vector ? x.sv[i] : x.sv Base.lastindex(x::ScalarOrVector) = x.sv isa Vector ? length(x.sv) : 1 -function text_quads(atlas::TextureAtlas, position::VecTypes, gc::GlyphCollection, offset, transfunc, space) - p = apply_transform(transfunc, position, space) +function text_quads(atlas::TextureAtlas, position::VecTypes, gc::GlyphCollection, offset, f32c, transfunc, space) + p = f32_convert(f32c, apply_transform(transfunc, position, space), space) pos = [to_ndim(Point3f, p, 0) for _ in gc.origins] pad = atlas.glyph_padding / atlas.pix_per_glyph @@ -301,8 +301,8 @@ function text_quads(atlas::TextureAtlas, position::VecTypes, gc::GlyphCollection return pos, char_offsets, quad_offsets, uvs, scales end -function text_quads(atlas::TextureAtlas, position::Vector, gcs::Vector{<: GlyphCollection}, offset, transfunc, space) - ps = apply_transform(transfunc, position, space) +function text_quads(atlas::TextureAtlas, position::Vector, gcs::Vector{<: GlyphCollection}, offset, f32c, transfunc, space) + ps = f32_convert(f32c, apply_transform(transfunc, position, space), space) pos = [to_ndim(Point3f, p, 0) for (p, gc) in zip(ps, gcs) for _ in gc.origins] pad = atlas.glyph_padding / atlas.pix_per_glyph diff --git a/src/layouting/maybe_unused.jl b/src/layouting/maybe_unused.jl new file mode 100644 index 00000000000..ce7699484e7 --- /dev/null +++ b/src/layouting/maybe_unused.jl @@ -0,0 +1,118 @@ +################################################################################ +### from data_limits +################################################################################ + +# TODO: boundingbox +# function data_limits(text::Text{<: Tuple{<: Union{GlyphCollection, AbstractVector{GlyphCollection}}}}) +# if is_data_space(text.markerspace[]) +# return boundingbox(text) +# else +# if text.position[] isa VecTypes +# return Rect3d(text.position[]) +# else +# # TODO: is this branch necessary? +# return Rect3d(convert_arguments(PointBased(), text.position[])[1]) +# end +# end +# end +# function data_limits(text::Text) + # return data_limits(text.plots[1]) +# end + +# TODO: unused? +# function br_getindex(vector::AbstractVector, idx::CartesianIndex, dim::Int) +# return vector[Tuple(idx)[dim]] +# end +# function br_getindex(matrix::AbstractMatrix, idx::CartesianIndex, dim::Int) +# return matrix[idx] +# end + +# TODO: boundingbox + +# function foreach_transformed(f, point_iterator, model, trans_func) +# for point in point_iterator +# point_t = apply_transform(trans_func, point) +# point_m = project(model, point_t) +# f(point_m) +# end +# return +# end + +# function foreach_transformed(f, plot) +# points = point_iterator(plot) +# t = transformation(plot) +# model = model_transform(t) +# trans_func = t.transform_func[] +# # use function barrier since trans_func is Any +# foreach_transformed(f, points, model, identity) +# end + + +# # TODO: What's your purpose? +# function point_iterator(list::AbstractVector) +# if length(list) == 1 +# # save a copy! +# return point_iterator(list[1]) +# else +# points = Point3d[] +# for elem in list +# for point in point_iterator(elem) +# push!(points, to_ndim(Point3d, point, 0)) +# end +# end +# return points +# end +# end + + +################################################################################ +### from boundingboxes/text +################################################################################ + +#= +function project_widths(matrix, vec) + pr = project(matrix, vec) + zero = project(matrix, zeros(typeof(vec))) + return pr - zero +end + +function height_insensitive_boundingbox(ext::GlyphExtent) + l = ext.ink_bounding_box.origin[1] + w = ext.ink_bounding_box.widths[1] + b = ext.descender + h = ext.ascender + return Rect2d((l, b), (w, h - b)) +end + +_inkboundingbox(ext::GlyphExtent) = ext.ink_bounding_box + +_is_latex_string(x::AbstractVector{<:LaTeXString}) = true +_is_latex_string(x::LaTeXString) = true +_is_latex_string(other) = false + +""" +Calculate an approximation of a tight rectangle around a 2D rectangle rotated by `angle` radians. +This is not perfect but works well enough. Check an A vs X to see the difference. +""" +function rotatedrect(rect::Rect{2, T}, angle)::Rect{2, T} where T + ox, oy = rect.origin + wx, wy = rect.widths + points = Mat{2, 4, T}( + ox, oy, + ox, oy+wy, + ox+wx, oy, + ox+wx, oy+wy + ) + mrot = Mat{2, 2, T}( + cos(angle), -sin(angle), + sin(angle), cos(angle) + ) + rotated = mrot * points + + rmins = minimum(rotated; dims=2) + rmaxs = maximum(rotated; dims=2) + + return Rect2(rmins..., (rmaxs .- rmins)...) +end + +=# \ No newline at end of file diff --git a/src/layouting/text_boundingbox.jl b/src/layouting/text_boundingbox.jl new file mode 100644 index 00000000000..5ea6925a040 --- /dev/null +++ b/src/layouting/text_boundingbox.jl @@ -0,0 +1,131 @@ +function boundingbox(plot::Text) + @warn """ + `boundingbox(::Text)` has been deprecated in favor of `Makie.text_boundingbox(::Text)`. + In the future `boundingbox(::Text)` will be adjusted to match the other + `boundingbox(plot)` functions. The new functionality is currently available + as `Makie._boundingbox(plot::Text)`. + """ + return text_boundingbox(plot) +end + +# TODO: Naming: not px, it's whatever markerspace is... +function text_boundingbox(plot::Text) + bb = Rect3d() + for p in plot.plots + _bb = text_boundingbox(p) + if !isfinite_rect(bb) + bb = _bb + elseif isfinite_rect(_bb) + bb = union(bb, _bb) + end + end + return bb +end + +# Text can contain linesegments. Use data_limits to avoid transformations as +# they are already in markerspace +text_boundingbox(x::LineSegments) = data_limits(x) + +function text_boundingbox(x::Text{<:Tuple{<:GlyphCollection}}) + if x.space[] == x.markerspace[] + pos = to_ndim(Point3d, x.position[], 0) + else + cam = parent_scene(x).camera + transformed = apply_transform(x.transformation.transform_func[], x.position[]) + pos = Makie.project(cam, x.space[], x.markerspace[], transformed) + end + return text_boundingbox(x[1][], pos, to_rotation(x.rotation[])) +end + +function text_boundingbox(x::Text{<:Tuple{<:AbstractArray{<:GlyphCollection}}}) + if x.space[] == x.markerspace[] + pos = to_ndim.(Point3d, x.position[], 0) + else + cam = (parent_scene(x).camera,) + transformed = apply_transform(x.transformation.transform_func[], x.position[]) + pos = Makie.project.(cam, x.space[], x.markerspace[], transformed) # TODO: vectorized project + end + return text_boundingbox(x[1][], pos, to_rotation(x.rotation[])) +end + +function text_boundingbox(x::Union{GlyphCollection,AbstractArray{<:GlyphCollection}}, args...) + bb = unchecked_boundingbox(x, args...) + isfinite_rect(bb) || error("Invalid text boundingbox") + return bb +end + +# Utility +function text_bb(str, font, size) + rot = Quaternionf(0,0,0,1) + fonts = nothing # TODO: remove the arg if possible + layout = layout_text( + str, size, font, fonts, Vec2f(0), rot, 0.5, 1.0, + RGBAf(0, 0, 0, 0), RGBAf(0, 0, 0, 0), 0f0, 0f0) + return text_boundingbox(layout, Point3d(0), rot) +end + + +################################################################################ + +function unchecked_boundingbox(glyphcollection::GlyphCollection, position::Point3, rotation::Quaternion) + return unchecked_boundingbox(glyphcollection, rotation) + position +end + +function unchecked_boundingbox(glyphcollection::GlyphCollection, rotation::Quaternion) + isempty(glyphcollection.glyphs) && return Rect3d(Point3d(0), Vec3d(0)) + + glyphorigins = glyphcollection.origins + glyphbbs = gl_bboxes(glyphcollection) + + bb = Rect3d() + for (charo, glyphbb) in zip(glyphorigins, glyphbbs) + charbb = rotate_bbox(Rect3d(glyphbb), rotation) + charo + if !isfinite_rect(bb) + bb = charbb + else + bb = union(bb, charbb) + end + end + return bb +end + +function unchecked_boundingbox(layouts::AbstractArray{<:GlyphCollection}, positions, rotations) + isempty(layouts) && return Rect3d((0, 0, 0), (0, 0, 0)) + + bb = Rect3d() + broadcast_foreach(layouts, positions, rotations) do layout, pos, rot + if !isfinite_rect(bb) + bb = text_boundingbox(layout, pos, rot) + else + bb = union(bb, text_boundingbox(layout, pos, rot)) + end + end + return bb +end + + +################################################################################ + +# used + +function gl_bboxes(gl::GlyphCollection) + scales = gl.scales.sv isa Vec2 ? (gl.scales.sv for _ in gl.extents) : gl.scales.sv + map(gl.glyphs, gl.extents, scales) do c, ext, scale + hi_bb = height_insensitive_boundingbox_with_advance(ext) + # TODO c != 0 filters out all non renderables, which is not always desired + return Rect2d(origin(hi_bb) * scale, (c != 0) * widths(hi_bb) * scale) + end +end + +function height_insensitive_boundingbox_with_advance(ext::GlyphExtent) + l = 0.0 + r = ext.hadvance + b = ext.descender + h = ext.ascender + return Rect2d((l, b), (r - l, h - b)) +end + +function rotate_bbox(bb::Rect3{T}, rot) where {T <: Real} + points = decompose(Point3{T}, bb) + return Rect3{T}(Ref(rot) .* points) +end \ No newline at end of file diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index 409328f6308..4adb376f41c 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -1,5 +1,10 @@ Base.parent(t::Transformation) = isassigned(t.parent) ? t.parent[] : nothing +function parent_transform(x) + p = parent(transformation(x)) + return isnothing(p) ? Mat4f(I) : p.model[] +end + function Observables.connect!(parent::Transformation, child::Transformation; connect_func=true) tfuncs = [] obsfunc = on(parent.model; update=true) do m @@ -45,8 +50,8 @@ end function transform!( t::Transformable; - translation = Vec3f(0), - scale = Vec3f(1), + translation = Vec3d(0), + scale = Vec3d(1), rotation = 0.0, ) translate!(t, to_value(translation)) @@ -66,7 +71,7 @@ transformation(t::Transformation) = t scale(t::Transformable) = transformation(t).scale -scale!(t::Transformable, s) = (scale(t)[] = to_ndim(Vec3f, Float32.(s), 1)) +scale!(t::Transformable, s) = (scale(t)[] = to_ndim(Vec3d, s, 1)) """ scale!(t::Transformable, x, y) @@ -128,7 +133,7 @@ This is the default setting. struct Absolute end function translate!(::Type{T}, t::Transformable, trans) where T - offset = to_ndim(Vec3f, Float32.(trans), 0) + offset = to_ndim(Vec3d, trans, 0) if T === Accum translation(t)[] = translation(t)[] .+ offset elseif T === Absolute @@ -154,7 +159,7 @@ Translate the given `Transformable` (a Scene or Plot), relative to its current p translate!(::Type{T}, t::Transformable, xyz...) where T = translate!(T, t, xyz) function transform!(t::Transformable, x::Tuple{Symbol, <: Number}) - plane, dimval = string(x[1]), Float32(x[2]) + plane, dimval = string(x[1]), Float64(x[2]) if length(plane) != 2 || (!all(x-> x in ('x', 'y', 'z'), plane)) error("plane needs to define a 2D plane in xyz. It should only contain 2 symbols out of (:x, :y, :z). Found: $plane") end @@ -178,28 +183,31 @@ 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) - + apply_transform_and_model(plot, pos, output_type = Point3d) + apply_transform_and_model(model, transfrom_func, pos, output_type = Point3d) 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) +function apply_transform_and_model(plot::AbstractPlot, pos, output_type = Point3d) 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) +function apply_transform_and_model(model::Mat4, f, pos::VecTypes, space = :data, output_type = Point3d) 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) + if space in (:data, :transformed) + p4d = to_ndim(Point4d, to_ndim(Point3d, transformed, 0), 1) + p4d = model * p4d + p4d = p4d ./ p4d[4] + return to_ndim(output_type, p4d, NaN) + else + return transformed + end end -function apply_transform_and_model(model::Mat4f, f, positions::Vector, space = :data, output_type = Point3f) +function apply_transform_and_model(model::Mat4, f, positions::AbstractArray, space = :data, output_type = Point3d) return map(positions) do pos apply_transform_and_model(model, f, pos, space, output_type) end @@ -256,14 +264,14 @@ function apply_transform(f::PointTrans{N}, point::Point{N}) where N return f.f(point) end -function apply_transform(f::PointTrans{N1}, point::Point{N2}) where {N1, N2} - p_dim = to_ndim(Point{N1, Float32}, point, 0.0) +function apply_transform(f::PointTrans{N1}, point::Point{N2, T}) where {N1, N2, T} + p_dim = to_ndim(Point{N1, T}, point, 0.0) p_trans = f.f(p_dim) if N1 < N2 p_large = ntuple(i-> i <= N1 ? p_trans[i] : point[i], N2) - return Point{N2, Float32}(p_large) + return Point{N2, T}(p_large) else - return to_ndim(Point{N2, Float32}, p_trans, 0.0) + return to_ndim(Point{N2, T}, p_trans, 0.0) end end @@ -271,8 +279,8 @@ function apply_transform(f, data::AbstractArray) map(point -> apply_transform(f, point), data) end -function apply_transform(f::Tuple{Any, Any}, point::VecTypes{2}) - Point2{Float32}( +function apply_transform(f::Tuple{Any, Any}, point::VecTypes{2, T}) where T + Point2{T}( f[1](point[1]), f[2](point[2]), ) @@ -287,8 +295,8 @@ end # ambiguity fix apply_transform(f::NTuple{2, typeof(identity)}, point::VecTypes{3}) = point -function apply_transform(f::Tuple{Any, Any, Any}, point::VecTypes{3}) - Point3{Float32}( +function apply_transform(f::Tuple{Any, Any, Any}, point::VecTypes{3, T}) where T + Point3{T}( f[1](point[1]), f[2](point[2]), f[3](point[3]), @@ -389,7 +397,7 @@ This struct defines a general polar-to-cartesian transformation, i.e. where θ is assumed to be in radians. Controls: -- `theta_as_x = true` controls the order of incoming arguments. If true, a `Point2f` +- `theta_as_x = true` controls the order of incoming arguments. If true, a `Point2` is interpreted as `(θ, r)`, otherwise `(r, θ)`. - `clip_r = true` controls whether negative radii are clipped. If true, `r < 0` produces `NaN`, otherwise they simply enter in the formula above as is. Note that @@ -430,13 +438,13 @@ end # Point2 may get expanded to Point3. In that case we leave z untransformed function apply_transform(f::Polar, point::VecTypes{N2, T}) where {N2, T} - p_dim = to_ndim(Point2f, point, 0.0) + p_dim = to_ndim(Point2{T}, point, 0.0) p_trans = apply_transform(f, p_dim) if 2 < N2 p_large = ntuple(i-> i <= 2 ? p_trans[i] : point[i], N2) - return Point{N2, Float32}(p_large) + return Point{N2, T}(p_large) else - return to_ndim(Point{N2, Float32}, p_trans, 0.0) + return to_ndim(Point{N2, T}, p_trans, 0.0) end end @@ -464,5 +472,5 @@ end # and this way we can use the z-value as a means to shift the drawing order # by translating e.g. the axis spines forward so they are not obscured halfway # by heatmaps or images -zvalue2d(x)::Float32 = Makie.translation(x)[][3] + zvalue2d(x.parent) +zvalue2d(x)::Float32 = Float32(Makie.translation(x)[][3] + zvalue2d(x.parent)) zvalue2d(::Nothing)::Float32 = 0f0 diff --git a/src/makielayout/blocks/axis.jl b/src/makielayout/blocks/axis.jl index 3afb56c6faa..1a3f9fd79a1 100644 --- a/src/makielayout/blocks/axis.jl +++ b/src/makielayout/blocks/axis.jl @@ -72,16 +72,19 @@ function register_events!(ax, scene) return end -function update_axis_camera(camera::Camera, t, lims, xrev::Bool, yrev::Bool) +function update_axis_camera(scene::Scene, t, lims, xrev::Bool, yrev::Bool) nearclip = -10_000f0 - farclip = 10_000f0 + farclip = 10_000f0 # we are computing transformed camera position, so this isn't space dependent tlims = Makie.apply_transform(t, lims) + camera = scene.camera - left, bottom = minimum(tlims) - right, top = maximum(tlims) - + # TODO: apply model + update_limits!(scene.float32convert, tlims) # update float32 scaling + lims32 = f32_convert(scene.float32convert, tlims) # get scaled limits + left, bottom = minimum(lims32) + right, top = maximum(lims32) leftright = xrev ? (right, left) : (left, right) bottomtop = yrev ? (top, bottom) : (bottom, top) @@ -107,7 +110,7 @@ function calculate_title_position(area, titlegap, subtitlegap, align, xaxisposit end local subtitlespace::Float32 = if ax.subtitlevisible[] && !iswhitespace(ax.subtitle[]) - boundingbox(subtitlet).widths[2] + subtitlegap + text_boundingbox(subtitlet).widths[2] + subtitlegap else 0f0 end @@ -132,8 +135,8 @@ function compute_protrusions(title, titlesize, titlegap, titlevisible, spinewidt top = xaxisprotrusion end - titleheight = boundingbox(titlet).widths[2] + titlegap - subtitleheight = boundingbox(subtitlet).widths[2] + subtitlegap + titleheight = text_boundingbox(titlet).widths[2] + titlegap + subtitleheight = text_boundingbox(subtitlet).widths[2] + subtitlegap titlespace = if !titlevisible || iswhitespace(title) 0f0 @@ -165,8 +168,8 @@ function initialize_block!(ax::Axis; palette = nothing) # initialize either with user limits, or pick defaults based on scales # so that we don't immediately error - targetlimits = Observable{Rect2f}(defaultlimits(ax.limits[], ax.xscale[], ax.yscale[])) - finallimits = Observable{Rect2f}(targetlimits[]; ignore_equal_values=true) + targetlimits = Observable{Rect2d}(defaultlimits(ax.limits[], ax.xscale[], ax.yscale[])) + finallimits = Observable{Rect2d}(targetlimits[]; ignore_equal_values=true) setfield!(ax, :targetlimits, targetlimits) setfield!(ax, :finallimits, finallimits) @@ -185,6 +188,8 @@ function initialize_block!(ax::Axis; palette = nothing) scene = Scene(blockscene, viewport=scenearea) ax.scene = scene + setfield!(scene, :float32convert, Float32Convert()) + if !isnothing(palette) # Backwards compatibility for when palette was part of axis! palette_attr = palette isa Attributes ? palette : Attributes(palette) @@ -255,8 +260,10 @@ function initialize_block!(ax::Axis; palette = nothing) notify(ax.xscale) # 3. Update the view onto the plot (camera matrices) - onany(update_axis_camera, blockscene, camera(scene), scene.transformation.transform_func, finallimits, - ax.xreversed, ax.yreversed; priority=-2) + onany(blockscene, scene.transformation.transform_func, finallimits, + ax.xreversed, ax.yreversed; priority=-2) do args... + update_axis_camera(scene, args...) + end xaxis_endpoints = lift(blockscene, ax.xaxisposition, scene.viewport; ignore_equal_values=true) do xaxisposition, area @@ -584,7 +591,7 @@ function reset_limits!(ax; xauto = true, yauto = true, zauto = true) (lo, hi) end else - convert(Tuple{Float32, Float32}, tuple(mxlims...)) + convert(Tuple{Float64, Float64}, tuple(mxlims...)) end ylims = if isnothing(mylims) || mylims[1] === nothing || mylims[2] === nothing l = if yauto @@ -600,7 +607,7 @@ function reset_limits!(ax; xauto = true, yauto = true, zauto = true) (lo, hi) end else - convert(Tuple{Float32, Float32}, tuple(mylims...)) + convert(Tuple{Float64, Float64}, tuple(mylims...)) end if ax isa Axis3 @@ -842,8 +849,24 @@ function getlimits(la::Axis, dim) # only use visible plots for limits return !to_value(get(plot, :visible, true)) end - # get all data limits, minus the excluded plots - boundingbox = Makie.data_limits(la.scene, exclude) + + # TODO: + # We used to include scale! and rotate! in data_limits. For compat we include + # them here again until we implement a full solution + + # # get all data limits, minus the excluded plots + # boundingbox = Makie.data_limits(la.scene, exclude) + bb_ref = Base.RefValue(Rect3d()) + for plot in la.scene + if !exclude(plot) + bb = data_limits(plot) + model = plot.model[][Vec(1,2,3), Vec(1,2,3)] + bb = Rect3d(map(p -> model * to_ndim(Point3d, p, 0), coordinates(bb))) + update_boundingbox!(bb_ref, bb) + end + end + boundingbox = bb_ref[] + # if there are no bboxes remaining, `nothing` signals that no limits could be determined Makie.isfinite_rect(boundingbox) || return nothing @@ -1234,7 +1257,10 @@ function Makie.xlims!(ax::Axis, xlims) end mlims = convert_limit_attribute(ax.limits[]) - + # if hasproperty(ax, :x_dim_convert) && ax.x_dim_convert[] isa Float32Conversion + # scaling = ax.x_dim_convert[].scaling[] + # xlims = scale_value.(Ref(scaling), xlims) + # end ax.limits.val = (xlims, mlims[2]) reset_limits!(ax, yauto = false) nothing @@ -1253,7 +1279,10 @@ function Makie.ylims!(ax::Axis, ylims) end mlims = convert_limit_attribute(ax.limits[]) - + # if hasproperty(ax, :y_dim_convert) && ax.y_dim_convert[] isa Float32Conversion + # scaling = ax.y_dim_convert[].scaling[] + # ylims = scale_value.(Ref(scaling), ylims) + # end ax.limits.val = (mlims[1], ylims) reset_limits!(ax, xauto = false) nothing @@ -1389,14 +1418,14 @@ Makie.transform_func(ax::Axis) = Makie.transform_func(ax.scene) # these functions pick limits for different x and y scales, so that # we don't pick values that are invalid, such as 0 for log etc. function defaultlimits(userlimits::Tuple{Real, Real, Real, Real}, xscale, yscale) - BBox(userlimits...) + BBox(Float64.(userlimits)...) end defaultlimits(l::Tuple{Any, Any, Any, Any}, xscale, yscale) = defaultlimits(((l[1], l[2]), (l[3], l[4])), xscale, yscale) function defaultlimits(userlimits::Tuple{Any, Any}, xscale, yscale) - xl = defaultlimits(userlimits[1], xscale) - yl = defaultlimits(userlimits[2], yscale) + xl = Float64.(defaultlimits(userlimits[1], xscale)) + yl = Float64.(defaultlimits(userlimits[2], yscale)) BBox(xl..., yl...) end diff --git a/src/makielayout/blocks/button.jl b/src/makielayout/blocks/button.jl index c52d71e7426..ea34f35879e 100644 --- a/src/makielayout/blocks/button.jl +++ b/src/makielayout/blocks/button.jl @@ -40,7 +40,7 @@ function initialize_block!(b::Button) translate!(labeltext, 0, 0, 1) onany(scene, b.label, b.fontsize, b.font, b.padding) do label, fontsize, font, padding - textbb = Rect2f(boundingbox(labeltext)) + textbb = Rect2f(text_boundingbox(labeltext)) autowidth = width(textbb) + padding[1] + padding[2] autoheight = height(textbb) + padding[3] + padding[4] b.layoutobservables.autosize[] = (autowidth, autoheight) diff --git a/src/makielayout/blocks/float32-scaling.jl b/src/makielayout/blocks/float32-scaling.jl new file mode 100644 index 00000000000..2b71553aef2 --- /dev/null +++ b/src/makielayout/blocks/float32-scaling.jl @@ -0,0 +1,109 @@ +#= +# Maybe parametrice this with the input target type (e.g. Int64, Float64, Int128) +struct Float32Scaling{TargetType} + scale::Base.TwicePrecision{Float64} + offset::Base.TwicePrecision{Float64} +end + +function Float32Scaling{TargetType}(s::Real, o::Real) where {TargetType} + return Float32Scaling{TargetType}(Base.TwicePrecision(s), Base.TwicePrecision(o)) +end + +""" + update_scaling_factors(scaling::Float32Scaling, scaled_min::Real, scaled_max::Real) + +Returns a new scaling if the scaled values fall outside the desired range. +Gets called with already scaled values, to be easily used with `finallimits`. +""" +function update_scaling_factors(scaling::Float32Scaling{T}, scaled_min::Real, scaled_max::Real) where {T} + TW = Base.TwicePrecision{Float64} + max_range = 100 + min_r, max_r = -max_range / 2, max_range / 2 + + if (scaled_min > min_r) && (scaled_max < max_r) + return scaling + end + # Recalculate the scale and offset to ensure the scaled values fall within the desired range + mini, maxi = unscale_value(scaling, scaled_min), unscale_value(scaling, scaled_max) + # The desired range is 100, but we always convert to a smaller target range + # to less often change the scaling + desired_range = TW(max_range / 10) + offset = TW(mini) + (TW(maxi - mini) / TW(2)) + # Adjust the scale + scale = TW(maxi - mini) / desired_range + return Float32Scaling{T}(scale, offset) +end + +function scale_value(scaling::Float32Scaling, value::Real) + return Float32((value - scaling.offset) / scaling.scale) +end + +function convert_to_target(::Type{T}, x::Base.TwicePrecision) where T + convert(T, x) +end + +function convert_to_target(::Type{T}, x::Base.TwicePrecision) where {T <: Integer} + return round(T, Float64(x)) +end + +function unscale_value(scaling::Float32Scaling{T}, value::Real) where T + # TODO, this should maybe not return Float64 + # The big question is, how do we make a lossless conversion from TwicePrecision back to Int64 + # But otherwise it will return TwicePrecision, which doesn't work well + # since lots of math functions are not defined for it + return convert_to_target(T, (value * scaling.scale) + scaling.offset) +end + +struct Float32Conversion{T} + scaling::Observable{Float32Scaling{T}} +end + +function dim_conversion_type(::Type{T}) where {T <: Int64} + return Float32Conversion{T}(Observable(Float32Scaling{T}(1.0, 0.0); ignore_equal_values=true)) +end + +MakieCore.can_axis_convert(::Type{<:Union{Lines, Scatter}}, ::AbstractVector{<:Int64}) = true + +dim_conversion_type(::Type{T}) where {T} = Float32Conversion{T}(Observable(Float32Scaling{T}(1.0, 0.0); ignore_equal_values=true)) +MakieCore.can_axis_convert(::Type{<:Union{Lines,Scatter}}, ::AbstractVector{<:Float64}) = true + + +function convert_axis_dim(conversion::Float32Conversion{T}, values::Observable) where {T} + # TODO update minimum in connect! on limit change! + scaling = update_scaling_factors(conversion.scaling[], extrema(values[])...) + conversion.scaling[] = scaling + return map(values, conversion.scaling) do vals, scaling + return scale_value.(Ref(scaling), vals) + end +end + +function connect_conversion!(ax::Axis, conversion_obs::Observable, conversion::Float32Conversion, dim) + on(ax.blockscene, ax.finallimits) do limits + # Don't update if nothing plotted yet + if isempty(ax.scene.plots) + return + end + scaling = conversion.scaling[] + # Get scaled extrema of the limits of the dimension + mini, maxi = getindex.(extrema(limits), dim) + # Calculate new scaling + new_scaling = update_scaling_factors(scaling, mini, maxi) + if new_scaling != scaling + # Only update if the scaling changed + # conversion.scaling[] = new_scaling + # notify(conversion_obs) + end + end +end + +function get_ticks(conversion::Float32Conversion, ticks, scale, formatter, vmin, vmax) + if scale != identity + error("$(scale) scale not supported for Float32Conversion") + end + f32scaling = conversion.scaling[] + umin = unscale_value(f32scaling, vmin) + umax = unscale_value(f32scaling, vmax) + ticks, labels = get_ticks(ticks, scale, formatter, umin, umax) + return scale_value.(Ref(f32scaling), ticks), labels +end +=# \ No newline at end of file diff --git a/src/makielayout/blocks/label.jl b/src/makielayout/blocks/label.jl index adc7de7fbc7..1f50af6725c 100644 --- a/src/makielayout/blocks/label.jl +++ b/src/makielayout/blocks/label.jl @@ -18,7 +18,7 @@ function initialize_block!(l::Label) onany(topscene, l.text, l.fontsize, l.font, l.rotation, word_wrap_width, l.padding) do _, _, _, _, _, padding - textbb[] = Rect2f(boundingbox(t)) + textbb[] = Rect2f(text_boundingbox(t)) autowidth = width(textbb[]) + padding[1] + padding[2] autoheight = height(textbb[]) + padding[3] + padding[4] if l.word_wrap[] diff --git a/src/makielayout/blocks/menu.jl b/src/makielayout/blocks/menu.jl index a88471351ff..31aeef8434b 100644 --- a/src/makielayout/blocks/menu.jl +++ b/src/makielayout/blocks/menu.jl @@ -104,7 +104,7 @@ function initialize_block!(m::Menu; default = 1) end end - selectionarea = Observable(Rect2f(0, 0, 0, 0); ignore_equal_values=true) + selectionarea = Observable(Rect2d(0, 0, 0, 0); ignore_equal_values=true) selectionpoly = poly!( blockscene, selectionarea, color = m.selection_cell_color_inactive[]; @@ -118,20 +118,20 @@ function initialize_block!(m::Menu; default = 1) ) onany(blockscene, selected_text, m.fontsize, m.textpadding) do _, _, (l, r, b, t) - bb = boundingbox(selectiontext) + bb = text_boundingbox(selectiontext) m.layoutobservables.autosize[] = width(bb) + l + r, height(bb) + b + t end notify(selected_text) on(blockscene, m.layoutobservables.computedbbox) do cbb - selectionarea[] = cbb + selectionarea[] = Rect2d(origin(cbb), widths(cbb)) ch = height(cbb) selectiontextpos[] = cbb.origin + Point2f(m.textpadding[][1], ch/2) end textpositions = Observable(zeros(Point2f, length(optionstrings[])); ignore_equal_values=true) - optionrects = Observable([Rect2f(0, 0, 0, 0)]; ignore_equal_values=true) + optionrects = Observable([Rect2d(0, 0, 0, 0)]; ignore_equal_values=true) optionpolycolors = Observable(RGBAf[RGBAf(0.5, 0.5, 0.5, 1)]; ignore_equal_values=true) function update_option_colors!(hovered) @@ -162,7 +162,7 @@ function initialize_block!(m::Menu; default = 1) onany(blockscene, optionstrings, m.textpadding, m.layoutobservables.computedbbox) do _, pad, bbox gcs = optiontexts.plots[1][1][]::Vector{GlyphCollection} - bbs = map(x -> boundingbox(x, zero(Point3f), Quaternion(0, 0, 0, 0)), gcs) + bbs = map(x -> text_boundingbox(x, zero(Point3f), Quaternion(0, 0, 0, 0)), gcs) heights = map(bb -> height(bb) + pad[3] + pad[4], bbs) heights_cumsum = [zero(eltype(heights)); cumsum(heights)] h = sum(heights) diff --git a/src/makielayout/blocks/polaraxis.jl b/src/makielayout/blocks/polaraxis.jl index 2250fad0d09..b89f74419c3 100644 --- a/src/makielayout/blocks/polaraxis.jl +++ b/src/makielayout/blocks/polaraxis.jl @@ -60,11 +60,11 @@ function initialize_block!(po::PolarAxis; palette=nothing) # (each boundingbox represents a string without text.position applied) max_widths = Vec2f(0) for gc in thetaticklabelplot.plots[1].plots[1][1][] - bbox = boundingbox(gc, Quaternionf(0, 0, 0, 1)) # no rotation + bbox = text_boundingbox(gc, Quaternionf(0, 0, 0, 1)) # no rotation max_widths = max.(max_widths, widths(bbox)[Vec(1,2)]) end for gc in rticklabelplot.plots[1].plots[1][1][] - bbox = boundingbox(gc, Quaternionf(0, 0, 0, 1)) # no rotation + bbox = text_boundingbox(gc, Quaternionf(0, 0, 0, 1)) # no rotation max_widths = max.(max_widths, widths(bbox)[Vec(1,2)]) end diff --git a/src/makielayout/blocks/scene.jl b/src/makielayout/blocks/scene.jl index e5cac177b0f..1e741594939 100644 --- a/src/makielayout/blocks/scene.jl +++ b/src/makielayout/blocks/scene.jl @@ -19,9 +19,9 @@ function initialize_block!(ls::LScene; scenekw = NamedTuple()) # update limits when scene limits change limits = lift(blockscene, ls.scene.theme.limits) do lims if lims === automatic - dl = data_limits(ls.scene, p -> Makie.isaxis(p) || Makie.not_in_data_space(p)) + dl = boundingbox(ls.scene, p -> Makie.isaxis(p) || Makie.not_in_data_space(p)) if any(isinf, widths(dl)) || any(isinf, Makie.origin(dl)) - Rect3f((0f0, 0f0, 0f0), (1f0, 1f0, 1f0)) + Rect3d((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)) else dl end diff --git a/src/makielayout/interactions.jl b/src/makielayout/interactions.jl index e086e160bb7..72ad656b2f1 100644 --- a/src/makielayout/interactions.jl +++ b/src/makielayout/interactions.jl @@ -108,16 +108,15 @@ end ############################################################################ function _chosen_limits(rz, ax) - - r = positivize(Rect2f(rz.from, rz.to .- rz.from)) + r = positivize(Rect2(rz.from, rz.to .- rz.from)) lims = ax.finallimits[] # restrict to y change if rz.restrict_x || !ax.xrectzoom[] - r = Rect2f(lims.origin[1], r.origin[2], widths(lims)[1], widths(r)[2]) + r = Rect2(lims.origin[1], r.origin[2], widths(lims)[1], widths(r)[2]) end # restrict to x change if rz.restrict_y || !ax.yrectzoom[] - r = Rect2f(r.origin[1], lims.origin[2], widths(r)[1], widths(lims)[2]) + r = Rect2(r.origin[1], lims.origin[2], widths(r)[1], widths(lims)[2]) end return r end @@ -205,11 +204,11 @@ function process_interaction(r::RectangleZoom, event::KeysEvent, ax::Axis) return Consume(true) end -function positivize(r::Rect2f) +function positivize(r::Rect2) negwidths = r.widths .< 0 newori = ifelse.(negwidths, r.origin .+ r.widths, r.origin) newwidths = ifelse.(negwidths, -r.widths, r.widths) - return Rect2f(newori, newwidths) + return Rect2(newori, newwidths) end function process_interaction(::LimitReset, event::MouseEvent, ax::Axis) @@ -245,9 +244,9 @@ function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::Axis) if zoom != 0 pa = viewport(scene)[] - z = (1f0 - s.speed)^zoom + z = (1.0 - s.speed)^zoom - mp_axscene = Vec4f((e.mouseposition[] .- pa.origin)..., 0, 1) + mp_axscene = Vec4d((e.mouseposition[] .- pa.origin)..., 0, 1) # first to normal -1..1 space mp_axfraction = (cam.pixel_space[] * mp_axscene)[Vec(1, 2)] .* @@ -277,11 +276,11 @@ function process_interaction(s::ScrollZoom, event::ScrollEvent, ax::Axis) timed_ticklabelspace_reset(ax, s.reset_timer, s.prev_xticklabelspace, s.prev_yticklabelspace, s.reset_delay) newrect_trans = if ispressed(scene, xzoomkey[]) - Rectf(newxorigin, yorigin, newxwidth, ywidth) + Rectd(newxorigin, yorigin, newxwidth, ywidth) elseif ispressed(scene, yzoomkey[]) - Rectf(xorigin, newyorigin, xwidth, newywidth) + Rectd(xorigin, newyorigin, xwidth, newywidth) else - Rectf(newxorigin, newyorigin, newxwidth, newywidth) + Rectd(newxorigin, newyorigin, newxwidth, newywidth) end inv_transf = Makie.inverse_transform(transf) @@ -347,7 +346,7 @@ function process_interaction(dp::DragPan, event::MouseEvent, ax) timed_ticklabelspace_reset(ax, dp.reset_timer, dp.prev_xticklabelspace, dp.prev_yticklabelspace, dp.reset_delay) inv_transf = Makie.inverse_transform(transf) - newrect_trans = Rectf(Vec2f(xori, yori), widths(tlimits_trans)) + newrect_trans = Rectd(Vec2(xori, yori), widths(tlimits_trans)) tlimits[] = Makie.apply_transform(inv_transf, newrect_trans) return Consume(true) diff --git a/src/makielayout/lineaxis.jl b/src/makielayout/lineaxis.jl index 04ff372fc58..e923d66e761 100644 --- a/src/makielayout/lineaxis.jl +++ b/src/makielayout/lineaxis.jl @@ -37,7 +37,7 @@ function calculate_protrusion( real_labelsize::Float32 = if label_is_empty 0f0 else - boundingbox(labeltext).widths[horizontal[] ? 2 : 1] + text_boundingbox(labeltext).widths[horizontal[] ? 2 : 1] end labelspace::Float32 = (labelvisible && !label_is_empty) ? real_labelsize + labelpadding : 0f0 @@ -194,7 +194,7 @@ is_within_limits(tv, limits) = (limits[1] ≤ tv || limits[1] ≈ tv) && (tv ≤ function update_tickpos_string(closure_args, tickvalues_labels_unfiltered, reversed::Bool, scale) tickstrings, tickpositions, tickvalues, pos_extents_horizontal, limits_obs = closure_args - limits = limits_obs[]::NTuple{2, Float32} + limits = limits_obs[]::NTuple{2, Float64} tickvalues_unfiltered, tickstrings_unfiltered = tickvalues_labels_unfiltered @@ -230,7 +230,7 @@ function update_tickpos_string(closure_args, tickvalues_labels_unfiltered, rever return end -function update_minor_ticks(minortickpositions, limits::NTuple{2, Float32}, pos_extents_horizontal, minortickvalues_unfiltered, scale, reversed::Bool) +function update_minor_ticks(minortickpositions, limits::NTuple{2, Float64}, pos_extents_horizontal, minortickvalues_unfiltered, scale, reversed::Bool) position::Float32, extents_uncorrected::NTuple{2, Float32}, horizontal::Bool = pos_extents_horizontal extents = reversed ? reverse(extents_uncorrected) : extents_uncorrected @@ -269,7 +269,7 @@ function LineAxis(parent::Scene, attrs::Attributes) pos_extents_horizontal = lift(calculate_horizontal_extends, parent, endpoints; ignore_equal_values=true) horizontal = lift(x -> x[3], parent, pos_extents_horizontal) # Tuple constructor converts more than `convert(NTuple{2, Float32}, x)` but we still need the conversion to Float32 tuple: - limits = lift(x -> convert(NTuple{2,Float32}, Tuple(x)), parent, attrs.limits; ignore_equal_values=true) + limits = lift(x -> convert(NTuple{2, Float64}, Tuple(x)), parent, attrs.limits; ignore_equal_values=true) flipped = lift(x -> convert(Bool, x), parent, attrs.flipped; ignore_equal_values=true) ticksnode = Observable(Point2f[]; ignore_equal_values=true) @@ -300,10 +300,10 @@ function LineAxis(parent::Scene, attrs::Attributes) map!(parent, ticklabel_ideal_space, ticklabel_annotation_obs, ticklabelalign, ticklabelrotation, ticklabelfont, ticklabelsvisible) do args... maxwidth = if pos_extents_horizontal[][3] # height - ticklabelsvisible[] ? (ticklabels === nothing ? 0f0 : height(Rect2f(boundingbox(ticklabels)))) : 0f0 + ticklabelsvisible[] ? (ticklabels === nothing ? 0f0 : height(Rect2f(text_boundingbox(ticklabels)))) : 0f0 else # width - ticklabelsvisible[] ? (ticklabels === nothing ? 0f0 : width(Rect2f(boundingbox(ticklabels)))) : 0f0 + ticklabelsvisible[] ? (ticklabels === nothing ? 0f0 : width(Rect2f(text_boundingbox(ticklabels)))) : 0f0 end # in case there is no string in the annotations and the boundingbox comes back all NaN if !isfinite(maxwidth) @@ -401,7 +401,7 @@ function LineAxis(parent::Scene, attrs::Attributes) xs::Float32, ys::Float32 = if labelrotation isa Automatic 0f0, 0f0 else - wx, wy = widths(boundingbox(labeltext)) + wx, wy = widths(text_boundingbox(labeltext)) sign::Int = flipped ? 1 : -1 if horizontal 0f0, Float32(sign * 0.5f0 * wy) @@ -414,9 +414,9 @@ function LineAxis(parent::Scene, attrs::Attributes) decorations[:labeltext] = labeltext - tickvalues = Observable(Float32[]; ignore_equal_values=true) + tickvalues = Observable(Float64[]; ignore_equal_values=true) - tickvalues_labels_unfiltered = Observable{Tuple{Vector{Float32},Vector{Any}}}() + tickvalues_labels_unfiltered = Observable{Tuple{Vector{Float64},Vector{Any}}}() map!(parent, tickvalues_labels_unfiltered, pos_extents_horizontal, dim_convert, limits, ticks, tickformat, attrs.scale) do (position, extents, horizontal), dim_convert, limits, ticks, tickformat, scale @@ -430,7 +430,7 @@ function LineAxis(parent::Scene, attrs::Attributes) Observable((tickstrings, tickpositions, tickvalues, pos_extents_horizontal, limits)), tickvalues_labels_unfiltered, reversed, attrs.scale) - minortickvalues = Observable(Float32[]; ignore_equal_values=true) + minortickvalues = Observable(Float64[]; ignore_equal_values=true) minortickpositions = Observable(Point2f[]; ignore_equal_values=true) onany(parent, tickvalues, minorticks) do tickvalues, minorticks @@ -519,10 +519,10 @@ function tight_ticklabel_spacing!(la::LineAxis) tls = la.elements[:ticklabels] maxwidth = if horizontal # height - tls.visible[] ? height(Rect2f(boundingbox(tls))) : 0f0 + tls.visible[] ? height(Rect2f(text_boundingbox(tls))) : 0f0 else # width - tls.visible[] ? width(Rect2f(boundingbox(tls))) : 0f0 + tls.visible[] ? width(Rect2f(text_boundingbox(tls))) : 0f0 end la.attributes.ticklabelspace = maxwidth return Float64(maxwidth) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index ca4d4dbe480..871f5ef9149 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -150,15 +150,15 @@ mutable struct RectangleZoom active::Observable{Bool} restrict_x::Bool restrict_y::Bool - from::Union{Nothing, Point2f} - to::Union{Nothing, Point2f} - rectnode::Observable{Rect2f} + from::Union{Nothing, Point2d} + to::Union{Nothing, Point2d} + rectnode::Observable{Rect2d} modifier::Any # e.g. Keyboard.left_alt, or some other button that needs to be pressed to start rectangle... Defaults to `true`, which means no modifier needed end function RectangleZoom(callback::Function; restrict_x=false, restrict_y=false, modifier=true) return RectangleZoom(callback, Observable(false), restrict_x, restrict_y, - nothing, nothing, Observable(Rect2f(0, 0, 1, 1)), modifier) + nothing, nothing, Observable(Rect2d(0, 0, 1, 1)), modifier) end struct ScrollZoom @@ -201,8 +201,8 @@ end scene::Scene xaxislinks::Vector{Axis} yaxislinks::Vector{Axis} - targetlimits::Observable{Rect2f} - finallimits::Observable{Rect2f} + targetlimits::Observable{Rect2d} + finallimits::Observable{Rect2d} block_limit_linking::Observable{Bool} mouseeventhandle::MouseEventHandle scrollevents::Observable{ScrollEvent} diff --git a/src/scenes.jl b/src/scenes.jl index fe16c2d868a..fe17e12cbc4 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -68,6 +68,9 @@ mutable struct Scene <: AbstractScene "The [`Transformation`](@ref) of the Scene." transformation::Transformation + "A transformation rescaling data to a Float32-save range." + float32convert::Union{Nothing, Float32Convert} + "The plots contained in the Scene." plots::Vector{AbstractPlot} @@ -114,6 +117,7 @@ mutable struct Scene <: AbstractScene camera, camera_controls, transformation, + nothing, plots, theme, children, @@ -547,10 +551,9 @@ end function center!(scene::Scene, padding=0.01, exclude = not_in_data_space) bb = boundingbox(scene, exclude) - bb = transformationmatrix(scene)[] * bb w = widths(bb) padd = w .* padding - bb = Rect3f(minimum(bb) .- padd, w .+ 2padd) + bb = Rect3d(minimum(bb) .- padd, w .+ 2padd) update_cam!(scene, bb) scene end @@ -562,7 +565,7 @@ parent_scene(x::Scene) = x Base.isopen(x::SceneLike) = events(x).window_open[] function is2d(scene::SceneLike) - lims = data_limits(scene) + lims = boundingbox(scene) lims === nothing && return nothing return is2d(lims) end diff --git a/src/stats/hexbin.jl b/src/stats/hexbin.jl index 0cd4b9411eb..8eda43aeafc 100644 --- a/src/stats/hexbin.jl +++ b/src/stats/hexbin.jl @@ -54,8 +54,9 @@ function data_limits(hb::Hexbin) nw = widths(bb) .+ (ms..., 0.0f0) no = bb.origin .- ((ms ./ 2.0f0)..., 0.0f0) - return Rect3f(no, nw) + return Rect3d(no, nw) end +boundingbox(p::Hexbin) = transform_bbox(p, data_limits(hb)) get_weight(weights, i) = Float64(weights[i]) get_weight(::StatsBase.UnitWeights, i) = 1e0 diff --git a/src/types.jl b/src/types.jl index a3144183a7b..cc8fd63f9a3 100644 --- a/src/types.jl +++ b/src/types.jl @@ -266,18 +266,19 @@ $(TYPEDFIELDS) """ struct Transformation <: Transformable parent::RefValue{Transformation} - translation::Observable{Vec3f} - scale::Observable{Vec3f} + translation::Observable{Vec3d} + scale::Observable{Vec3d} rotation::Observable{Quaternionf} - model::Observable{Mat4f} - parent_model::Observable{Mat4f} + model::Observable{Mat4d} + parent_model::Observable{Mat4d} # data conversion observable, for e.g. log / log10 etc transform_func::Observable{Any} + function Transformation(translation, scale, rotation, transform_func) - translation_o = convert(Observable{Vec3f}, translation) - scale_o = convert(Observable{Vec3f}, scale) + translation_o = convert(Observable{Vec3d}, translation) + scale_o = convert(Observable{Vec3d}, scale) rotation_o = convert(Observable{Quaternionf}, rotation) - parent_model = Observable(Mat4f(I)) + parent_model = Observable(Mat4d(I)) model = map(translation_o, scale_o, rotation_o, parent_model) do t, s, r, p return p * transformationmatrix(t, s, r) end @@ -288,18 +289,15 @@ struct Transformation <: Transformable end function Transformation(transform_func=identity; - scale=Vec3f(1), - translation=Vec3f(0), + scale=Vec3d(1), + translation=Vec3d(0), rotation=Quaternionf(0, 0, 0, 1)) - return Transformation(translation, - scale, - rotation, - transform_func) + return Transformation(translation, scale, rotation, transform_func) end function Transformation(parent::Transformable; - scale=Vec3f(1), - translation=Vec3f(0), + scale=Vec3d(1), + translation=Vec3d(0), rotation=Quaternionf(0, 0, 0, 1), transform_func=nothing) connect_func = isnothing(transform_func)