diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index 1a800c77a59..823667b3eb6 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -3,11 +3,13 @@ on: pull_request: branches: - master + - breaking-0.21 push: tags: - '*' branches: - master + - breaking-0.21 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72993352e49..0fb71eb4251 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,11 +6,13 @@ on: - '*.md' branches: - master + - breaking-0.21 push: tags: - '*' branches: - master + - breaking-0.21 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index 70ffd1a3e09..62b07be55d6 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -6,6 +6,7 @@ on: - '*.md' branches: - master + - breaking-0.21 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true diff --git a/.github/workflows/reference_tests.yml b/.github/workflows/reference_tests.yml index bb398c01621..f5afbbd21f6 100644 --- a/.github/workflows/reference_tests.yml +++ b/.github/workflows/reference_tests.yml @@ -6,11 +6,13 @@ on: - '*.md' branches: - master + - breaking-0.21 push: tags: - '*' branches: - master + - breaking-0.21 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -179,28 +181,28 @@ jobs: - name: Consolidate reference image folders run: | baseDir="./ReferenceImages" - + # Create new top-level directory for combined files mkdir -p "./ReferenceImagesCombined" - + # Copy the reference folder from GLMakie, it's the same for all backends cp -r "${baseDir}/GLMakie/reference/." "./ReferenceImagesCombined/reference/" - + # Initialize empty files for concatenation > "./ReferenceImagesCombined/scores.tsv" > "./ReferenceImagesCombined/new_files.txt" - + # Loop through the directories and concatenate the files, and copy recorded folders for dir in WGLMakie CairoMakie GLMakie; do # Concatenate scores.tsv and new_files.txt cat "${baseDir}/${dir}/scores.tsv" >> "./ReferenceImagesCombined/scores.tsv" cat "${baseDir}/${dir}/new_files.txt" >> "./ReferenceImagesCombined/new_files.txt" - + # Copy recorded folder mkdir -p "./ReferenceImagesCombined/recorded/${dir}/" cp -r "${baseDir}/${dir}/recorded/${dir}/." "./ReferenceImagesCombined/recorded/${dir}/" done - + echo "Files and folders have been successfully combined into ReferenceImagesCombined." - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/relocatability.yml b/.github/workflows/relocatability.yml index 5612a9e08e8..c46e2ab3c26 100644 --- a/.github/workflows/relocatability.yml +++ b/.github/workflows/relocatability.yml @@ -6,11 +6,13 @@ on: - '*.md' branches: - master + - breaking-0.21 push: tags: - '*' branches: - master + - breaking-0.21 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/rprmakie.yaml b/.github/workflows/rprmakie.yaml index 73cf6e30a33..1d7bb26a1d9 100644 --- a/.github/workflows/rprmakie.yaml +++ b/.github/workflows/rprmakie.yaml @@ -6,11 +6,13 @@ on: - '*.md' branches: - master + - breaking-0.21 push: tags: - '*' branches: - master + - breaking-0.21 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 012493dd6c7..cbb4df48c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,32 @@ # Changelog -## [Unreleased] +## [0.21.0] - 2024-03-0X +- Add `voxels` plot [#3527](https://github.com/MakieOrg/Makie.jl/pull/3527) - Added supported markers hint to unsupported marker warn message [#3666](https://github.com/MakieOrg/Makie.jl/pull/3666). - Fixed bug in CairoMakie line drawing when multiple successive points had the same color [#3712](https://github.com/MakieOrg/Makie.jl/pull/3712). - 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. +- **Breaking** Streamlined `data_limits` and `boundingbox` [#3671](https://github.com/MakieOrg/Makie.jl/pull/3671) + - `data_limits` now only considers plot positions, completely ignoring transformations + - `boundingbox(p::Text)` is deprecated in favor of `boundingbox(p::Text, p.markerspace[])`. The more internal methods use `string_boundingbox(p)`. [#3723](https://github.com/MakieOrg/Makie.jl/pull/3723) + - `boundingbox` overwrites must now include a secondary space argument to work `boundingbox(plot, space::Symbol = :data)` [#3723](https://github.com/MakieOrg/Makie.jl/pull/3723) + - `boundingbox` now always consider `transform_func` and `model` (except for Text for the time being) + - `data_limits(::Scatter)` and `boundingbox(::Scatter)` now consider marker transformations [#3716](https://github.com/MakieOrg/Makie.jl/pull/3716) +- **Breaking** Improved Float64 compatability of Axis [#3681](https://github.com/MakieOrg/Makie.jl/pull/3681) + - This added an extra conversion step which only takes effect when Float32 precision becomes relevant. In those cases code using `project()` functions will be wrong as the transformation is not applied. Use `project(plot_or_scene, ...)` or apply the conversion yourself beforehand with `Makie.f32_convert(plot_or_scene, transformed_point)` and use `patched_model = Makie.patch_model(plot_or_scene, model)`. + - `Makie.to_world(point, matrix, resolution)` has been deprecated in favor of `Makie.to_world(scene_or_plot, point)` to include float32 conversions. +- **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 + - GLMakie: Adjusted linestyles to scale with linewidth dynamically so that dots remain dots with changing linewidth + - GLMakie: Cleaned up anti-aliasing for truncated joints + - WGLMakie: Added support for linestyles + - WGLMakie: Added line joints + - WGLMakie: Added native anti-aliasing which generally improves quality but introduces outline artifacts in some cases (same as GLMakie) + - 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). +- **Breaking** Renamed attribute `rotations` to `rotation` for `scatter` and `meshscatter` which had been inconsistent with the otherwise singular naming scheme and other plots like `text` [#3724](https://github.com/MakieOrg/Makie.jl/pull/3724). - Fixed `contourf` bug where n levels would sometimes miss the uppermost value, causing gaps [#3713](https://github.com/MakieOrg/Makie.jl/pull/3713). - Added `scale` attribute to `violin` [#3352](https://github.com/MakieOrg/Makie.jl/pull/3352). - Use label formatter in barplot [#3718](https://github.com/MakieOrg/Makie.jl/pull/3718). diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index 35348cc2b8a..669f74a237e 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -1,7 +1,7 @@ name = "CairoMakie" uuid = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" author = ["Simon Danisch "] -version = "0.11.9" +version = "0.12.0" [deps] CRC32c = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" @@ -24,7 +24,7 @@ FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.1" LinearAlgebra = "1.0, 1.6" -Makie = "=0.20.8" +Makie = "=0.21.0" PrecompileTools = "1.0" julia = "1.3" 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 1c760e12bb0..212678b74d2 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -27,7 +27,82 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio end space = to_value(get(primitive, :space, :data)) - projected_positions = project_position.(Ref(scene), (Makie.transform_func(primitive),), Ref(space), positions, Ref(model)) + # Lines need to be handled more carefully with perspective projections to + # avoid them inverting. + projected_positions, indices = let + # Standard transform from input space to clip space + points = Makie.apply_transform(Makie.transform_func(primitive), positions, space) + res = scene.camera.resolution[] + 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) + s = Vec2f(0.5f0, -0.5f0) .* p[Vec(1, 2)] / p[4] .+ 0.5f0 + return res .* s + end + + screen_points = sizehint!(Vector{Vec2f}(undef, 0), length(clip_points)) + indices = sizehint!(Vector{Int}(undef, 0), length(clip_points)) + + # Adjust points such that they are always in front of the camera. + # TODO: Consider skipping this if there is no perspetive projection. + # (i.e. use project_position.(..., positions) and indices = eachindex(positions)) + for (i, p) in enumerate(clip_points) + if p[4] < 0.0 # point behind camera and ... + if primitive isa Lines # ... part of a continuous line + # create an extra point for the incoming line segment at the + # near clipping plane (i.e. on line prev --> this) + if i > 1 + prev = clip_points[i-1] + v = p - prev + # + p2 = p + (-p[4] - p[3]) / (v[3] + v[4]) * v + push!(screen_points, clip2screen(res, p2)) + push!(indices, i) + end + + # disconnect the line + push!(screen_points, Vec2f(NaN)) + + # and create another point for the outgoing line segment at + # the near clipping plane (on this ---> next) + if i < length(clip_points) + next = clip_points[i+1] + v = next - p + p2 = p + (-p[4] - p[3]) / (v[3] + v[4]) * v + push!(screen_points, clip2screen(res, p2)) + push!(indices, i) + end + + else # ... part of a discontinuous set of segments + if iseven(i) + # if this is the last point of the segment we move towards + # the previous (start) point + prev = clip_points[i-1] + v = p - prev + p = p + (-p[4] - p[3]) / (v[3] + v[4]) * v + push!(screen_points, clip2screen(res, p)) + else + # otherwise we move to the next (end) point + next = clip_points[i+1] + v = next - p + p = p + (-p[4] - p[3]) / (v[3] + v[4]) * v + push!(screen_points, clip2screen(res, p)) + end + end + else + # otherwise we can just draw the point + push!(screen_points, clip2screen(res, p)) + end + + # we always have at least one point + push!(indices, i) + end + + screen_points, indices + end color = to_color(primitive.calculated_colors[]) @@ -64,7 +139,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio draw_multi( primitive, ctx, projected_positions, - color, linewidth, + color, linewidth, indices, isnothing(linestyle) ? nothing : diff(Float64.(linestyle)) ) else @@ -150,16 +225,16 @@ function draw_single(primitive::LineSegments, ctx, positions) end # if linewidth is not an array -function draw_multi(primitive, ctx, positions, colors::AbstractArray, linewidth, dash) - draw_multi(primitive, ctx, positions, colors, [linewidth for c in colors], dash) +function draw_multi(primitive, ctx, positions, colors::AbstractArray, linewidth, indices, dash) + draw_multi(primitive, ctx, positions, colors, [linewidth for c in colors], indices, dash) end # if color is not an array -function draw_multi(primitive, ctx, positions, color, linewidths::AbstractArray, dash) - draw_multi(primitive, ctx, positions, [color for l in linewidths], linewidths, dash) +function draw_multi(primitive, ctx, positions, color, linewidths::AbstractArray, indices, dash) + draw_multi(primitive, ctx, positions, [color for l in linewidths], linewidths, indices, dash) end -function draw_multi(primitive::LineSegments, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, dash) +function draw_multi(primitive::LineSegments, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, indices, dash) @assert iseven(length(positions)) @assert length(positions) == length(colors) @assert length(linewidths) == length(colors) @@ -194,7 +269,9 @@ function draw_multi(primitive::LineSegments, ctx, positions, colors::AbstractArr end end -function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, dash) +function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, indices, dash) + colors = colors[indices] + linewidths = linewidths[indices] @assert length(positions) == length(colors) @assert length(linewidths) == length(colors) @@ -299,13 +376,13 @@ end ################################################################################ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Scatter)) - @get_attribute(primitive, (markersize, strokecolor, strokewidth, marker, marker_offset, rotations, transform_marker)) + @get_attribute(primitive, (markersize, strokecolor, strokewidth, marker, marker_offset, rotation, transform_marker)) ctx = screen.context 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()))) @@ -316,13 +393,16 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Scat transfunc = Makie.transform_func(primitive) return draw_atomic_scatter(scene, ctx, transfunc, colors, markersize, strokecolor, strokewidth, marker, - marker_offset, rotations, model, positions, size_model, font, markerspace, + marker_offset, rotation, model, positions, size_model, font, markerspace, space) end -function draw_atomic_scatter(scene, ctx, transfunc, colors, markersize, strokecolor, strokewidth, marker, marker_offset, rotations, model, positions, size_model, font, markerspace, space) +function draw_atomic_scatter(scene, ctx, transfunc, colors, markersize, strokecolor, strokewidth, marker, marker_offset, rotation, model, positions, size_model, font, markerspace, space) + # TODO Optimization: + # avoid calling project functions per element as they recalculate the + # combined projection matrix for each element like this broadcast_foreach(positions, colors, markersize, strokecolor, - strokewidth, marker, marker_offset, remove_billboard(rotations)) do point, col, + strokewidth, marker, marker_offset, remove_billboard(rotation)) do point, col, markersize, strokecolor, strokewidth, m, mo, rotation scale = project_scale(scene, markerspace, markersize, size_model) @@ -561,16 +641,20 @@ 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 + # TODO: f32convert may run into issues here if markerspace is :data or + # :transformed (repeated application in glyphpos etc) transform_func = transformation.transform_func[] p = Makie.apply_transform(transform_func, position, space) Makie.clip_to_space(scene.camera, markerspace) * Makie.space_to_clip(scene.camera, space) * - model * to_ndim(Point4f, to_ndim(Point3f, p, 0), 1) + Makie.f32_convert_matrix(scene.float32convert, space) * + model * + to_ndim(Point4d, to_ndim(Point3d, p, 0), 1) end Cairo.save(ctx) @@ -605,8 +689,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) @@ -698,7 +782,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 @@ -735,8 +819,8 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio # find projected image corners # this already takes care of flipping the image to correct cairo orientation space = to_value(get(primitive, :space, :data)) - xy = project_position(primitive, space, Point2f(first.(imsize)), model) - xymax = project_position(primitive, space, Point2f(last.(imsize)), model) + xy = project_position(primitive, space, Point2(first.(imsize)), model) + xymax = project_position(primitive, space, Point2(last.(imsize)), model) w, h = xymax .- xy can_use_fast_path = !(is_vector && !interpolate) && regular_grid && identity_transform && @@ -766,7 +850,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio # find projected image corners # this already takes care of flipping the image to correct cairo orientation space = to_value(get(primitive, :space, :data)) - xys = project_position.(scene, (Makie.transform_func(primitive),), space, [Point2f(x, y) for x in xs, y in ys], (model,)) + xys = project_position(scene, Makie.transform_func(primitive), space, [Point2(x, y) for x in xs, y in ys], model) colors = to_color(primitive.calculated_colors[]) # Note: xs and ys should have size ni+1, nj+1 @@ -832,27 +916,25 @@ 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} + space = to_value(get(plot, :space, :data))::Symbol + transform_func = Makie.transform_func(plot) + model = plot.model[]::Mat4d + vs = project_position(scene, transform_func, space, decompose(Point, mesh), model) fs = decompose(GLTriangleFace, mesh)::Vector{GLTriangleFace} uv = decompose_uv(mesh)::Union{Nothing, Vector{Vec2f}} - model = plot.model[]::Mat4f 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 - transform_func = Makie.transform_func(plot) - return draw_mesh2D(scene, screen, cols, space, transform_func, vs, fs, model) + return draw_mesh2D(screen, cols, vs, fs) end -function draw_mesh2D(scene, screen, per_face_cols, space::Symbol, transform_func, - vs::Vector{Point2f}, fs::Vector{GLTriangleFace}, model::Mat4f) +function draw_mesh2D(screen, per_face_cols, vs::Vector{<: Point2}, fs::Vector{GLTriangleFace}) ctx = screen.context # Priorize colors of the mesh if present # This is a hack, which needs cleaning up in the Mesh plot type! for (f, (c1, c2, c3)) in zip(fs, per_face_cols) - - t1, t2, t3 = project_position.(scene, (transform_func,), space, vs[f], (model,)) #triangle points + t1, t2, t3 = vs[f] #triangle points # don't draw any mesh faces with NaN components. if isnan(t1) || isnan(t2) || isnan(t3) @@ -902,7 +984,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) @@ -933,16 +1015,17 @@ function draw_mesh3D( eyeposition = scene.camera.eyeposition[] i = Vec(1, 2, 3) - local_model = rotation * Makie.scalematrix(Vec3f(scale)) + local_model = rotation * Makie.scalematrix(Vec3d(scale)) normalmatrix = transpose(inv(model[i, i] * local_model[i, i])) # see issue #3702 # pass transform_func as argument to function, so that we get a function barrier # and have `transform_func` be fully typed inside closure + model_f32 = model * Makie.f32_convert_matrix(scene.float32convert, space) vs = broadcast(meshpoints, (transform_func,)) do v, f # Should v get a nan2zero? v = Makie.apply_transform(f, v, space) - p4d = to_ndim(Vec4f, to_ndim(Vec3f, v, 0f0), 1f0) - model * (local_model * p4d .+ to_ndim(Vec4f, pos, 0f0)) + p4d = to_ndim(Vec4d, to_ndim(Vec3d, v, 0), 1) + return to_ndim(Vec4f, model_f32 * (local_model * p4d .+ to_ndim(Vec4f, pos, 0f0)), NaN32) end ns = map(n -> normalize(normalmatrix * n), meshnormals) @@ -1092,16 +1175,16 @@ end function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Makie.MeshScatter)) - @get_attribute(primitive, (model, marker, markersize, rotations)) + @get_attribute(primitive, (model, marker, markersize, rotation)) pos = primitive[1][] # For correct z-ordering we need to be in view/camera or screen space model = copy(model) view = scene.camera.view[] zorder = sortperm(pos, by = p -> begin - p4d = to_ndim(Vec4f, to_ndim(Vec3f, p, 0f0), 1f0) - cam_pos = view * model * p4d - cam_pos[3] / cam_pos[4] + p4d = to_ndim(Vec4d, to_ndim(Vec3d, p, 0), 1) + cam_pos = (view * model)[Vec(3,4), Vec(1,2,3,4)] * p4d + cam_pos[1] / cam_pos[2] end, rev=false) color = to_color(primitive.calculated_colors[]) @@ -1123,18 +1206,57 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki submesh[:calculated_colors] = color[i] end scale = markersize isa Vector ? markersize[i] : markersize - rotation = if rotations isa Vector - Makie.rotationmatrix4(to_rotation(rotations[i])) + _rotation = if rotation isa Vector + Makie.rotationmatrix4(to_rotation(rotation[i])) else - Makie.rotationmatrix4(to_rotation(rotations)) + Makie.rotationmatrix4(to_rotation(rotation)) end draw_mesh3D( scene, screen, submesh, marker, pos = p, scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0), - rotation = rotation + rotation = _rotation ) end return nothing end + + + +################################################################################ +# Voxel # +################################################################################ + + +function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Makie.Voxels)) + pos = Makie.voxel_positions(primitive) + scale = Makie.voxel_size(primitive) + colors = Makie.voxel_colors(primitive) + marker = normal_mesh(Rect3f(Point3f(-0.5), Vec3f(1))) + + # For correct z-ordering we need to be in view/camera or screen space + model = copy(primitive.model[]) + view = scene.camera.view[] + + zorder = sortperm(pos, by = p -> begin + p4d = to_ndim(Vec4f, to_ndim(Vec3f, p, 0f0), 1f0) + cam_pos = view * model * p4d + cam_pos[3] / cam_pos[4] + end, rev=false) + + submesh = Attributes( + model=model, + shading=primitive.shading, diffuse=primitive.diffuse, + specular=primitive.specular, shininess=primitive.shininess, + faceculling=get(primitive, :faceculling, -10), + transformation=Makie.transformation(primitive) + ) + + for i in zorder + submesh[:calculated_colors] = colors[i] + draw_mesh3D(scene, screen, submesh, marker, pos = pos[i], scale = scale) + end + + return nothing +end diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 9f87e61af9d..14109541262 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::AbstractArray{<: 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 <: Real} + 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 @@ -53,13 +78,13 @@ function project_shape(@nospecialize(scenelike), space, rect::Rect, model) return Rect(mini, maxi .- mini) end -function project_polygon(@nospecialize(scenelike), space, poly::P, model) where P <: Polygon - ext = decompose(Point2f, poly.exterior) - interiors = decompose.(Point2f, poly.interiors) - Polygon( - Point2f.(project_position.(Ref(scenelike), space, ext, Ref(model))), - [Point2f.(project_position.(Ref(scenelike), space, interior, Ref(model))) for interior in interiors], - ) +function project_polygon(@nospecialize(scenelike), space, poly::Polygon{N, T}, model) where {N, T} + PT = Point{N, Makie.float_type(T)} + ext = decompose(PT, poly.exterior) + project(p) = PT(project_position(scenelike, space, p, model)) + ext_proj = PT[project(p) for p in ext] + interiors_proj = Vector{PT}[PT[project(p) for p in decompose(PT, points)] for points in poly.interiors] + return Polygon(ext_proj, interiors_proj) end function project_multipolygon(@nospecialize(scenelike), space, multipoly::MP, model) where MP <: MultiPolygon diff --git a/CairoMakie/test/Project.toml b/CairoMakie/test/Project.toml index 8878262e374..171b1dbce34 100644 --- a/CairoMakie/test/Project.toml +++ b/CairoMakie/test/Project.toml @@ -1,6 +1,5 @@ [deps] GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" -GeoInterfaceMakie = "0edc0954-3250-4c18-859d-ec71c1660c08" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" ReferenceTests = "d37af2e0-5618-4e00-9939-d430db56ee94" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index afac97bc2d5..c066b345543 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -184,7 +184,9 @@ excludes = Set([ "scatter with glow", "scatter with stroke", "heatmaps & surface", - "Textured meshscatter" # not yet implemented + "Textured meshscatter", # not yet implemented + "Voxel - texture mapping", # not yet implemented + "Miter Joints for line rendering", # CairoMakie does not show overlap here and extrudes lines a little more ]) functions = [:volume, :volume!, :uv_mesh] diff --git a/CairoMakie/test/svg_tests.jl b/CairoMakie/test/svg_tests.jl index 93cb0b9349f..5efb72c4baf 100644 --- a/CairoMakie/test/svg_tests.jl +++ b/CairoMakie/test/svg_tests.jl @@ -36,15 +36,27 @@ end @test svg_isnt_rasterized(poly(Makie.GeometryBasics.MultiPolygon([poly1, poly1]))) @test svg_isnt_rasterized(poly(Makie.GeometryBasics.MultiPolygon([poly1, poly1]), color = :red)) @test svg_isnt_rasterized(poly(Makie.GeometryBasics.MultiPolygon([poly1, poly1]), color = [:red, :blue])) +end - @testset "GeoInterface polygons" begin - using GeoInterface, GeoInterfaceMakie - poly2 = GeoInterface.convert(GeoInterface.Wrappers, poly1) - @test svg_isnt_rasterized(poly(poly2)) - @test svg_isnt_rasterized(poly(poly2, color = :red)) - @test svg_isnt_rasterized(poly(GeoInterface.Wrappers.MultiPolygon([poly2, poly2]), color = [:red, :blue])) - end +struct PolyWrapper + poly::Any +end +function Makie.convert_arguments(::Type{<: Poly}, poly::PolyWrapper) + return convert_arguments(Poly, poly.poly) +end +struct MultiPolyWrapper + poly::Vector +end +function Makie.convert_arguments(::Type{<:Poly}, poly::MultiPolyWrapper) + return convert_arguments(Poly, poly.poly) +end +@testset "Polygon Wrappers" begin + poly1 = Makie.GeometryBasics.Polygon(rand(Point2f, 10)) + poly2 = PolyWrapper(poly1) + @test svg_isnt_rasterized(poly(poly2)) + @test svg_isnt_rasterized(poly(poly2; color=:red)) + @test svg_isnt_rasterized(poly(MultiPolyWrapper([poly1, poly1]); color=[:red, :blue])) end @testset "reproducable svg ids" begin diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index ca7f9892dad..c17d0d90608 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -1,6 +1,6 @@ name = "GLMakie" uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" -version = "0.9.9" +version = "0.10.0" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -30,7 +30,7 @@ FreeTypeAbstraction = "0.10" GLFW = "3.3" GeometryBasics = "0.4.1" LinearAlgebra = "1.0, 1.6" -Makie = "=0.20.8" +Makie = "=0.21.0" Markdown = "1.0, 1.6" MeshIO = "0.4" ModernGL = "1" diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom index ad8b9399b13..fd0dab16f1f 100644 --- a/GLMakie/assets/shader/line_segment.geom +++ b/GLMakie/assets/shader/line_segment.geom @@ -1,8 +1,6 @@ {{GLSL_VERSION}} {{GLSL_EXTENSIONS}} -{{define_fast_path}} - struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; @@ -14,37 +12,36 @@ uniform vec2 resolution; uniform float pattern_length; {{pattern_type}} pattern; -in vec4 g_color[]; +in {{stripped_color_type}} g_color[]; in uvec2 g_id[]; in float g_thickness[]; -out float f_thickness; -out vec4 f_color; -out vec2 f_uv; -flat out uvec2 f_id; -flat out vec2 f_uv_minmax; - -#define AA_THICKNESS 4.0 - -// Get pattern values -float fetch(Nothing _, float u){return 10000000.0;} -float fetch(sampler1D pattern, float u){return texture(pattern, u).x;} +out float f_quad_sdf0; +out vec3 f_quad_sdf1; +out float f_quad_sdf2; +out vec2 f_truncation; +out float f_linestart; +out float f_linelength; -vec3 screen_space(vec4 vertex) -{ - return vec3(vertex.xy * resolution, vertex.z) / vertex.w; +flat out float f_linewidth; +flat out vec4 f_pattern_overwrite; +flat out uvec2 f_id; +flat out vec2 f_extrusion; +flat out vec2 f_discard_limit; +flat out {{stripped_color_type}} f_color1; +flat out {{stripped_color_type}} f_color2; +flat out float f_alpha_weight; +flat out float f_cumulative_length; + +const float AA_RADIUS = 0.8; +const float AA_THICKNESS = 2.0 * AA_RADIUS; + +vec3 screen_space(vec4 vertex) { + return vec3((0.5 * vertex.xy / vertex.w + 0.5) * resolution, vertex.z / vertex.w); } -void emit_vertex(vec3 position, vec2 uv, int index) -{ - vec4 inpos = gl_in[index].gl_Position; - f_uv = uv; - f_color = g_color[index]; - gl_Position = vec4((position.xy / resolution), position.z, 1.0); - f_id = g_id[index]; - f_thickness = g_thickness[index]; - EmitVertex(); -} +vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); } +vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); } out vec3 o_view_pos; out vec3 o_view_normal; @@ -54,54 +51,83 @@ void main(void) o_view_pos = vec3(0); o_view_normal = vec3(0); - // get the four vertices passed to the shader: - vec3 p0 = screen_space(gl_in[0].gl_Position); // start of previous segment - vec3 p1 = screen_space(gl_in[1].gl_Position); // end of previous segment, start of current segment - - float thickness_aa0 = g_thickness[0] + AA_THICKNESS; - float thickness_aa1 = g_thickness[1] + AA_THICKNESS; - // determine the direction of each of the 3 segments (previous, current, next) - vec3 vun0 = p1 - p0; - vec3 v0 = vun0 / length(vun0.xy); - // determine the normal of each of the 3 segments (previous, current, next) - vec3 n0 = vec3(-v0.y, v0.x, 0); - float l = length(p1 - p0); - float px2u = 0.5 / pattern_length; - float u = l * px2u; - - vec3 AA_offset = AA_THICKNESS * v0; - float AA = AA_THICKNESS * px2u; - - /* 0 v0 l - | --> | - -thickness_aa0 - .----------------------------------. - -thickness_aa1 - -g_thickness[0] - | .------------------------------. | - -g_thickness[1] - | | | | - n0 ↑ | | | | - | | | | - +g_thickness[0] - | '------------------------------' | - +g_thickness[1] - +thickness_aa0 - '----------------------------------' - +thickness_aa1 - | | - -AA_THICKNESS l + AA_THICKNESS - */ - - #ifdef FAST_PATH - // For solid lines the uv cordinates are used as a signed distance field. - // We keep the values positive to draw a solid line and add limits to - // f_uv_minmax to add anti-aliasing at the start and end of the line - f_uv_minmax = vec2(u, 2*u); - emit_vertex(p0 + thickness_aa0 * n0 - AA_offset, vec2( u - AA, -thickness_aa0), 0); - emit_vertex(p0 - thickness_aa0 * n0 - AA_offset, vec2( u - AA, thickness_aa0), 0); - emit_vertex(p1 + thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, -thickness_aa1), 1); - emit_vertex(p1 - thickness_aa1 * n0 + AA_offset, vec2(2*u + AA, thickness_aa1), 1); - #else - // For patterned lines AA is mostly done by the pattern sampling. We - // still set f_uv_minmax here to ensure that cut off patterns als have - // anti-aliasing at the start/end of this segment - f_uv_minmax = vec2(0, u); - emit_vertex(p0 + thickness_aa0 * n0 - AA_offset, vec2( - AA, -thickness_aa0), 0); - emit_vertex(p0 - thickness_aa0 * n0 - AA_offset, vec2( - AA, thickness_aa0), 0); - emit_vertex(p1 + thickness_aa1 * n0 + AA_offset, vec2(u + AA, -thickness_aa1), 1); - emit_vertex(p1 - thickness_aa1 * n0 + AA_offset, vec2(u + AA, thickness_aa1), 1); - #endif + // we generate very thin lines for linewidth 0, so we manually skip them: + if (g_thickness[0] == 0.0 && g_thickness[1] == 0.0) { + return; + } + + // get start and end point of line segment + // restrict to visible area (see lines.geom) + vec3 p1, p2; + { + vec4 _p1 = gl_in[0].gl_Position, _p2 = gl_in[1].gl_Position; + vec4 v1 = _p2 - _p1; + + if (_p1.w < 0.0) + _p1 = _p1 + (-_p1.w - _p1.z) / (v1.z + v1.w) * v1; + if (_p2.w < 0.0) + _p2 = _p2 + (-_p2.w - _p2.z) / (v1.z + v1.w) * v1; + + p1 = screen_space(_p1); + p2 = screen_space(_p2); + } + + // get vector in line direction and vector in linewidth direction + vec3 v1 = (p2 - p1); + float segment_length = length(p2.xy - p1.xy); + v1 /= segment_length; + vec2 n1 = normal_vector(v1); + + // Set invalid / ignored outputs + f_quad_sdf0 = 10.0; // no joint to previous segment + f_quad_sdf2 = 10.0; // not joint to next segment + f_truncation = vec2(-10.0); // no truncated joint + f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); // no joints to overwrite + f_extrusion = vec2(0.5); // no joints needing extrusion + f_discard_limit = vec2(10.0); // no joints needing discards + + // constants + f_color1 = g_color[0]; + f_color2 = g_color[1]; + f_alpha_weight = min(1.0, g_thickness[0] / AA_RADIUS); + f_linestart = 0; // no corners so no joint extrusion to consider + f_linelength = segment_length; // and also no changes in line length + f_cumulative_length = 0.0; // resets for each new segment + + // Generate vertices + + for (int x = 0; x < 2; x++) { + // Get offset in line direction + float v_offset = (2 * x - 1) * AA_THICKNESS; + // pass on linewidth and id (picking) for the current line vertex + float halfwidth = 0.5 * max(AA_RADIUS, g_thickness[x]); + // TODO: if we just make this a varying output we probably get var linewidths here + f_linewidth = halfwidth; + f_id = g_id[x]; + + for (int y = 0; y < 2; y++) { + // Get offset in y direction & compute vertex position + float n_offset = (2 * y - 1) * (halfwidth + AA_THICKNESS); + vec3 position = vec3[2](p1, p2)[x] + v_offset * v1 + n_offset * vec3(n1, 0); + gl_Position = vec4(2.0 * position.xy / resolution - 1.0, position.z, 1.0); + + // Generate SDF's + + // distance from quad vertex to line control points + vec2 VP1 = position.xy - p1.xy; + vec2 VP2 = position.xy - p2.xy; + + // sdf of this segment + f_quad_sdf1.x = dot(VP1, -v1.xy); + f_quad_sdf1.y = dot(VP2, v1.xy); + f_quad_sdf1.z = n_offset; + + // finalize vertex + EmitVertex(); + } + } + + EndPrimitive(); + + return; } diff --git a/GLMakie/assets/shader/line_segment.vert b/GLMakie/assets/shader/line_segment.vert index b158327fd20..3c4f4d14659 100644 --- a/GLMakie/assets/shader/line_segment.vert +++ b/GLMakie/assets/shader/line_segment.vert @@ -8,10 +8,7 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d in float lastlen; {{vertex_type}} vertex; {{thickness_type}} thickness; - {{color_type}} color; -{{color_map_type}} color_map; -{{color_norm_type}} color_norm; uniform mat4 projectionview, model; uniform uint objectid; @@ -19,30 +16,16 @@ uniform float depth_shift; uniform float px_per_unit; out uvec2 g_id; -out vec4 g_color; +out {{stripped_color_type}} g_color; out float g_thickness; -vec4 getindex(sampler2D tex, int index); -vec4 getindex(sampler1D tex, int index); -vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm); - vec4 to_vec4(vec3 v){return vec4(v, 1);} vec4 to_vec4(vec2 v){return vec4(v, 0, 1);} -vec4 to_color(vec4 v, Nothing color_map, Nothing color_norm, int index){return v;} -vec4 to_color(vec3 v, Nothing color_map, Nothing color_norm, int index){return vec4(v, 1);} -vec4 to_color(sampler1D tex, Nothing color_map, Nothing color_norm, int index){return getindex(tex, index);} -vec4 to_color(sampler2D tex, Nothing color_map, Nothing color_norm, int index){return getindex(tex, index);} -vec4 to_color(float color, sampler1D color_map, vec2 color_norm, int index){ - return color_lookup(color, color_map, color_norm); -} - - void main() { - int index = gl_VertexID; - g_id = uvec2(objectid, index+1); - g_color = to_color(color, color_map, color_norm, index); + g_id = uvec2(objectid, gl_VertexID + 1); + g_color = color; g_thickness = px_per_unit * thickness; gl_Position = projectionview * model * to_vec4(vertex); gl_Position.z += gl_Position.w * depth_shift; diff --git a/GLMakie/assets/shader/lines.frag b/GLMakie/assets/shader/lines.frag index 0de6518c2da..5e6515fa53a 100644 --- a/GLMakie/assets/shader/lines.frag +++ b/GLMakie/assets/shader/lines.frag @@ -2,90 +2,236 @@ {{GLSL_EXTENSIONS}} {{SUPPORTED_EXTENSIONS}} +// show the various regions of the rendered segment +// (anti-aliased edges, joint truncation, overlap cutoff, patterns) +// #define DEBUG +uniform bool debug; + struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data bool _; //empty structs are not allowed }; -in vec4 f_color; -in vec2 f_uv; -in float f_thickness; +in highp float f_quad_sdf0; +in highp vec3 f_quad_sdf1; +in highp float f_quad_sdf2; +in vec2 f_truncation; +in float f_linestart; +in float f_linelength; + +flat in float f_linewidth; +flat in vec4 f_pattern_overwrite; +flat in vec2 f_extrusion; +flat in vec2 f_discard_limit; +flat in {{stripped_color_type}} f_color1; +flat in {{stripped_color_type}} f_color2; +flat in float f_alpha_weight; flat in uvec2 f_id; -flat in vec2 f_uv_minmax; -{{pattern_type}} pattern; +flat in float f_cumulative_length; +{{pattern_type}} pattern; uniform float pattern_length; uniform bool fxaa; +{{color_map_type}} color_map; +{{color_norm_type}} color_norm; +uniform vec4 highclip; +uniform vec4 lowclip; +uniform vec4 nan_color; + // Half width of antialiasing smoothstep -#define ANTIALIAS_RADIUS 0.8 +const float AA_RADIUS = 0.8; float aastep(float threshold1, float dist) { - return smoothstep(threshold1-ANTIALIAS_RADIUS, threshold1+ANTIALIAS_RADIUS, dist); + return smoothstep(threshold1-AA_RADIUS, threshold1+AA_RADIUS, dist); +} + +//////////////////////////////////////////////////////////////////////// +// Color handling +//////////////////////////////////////////////////////////////////////// + + +vec4 get_color_from_cmap(float value, sampler1D colormap, vec2 colorrange) { + float cmin = colorrange.x; + float cmax = colorrange.y; + if (value <= cmax && value >= cmin) { + // in value range, continue! + } else if (value < cmin) { + return lowclip; + } else if (value > cmax) { + return highclip; + } else { + // isnan CAN be broken (of course) -.- + // so if outside value range and not smaller/bigger min/max we assume NaN + return nan_color; + } + float i01 = clamp((value - cmin) / (cmax - cmin), 0.0, 1.0); + // 1/0 corresponds to the corner of the colormap, so to properly interpolate + // between the colors, we need to scale it, so that the ends are at 1 - (stepsize/2) and 0+(stepsize/2). + float stepsize = 1.0 / float(textureSize(colormap, 0)); + i01 = (1.0 - stepsize) * i01 + 0.5 * stepsize; + return texture(colormap, i01); } -float aastep(float threshold1, float threshold2, float dist) { - // We use 2x pixel space in the geometry shaders which passes through - // in uv.y, so we need to treat it here by using 2 * ANTIALIAS_RADIUS - float AA = 2 * ANTIALIAS_RADIUS; - return smoothstep(threshold1 - AA, threshold1 + AA, dist) - - smoothstep(threshold2 - AA, threshold2 + AA, dist); +vec4 get_color(float color, sampler1D colormap, vec2 colorrange) { + return get_color_from_cmap(color, colormap, colorrange); } -float aastep_scaled(float threshold1, float threshold2, float dist) { - float AA = ANTIALIAS_RADIUS / pattern_length; - return smoothstep(threshold1 - AA, threshold1 + AA, dist) - - smoothstep(threshold2 - AA, threshold2 + AA, dist); +vec4 get_color(vec4 color, Nothing colormap, Nothing colorrange) { + return color; +} +vec4 get_color(vec3 color, Nothing colormap, Nothing colorrange) { + return vec4(color, 1.0); } +//////////////////////////////////////////////////////////////////////////////// +// Pattern sampling +//////////////////////////////////////////////////////////////////////////////// -void write2framebuffer(vec4 color, uvec2 id); -// Signed distance fields for lines -// x/y pattern -float get_sd(sampler2D pattern, vec2 uv){ - return texture(pattern, uv).x; +float get_pattern_sdf(sampler2D pattern, vec2 uv){ + return 2.0 * f_linewidth * texture(pattern, uv).x; } - -// x pattern -vec2 get_sd(sampler1D pattern, vec2 uv){ - return vec2(texture(pattern, uv.x).x, uv.y); +float get_pattern_sdf(sampler1D pattern, vec2 uv){ + + // f_pattern_overwrite.x + // v joint + // ---------------- + // | | + // ---------------- + // joint ^ + // f_pattern_overwrite.z + + float w = 2.0 * f_linewidth; + if (uv.x <= f_pattern_overwrite.x) { + // overwrite for pattern with "ON" to the right (positive uv.x) + float sdf_overwrite = w * pattern_length * (f_pattern_overwrite.x - uv.x); + // pattern value where we start overwriting + float edge_sample = w * texture(pattern, f_pattern_overwrite.x).x; + // offset for overwrite to smoothly connect between sampling and edge + float sdf_offset = max(f_pattern_overwrite.y * edge_sample, -AA_RADIUS); + // add offset and apply direction ("ON" to left or right) to overwrite + return f_pattern_overwrite.y * (sdf_overwrite + sdf_offset); + } else if (uv.x >= f_pattern_overwrite.z) { + // same as above (other than mirroring overwrite direction) + float sdf_overwrite = w * pattern_length * (uv.x - f_pattern_overwrite.z); + float edge_sample = w * texture(pattern, f_pattern_overwrite.z).x; + float sdf_offset = max(f_pattern_overwrite.w * edge_sample, -AA_RADIUS); + return f_pattern_overwrite.w * (sdf_overwrite + sdf_offset); + } else + // in allowed range + return w * texture(pattern, uv.x).x; } - -// normal line type -// Note that this just returns uv, so get full manual control in geom shader -vec2 get_sd(Nothing _, vec2 uv){ - return uv; +float get_pattern_sdf(Nothing _, vec2 uv){ + return -10.0; } + +void write2framebuffer(vec4 color, uvec2 id); + void main(){ - vec4 color = vec4(f_color.rgb, 0.0); - vec2 xy = get_sd(pattern, f_uv); + vec4 color; + + // f_quad_sdf1.x is the negative distance from p1 in v1 direction + // (where f_cumulative_length applies) so we need to subtract here + vec2 uv = vec2( + (f_cumulative_length - f_quad_sdf1.x + 0.5) / (2.0 * f_linewidth * pattern_length), + 0.5 + 0.5 * f_quad_sdf1.z / f_linewidth + ); + +// #ifndef DEBUG +if (!debug) { + // discard fragments that are "more inside" the other segment to remove + // overlap between adjacent line segments. (truncated joints) + float dist_in_prev = max(f_quad_sdf0, - f_discard_limit.x); + float dist_in_next = max(f_quad_sdf2, - f_discard_limit.y); + if (dist_in_prev < f_quad_sdf1.x || dist_in_next < f_quad_sdf1.y) + discard; + + // SDF for inside vs outside along the line direction. extrusion adjusts + // the distance from p1/p2 for joints etc + float sdf = max(f_quad_sdf1.x - f_extrusion.x, f_quad_sdf1.y - f_extrusion.y); + + // distance in linewidth direction + sdf = max(sdf, abs(f_quad_sdf1.z) - f_linewidth); + + // outer truncation of truncated joints (smooth outside edge) + sdf = max(sdf, f_truncation.x); + sdf = max(sdf, f_truncation.y); + + // inner truncation (AA for overlapping parts) + // min(a, b) keeps what is inside a and b + // where a is the smoothly cut of part just before discard triggers (i.e. visible) + // and b is the (smoothly) cut of part just after discard triggers (i.e not visible) + // 100.0x sdf makes the sdf much more sharply, avoiding overdraw in the center + sdf = max(sdf, min(f_quad_sdf1.x + 1.0, 100.0 * (f_quad_sdf1.x - f_quad_sdf0) - 1.0)); + sdf = max(sdf, min(f_quad_sdf1.y + 1.0, 100.0 * (f_quad_sdf1.y - f_quad_sdf2) - 1.0)); + + // pattern application + sdf = max(sdf, get_pattern_sdf(pattern, uv)); + + // draw + + // v- edge + // .--------------- + // '. + // p1 v1 + // '. ---> + // '---------- + // -f_quad_sdf1.x is the distance from p1, positive in v1 direction + // f_linestart is the distance between p1 and the left edge along v1 direction + // f_start_length.y is the distance between the edges of this segment, in v1 direction + // so this is 0 at the left edge and 1 at the right edge (with extrusion considered) + float factor = (-f_quad_sdf1.x - f_linestart) / f_linelength; + color = get_color(f_color1 + factor * (f_color2 - f_color1), color_map, color_norm); + color.a *= f_alpha_weight; - float alpha, alpha2, alpha3; if (!fxaa) { - alpha = aastep(0.0, xy.x); - alpha2 = aastep(-f_thickness, f_thickness, xy.y); - alpha3 = aastep_scaled(f_uv_minmax.x, f_uv_minmax.y, f_uv.x); + color.a *= aastep(0.0, -sdf); } else { - alpha = step(0.0, xy.x); - alpha2 = step(-f_thickness, xy.y) - step(f_thickness, xy.y); - alpha3 = step(f_uv_minmax.x, f_uv.x) - step(f_uv_minmax.y, f_uv.x); + color.a *= step(0.0, -sdf); + } +// #endif + +} else { + +// #ifdef DEBUG + // base color + color = vec4(0.5, 0.5, 0.5, 0.2); + color.rgb += (2 * mod(f_id.y, 2) - 1) * 0.1; + + // mark "outside" define by quad_sdf in black + float sdf = max(f_quad_sdf1.x - f_extrusion.x, f_quad_sdf1.y - f_extrusion.y); + sdf = max(sdf, abs(f_quad_sdf1.z) - f_linewidth); + color.rgb -= vec3(0.4) * step(0.0, sdf); + + // Mark discarded space in red/blue + float dist_in_prev = max(f_quad_sdf0, - f_discard_limit.x); + float dist_in_next = max(f_quad_sdf2, - f_discard_limit.y); + if (dist_in_prev < f_quad_sdf1.x) + color.r += 0.5; + if (dist_in_next <= f_quad_sdf1.y) { + color.b += 0.5; } - color = vec4(f_color.rgb, f_color.a * alpha * alpha2 * alpha3); + // remaining overlap as softer red/blue + if (f_quad_sdf1.x - f_quad_sdf0 - 1.0 > 0.0) + color.r += 0.2; + if (f_quad_sdf1.y - f_quad_sdf2 - 1.0 > 0.0) + color.b += 0.2; - // Debug: Show uv values in line direction (repeating) - // color = vec4(mod(f_uv.x, 1.0), 0, 0, 1); + // Mark regions excluded via truncation in green + color.g += 0.5 * step(0.0, max(f_truncation.x, f_truncation.y)); - // Debug: Show uv values in line direction with pattern - // color.r = 0.5; - // color.g = mod(f_uv.x, 1.0); - // color.b = mod(f_uv.x, 1.0); - // color.a = 0.2 + 0.8 * color.a; + // and inner truncation as softer green + if (min(f_quad_sdf1.x + 1.0, 100.0 * (f_quad_sdf1.x - f_quad_sdf0) - 1.0) > 0.0) + color.g += 0.2; + if (min(f_quad_sdf1.y + 1.0, 100.0 * (f_quad_sdf1.y - f_quad_sdf2) - 1.0) > 0.0) + color.g += 0.2; - // Debug: Show AA padding in red - // color.r = 1 - color.a; - // color.a = 0.5 + 0.5 * color.a; + // mark pattern in white + color.rgb += vec3(0.3) * step(0.0, get_pattern_sdf(pattern, uv)); +// #endif +} write2framebuffer(color, f_id); -} +} \ No newline at end of file diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index f5683063430..38774f88047 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -9,804 +9,471 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d {{define_fast_path}} layout(lines_adjacency) in; -layout(triangle_strip, max_vertices = 11) out; +layout(triangle_strip, max_vertices = 4) out; -in vec4 g_color[]; +in {{stripped_color_type}} g_color[]; in float g_lastlen[]; in uvec2 g_id[]; in int g_valid_vertex[]; in float g_thickness[]; -out vec4 f_color; -out vec2 f_uv; -out float f_thickness; +out highp float f_quad_sdf0; +out highp vec3 f_quad_sdf1; +out highp float f_quad_sdf2; +out vec2 f_truncation; +out float f_linestart; +out float f_linelength; + +flat out vec2 f_extrusion; +flat out float f_linewidth; +flat out vec4 f_pattern_overwrite; +flat out vec2 f_discard_limit; flat out uvec2 f_id; -flat out vec2 f_uv_minmax; +flat out {{stripped_color_type}} f_color1; +flat out {{stripped_color_type}} f_color2; +flat out float f_alpha_weight; +flat out float f_cumulative_length; out vec3 o_view_pos; out vec3 o_view_normal; -uniform vec2 resolution; +{{pattern_type}} pattern; uniform float pattern_length; -uniform sampler1D pattern_sections; - -float px2uv = 0.5 / pattern_length; +uniform vec2 resolution; // Constants -#define MITER_LIMIT -0.4 -#define AA_THICKNESS 4 +const float MITER_LIMIT = -0.4; +const float AA_RADIUS = 0.8; +const float AA_THICKNESS = 4.0 * AA_RADIUS; +// NOTE: if MITER_LIMIT becomes a variable AA_THICKNESS needs to scale with the joint extrusion -vec3 screen_space(vec4 vertex) -{ - return vec3(vertex.xy * resolution, vertex.z) / vertex.w; +vec3 screen_space(vec4 vertex) { + return vec3((0.5 * vertex.xy / vertex.w + 0.5) * resolution, vertex.z / vertex.w); } -//////////////////////////////////////////////////////////////////////////////// -/// Emit Vertex Methods -//////////////////////////////////////////////////////////////////////////////// +struct LineVertex { + vec3 position; + int index; + float quad_sdf0; + vec3 quad_sdf1; + float quad_sdf2; + vec2 truncation; -// Manual uv calculation -// - position in screen space (double resolution as generally used) -// - uv with uv.u normalized (0..1), uv.v unnormalized (0..pattern_length) -void emit_vertex(vec3 position, vec2 uv, int index, float thickness) -{ - f_uv = uv; - f_color = g_color[index]; - gl_Position = vec4((position.xy / resolution), position.z, 1.0); - f_id = g_id[index]; - // linewidth scaling may shrink the effective linewidth - f_thickness = thickness; - EmitVertex(); -} -// default for miter joins -void emit_vertex(vec3 position, vec2 uv, int index) -{ - emit_vertex(position, uv, index, g_thickness[index]); -} + float linestart; + float linelength; +}; -// For center point -void emit_vertex(vec3 position, vec2 uv) -{ - f_uv = uv; - f_color = 0.5 * (g_color[1] + g_color[2]); - gl_Position = vec4((position.xy / resolution), position.z, 1.0); - f_id = g_id[1]; - f_thickness = 0.5 * (g_thickness[1] + g_thickness[2]); +void emit_vertex(LineVertex vertex) { + gl_Position = vec4((2.0 * vertex.position.xy / resolution) - 1.0, vertex.position.z, 1.0); + f_quad_sdf0 = vertex.quad_sdf0; + f_quad_sdf1 = vertex.quad_sdf1; + f_quad_sdf2 = vertex.quad_sdf2; + f_truncation = vertex.truncation; + f_linestart = vertex.linestart; + f_linelength = vertex.linelength; + f_id = g_id[vertex.index]; EmitVertex(); } -// Debug -void emit_vertex(vec3 position, vec2 uv, int index, vec4 color, float thickness) -{ - f_uv = uv; - f_color = color; - gl_Position = vec4((position.xy / resolution), position.z, 1.0); - f_id = g_id[index]; - f_thickness = thickness; - EmitVertex(); -} -// default for miter joins -void emit_vertex(vec3 position, vec2 uv , int index, vec4 color) -{ - emit_vertex(position, uv , index, color, g_thickness[index]); -} -void emit_vertex(vec3 position, vec2 uv, vec4 color) -{ - f_uv = uv; - f_color = color; - gl_Position = vec4((position.xy / resolution), position.z, 1.0); - f_id = g_id[1]; - f_thickness = 0.5 * (g_thickness[1] + g_thickness[2]); - EmitVertex(); -} - - -// With offset calculations for core line segment -void emit_vertex(vec3 position, vec2 offset, vec2 line_dir, vec2 uv, int index) -{ - emit_vertex( - position + vec3(offset, 0), - vec2(uv.x + px2uv * dot(line_dir, offset), uv.y), - index, - abs(uv.y) - AA_THICKNESS - ); - // `abs(uv.y) - AA_THICKNESS` corrects for enlarged AA padding between - // segments of different linewidth, see #2953 -} - -void emit_vertex(vec3 position, vec2 offset, vec2 line_dir, vec2 uv) -{ - emit_vertex( - position + vec3(offset, 0), - vec2(uv.x + px2uv * dot(line_dir, offset), uv.y) - ); -} +vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); } +vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); } //////////////////////////////////////////////////////////////////////////////// -/// Draw full line segment +// Linestyle Support // //////////////////////////////////////////////////////////////////////////////// -// Generate line segment with 3 triangles -// - p1, p2 are the line start and end points in pixel space -// - miter_a and miter_b are the offsets from p1 and p2 respectively that -// generate the line segment quad. This should include thickness and AA -// - u1, u2 are the u values at p1 and p2. These should be in uv scale (px2uv applied) -// - thickness_aa1, thickness_aa2 are linewidth at p1 and p2 with AA added. They -// double as uv.y values, which are in pixel space -// - v1 is the line direction of this segment (xy component) -void generate_line_segment( - vec3 p1, vec2 miter_a, float u1, float thickness_aa1, - vec3 p2, vec2 miter_b, float u2, float thickness_aa2, - vec2 v1, float segment_length - ) -{ - float line_offset_a = dot(miter_a, v1); - float line_offset_b = dot(miter_b, v1); - - if (abs(line_offset_a) + abs(line_offset_b) < segment_length+1){ - // _________ - // \ / - // \_____/ - // <---> - // Line segment is extensive (minimum width positive) - - emit_vertex(p1, +miter_a, v1, vec2(u1, -thickness_aa1), 1); - emit_vertex(p1, -miter_a, v1, vec2(u1, thickness_aa1), 1); - emit_vertex(p2, +miter_b, v1, vec2(u2, -thickness_aa2), 2); - emit_vertex(p2, -miter_b, v1, vec2(u2, thickness_aa2), 2); - } else { - // ____ - // \ / - // \/ - // /\ - // >--< - // Line segment has zero or negative width on short side - - // Pulled apart, we draw these two triangles (vertical lines added) - // ___ ___ - // \ | | / - // X | | X - // \| |/ - // - // where X is u1/p1 (left) and u2/p2 (right) respectively. To avoid - // drawing outside the line segment due to AA padding, we cut off the - // left triangle on the right side at u2 via f_uv_minmax.y, and - // analogously the right triangle at u1 via f_uv_minmax.x. - // These triangles will still draw over each other like this. - - // incoming side - float old = f_uv_minmax.y; - f_uv_minmax.y = u2; - - emit_vertex(p1, -miter_a, v1, vec2(u1, -thickness_aa1), 1); - emit_vertex(p1, +miter_a, v1, vec2(u1, +thickness_aa1), 1); - if (line_offset_a > 0){ // finish triangle on -miter_a side - emit_vertex(p1, 2 * line_offset_a * v1 - miter_a, v1, vec2(u1, -thickness_aa1)); - } else { - emit_vertex(p1, -2 * line_offset_a * v1 + miter_a, v1, vec2(u1, +thickness_aa1)); - } - - EndPrimitive(); - - // outgoing side - f_uv_minmax.x = u1; - f_uv_minmax.y = old; - - emit_vertex(p2, -miter_b, v1, vec2(u2, -thickness_aa2), 2); - emit_vertex(p2, +miter_b, v1, vec2(u2, +thickness_aa2), 2); - if (line_offset_b < 0){ // finish triangle on -miter_b side - emit_vertex(p2, 2 * line_offset_b * v1 - miter_b, v1, vec2(u2, -thickness_aa2)); - } else { - emit_vertex(p2, -2 * line_offset_b * v1 + miter_b, v1, vec2(u2, +thickness_aa2)); - } - } +vec2 process_pattern(Nothing pattern, bool[4] isvalid, mat2 extrusion, float segment_length, float halfwidth) { + // do not adjust stuff + f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); + return vec2(0); +} +vec2 process_pattern(sampler2D pattern, bool[4] isvalid, mat2 extrusion, float segment_length, float halfwidth) { + // TODO + // This is not a case that's used at all yet. Maybe consider it in the future... + f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); + return vec2(0); } -// Debug Version -// Generates more triangles and colors them individually so they can be differentiated -void generate_line_segment_debug( - vec3 p1, vec2 miter_a, float u1, float thickness_aa1, - vec3 p2, vec2 miter_b, float u2, float thickness_aa2, - vec2 v1, float segment_length - ) -{ - float line_offset_a = dot(miter_a, v1); - float line_offset_b = dot(miter_b, v1); - - if (abs(line_offset_a) + abs(line_offset_b) < segment_length + 1 ){ - emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(1, 0, 0, 0.5)); - - EndPrimitive(); - - emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(0, 0, 1, 0.5)); - emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), -thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - - // Mid point version - /* - vec3 pc = 0.5 * (p1 + p2); - vec2 miter_c = 0.5 * (miter_a + miter_b); - float uc = 0.5 * (u1 + u2); - float thickness_aac = 0.5 * (thickness_aa1 + thickness_aa2); - - if (dot(miter_a, v1) < dot(miter_b, v1)){ - emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - emit_vertex(pc + vec3(miter_c, 0), vec2(uc + px2uv * dot(v1, miter_c), -thickness_aac), vec4(1, 0, 0, 0.5)); - - EndPrimitive(); - - emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(0, 1, 0, 0.5)); - emit_vertex(pc + vec3(miter_c, 0), vec2(uc + px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 1, 0, 0.5)); - emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 1, 0, 0.5)); - - EndPrimitive(); - - emit_vertex(pc + vec3(miter_c, 0), vec2(uc + px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 0, 1, 0.5)); - emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), -thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - - } else { - // subtractive side has more space - emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - emit_vertex(pc - vec3(miter_c, 0), vec2(uc - px2uv * dot(v1, miter_c), -thickness_aac), vec4(1, 0, 0, 0.5)); - - EndPrimitive(); - - emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(0, 1, 0, 0.5)); - emit_vertex(pc - vec3(miter_c, 0), vec2(uc - px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 1, 0, 0.5)); - emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 1, 0, 0.5)); - - EndPrimitive(); - - emit_vertex(pc - vec3(miter_c, 0), vec2(uc - px2uv * dot(v1, miter_c), -thickness_aac), vec4(0, 0, 1, 0.5)); - emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), -thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - } - */ - } else { - // incoming side - float old = f_uv_minmax.y; - f_uv_minmax.y = u2; - - emit_vertex(p1 - vec3(miter_a, 0), vec2(u1 - px2uv * dot(v1, miter_a), -thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - emit_vertex(p1 + vec3(miter_a, 0), vec2(u1 + px2uv * dot(v1, miter_a), thickness_aa1), 1, vec4(1, 0, 0, 0.5)); - if (line_offset_a > 0){ // finish triangle on -miter_a side - emit_vertex( - p1 + vec3(2 * line_offset_a * v1 - miter_a, 0), - vec2(u1 + px2uv * (2 * line_offset_a - dot(v1, miter_a)), -thickness_aa1), - 1, vec4(1, 0, 0, 0.5) - ); +vec2 process_pattern(sampler1D pattern, bool[4] isvalid, mat2 extrusion, float segment_length, float halfwidth) { + // samples: + // -ext1 p1 ext1 -ext2 p2 ext2 + // 1 2 3 4 5 6 + // prev | joint | this | joint | next + + // default to no overwrite + f_pattern_overwrite.x = -1e12; + f_pattern_overwrite.z = +1e12; + vec2 adjust = vec2(0); + float width = 2.0 * halfwidth; + float uv_scale = 1.0 / (width * pattern_length); + float left, center, right; + + if (isvalid[0]) { + // using this would allow dots to never bend across a joint but currently + // results in artifacts in dense patterned lines (e.g. bracket tests) + // float offset = max(abs(extrusion[0][0]), halfwidth); + float offset = abs(extrusion[0][0]); + left = width * texture(pattern, uv_scale * (g_lastlen[1] - offset)).x; + center = width * texture(pattern, uv_scale * (g_lastlen[1] )).x; + right = width * texture(pattern, uv_scale * (g_lastlen[1] + offset)).x; + + // cases: + // ++-, +--, +-+ => elongate backwards + // -++, --+ => shrink forward + // +++, ---, -+- => freeze around joint + + if ((left > 0 && center > 0 && right > 0) || (left < 0 && right < 0)) { + // default/freeze + // overwrite until one AA gap past the corner/joint + f_pattern_overwrite.x = uv_scale * (g_lastlen[1] + abs(extrusion[0][0]) + AA_RADIUS); + // using the sign of the center to decide between drawing or not drawing + f_pattern_overwrite.y = sign(center); + } else if (left > 0) { + // elongate backwards + adjust.x = -1.0; + } else if (right > 0) { + // shorten forward + adjust.x = 1.0; } else { - emit_vertex( - p1 + vec3(-2 * line_offset_a * v1 + miter_a, 0), - vec2(u1 + px2uv * (-2 * line_offset_a + dot(v1, miter_a)), thickness_aa1), - 1, vec4(1, 0, 0, 0.5) - ); + // default - see above + f_pattern_overwrite.x = uv_scale * (g_lastlen[1] + abs(extrusion[0][0]) + AA_RADIUS); + f_pattern_overwrite.y = sign(center); } - EndPrimitive(); - f_uv_minmax.x = u1; - f_uv_minmax.y = old; - - // outgoing side - emit_vertex(p2 - vec3(miter_b, 0), vec2(u2 - px2uv * dot(v1, miter_b), -thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - emit_vertex(p2 + vec3(miter_b, 0), vec2(u2 + px2uv * dot(v1, miter_b), thickness_aa2), 2, vec4(0, 0, 1, 0.5)); - if (line_offset_b < 0){ // finish triangle on -miter_b side - emit_vertex( - p2 + vec3(2 * line_offset_b * v1 - miter_b, 0), - vec2(u2 + px2uv * (2 * line_offset_b - dot(v1, miter_b)), -thickness_aa2), - 2, vec4(0, 0, 1, 0.5) - ); + } // else there is no left segment, no left join, so no overwrite + + if (isvalid[3]) { + // float offset = max(abs(extrusion[1][0]), halfwidth + AA_RADIUS); + float offset = abs(extrusion[1][0]); + left = width * texture(pattern, uv_scale * (g_lastlen[1] + segment_length - offset)).x; + center = width * texture(pattern, uv_scale * (g_lastlen[1] + segment_length )).x; + right = width * texture(pattern, uv_scale * (g_lastlen[1] + segment_length + offset)).x; + + if ((left > 0 && center > 0 && right > 0) || (left < 0 && right < 0)) { + // default/freeze + f_pattern_overwrite.z = uv_scale * (g_lastlen[1] + segment_length - abs(extrusion[1][0]) - AA_RADIUS); + f_pattern_overwrite.w = sign(center); + } else if (left > 0) { + // shrink backwards + adjust.y = -1.0; + } else if (right > 0) { + // elongate forward + adjust.y = 1.0; } else { - emit_vertex( - p2 + vec3(-2 * line_offset_b * v1 + miter_b, 0), - vec2(u2 + px2uv * (-2 * line_offset_b + dot(v1, miter_b)), thickness_aa2), - 2, vec4(0, 0, 1, 0.5) - ); + // default - see above + f_pattern_overwrite.z = uv_scale * (g_lastlen[1] + segment_length - abs(extrusion[1][0]) - AA_RADIUS); + f_pattern_overwrite.w = sign(center); } } + + return adjust; } //////////////////////////////////////////////////////////////////////////////// -/// Patterned line +// Main // //////////////////////////////////////////////////////////////////////////////// - -void draw_patterned_line(bool isvalid[4]) +void main(void) { - // This sets a min and max value foir uv.u at which anti-aliasing is forced. - // With this setting it's never triggered. - f_uv_minmax = vec2(-1.0e12, 1.0e12); - - // get the four vertices passed to the shader - // without FAST_PATH the conversions happen on the CPU - vec3 p0 = gl_in[0].gl_Position.xyz; // start of previous segment - vec3 p1 = gl_in[1].gl_Position.xyz; // end of previous segment, start of current segment - vec3 p2 = gl_in[2].gl_Position.xyz; // end of current segment, start of next segment - vec3 p3 = gl_in[3].gl_Position.xyz; // end of next segment + // These need to be set but don't have reasonable values here + o_view_pos = vec3(0); + o_view_normal = vec3(0); - // linewidth with padding for anti aliasing - float thickness_aa1 = g_thickness[1] + AA_THICKNESS; - float thickness_aa2 = g_thickness[2] + AA_THICKNESS; + // Shouldn't be necessary anymore but it may still be worth skipping work + if (g_thickness[1] == 0.0 && g_thickness[2] == 0.0) { + return; + } - // determine the direction of each of the 3 segments (previous, current, next) - vec3 v1 = p2 - p1; - float segment_length = length(v1.xy); - v1 /= segment_length; - vec3 v0 = v1; - vec3 v2 = v1; + // We mark each of the four vertices as valid or not. Vertices can be + // marked invalid on input (eg, if they contain NaN). We also mark them + // invalid if they repeat in the index buffer. This allows us to render to + // the very ends of a polyline without clumsy buffering the position data on the + // CPU side by repeating the first and last points via the index buffer. It + // just requires a little care further down to avoid degenerate normals. + bool isvalid[4] = bool[]( + g_valid_vertex[0] == 1 && g_id[0].y != g_id[1].y, + g_valid_vertex[1] == 1, + g_valid_vertex[2] == 1, + g_valid_vertex[3] == 1 && g_id[2].y != g_id[3].y + ); - if (p1 != p0 && isvalid[0]) { - v0 = (p1 - p0) / length((p1 - p0).xy); - } - if (p3 != p2 && isvalid[3]) { - v2 = (p3 - p2) / length((p3 - p2).xy); + if(!isvalid[1] || !isvalid[2]){ + // If one of the central vertices is invalid or there is a break in the + // line, we don't emit anything. + return; } - // determine the normal of each of the 3 segments (previous, current, next) - vec2 n0 = vec2(-v0.y, v0.x); - vec2 n1 = vec2(-v1.y, v1.x); - vec2 n2 = vec2(-v2.y, v2.x); - - // The pattern may look like this: - // - // pattern_sections index - // 0 1 2 3 - // |########| |####| |(repeat) - // left right left right - // variable in loop - // - // We first figure out the extended size of this line segment, starting - // from the left end of the first relevant pattern section and ending at the - // right end of the last relevant pattern section. E.g.: - // - // g_lastlen[1] g_lastlen[2] (2x pixel coords) - // edge1 edge2 (1x pixel coords) - // | | - // |####| |########| |####| |########| |####| |########| - // | first | | last | - // | pattern| | pattern| - // | section| | section| - // start stop (pattern coords (normalized)) - // - // start_width and stop_width are the widths of the start and stop sections. - float start, stop, start_width, stop_width, temp; - float left, right, edge1, edge2, inv_pl, left_offset, right_offset; - - // normalized single pixel scale - start = g_lastlen[2] * px2uv; - stop = g_lastlen[1] * px2uv; - start_width = 0.0; - stop_width = 0.0; - - inv_pl = 1.0 / pattern_length; - edge1 = 0.5 * g_lastlen[1]; - edge2 = 0.5 * g_lastlen[2]; - - int pattern_texsize = textureSize(pattern_sections, 0); - - for (int i = 0; i < pattern_texsize - 1; i = i + 2) - { - left = texelFetch(pattern_sections, i, 0).x; - right = texelFetch(pattern_sections, i+1, 0).x; - - // update left side - temp = ceil((edge1 - right) * inv_pl) + left * inv_pl; - if (temp < start) - start_width = right - left; - start = min(start, temp); - - // update right side - temp = floor((edge2 - left) * inv_pl) + right * inv_pl; - if (temp > stop) - stop_width = right - left; - stop = max(stop, temp); - } - // Technically start and stop should be offset by another - // 1 / (2 * textureSize(pattern)) so the line segment is normalized to - // pattern texel centers rather than the left edge, but we have enough - // AA_THICKNESS for it to be irrelevant. - - // if there is something to draw... - if (stop > start){ - - // setup for sharp corners - // miter_a / miter_b - // ___ ↑ ___ - // | .|. | - // length_a _|_ .' | '. _|_ length_b - // .' | '. - // .' | '. - // .' .' '. '. - // .' '. - // - vec2 miter_a = normalize(n0 + n1); - vec2 miter_b = normalize(n1 + n2); - float length_a = 1.0 / dot(miter_a, n1); - float length_b = 1.0 / dot(miter_b, n1); - - // if we have a sharp corner: - // max(g_thickness[1], proj(length_a * miter_a, v1)) without AA padding - // otherwise just g_thickness[1] - left_offset = g_thickness[1] * max(1.0, float(dot(v0.xy, v1.xy) >= MITER_LIMIT) * abs(dot(miter_a, v1.xy)) * length_a); - right_offset = g_thickness[2] * max(1.0, float(dot(v1.xy, v2.xy) >= MITER_LIMIT) * abs(dot(miter_b, v1.xy)) * length_b); - - // Finish length_a/b - length_a *= thickness_aa1; - length_b *= thickness_aa2; - - // if the "on" section of the pattern at start extends over the whole - // potential corner we draw the corner. If not we extend the line. - // - // g_lastlen[1] - // . - |---.---------- - // : | : - // : | : - // : | : - // : - '---:---------- - // start : - // start + start_width - // - // Equivalent to - // (start * pattern_length < g_lastlen[1] - left_offset) && - // (start * pattern_length + 2 * start_width > g_lastlen[1] + left_offset) - if ( - isvalid[0] && - abs(2 * start * pattern_length - g_lastlen[1] + start_width) < (start_width - left_offset) - ) - { - // if the corner is too sharp, we do a truncated miter join - // ----------c. - // ----------a.'. - // | '.'. - // x_ '.'. - // ------. '--b d - // / / / - // / / / - // - // x is the point the two line segments meet (here p1) - // a, b are the outer corners of the line segments - // a, b, x define the triangle we need to fill to make the line continuous - // c, d are a, b with padding for AA included - // Note that the padding generated by c, d is reduced on the triangle - // so we need to add another rectangle there to ensure enough padding - if( dot( v0.xy, v1.xy ) < MITER_LIMIT ){ - - bool gap = dot( v0.xy, n1 ) > 0; - - // Another view of a truncated join (with lines joining like a V). - // - // uv.y = 0 in line segment - // / - // . -- uv.x = u0 in truncated join - // .' '. uv.y = thickness in line segment - // .' '. / uv.y = thickness + AA_THICKNESS in line segment - // .'_________'. /_ uv.x = start in truncated join (constraint for AA) - // .'_____________'. _ uv.x = -proj_AA in truncated join (derived from line segment + constraint) - // | | - // |_______________| _ uv.x = -proj_AA - AA_THICKNESS in truncated join - // - // Here the / annotations come from the connecting line segment and are to - // be viewed on the diagonal. The -- and _ annotations are relevant to the - // truncated join and viewed vertically. - // Note that `start` marks off-to-on edge in the pattern. So values - // greater than `start` will be drawn and smaller will be discarded. - // With how we pick start and get in this branch u0 will always be - // in a solidly drawn region of the pattern. - float u0 = start + thickness_aa1 * abs(dot(miter_a, n1)) * px2uv; - float proj_AA = start - AA_THICKNESS * abs(dot(miter_a, n1)) * px2uv; - - // to save some space - vec2 off0 = thickness_aa1 * n0; - vec2 off1 = thickness_aa1 * n1; - vec2 off_AA = AA_THICKNESS * miter_a; - float u_AA = AA_THICKNESS * px2uv; - - if(gap){ - emit_vertex(p1, vec2(u0, 0), 1); - emit_vertex(p1 + vec3(off0, 0), vec2(proj_AA, +thickness_aa1), 1); - emit_vertex(p1 + vec3(off1, 0), vec2(proj_AA, -thickness_aa1), 1); - emit_vertex(p1 + vec3(off0 + off_AA, 0), vec2(proj_AA - u_AA, +thickness_aa1), 1); - emit_vertex(p1 + vec3(off1 + off_AA, 0), vec2(proj_AA - u_AA, -thickness_aa1), 1); - EndPrimitive(); - }else{ - emit_vertex(p1, vec2(u0, 0), 1); - emit_vertex(p1 - vec3(off1, 0), vec2(proj_AA, +thickness_aa1), 1); - emit_vertex(p1 - vec3(off0, 0), vec2(proj_AA, -thickness_aa1), 1); - emit_vertex(p1 - vec3(off1 + off_AA, 0), vec2(proj_AA - u_AA, +thickness_aa1), 1); - emit_vertex(p1 - vec3(off0 + off_AA, 0), vec2(proj_AA - u_AA, -thickness_aa1), 1); - EndPrimitive(); - } + // Time to generate our quad. For this we need to find out how far a join + // extends the line. First let's get some vectors we need. - miter_a = n1; - length_a = thickness_aa1; - start = g_lastlen[1] * px2uv; + // Get the four vertices passed to the shader in pixel space. - } else { // otherwise we do a sharp join - start = g_lastlen[1] * px2uv; - } - } else { - // We don't need to treat the join, so resize the line segment to - // the drawn region. (This may extend the line too) - miter_a = n1; - length_a = thickness_aa1; - // If the line starts with this segment or the center of the "on" - // section of the pattern is in this segment, we draw it, else - // we skip past the first "on" section. - if (!isvalid[0] || (start > (g_lastlen[1] - start_width) * px2uv)) - start = start - AA_THICKNESS * px2uv; - else - start = start + (start_width + 0.5 * AA_THICKNESS) * inv_pl; - p1 += (2 * start * pattern_length - g_lastlen[1]) * v1; + // To apply pixel space linewidths we transform line vertices to pixel space + // here. This is dangerous with perspective projection as p.xyz / p.w sends + // points from behind the camera to beyond far (clip z > 1), causing lines + // to invert. To avoid this we translate points along the line direction, + // moving them to the edge of the visible area. + vec3 p0, p1, p2, p3; + { + // All in clip space + vec4 clip_p0 = gl_in[0].gl_Position; // start of previous segment + vec4 clip_p1 = gl_in[1].gl_Position; // end of previous segment, start of current segment + vec4 clip_p2 = gl_in[2].gl_Position; // end of current segment, start of next segment + vec4 clip_p3 = gl_in[3].gl_Position; // end of next segment + + vec4 v1 = clip_p2 - clip_p1; + + // With our perspective projection matrix clip.w = -view.z with + // clip.w < 0.0 being behind the camera. + // Note that if the signs in the projectionmatrix change, this may become wrong. + if (clip_p1.w < 0.0) { + // the line connects outside the visible area so we may consider it disconnected + isvalid[0] = false; + // A clip position is visible if -w <= z <= w. To move the line along + // the line direction v to the start of the visible area, we solve: + // p.z + t * v.z = +-(p.w + t * v.w) + // where (-) gives us the result for the near clipping plane as p.z + // and p.w share the same sign and p.z/p.w = -1.0 is the near plane. + clip_p1 = clip_p1 + (-clip_p1.w - clip_p1.z) / (v1.z + v1.w) * v1; } - - - // The other end of the line is analogous - // (stop * pattern_length - 2 * stop_width < g_lastlen[2] - right_offset) && - // (stop * pattern_length > g_lastlen[2] + right_offset) - // (stop * pattern_length - stop_width - g_lastlen[2] < (stop_width - right_offset)) && - // (stop * pattern_length - stop_width - g_lastlen[2] > -(stop_width - right_offset)) - if ( - isvalid[3] && - abs(2*stop * pattern_length - g_lastlen[2] - stop_width) < (stop_width - right_offset) - ) - { - if( dot( v1.xy, v2.xy ) < MITER_LIMIT ){ - // setup for truncated join (flat line end) - miter_b = n1; - length_b = thickness_aa2; - stop = g_lastlen[2] * px2uv; - } else { - // setup for sharp join - stop = g_lastlen[2] * px2uv; - } - } else { - miter_b = n1; - length_b = thickness_aa2; - if (isvalid[3] && (stop > (g_lastlen[2] + stop_width) * px2uv)) - stop = stop - (stop_width + 0.5 * AA_THICKNESS) * inv_pl; - else - stop = stop + AA_THICKNESS * px2uv; - p2 += (2 * stop * pattern_length - g_lastlen[2]) * v1; + if (clip_p2.w < 0.0) { + isvalid[3] = false; + clip_p2 = clip_p2 + (-clip_p2.w - clip_p2.z) / (v1.z + v1.w) * v1; } - // to save some space - miter_a *= length_a; - miter_b *= length_b; - - // If this segment starts or ends a line we force anti-aliasing to - // happen at the respective edge. - if (!isvalid[0]) - f_uv_minmax.x = g_lastlen[1] * px2uv; - if (!isvalid[3]) - f_uv_minmax.y = g_lastlen[2] * px2uv; - - // generate rectangle for this segment - - // Normal Version - generate_line_segment( - p1, miter_a, start, thickness_aa1, - p2, miter_b, stop, thickness_aa2, - v1.xy, segment_length - ); - - // Debug - show each triangle - // generate_line_segment_debug( - // p1, miter_a, start, thickness_aa1, - // p2, miter_b, stop, thickness_aa2, - // v1.xy, segment_length - // ); - + // transform clip -> screen space, applying xyz / w normalization (which + // is now save as all vertices are in front of the camera) + p0 = screen_space(clip_p0); // start of previous segment + p1 = screen_space(clip_p1); // end of previous segment, start of current segment + p2 = screen_space(clip_p2); // end of current segment, start of next segment + p3 = screen_space(clip_p3); // end of next segment } - return; -} - - - -//////////////////////////////////////////////////////////////////////////////// -/// Solid lines -//////////////////////////////////////////////////////////////////////////////// - - - -void draw_solid_line(bool isvalid[4]) -{ - // This sets a min and max value foir uv.u at which anti-aliasing is forced. - // With this setting it's never triggered. - f_uv_minmax = vec2(-1.0e12, 1.0e12); - - // get the four vertices passed to the shader - // without FAST_PATH the conversions happen on the CPU - vec3 p0 = screen_space(gl_in[0].gl_Position); // start of previous segment - vec3 p1 = screen_space(gl_in[1].gl_Position); // end of previous segment, start of current segment - vec3 p2 = screen_space(gl_in[2].gl_Position); // end of current segment, start of next segment - vec3 p3 = screen_space(gl_in[3].gl_Position); // end of next segment + // Since we are measuring from the center of the line we will need half + // the thickness/linewidth for most things. + // Note that if a line becomes very thin the alpha value generated by the + // signed distance field (SDF) will be location dependent, causing the line + // to flicker if it moves. It also becomes darker than it should be due to + // the AA smoothstep becoming unbalanced (< AA_RADIUS inside, full AA_RADIUS + // outside). To avoid these issues we reduce alpha directly rather than + // shrinking the linewidth further at some point. + float halfwidth = 0.5 * max(AA_RADIUS, g_thickness[1]); // determine the direction of each of the 3 segments (previous, current, next) - vec3 v1 = p2 - p1; + vec3 v1 = (p2 - p1); float segment_length = length(v1.xy); v1 /= segment_length; - vec3 v0 = v1; - vec3 v2 = v1; - if (p1 != p0 && isvalid[0]) { - v0 = (p1 - p0) / length((p1 - p0).xy); - } - if (p3 != p2 && isvalid[3]) { - v2 = (p3 - p2) / length((p3 - p2).xy); - } + // depth is irrelevant for these + vec2 v0 = v1.xy; + vec2 v2 = v1.xy; + if (p1 != p0 && isvalid[0]) + v0 = normalize(p1.xy - p0.xy); + if (p3 != p2 && isvalid[3]) + v2 = normalize(p3.xy - p2.xy); // determine the normal of each of the 3 segments (previous, current, next) - vec2 n0 = vec2(-v0.y, v0.x); - vec2 n1 = vec2(-v1.y, v1.x); - vec2 n2 = vec2(-v2.y, v2.x); - - // determine stretching of AA border due to linewidth change - float temp = (g_thickness[2] - g_thickness[1]) / segment_length; - float edge_scale = sqrt(1 + temp * temp); - - // linewidth with padding for anti aliasing (used for geometry) - float thickness_aa1 = g_thickness[1] + edge_scale * AA_THICKNESS; - float thickness_aa2 = g_thickness[2] + edge_scale * AA_THICKNESS; - - // Setup for sharp corners (see above) - vec2 miter_a = normalize(n0 + n1); - vec2 miter_b = normalize(n1 + n2); - float length_a = thickness_aa1 / dot(miter_a, n1); - float length_b = thickness_aa2 / dot(miter_b, n1); - - // truncated miter join (see above) - if( dot( v0.xy, v1.xy ) < MITER_LIMIT ){ - bool gap = dot( v0.xy, n1 ) > 0; - // In this case uv's are used as signed distance field values, so we - // want 0 where we had start before. - float u0 = thickness_aa1 * abs(dot(miter_a, n1)) * px2uv; - float proj_AA = AA_THICKNESS * abs(dot(miter_a, n1)) * px2uv; - - // to save some space - vec2 off0 = thickness_aa1 * n0; - vec2 off1 = thickness_aa1 * n1; - vec2 off_AA = AA_THICKNESS * miter_a; - float u_AA = AA_THICKNESS * px2uv; - - if(gap){ - emit_vertex(p1, vec2(+ u0, 0), 1); - emit_vertex(p1 + vec3(off0, 0), vec2(- proj_AA, +thickness_aa1), 1); - emit_vertex(p1 + vec3(off1, 0), vec2(- proj_AA, -thickness_aa1), 1); - emit_vertex(p1 + vec3(off0 + off_AA, 0), vec2(- proj_AA - u_AA, +thickness_aa1), 1); - emit_vertex(p1 + vec3(off1 + off_AA, 0), vec2(- proj_AA - u_AA, -thickness_aa1), 1); - EndPrimitive(); - }else{ - emit_vertex(p1, vec2(+ u0, 0), 1); - emit_vertex(p1 - vec3(off1, 0), vec2(- proj_AA, +thickness_aa1), 1); - emit_vertex(p1 - vec3(off0, 0), vec2(- proj_AA, -thickness_aa1), 1); - emit_vertex(p1 - vec3(off1 + off_AA, 0), vec2(- proj_AA - u_AA, +thickness_aa1), 1); - emit_vertex(p1 - vec3(off0 + off_AA, 0), vec2(- proj_AA - u_AA, -thickness_aa1), 1); - EndPrimitive(); - } - - miter_a = n1; - length_a = thickness_aa1; - } - - // we have miter join on next segment, do normal line cut off - if( dot( v1.xy, v2.xy ) <= MITER_LIMIT ){ - miter_b = n1; - length_b = thickness_aa2; - } - - // Without a pattern (linestyle) we use uv.u directly as a signed distance - // field. We only care about u1 - u0 being the correct distance and - // u0 > AA_THICHKNESS at all times. - float u1 = 10000.0; - float u2 = u1 + segment_length; - - miter_a *= length_a; - miter_b *= length_b; + vec2 n0 = normal_vector(v0); + vec2 n1 = normal_vector(v1); + vec2 n2 = normal_vector(v2); + + // Are we truncating the joint? + bvec2 is_truncated = bvec2( + dot(v0, v1.xy) < MITER_LIMIT, + dot(v1.xy, v2) < MITER_LIMIT + ); - // To treat line starts and ends we elongate the line in the respective - // direction and enforce an AA border at the original start/end position - // with f_uv_minmax. - if (!isvalid[0]) - { - float corner_offset = max(0, abs(dot(miter_b, v1.xy)) - segment_length); - f_uv_minmax.x = px2uv * (u1 - corner_offset); - p1 -= (corner_offset + AA_THICKNESS) * v1; - u1 -= (corner_offset + AA_THICKNESS); - segment_length += corner_offset; + // Miter normals (normal of truncated edge / vector to sharp corner) + // Note: n0 + n1 = vec(0) for a 180° change in direction. +-(v0 - v1) is the + // same direction, but becomes vec(0) at 0°, so we can use it instead + vec2 miter_n1 = is_truncated[0] ? normalize(v0.xy - v1.xy) : normalize(n0 + n1); + vec2 miter_n2 = is_truncated[1] ? normalize(v1.xy - v2.xy) : normalize(n1 + n2); + + // miter vectors (line vector matching miter normal) + vec2 miter_v1 = -normal_vector(miter_n1); + vec2 miter_v2 = -normal_vector(miter_n2); + + // distance between p1/2 and respective sharp corner + float miter_offset1 = dot(miter_n1, n1); // = dot(miter_v1, v1) + float miter_offset2 = dot(miter_n2, n1); // = dot(miter_v2, v1) + + // How far the line needs to extend in v1 directionto accomodate the joint. + // The line quad (w/o width) is given by: + // p1 + w * extrusion[0][1] * v1 ----- p2 + w * extrusion[1][1] * v1 + // | | + // p1 + w * extrusion[0][0] * v1 ----- p2 + w * extrusion[1][0] * v1 + // where w = halfwidth for drawn corners and w = halfwidth + AA_THICKNESS + // for the corners of quad. + mat2 extrusion; + + if (is_truncated[0]) { + // need to extend segment to include previous segments corners for truncated join + extrusion[0][1] = -abs(miter_offset1 / dot(miter_v1, n1)); + extrusion[0][0] = extrusion[0][1]; + } else { + // shallow/spike join needs to include point where miter normal meets outer line edge + extrusion[0][1] = dot(miter_n1, v1.xy) / miter_offset1; + extrusion[0][0] = -extrusion[0][1]; } - if (!isvalid[3]) - { - float corner_offset = max(0, abs(dot(miter_a, v1.xy)) - segment_length); - f_uv_minmax.y = px2uv * (u2 + corner_offset); - p2 += (corner_offset + AA_THICKNESS) * v1; - u2 += (corner_offset + AA_THICKNESS); - segment_length += corner_offset; + if (is_truncated[1]) { + // extrusion[1] = halfwidth * miter_offset2 / dot(miter_v2, n1); + extrusion[1][1] = abs(miter_offset2 / dot(miter_n2, v1.xy)); + extrusion[1][0] = extrusion[1][1]; + } else { + extrusion[1][1] = dot(miter_n2, v1.xy) / miter_offset2; + extrusion[1][0] = -extrusion[1][1]; } - // scaling of uv.y due to different linewidths - // the padding for AA_THICKNESS should always have AA_THICKNESS width in uv - thickness_aa1 = g_thickness[1] / edge_scale + AA_THICKNESS; - thickness_aa2 = g_thickness[2] / edge_scale + AA_THICKNESS; - - // Generate line segment - u1 *= px2uv; - u2 *= px2uv; - // Normal Version - generate_line_segment( - p1, miter_a, u1, thickness_aa1, - p2, miter_b, u2, thickness_aa2, - v1.xy, segment_length + // Miter joints can cause vertices to move past each other, e.g. + // _______ + // '. .' + // x + // '---' + // To avoid drawing the "inverted" section we move the relevant + // vertices to the crossing point (x) using this scaling factor. + vec2 shape_factor = vec2( + max(0.0, segment_length / max(segment_length, (halfwidth + AA_THICKNESS) * (extrusion[0][0] - extrusion[1][0]))), // -n + max(0.0, segment_length / max(segment_length, (halfwidth + AA_THICKNESS) * (extrusion[0][1] - extrusion[1][1]))) // +n ); - // Debug - show each triangle - // generate_line_segment_debug( - // p1, miter_a, u1, thickness_aa1, - // p2, miter_b, u2, thickness_aa2, - // v1.xy, segment_length - // ); - - return; -} - - + // Generate static/flat outputs + + // If a pattern starts or stops drawing in a joint it will get + // fractured across the joint. To avoid this we either: + // - adjust the involved line segments so that the patterns ends + // on straight line quad (adjustment becomes +1.0 or -1.0) + // - or adjust the pattern to start/stop outside of the joint + // (f_pattern_overwrite is set, adjustment is 0.0) + vec2 adjustment = process_pattern(pattern, isvalid, halfwidth * extrusion, segment_length, halfwidth); + + // If adjustment != 0.0 we replace a joint by an extruded line, so we no longer + // need to shrink the line for the joint to fit. + if (adjustment[0] != 0.0 || adjustment[1] != 0.0) + shape_factor = vec2(1.0); + + // For truncated miter joints we discard overlapping sections of + // the two involved line segments. To avoid discarding far into + // the line segment we limit the range here. (Without this short + // segments can cut holes into longer sections.) + f_discard_limit = vec2( + is_truncated[0] ? 0.0 : 1e12, + is_truncated[1] ? 0.0 : 1e12 + ); -//////////////////////////////////////////////////////////////////////////////// -/// Main -//////////////////////////////////////////////////////////////////////////////// + // used to elongate sdf to include joints + // if start/end elongate slightly so that there is no AA gap in loops + // if joint skipped elongate to new length + // if normal joint elongate a lot to let discard/truncation handle joint + f_extrusion = vec2( + !isvalid[0] ? min(AA_RADIUS, halfwidth) : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0][0])), + !isvalid[3] ? min(AA_RADIUS, halfwidth) : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1][0])) + ); + // used to compute width sdf + f_linewidth = halfwidth; + + // for color sampling + f_color1 = g_color[1]; + f_color2 = g_color[2]; + + // handle very thin lines by adjusting alpha rather than linewidth/sdfs + f_alpha_weight = min(1.0, g_thickness[1] / AA_RADIUS); + + // for uv's + f_cumulative_length = g_lastlen[1]; + + // Generate interpolated/varying outputs: + + LineVertex vertex; + + for (int x = 0; x < 2; x++) { + vertex.index = x+1; + + for (int y = 0; y < 2; y++) { + // Calculate offset from p1/p2 + vec3 offset; + if (adjustment[x] == 0.0) { + if (is_truncated[x] || !isvalid[3*x]) { + // handle overlap in fragment shader via SDF comparison + offset = shape_factor[y] * ( + (halfwidth * extrusion[x][y] + (2 * x - 1) * AA_THICKNESS) * v1 + + vec3((2 * y - 1) * (halfwidth + AA_THICKNESS) * n1, 0) + ); + } else { + // handle overlap by adjusting geometry + // TODO: should this include z in miter_n? + offset = (2 * y - 1) * shape_factor[y] * + (halfwidth + AA_THICKNESS) / + float[2](miter_offset1, miter_offset2)[x] * + vec3(vec2[2](miter_n1, miter_n2)[x], 0); + } + } else { + // discard joint for cleaner pattern handling + offset = + adjustment[x] * (halfwidth * abs(extrusion[x][1]) + AA_THICKNESS) * v1 + + vec3((2 * y - 1) * (halfwidth + AA_THICKNESS) * n1, 0); + } + vertex.position = vec3[2](p1, p2)[x] + offset; -void main(void) -{ - // These need to be set but don't have reasonable values here - o_view_pos = vec3(0); - o_view_normal = vec3(0); + // Generate SDF's - // we generate very thin lines for linewidth 0, so we manually skip them: - if (g_thickness[1] == 0.0 && g_thickness[2] == 0.0) { - return; - } + // distance from quad vertex to line control points + vec2 VP1 = vertex.position.xy - p1.xy; + vec2 VP2 = vertex.position.xy - p2.xy; + // Signed distance of the previous segment from the shared point + // p1 in line direction. Used decide which segments renders + // which joint fragment/pixel for truncated joints. + if (isvalid[0] && (adjustment[0] == 0) && is_truncated[0]) + vertex.quad_sdf0 = dot(VP1, v0.xy); + else + vertex.quad_sdf0 = 1e12; - // We mark each of the four vertices as valid or not. Vertices can be - // marked invalid on input (eg, if they contain NaN). We also mark them - // invalid if they repeat in the index buffer. This allows us to render to - // the very ends of a polyline without clumsy buffering the position data on the - // CPU side by repeating the first and last points via the index buffer. It - // just requires a little care further down to avoid degenerate normals. - bool isvalid[4] = bool[]( - g_valid_vertex[0] == 1 && g_id[0].y != g_id[1].y, - g_valid_vertex[1] == 1, - g_valid_vertex[2] == 1, - g_valid_vertex[3] == 1 && g_id[2].y != g_id[3].y - ); + // sdf of this segment + vertex.quad_sdf1.x = dot(VP1, -v1.xy); + vertex.quad_sdf1.y = dot(VP2, v1.xy); + vertex.quad_sdf1.z = dot(VP1, n1); - if(!isvalid[1] || !isvalid[2]){ - // If one of the central vertices is invalid or there is a break in the - // line, we don't emit anything. - return; + // SDF for next segment, see quad_sdf0 + if (isvalid[3] && (adjustment[1] == 0) && is_truncated[1]) + vertex.quad_sdf2 = dot(VP2, -v2.xy); + else + vertex.quad_sdf2 = 1e12; + + // sdf for creating a flat cap on truncated joints + // (sign(dot(...)) detects if line bends left or right) + // left/right adjustments disable + vertex.truncation.x = !is_truncated[0] ? -1.0 : + dot(VP1, sign(dot(miter_n1, -v1.xy)) * miter_n1) - halfwidth * abs(miter_offset1) + - abs(adjustment[0]) * 1e12; + vertex.truncation.y = !is_truncated[1] ? -1.0 : + dot(VP2, sign(dot(miter_n2, +v1.xy)) * miter_n2) - halfwidth * abs(miter_offset2) + - abs(adjustment[1]) * 1e12; + + // colors should be sampled based on the normalized distance from the + // extruded edge (varies with offset in n direction) + // - correcting for this with per-vertex colors results visible face border + // - calculating normalized distance here will cause div 0/negative + // issues as (linelength +- (extrusion[0] + extrusion[1])) <= 0 is possible + // So defer color interpolation to fragment shader + vertex.linestart = shape_factor[y] * halfwidth * extrusion[0][y]; + vertex.linelength = max(1, segment_length - shape_factor[y] * halfwidth * (extrusion[0][y] - extrusion[1][y])); + + // finalize vertex + emit_vertex(vertex); + } } - // get the four vertices passed to the shader - // without FAST_PATH the conversions happen on the CPU -#ifdef FAST_PATH - draw_solid_line(isvalid); -#else - draw_patterned_line(isvalid); -#endif + // finalize primitive + EndPrimitive(); return; -} +} \ No newline at end of file diff --git a/GLMakie/assets/shader/lines.vert b/GLMakie/assets/shader/lines.vert index d789fdbd04f..80a355b398f 100644 --- a/GLMakie/assets/shader/lines.vert +++ b/GLMakie/assets/shader/lines.vert @@ -11,17 +11,8 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d in float lastlen; {{valid_vertex_type}} valid_vertex; - -{{color_type}} color; -{{color_map_type}} color_map; -{{intensity_type}} intensity; -{{color_norm_type}} color_norm; {{thickness_type}} thickness; - -vec4 _color(vec3 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); -vec4 _color(vec4 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len); -vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm, int index, int len); -vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_norm, int index, int len); +{{color_type}} color; uniform mat4 projectionview, model; uniform uint objectid; @@ -29,14 +20,12 @@ uniform int total_length; uniform float px_per_unit; out uvec2 g_id; -out vec4 g_color; +out {{stripped_color_type}} g_color; out float g_lastlen; out int g_valid_vertex; out float g_thickness; -vec4 getindex(sampler2D tex, int index); -vec4 getindex(sampler1D tex, int index); - +vec4 to_vec4(vec4 v){return v;} vec4 to_vec4(vec3 v){return vec4(v, 1);} vec4 to_vec4(vec2 v){return vec4(v, 0, 1);} @@ -53,7 +42,7 @@ void main() g_valid_vertex = get_valid_vertex(valid_vertex); g_thickness = px_per_unit * thickness; - g_color = _color(color, intensity, color_map, color_norm, index, total_length); + g_color = color; #ifdef FAST_PATH gl_Position = projectionview * model * to_vec4(vertex); #else diff --git a/GLMakie/assets/shader/voxel.frag b/GLMakie/assets/shader/voxel.frag new file mode 100644 index 00000000000..cc5caf52c34 --- /dev/null +++ b/GLMakie/assets/shader/voxel.frag @@ -0,0 +1,133 @@ +#version 330 core +// {{GLSL VERSION}} +// {{GLSL_EXTENSIONS}} + +// debug FLAGS +// #define DEBUG_RENDER_ORDER 0 // (0, 1, 2) - dimensions + +struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data + bool _; //empty structs are not allowed +}; + +// Sets which shading procedures to use +{{shading}} + +flat in vec3 o_normal; +in vec3 o_uvw; +flat in int o_side; +in vec2 o_tex_uv; + +#ifdef DEBUG_RENDER_ORDER +flat in float plane_render_idx; // debug +flat in int plane_dim; +flat in int plane_front; +#endif + +uniform lowp usampler3D voxel_id; +uniform uint objectid; +uniform float gap; + +{{uv_map_type}} uv_map; +{{color_map_type}} color_map; +{{color_type}} color; + +vec4 debug_color(uint id) { + return vec4( + float((id & uint(225)) >> uint(5)) / 5.0, + float((id & uint(25)) >> uint(3)) / 3.0, + float((id & uint(7)) >> uint(1)) / 3.0, + 1.0 + ); +} +vec4 debug_color(int id) { return debug_color(uint(id)); } + +// unused but compilation requires it +vec4 get_lrbt(Nothing uv_map, int id, int side) { + return vec4(0,0,1,1); +} +vec4 get_lrbt(sampler1D uv_map, int id, int side) { + return texelFetch(uv_map, id-1, 0); +} +vec4 get_lrbt(sampler2D uv_map, int id, int side) { + return texelFetch(uv_map, ivec2(id-1, side), 0); +} + +vec4 get_color_from_texture(sampler2D color, int id) { + vec4 lrbt = get_lrbt(uv_map, id, o_side); + // compute uv normalized to voxel + // TODO: float precision causes this to wrap sometimes (e.g. 5.999..7.0002) + vec2 voxel_uv = mod(o_tex_uv, 1.0); + voxel_uv = mix(lrbt.xz, lrbt.yw, voxel_uv); + return texture(color, voxel_uv); +} + + +vec4 get_color(Nothing color, Nothing color_map, int id) { + return debug_color(id); +} +vec4 get_color(Nothing color, sampler1D color_map, int id) { + return texelFetch(color_map, id-1, 0); +} +vec4 get_color(sampler1D color, sampler1D color_map, int id) { + return texelFetch(color, id-1, 0); +} +vec4 get_color(sampler1D color, Nothing color_map, int id) { + return texelFetch(color, id-1, 0); +} +vec4 get_color(sampler2D color, sampler1D color_map, int id) { + return get_color_from_texture(color, id); +} +vec4 get_color(sampler2D color, Nothing color_map, int id) { + return get_color_from_texture(color, id); +} + + +void write2framebuffer(vec4 color, uvec2 id); + +#ifndef NO_SHADING +vec3 illuminate(vec3 normal, vec3 base_color); +#endif + +void main() +{ + vec2 voxel_uv = mod(o_tex_uv, 1.0); + if (voxel_uv.x < 0.5 * gap || voxel_uv.x > 1.0 - 0.5 * gap || + voxel_uv.y < 0.5 * gap || voxel_uv.y > 1.0 - 0.5 * gap) + discard; + + // grab voxel id + int id = int(texture(voxel_id, o_uvw).x); + + // id is invisible so we simply discard + if (id == 0) { + discard; + } + + // otherwise we draw. For now just some color... + vec4 voxel_color = get_color(color, color_map, id); + +#ifdef DEBUG_RENDER_ORDER + if (plane_dim != DEBUG_RENDER_ORDER) + discard; + voxel_color = vec4( + plane_front * plane_render_idx, + -plane_front * plane_render_idx, + 0, + id == 0 ? 0.1 : 1.0 + ); + // voxel_color = vec4(o_normal, id == 0 ? 0.1 : 1.0); + // voxel_color = vec4(plane_front, 0, 0, 1.0); +#endif + +#ifndef NO_SHADING + voxel_color.rgb = illuminate(o_normal, voxel_color.rgb); +#endif + + // TODO: index into 3d array + ivec3 size = ivec3(textureSize(voxel_id, 0).xyz); + ivec3 idx = ivec3(o_uvw * size); + int lin = 1 + idx.x + size.x * (idx.y + size.y * idx.z); + + // draw + write2framebuffer(voxel_color, uvec2(objectid, lin)); +} \ No newline at end of file diff --git a/GLMakie/assets/shader/voxel.vert b/GLMakie/assets/shader/voxel.vert new file mode 100644 index 00000000000..fa1ace9f348 --- /dev/null +++ b/GLMakie/assets/shader/voxel.vert @@ -0,0 +1,184 @@ +#version 330 core +// {{GLSL_VERSION}} +// {{GLSL_EXTENSIONS}} + +// debug FLAGS +// #define DEBUG_RENDER_ORDER + +in vec2 vertices; + +flat out vec3 o_normal; +out vec3 o_uvw; +flat out int o_side; +out vec2 o_tex_uv; + +#ifdef DEBUG_RENDER_ORDER +flat out float plane_render_idx; +flat out int plane_dim; +flat out int plane_front; +#endif + +out vec3 o_camdir; +out vec3 o_world_pos; + +uniform mat4 model; +uniform mat3 world_normalmatrix; +uniform mat4 projectionview; +uniform vec3 eyeposition; +uniform vec3 view_direction; +uniform lowp usampler3D voxel_id; +uniform float depth_shift; +uniform bool depthsorting; +uniform float gap; + +const vec3 unit_vecs[3] = vec3[]( vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1) ); +const mat2x3 orientations[3] = mat2x3[]( + mat2x3(0, 1, 0, 0, 0, 1), // xy -> _yz (x normal) + mat2x3(1, 0, 0, 0, 0, 1), // xy -> x_z (y normal) + mat2x3(1, 0, 0, 0, 1, 0) // xy -> xy_ (z normal) +); + +void main() { + /* How this works: + To simplify lets consider a 2d grid of pixel where the voxel surface would + be the square outline of around a data point x. + +---+---+---+ + | x | x | x | + +---+---+---+ + | x | x | x | + +---+---+---+ + | x | x | x | + +---+---+---+ + Naively we would draw 4 lines for each point x, coloring them based on the + data attached to x. This would result in 4 * N^2 lines with N^2 = number of + pixels. We can do much better though by drawing a line for each column and + row of pixels: + 1 +---+---+---+ + | x | x | x | + 2 +---+---+---+ + | x | x | x | + 3 +---+---+---+ + | x | x | x | + 4 +---+---+---+ + 5 6 7 8 + This results in 2 * (N+1) lines. We can adjust the color of the line by + sampling a Texture containing the information previously attached to vertices. + + Generalized to 3D voxels, lines become planes and the texture becomes 3D. + We draw the planes through instancing. So first we will need to map the + instance id to a dimension (xy, xz or yz plane) and an offset (in z, y or + x direction respectively). + */ + + // TODO: might be better for transparent rendering to alternate xyz? + // But how would we do this for non-cubic chunks? + + // Map instance id to dimension and index along dimension (0..N+1 or 0..2N) + ivec3 size = textureSize(voxel_id, 0); + int dim, id = gl_InstanceID, front = 1; + if (gap > 0.01) { + front = 1 - 2 * int(gl_InstanceID & 1); + if (id < 2 * size.z) { + dim = 2; + id = id; + } else if (id < 2 * (size.z + size.y)) { + dim = 1; + id = id - 2 * size.z; + } else { // if (id > 2 * (size.z + size.y)) { + dim = 0; + id = id - 2 * (size.z + size.y); + } + } else { + if (id < size.z + 1) { + dim = 2; + id = id; + } else if (id < size.z + size.y + 2) { + dim = 1; + id = id - (size.z + 1); + } else { + dim = 0; + id = id - (size.z + size.y + 2); + } + } + +#ifdef DEBUG_RENDER_ORDER + plane_render_idx = float(id) / float(size[dim]-1); + plane_dim = dim; + plane_front = front; +#endif + + // plane placement + // Figure out which plane to start with + vec3 normal = world_normalmatrix * unit_vecs[dim]; + int dir = int(sign(dot(view_direction, normal))), start; + if (depthsorting) { + // TODO: depthsorted should start far away from viewer so every plane draws + start = int((0.5 + 0.5 * dir) * size[dim]); + dir *= -1; + } else { + // otherwise we should start at viewer and expand in view direction so + // that the depth test can quickly eliminate unnecessary fragments + // Note that model can have rotations and (uneven) scaling + vec4 origin = model * vec4(0, 0, 0, 1); + vec4 back = model * vec4(size, 1); + float front_dist = dot(origin.xyz / origin.w, normal); + float back_dist = dot(back.xyz / back.w, normal); + float cam_dist = dot(eyeposition, normal); + float dist01 = (cam_dist - front_dist) / (back_dist - front_dist); + + // index of voxel closest to (and in front of) the camera + start = clamp(int(float(size[dim]) * dist01), 0, size[dim]); + } + + vec3 displacement; + if (gap > 0.01) { + // planes are doubled + // 2 * start + min(dir, 0) closest (camera facing) plane + // dir * id iterate away from first plane + // (idx + 2 * size[dim]) % 2 * size[dim] normalize to [0, 2size[dim]) + int plane_idx = (2 * start + min(dir, 0) + dir * id + 2 * size[dim]) % (2 * size[dim]); + // (plane_idx + 1) / 2 map to idx 0, 1, 2, 3, 4 -> displacements 0, 1, 1, 2, 2, ... + // 0.5 * dir * gap * front gap based offset from space filling placements + displacement = ((plane_idx + 1) / 2 + 0.5 * dir * front * gap) * unit_vecs[dim]; + } else { + // similar to above but with N+1 indices around N voxels + displacement = ((start + dir * id + size[dim] + 1) % (size[dim] + 1)) * unit_vecs[dim]; + } + + // place plane vertices + vec3 plane_vertex = size * (orientations[dim] * vertices) + displacement; + vec4 world_pos = model * vec4(plane_vertex, 1.0f); + o_world_pos = world_pos.xyz; + gl_Position = projectionview * world_pos; + gl_Position.z += gl_Position.w * depth_shift; + + // For each plane the normal is constant in + // +- normal = +- world_normalmatrix * unit_vecs[dim] direction. We just need + // the correct prefactor + o_camdir = eyeposition - world_pos.xyz / world_pos.w; + float normal_dir; + if (gap > 0.01) { + // With a gap we render front and back faces. Since the gap always takes + // away from a voxel the normal should go in the opposite direction. + normal_dir = -float(dir * front); + } else { + // Without a gap we skip back facing faces so every normal faces the camera + normal_dir = sign(dot(o_camdir, normal)); + } + o_normal = normal_dir * normalize(normal); + + // The quad we render here is directly between two slices of voxels in our + // chunk. Each `plane_vertex` of the quad is in a 0 .. size scale, so they + // can be mapped directly to texture indices. We just need to figure out if + // the quad is associated with the "previous" or "next" slice of voxels. We + // can derive that from the normal direction, as the normal always points + // away from the voxel center. + o_uvw = (plane_vertex - 0.5 * (1.0 - gap) * o_normal) / size; + + // normal in: -x -y -z +x +y +z direction + o_side = dim + 3 * int(0.5 + 0.5 * normal_dir); + + // map plane_vertex (-w/2 .. w/2 scale) back to 2d (scaled 0 .. w) + // if the normal is negative invert range (w .. 0) + o_tex_uv = transpose(orientations[dim]) * (vec3(-normal_dir, normal_dir, 1.0) * plane_vertex); +} \ No newline at end of file diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 028c23db012..39d3cf3bb60 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -381,21 +381,25 @@ function gpu_resize!(t::Texture{T, ND}, newdims::NTuple{ND, Int}) where {T, ND} return t end -texsubimage(t::Texture{T, 1}, newvalue::Array{T, 1}, xrange::UnitRange, level=0) where {T} = glTexSubImage1D( - t.texturetype, level, first(xrange)-1, length(xrange), t.format, t.pixeltype, newvalue -) -function texsubimage(t::Texture{T, 2}, newvalue::Array{T, 2}, xrange::UnitRange, yrange::UnitRange, level=0) where T +function texsubimage(t::Texture{T, 1}, newvalue::Array{T}, xrange::UnitRange, level=0) where {T} + glTexSubImage1D( + t.texturetype, level, first(xrange)-1, length(xrange), t.format, t.pixeltype, newvalue + ) +end +function texsubimage(t::Texture{T, 2}, newvalue::Array{T}, xrange::UnitRange, yrange::UnitRange, level=0) where T glTexSubImage2D( t.texturetype, level, first(xrange)-1, first(yrange)-1, length(xrange), length(yrange), t.format, t.pixeltype, newvalue ) end -texsubimage(t::Texture{T, 3}, newvalue::Array{T, 3}, xrange::UnitRange, yrange::UnitRange, zrange::UnitRange, level=0) where {T} = glTexSubImage3D( - t.texturetype, level, - first(xrange)-1, first(yrange)-1, first(zrange)-1, length(xrange), length(yrange), length(zrange), - t.format, t.pixeltype, newvalue -) +function texsubimage(t::Texture{T, 3}, newvalue::Array{T}, xrange::UnitRange, yrange::UnitRange, zrange::UnitRange, level=0) where {T} + glTexSubImage3D( + t.texturetype, level, + first(xrange)-1, first(yrange)-1, first(zrange)-1, length(xrange), length(yrange), length(zrange), + t.format, t.pixeltype, newvalue + ) +end Base.iterate(t::TextureBuffer{T}) where {T} = iterate(t.buffer) function Base.iterate(t::TextureBuffer{T}, state::Tuple{Ptr{T}, Int}) where T diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 206a8dda42a..0c1a6a1ebc0 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -385,6 +385,7 @@ function RenderObject( try data[k] = gl_convert(v) catch e + @error "gl_convert for key `$k` failed" rethrow(e) end diff --git a/GLMakie/src/GLMakie.jl b/GLMakie/src/GLMakie.jl index 47361006a81..49296d13556 100644 --- a/GLMakie/src/GLMakie.jl +++ b/GLMakie/src/GLMakie.jl @@ -56,15 +56,25 @@ end const GL_ASSET_DIR = RelocatableFolders.@path joinpath(@__DIR__, "..", "assets") const SHADER_DIR = RelocatableFolders.@path joinpath(GL_ASSET_DIR, "shader") -const LOADED_SHADERS = Dict{String, ShaderSource}() +const LOADED_SHADERS = Dict{String, Tuple{Float64, ShaderSource}}() function loadshader(name) - # Turns out, joinpath is so slow, that it actually makes sense - # To memoize it :-O + # Turns out, loading shaders is so slow, that it actually makes sense to memoize it :-O # when creating 1000 plots with the PlotSpec API, timing drop from 1.5s to 1s just from this change: - return get!(LOADED_SHADERS, name) do - return ShaderSource(joinpath(SHADER_DIR, name)) + # Note that we need to check if the file is still valid to enable hot reloading of shaders + path = joinpath(SHADER_DIR, name) + if haskey(LOADED_SHADERS, name) + cached_time, src = LOADED_SHADERS[name] + file_time = Base.Filesystem.mtime(joinpath(SHADER_DIR, name)) + # return source if valid + (file_time == cached_time) && return src end + + # replace source if invalid/add new source + mtime = Base.Filesystem.mtime(path) + src = ShaderSource(path) + LOADED_SHADERS[name] = (mtime, src) + return src end gl_texture_atlas() = Makie.get_texture_atlas(2048, 64) diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 44b7a30026f..9f877a92007 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -1,6 +1,7 @@ using Makie: transform_func_obs, apply_transform using Makie: attribute_per_char, FastPixel, el32convert, Pixel using Makie: convert_arguments +using Makie: apply_transform_and_f32_conversion, f32_conversion_obs function handle_lights(attr::Dict, screen::Screen, lights::Vector{Makie.AbstractLight}) @inline function push_inplace!(trg, idx, src) @@ -154,7 +155,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 +163,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 @@ -225,7 +226,7 @@ const EXCLUDE_KEYS = Set([:transformation, :tickranges, :ticklabels, :raw, :SSAO :lightposition, :material, :axis_cycler, :inspector_label, :inspector_hover, :inspector_clear, :inspectable, :colorrange, :colormap, :colorscale, :highclip, :lowclip, :nan_color, - :calculated_colors, :space, :markerspace, :model]) + :calculated_colors, :space, :markerspace, :model, :userdata]) function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) @@ -256,7 +257,7 @@ function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) gl_value = lift_convert(key, value, plot, screen) gl_key => gl_value end) - gl_attributes[:model] = plot.model + gl_attributes[:model] = map(Makie.patch_model, f32_conversion_obs(plot), plot.model) if haskey(plot, :markerspace) gl_attributes[:markerspace] = plot.markerspace end @@ -382,7 +383,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 @@ -393,7 +395,7 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Sca end # fast pixel does its own setup if !(marker[] isa FastPixel) - gl_attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotations) + gl_attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotation) atlas = gl_texture_atlas() isnothing(gl_attributes[:distancefield][]) && delete!(gl_attributes, :distancefield) shape = lift(m -> Cint(Makie.marker_to_sdf_shape(m)), plot, marker) @@ -438,67 +440,53 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Sca end end - -_mean(xs) = sum(xs) / length(xs) # skip Statistics import - function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Lines)) return cached_robj!(screen, scene, plot) do gl_attributes linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) positions = handle_view(plot[1], data) - transform_func = transform_func_obs(plot) - ls = to_value(linestyle) space = plot.space - if isnothing(ls) - data[:pattern] = ls + 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 - linewidth = gl_attributes[:thickness] - px_per_unit = data[:px_per_unit] - data[:pattern] = map(linestyle, linewidth, px_per_unit) do ls, lw, ppu - ppu * _mean(lw) .* ls - end + data[:pattern] = linestyle data[:fast] = false pvm = lift(*, plot, data[:projectionview], data[:model]) - positions = lift(plot, transform_func, positions, space, pvm, - data[:resolution]) do f, ps, space, pvm, res - transformed = apply_transform(f, ps, space) - output = Vector{Point3f}(undef, length(transformed)) - scale = Vec3f(res[1], res[2], 1f0) + transform_func = transform_func_obs(plot) + positions = lift(plot, f32_conversion_obs(scene), transform_func, positions, + space, pvm) do f32c, f, ps, space, pvm + + transformed = apply_transform_and_f32_conversion(f32c, f, ps, space) + output = Vector{Point4f}(undef, length(transformed)) for i in eachindex(transformed) - clip = pvm * to_ndim(Point4f, to_ndim(Point3f, transformed[i], 0f0), 1f0) - output[i] = scale .* Point3f(clip) ./ clip[4] + output[i] = pvm * to_ndim(Point4f, to_ndim(Point3f, transformed[i], 0f0), 1f0) end output end end + + if haskey(data, :intensity) + data[:color] = pop!(data, :intensity) + end + return draw_lines(screen, positions, data) end end function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::LineSegments)) return cached_robj!(screen, scene, plot) do gl_attributes - linestyle = pop!(gl_attributes, :linestyle) data = Dict{Symbol, Any}(gl_attributes) - px_per_unit = data[:px_per_unit] - ls = to_value(linestyle) - if isnothing(ls) - data[:pattern] = nothing - data[:fast] = true - else - linewidth = gl_attributes[:thickness] - data[:pattern] = lift(plot, linestyle, linewidth, px_per_unit) do ls, lw, ppu - ppu * _mean(lw) .* ls - end - data[:fast] = false - end - positions = handle_view(plot[1], data) + data[:pattern] = pop!(data, :linestyle) - positions = lift(apply_transform, plot, transform_func_obs(plot), positions, plot.space) + positions = handle_view(plot[1], data) + # 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 @@ -520,8 +508,10 @@ 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, f32_conversion_obs(scene), transfunc, space + ) do pos, gc, offset, f32c, transfunc, space + return Makie.text_quads(atlas, pos, to_value(gc), offset, f32c, transfunc, space) end # unpack values from the one signal: @@ -585,30 +575,25 @@ end # el32convert doesn't copy for array of Float32 # But we assume that xy_convert copies when we use it -xy_convert(x::AbstractArray{Float32}, n) = copy(x) -xy_convert(x::AbstractArray, n) = el32convert(x) -xy_convert(x, n) = Float32[LinRange(extrema(x)..., n + 1);] +xy_convert(x::AbstractArray, n) = copy(x) +xy_convert(x, n) = [LinRange(extrema(x)..., n + 1);] function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) return cached_robj!(screen, scene, plot) do gl_attributes t = Makie.transform_func_obs(plot) mat = plot[3] space = plot.space # needs to happen before connect_camera! call - xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space + xypos = lift(plot, f32_conversion_obs(scene), t, plot[1], plot[2], space) do f32c, 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] - end - map!(y1d, y1d) do y - return apply_transform(t, Point(0, y), space)[2] - end + x1d = Makie.apply_transform_and_f32_conversion(f32c, t, x1d, 1, space) + y1d = Makie.apply_transform_and_f32_conversion(f32c, t, y1d, 2, space) return (x1d, y1d) end end @@ -638,10 +623,10 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Image) position = lift(plot, plot[1], plot[2]) do x, y xmin, xmax = extrema(x) ymin, ymax = extrema(y) - rect = Rect2f(xmin, ymin, xmax - xmin, ymax - ymin) - return decompose(Point2f, rect) + rect = Rect2(xmin, ymin, xmax - xmin, ymax - ymin) + 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 @@ -697,9 +682,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) @@ -753,17 +738,17 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) if all(T -> T <: Union{AbstractMatrix, AbstractVector}, types) t = Makie.transform_func_obs(plot) mat = plot[3] - xypos = lift(plot, t, plot[1], plot[2], space) do t, x, y, space + xypos = lift(plot, f32_conversion_obs(scene), t, plot[1], plot[2], space) do f32c, 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) 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 @@ -816,3 +801,73 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Volume) end end end + +function draw_atomic(screen::Screen, scene::Scene, plot::Voxels) + return cached_robj!(screen, scene, plot) do gl_attributes + @assert to_value(plot.converted[end]) isa Array{UInt8, 3} + + # voxel ids + tex = Texture(plot.converted[end], minfilter = :nearest) + + # local update + buffer = Vector{UInt8}(undef, 1) + on(plot, pop!(gl_attributes, :_local_update)) do (is, js, ks) + required_length = length(is) * length(js) * length(ks) + if length(buffer) < required_length + resize!(buffer, required_length) + end + idx = 1 + for k in ks, j in js, i in is + buffer[idx] = plot.converted[end].val[i, j, k] + idx += 1 + end + GLAbstraction.texsubimage(tex, buffer, is, js, ks) + return + end + + # adjust model matrix according to x/y/z limits + gl_attributes[:model] = map( + plot, plot.converted..., pop!(gl_attributes, :model) + ) do xs, ys, zs, chunk, model + mini = minimum.((xs, ys, zs)) + width = maximum.((xs, ys, zs)) .- mini + return model * + Makie.translationmatrix(Vec3f(mini)) * + Makie.scalematrix(Vec3f(width ./ size(chunk))) + end + + # color attribute adjustments + pop!(gl_attributes, :lowclip, nothing) + pop!(gl_attributes, :highclip, nothing) + # Invalid: + pop!(gl_attributes, :nan_color, nothing) + pop!(gl_attributes, :alpha, nothing) # Why is this even here? + pop!(gl_attributes, :intensity, nothing) + pop!(gl_attributes, :color_norm, nothing) + # cleanup + pop!(gl_attributes, :_limits) + pop!(gl_attributes, :is_air) + + # make sure these exist + get!(gl_attributes, :color, nothing) + get!(gl_attributes, :color_map, nothing) + + # process texture mapping + uv_map = pop!(gl_attributes, :uvmap) + if !isnothing(to_value(uv_map)) + gl_attributes[:uv_map] = Texture(uv_map, minfilter = :nearest) + + interp = to_value(pop!(gl_attributes, :interpolate)) + interp = interp ? :linear : :nearest + color = gl_attributes[:color] + gl_attributes[:color] = Texture(color, minfilter = interp) + elseif !isnothing(to_value(gl_attributes[:color])) + gl_attributes[:color] = Texture(gl_attributes[:color], minfilter = :nearest) + end + + # for depthsorting + gl_attributes[:view_direction] = camera(scene).view_direction + + return draw_voxels(screen, tex, gl_attributes) + end +end diff --git a/GLMakie/src/gl_backend.jl b/GLMakie/src/gl_backend.jl index b8ed7a35dfd..ee4bffe3402 100644 --- a/GLMakie/src/gl_backend.jl +++ b/GLMakie/src/gl_backend.jl @@ -69,6 +69,7 @@ include("glshaders/image_like.jl") include("glshaders/mesh.jl") include("glshaders/particles.jl") include("glshaders/surface.jl") +include("glshaders/voxel.jl") include("picking.jl") include("rendering.jl") diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 562460de1f6..a26951393f8 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -1,64 +1,33 @@ -function sumlengths(points) +function sumlengths(points, resolution) + # normalize w component if availabke + f(p::VecTypes{4}) = p[Vec(1, 2)] / p[4] + f(p::VecTypes) = p[Vec(1, 2)] + + invalid(p::VecTypes{4}) = p[4] <= 1e-6 + invalid(p::VecTypes) = false + T = eltype(eltype(typeof(points))) result = zeros(T, length(points)) - i12 = Vec(1, 2) for i in eachindex(points) i0 = max(i-1, 1) p1, p2 = points[i0], points[i] - if !(any(map(isnan, p1)) || any(map(isnan, p2))) - result[i] = result[i0] + norm(p1[i12] - p2[i12]) + if any(map(isnan, p1)) || any(map(isnan, p2)) || invalid(p1) || invalid(p2) + result[i] = 0f0 else - result[i] = result[i0] + result[i] = result[i0] + 0.5 * norm(resolution .* (f(p1) - f(p2))) end end result end -intensity_convert(intensity, verts) = intensity -function intensity_convert(intensity::VecOrSignal{T}, verts) where T - if length(to_value(intensity)) == length(to_value(verts)) - GLBuffer(intensity) - else - Texture(intensity) - end -end -function intensity_convert_tex(intensity::VecOrSignal{T}, verts) where T - if length(to_value(intensity)) == length(to_value(verts)) - TextureBuffer(intensity) - else - Texture(intensity) - end -end -#TODO NaNMath.min/max? -dist(a, b) = abs(a-b) -mindist(x, a, b) = min(dist(a, x), dist(b, x)) -function gappy(x, ps) - n = length(ps) - x <= first(ps) && return first(ps) - x - for j=1:(n-1) - p0 = ps[j] - p1 = ps[min(j+1, n)] - if p0 <= x && p1 >= x - return mindist(x, p0, p1) * (isodd(j) ? 1 : -1) - end - end - return last(ps) - x -end -function ticks(points, resolution) - # This is used to map a vector of `points` to a signed distance field. The - # points mark transition between "on" and "off" section of the pattern. - - # The output should be periodic so the signed distance field value - # representing points[1] should be equal to the one representing points[end]. - # => range(..., length = resolution+1)[1:end-1] - - # points[end] should still represent the full length of the pattern though, - # so we need rescaling by ((resolution + 1) / resolution) - - scaled = ((resolution + 1) / resolution) .* points - r = range(first(scaled), stop=last(scaled), length=resolution+1)[1:end-1] - return Float16[gappy(x, scaled) for x = r] -end +# because the "color_type" generated in GLAbstraction also include "uniform" +gl_color_type_annotation(x::Observable) = gl_color_type_annotation(x.val) +gl_color_type_annotation(::Vector{<:Real}) = "float" +gl_color_type_annotation(::Vector{<:Makie.RGB}) = "vec3" +gl_color_type_annotation(::Vector{<:Makie.RGBA}) = "vec4" +gl_color_type_annotation(::Real) = "float" +gl_color_type_annotation(::Makie.RGB) = "vec3" +gl_color_type_annotation(::Makie.RGBA) = "vec4" @nospecialize function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data::Dict) where T<:Point @@ -68,10 +37,12 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: const_lift(vec, position) end + color_type = gl_color_type_annotation(data[:color]) + resolution = data[:resolution] + @gen_defaults! data begin total_length::Int32 = const_lift(x-> Int32(length(x)), position) vertex = p_vec => GLBuffer - intensity = nothing color = nothing => GLBuffer color_map = nothing => Texture color_norm = nothing @@ -90,39 +61,42 @@ function draw_lines(screen, position::Union{VectorTypes{T}, MatTypes{T}}, data:: fast = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "lines.vert", "lines.geom", "lines.frag", + "fragment_output.frag", "lines.vert", "lines.geom", "lines.frag", view = Dict( "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)), - "define_fast_path" => to_value(fast) ? "#define FAST_PATH" : "" + "define_fast_path" => to_value(fast) ? "#define FAST_PATH" : "", + "stripped_color_type" => color_type ) ) gl_primitive = GL_LINE_STRIP_ADJACENCY valid_vertex = const_lift(p_vec) do points map(p-> Float32(all(isfinite, p)), points) end => GLBuffer - lastlen = const_lift(sumlengths, p_vec) => GLBuffer + lastlen = const_lift(sumlengths, p_vec, resolution) => GLBuffer pattern_length = 1f0 # we divide by pattern_length a lot. + debug = false end if to_value(pattern) !== nothing if !isa(pattern, Texture) if !isa(to_value(pattern), Vector) error("Pattern needs to be a Vector of floats. Found: $(typeof(pattern))") end - tex = GLAbstraction.Texture(map(pt -> ticks(pt, 100), pattern), x_repeat = :repeat) + tex = GLAbstraction.Texture(lift(Makie.linestyle_to_sdf, pattern); x_repeat=:repeat) data[:pattern] = tex end - data[:pattern_length] = map(pt -> Float32(last(pt) - first(pt)), pattern) + data[:pattern_length] = lift(pt -> Float32(last(pt) - first(pt)), pattern) end - data[:intensity] = intensity_convert(intensity, vertex) return assemble_shader(data) end function draw_linesegments(screen, positions::VectorTypes{T}, data::Dict) where T <: Point + color_type = gl_color_type_annotation(data[:color]) + @gen_defaults! data begin vertex = positions => GLBuffer - color = default(RGBA, s, 1) => GLBuffer + color = nothing => GLBuffer color_map = nothing => Texture color_norm = nothing thickness = 2f0 => GLBuffer @@ -135,23 +109,25 @@ function draw_linesegments(screen, positions::VectorTypes{T}, data::Dict) where transparency = false shader = GLVisualizeShader( screen, - "fragment_output.frag", "util.vert", "line_segment.vert", "line_segment.geom", "lines.frag", + "fragment_output.frag", "line_segment.vert", "line_segment.geom", + "lines.frag", view = Dict( "buffers" => output_buffers(screen, to_value(transparency)), "buffer_writes" => output_buffer_writes(screen, to_value(transparency)), - "define_fast_path" => to_value(fast) ? "#define FAST_PATH" : "" + "stripped_color_type" => color_type ) ) - gl_primitive = GL_LINES - pattern_length = 1f0 + gl_primitive = GL_LINES + pattern_length = 1f0 + debug = false end if !isa(pattern, Texture) && to_value(pattern) !== nothing if !isa(to_value(pattern), Vector) error("Pattern needs to be a Vector of floats. Found: $(typeof(pattern))") end - tex = GLAbstraction.Texture(map(pt -> ticks(pt, 100), pattern), x_repeat = :repeat) + tex = GLAbstraction.Texture(lift(Makie.linestyle_to_sdf, pattern); x_repeat=:repeat) data[:pattern] = tex - data[:pattern_length] = map(pt -> Float32(last(pt) - first(pt)), pattern) + data[:pattern_length] = lift(pt -> Float32(last(pt) - first(pt)), pattern) end robj = assemble_shader(data) return robj diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index b0066970c23..2f5d116587c 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -41,6 +41,22 @@ is_all_equal_scale(v::Vec2f) = v[1] == v[2] # could use ≈ too is_all_equal_scale(vs::Vector{Vec2f}) = all(is_all_equal_scale, vs) +intensity_convert(intensity, verts) = intensity +function intensity_convert(intensity::VecOrSignal{T}, verts) where T + if length(to_value(intensity)) == length(to_value(verts)) + GLBuffer(intensity) + else + Texture(intensity) + end +end +function intensity_convert_tex(intensity::VecOrSignal{T}, verts) where T + if length(to_value(intensity)) == length(to_value(verts)) + TextureBuffer(intensity) + else + Texture(intensity) + end +end + @nospecialize @@ -59,6 +75,7 @@ function draw_mesh_particle(screen, p, data) texturecoordinates = nothing end + shading = pop!(data, :shading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin color_map = nothing => Texture diff --git a/GLMakie/src/glshaders/voxel.jl b/GLMakie/src/glshaders/voxel.jl new file mode 100644 index 00000000000..f0a3c679405 --- /dev/null +++ b/GLMakie/src/glshaders/voxel.jl @@ -0,0 +1,35 @@ +@nospecialize +function draw_voxels(screen, main::VolumeTypes, data::Dict) + geom = Rect2f(Point2f(0), Vec2f(1.0)) + to_opengl_mesh!(data, const_lift(GeometryBasics.triangle_mesh, geom)) + shading = pop!(data, :shading, FastShading) + @gen_defaults! data begin + voxel_id = main => Texture + gap = 0f0 + instances = const_lift(gap, voxel_id) do gap, chunk + N = sum(size(chunk)) + ifelse(gap > 0.01, 2 * N, N + 3) + end + model = Mat4f(I) + transparency = false + backlight = 0f0 + color = nothing => Texture + color_map = nothing => Texture + uv_map = nothing => Texture + shader = GLVisualizeShader( + screen, + "voxel.vert", + "fragment_output.frag", "voxel.frag", "lighting.frag", + view = Dict( + "shading" => light_calc(shading), + "MAX_LIGHTS" => "#define MAX_LIGHTS $(screen.config.max_lights)", + "MAX_LIGHT_PARAMETERS" => "#define MAX_LIGHT_PARAMETERS $(screen.config.max_light_parameters)", + "buffers" => output_buffers(screen, to_value(transparency)), + "buffer_writes" => output_buffer_writes(screen, to_value(transparency)) + ) + ) + end + + return assemble_shader(data) +end +@specialize \ No newline at end of file diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index 7aadbc3fa4b..d4e911474db 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -6,29 +6,6 @@ using GLMakie.ShaderAbstractions: Sampler using GLMakie.GeometryBasics using ReferenceTests.RNG -# A test case for wide lines and mitering at joints -@reference_test "Miter Joints for line rendering" begin - scene = Scene() - cam2d!(scene) - r = 4 - sep = 4*r - scatter!(scene, (sep+2*r)*[-1,-1,1,1], (sep+2*r)*[-1,1,-1,1]) - - for i=-1:1 - for j=-1:1 - angle = pi/2 + pi/4*i - x = r*[-cos(angle/2),0,-cos(angle/2)] - y = r*[-sin(angle/2),0,sin(angle/2)] - - linewidth = 40 * 2.0^j - lines!(scene, x .+ sep*i, y .+ sep*j, color=RGBAf(0,0,0,0.5), linewidth=linewidth) - lines!(scene, x .+ sep*i, y .+ sep*j, color=:red) - end - end - center!(scene) - scene -end - @reference_test "Sampler type" begin # Directly access texture parameters: x = Sampler(fill(to_color(:yellow), 100, 100), minfilter=:nearest) @@ -60,7 +37,7 @@ end end fig, ax, p = meshscatter(pos, - rotations=rot, + rotation=rot, color=color, markersize=size, axis = (; scenekw = (;limits=Rect3f(Point3(0), Point3(1)))) @@ -82,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..331c813a996 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -8,7 +8,7 @@ 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/MakieCore/Project.toml b/MakieCore/Project.toml index 275e64d33c2..683219e08bf 100644 --- a/MakieCore/Project.toml +++ b/MakieCore/Project.toml @@ -1,7 +1,7 @@ name = "MakieCore" uuid = "20f20a25-4f0e-4fdf-b5d1-57303727442b" authors = ["Simon Danisch"] -version = "0.7.3" +version = "0.8.0" [deps] Observables = "510215fc-4207-5dde-b226-833fc4488ee2" diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl index 326657fb33e..3e49580758c 100644 --- a/MakieCore/src/basic_plots.jl +++ b/MakieCore/src/basic_plots.jl @@ -12,6 +12,7 @@ default_theme(scene) = generic_plot_attributes!(Attributes()) - `depth_shift::Float32 = 0f0` adjusts the depth value of a plot after all other transformations, i.e. in clip space, where `0 <= depth <= 1`. This only applies to GLMakie and WGLMakie and can be used to adjust render order (like a tunable overdraw). - `model::Makie.Mat4f` sets a model matrix for the plot. This replaces adjustments made with `translate!`, `rotate!` and `scale!`. - `space::Symbol = :data` sets the transformation space for box encompassing the volume plot. See `Makie.spaces()` for possible inputs. +- `userdata = Attributes()` allows for data to be attached to a plot without validation. """ function generic_plot_attributes!(attr) attr[:transformation] = automatic @@ -23,6 +24,10 @@ function generic_plot_attributes!(attr) attr[:inspectable] = true attr[:depth_shift] = 0.0f0 attr[:space] = :data + attr[:inspector_label] = automatic + attr[:inspector_clear] = automatic + attr[:inspector_hover] = automatic + attr[:userdata] = Attributes() return attr end @@ -36,10 +41,46 @@ function generic_plot_attributes(attr) ssao = attr[:ssao], inspectable = attr[:inspectable], depth_shift = attr[:depth_shift], - space = attr[:space] + space = attr[:space], + inspector_label = attr[:inspector_label], + inspector_clear = attr[:inspector_clear], + inspector_hover = attr[:inspector_hover], + userdata = attr[:userdata] ) end +function mixin_generic_plot_attributes() + @DocumentedAttributes begin + transformation = automatic + "Sets a model matrix for the plot. This overrides adjustments made with `translate!`, `rotate!` and `scale!`." + model = automatic + "Controls whether the plot will be rendered or not." + visible = true + "Adjusts how the plot deals with transparency. In GLMakie `transparency = true` results in using Order Independent Transparency." + transparency = false + "Controls if the plot will draw over other plots. This specifically means ignoring depth checks in GL backends" + overdraw = false + "Adjusts whether the plot is rendered with ssao (screen space ambient occlusion). Note that this only makes sense in 3D plots and is only applicable with `fxaa = true`." + ssao = false + "sets whether this plot should be seen by `DataInspector`." + inspectable = true + "adjusts the depth value of a plot after all other transformations, i.e. in clip space, where `0 <= depth <= 1`. This only applies to GLMakie and WGLMakie and can be used to adjust render order (like a tunable overdraw)." + depth_shift = 0.0f0 + "sets the transformation space for box encompassing the plot. See `Makie.spaces()` for possible inputs." + space = :data + "adjusts whether the plot is rendered with fxaa (anti-aliasing, GLMakie only)." + fxaa = true + "Sets a callback function `(plot, index, position) -> string` which replaces the default label generated by DataInspector." + inspector_label = automatic + "Sets a callback function `(inspector, plot) -> ...` for cleaning up custom indicators in DataInspector." + inspector_clear = automatic + "Sets a callback function `(inspector, plot, index) -> ...` which replaces the default `show_data` methods." + inspector_hover = automatic + "Allows for users to attach any data to a plot without validation." + userdata = Attributes() + end +end + """ ### Color attributes @@ -76,6 +117,31 @@ function colormap_attributes(attr) ) end +function mixin_colormap_attributes() + @DocumentedAttributes begin + """ + Sets the colormap that is sampled for numeric `color`s. + `PlotUtils.cgrad(...)`, `Makie.Reverse(any_colormap)` can be used as well, or any symbol from ColorBrewer or PlotUtils. + To see all available color gradients, you can call `Makie.available_gradients()`. + """ + colormap = @inherit colormap :viridis + """ + The color transform function. Can be any function, but only works well together with `Colorbar` for `identity`, `log`, `log2`, `log10`, `sqrt`, `logit`, `Makie.pseudolog10` and `Makie.Symlog10`. + """ + colorscale = identity + "The values representing the start and end points of `colormap`." + colorrange = automatic + "The color for any value below the colorrange." + lowclip = automatic + "The color for any value above the colorrange." + highclip = automatic + "The color for NaN values." + nan_color = :transparent + "The alpha value of the colormap or color attribute. Multiple alphas like in `plot(alpha=0.2, color=(:red, 0.5)`, will get multiplied." + alpha = 1.0 + end +end + """ ### 3D shading attributes @@ -106,6 +172,21 @@ function shading_attributes(attr) ) end +function mixin_shading_attributes() + @DocumentedAttributes begin + "Sets the lighting algorithm used. Options are `NoShading` (no lighting), `FastShading` (AmbientLight + PointLight) or `MultiLightShading` (Multiple lights, GLMakie only). Note that this does not affect RPRMakie." + shading = automatic + "Sets how strongly the red, green and blue channel react to diffuse (scattered) light." + diffuse = 1.0 + "Sets how strongly the object reflects light in the red, green and blue channels." + specular = 0.2 + "Sets how sharp the reflection is." + shininess = 32.0f0 + "Sets a weight for secondary light calculation with inverted normals." + backlight = 0f0 + end +end + """ `calculated_attributes!(trait::Type{<: AbstractPlot}, plot)` trait version of calculated_attributes @@ -123,24 +204,14 @@ calculated_attributes!(plot::T) where T = calculated_attributes!(T, plot) image(image) Plots an image on a rectangle bounded by `x` and `y` (defaults to size of image). - -## Attributes - -### Specific to `Image` - -- `interpolate::Bool = true` sets whether colors should be interpolated. - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Image, x, y, image) do scene - attr = Attributes(; - interpolate = true, - fxaa = false, - ) - generic_plot_attributes!(attr) - return colormap_attributes!(attr, [:black, :white]) +@recipe Image x y image begin + "Sets whether colors should be interpolated between pixels." + interpolate = true + mixin_generic_plot_attributes()... + mixin_colormap_attributes()... + fxaa = false + colormap = [:black, :white] end """ @@ -171,27 +242,12 @@ Pairs that are missing from the resulting grid will be treated as if `zvector` h If `x` and `y` are omitted with a matrix argument, they default to `x, y = axes(matrix)`. Note that `heatmap` is slower to render than `image` so `image` should be preferred for large, regularly spaced grids. - -## Attributes - -### Specific to `Heatmap` - -- `interpolate::Bool = false` sets whether colors should be interpolated. - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Heatmap, x, y, values) do scene - attr = Attributes(; - - interpolate = false, - - linewidth = 0.0, - fxaa = true, - ) - generic_plot_attributes!(attr) - return colormap_attributes!(attr, theme(scene, :colormap)) +@recipe Heatmap x y values begin + "Sets whether colors should be interpolated" + interpolate = false + mixin_generic_plot_attributes()... + mixin_colormap_attributes()... end """ @@ -206,34 +262,23 @@ Available algorithms are: * `:absorptionrgba` => AbsorptionRGBA * `:additive` => AdditiveRGBA * `:indexedabsorption` => IndexedAbsorptionRGBA - -## Attributes - -### Specific to `Volume` - -- `algorithm::Union{Symbol, RaymarchAlgorithm} = :mip` sets the volume algorithm that is used. -- `isorange::Real = 0.05` sets the range of values picked up by the IsoValue algorithm. -- `isovalue = 0.5` sets the target value for the IsoValue algorithm. -- `interpolate::Bool = true` sets whether the volume data should be sampled with interpolation. - -$(Base.Docs.doc(shading_attributes!)) - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Volume, x, y, z, volume) do scene - attr = Attributes(; - - algorithm = :mip, - isovalue = 0.5, - isorange = 0.05, - interpolate = true, - fxaa = true, - ) - generic_plot_attributes!(attr) - shading_attributes!(attr) - return colormap_attributes!(attr, theme(scene, :colormap)) +@recipe Volume x y z volume begin + "Sets the volume algorithm that is used." + algorithm = :mip + "Sets the range of values picked up by the IsoValue algorithm." + isovalue = 0.5 + "Sets the target value for the IsoValue algorithm." + isorange = 0.05 + "Sets whether the volume data should be sampled with interpolation." + interpolate = true + "Enables depth write for Volume, so that volume correctly occludes other objects." + enable_depth = true + "Absorption multiplier for algorithm=:absorption. This changes how much light each voxel absorbs." + absorption = 1f0 + mixin_generic_plot_attributes()... + mixin_shading_attributes()... + mixin_colormap_attributes()... end """ @@ -242,30 +287,15 @@ end Plots a surface, where `(x, y)` define a grid whose heights are the entries in `z`. `x` and `y` may be `Vectors` which define a regular grid, **or** `Matrices` which define an irregular grid. - -## Attributes - -### Specific to `Surface` - -- `invert_normals::Bool = false` inverts the normals generated for the surface. This can be useful to illuminate the other side of the surface. -- `color = nothing`, can be set to an `Matrix{<: Union{Number, Colorant}}` to color surface independent of the `z` component. If `color=nothing`, it defaults to `color=z`. - -$(Base.Docs.doc(shading_attributes!)) - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Surface, x, y, z) do scene - attr = Attributes(; - color = nothing, - invert_normals = false, - - fxaa = true, - ) - shading_attributes!(attr) - generic_plot_attributes!(attr) - return colormap_attributes!(attr, theme(scene, :colormap)) +@recipe Surface x y z begin + "Can be set to an `Matrix{<: Union{Number, Colorant}}` to color surface independent of the `z` component. If `color=nothing`, it defaults to `color=z`." + color = nothing + "Inverts the normals generated for the surface. This can be useful to illuminate the other side of the surface." + invert_normals = false + mixin_generic_plot_attributes()... + mixin_shading_attributes()... + mixin_colormap_attributes()... end """ @@ -276,33 +306,19 @@ end Creates a connected line plot for each element in `(x, y, z)`, `(x, y)` or `positions`. `NaN` values are displayed as gaps in the line. - -## Attributes - -### Specific to `Lines` - -- `color=theme(scene, :linecolor)` sets the color of the line. If no color is set, multiple calls to `line!` will cycle through the axis color palette. - Otherwise, one can set one color per line point by passing a `Vector{<:Colorant}`, or one colorant for the whole line. If color is a vector of numbers, the colormap args are used to map the numbers to colors. -- `cycle::Vector{Symbol} = [:color]` sets which attributes to cycle when creating multiple plots. -- `linestyle::Union{Nothing, Symbol, Linestyle} = nothing` sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`. -- `linewidth::Union{Real, Vector} = 1.5` sets the width of the line in pixel units. - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Lines, positions) do scene - attr = Attributes(; - - color = theme(scene, :linecolor), - linewidth = theme(scene, :linewidth), - - linestyle = nothing, - fxaa = false, - cycle = [:color], - ) - generic_plot_attributes!(attr, ) - return colormap_attributes!(attr, theme(scene, :colormap)) +@recipe Lines positions begin + "The color of the line." + color = @inherit linecolor + "Sets the width of the line in screen units" + linewidth = @inherit linewidth + "Sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`" + linestyle = nothing + "Sets which attributes to cycle when creating multiple plots." + cycle = [:color] + mixin_generic_plot_attributes()... + mixin_colormap_attributes()... + fxaa = false end """ @@ -327,8 +343,18 @@ $(Base.Docs.doc(colormap_attributes!)) $(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(LineSegments, positions) do scene - default_theme(scene, Lines) +@recipe LineSegments positions begin + "The color of the line." + color = @inherit linecolor + "Sets the width of the line in pixel units" + linewidth = @inherit linewidth + "Sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`" + linestyle = nothing + "Sets which attributes to cycle when creating multiple plots." + cycle = [:color] + mixin_generic_plot_attributes()... + mixin_colormap_attributes()... + fxaa = false end # alternatively, mesh3d? Or having only mesh instead of poly + mesh and figure out 2d/3d via dispatch @@ -339,33 +365,17 @@ end mesh(xyz, faces) Plots a 3D or 2D mesh. Supported `mesh_object`s include `Mesh` types from [GeometryBasics.jl](https://github.com/JuliaGeometry/GeometryBasics.jl). - -## Attributes - -### Specific to `Mesh` - -- `color=theme(scene, :patchcolor)` sets the color of the mesh. Can be a `Vector{<:Colorant}` for per vertex colors or a single `Colorant`. - A `Matrix{<:Colorant}` can be used to color the mesh with a texture, which requires the mesh to contain texture coordinates. - Vector or Matrices of numbers can be used as well, which will use the colormap arguments to map the numbers to colors. -- `interpolate::Bool = false` sets whether colors should be interpolated. - -$(Base.Docs.doc(shading_attributes!)) - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Mesh, mesh) do scene - attr = Attributes(; - color = :black, - interpolate = true, - - fxaa = true, - cycle = [:color => :patchcolor], - ) - shading_attributes!(attr) - generic_plot_attributes!(attr) - return colormap_attributes!(attr, theme(scene, :colormap)) +@recipe Mesh mesh begin + "Sets the color of the mesh. Can be a `Vector{<:Colorant}` for per vertex colors or a single `Colorant`. A `Matrix{<:Colorant}` can be used to color the mesh with a texture, which requires the mesh to contain texture coordinates." + color = @inherit patchcolor + "sets whether colors should be interpolated" + interpolate = true + cycle = [:color => :patchcolor] + matcap = nothing + mixin_generic_plot_attributes()... + mixin_shading_attributes()... + mixin_colormap_attributes()... end """ @@ -374,53 +384,47 @@ end scatter(x, y, z) Plots a marker for each element in `(x, y, z)`, `(x, y)`, or `positions`. - -## Attributes - -### Specific to `Scatter` - -- `color=theme(scene, :markercolor)` sets the color of the marker. If no color is set, multiple calls to `scatter!` will cycle through the axis color palette. - Otherwise, one can set one color per point by passing a `Vector{<:Colorant}`, or one colorant for the whole scatterplot. If color is a vector of numbers, the colormap args are used to map the numbers to colors. -- `cycle::Vector{Symbol} = [:color]` sets which attributes to cycle when creating multiple plots. -- `marker::Union{Symbol, Char, Matrix{<:Colorant}, BezierPath, Polygon}` sets the scatter marker. -- `markersize::Union{<:Real, Vec2f} = 9` sets the size of the marker. -- `markerspace::Symbol = :pixel` sets the space in which `markersize` is given. See `Makie.spaces()` for possible inputs. -- `strokewidth::Real = 0` sets the width of the outline around a marker. -- `strokecolor::Union{Symbol, <:Colorant} = :black` sets the color of the outline around a marker. -- `glowwidth::Real = 0` sets the size of a glow effect around the marker. -- `glowcolor::Union{Symbol, <:Colorant} = (:black, 0)` sets the color of the glow effect. -- `rotations::Union{Real, Billboard, Quaternion} = Billboard(0f0)` sets the rotation of the marker. A `Billboard` rotation is always around the depth axis. -- `transform_marker::Bool = false` controls whether the model matrix (without translation) applies to the marker itself, rather than just the positions. (If this is true, `scale!` and `rotate!` will affect the marker.) - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Scatter, positions) do scene - attr = Attributes(; - color = theme(scene, :markercolor), - - marker = theme(scene, :marker), - markersize = theme(scene, :markersize), - - strokecolor = theme(scene, :markerstrokecolor), - strokewidth = theme(scene, :markerstrokewidth), - glowcolor = (:black, 0.0), - glowwidth = 0.0, - - rotations = Billboard(), - marker_offset = automatic, - - transform_marker = false, # Applies the plots transformation to marker - distancefield = nothing, - uv_offset_width = (0.0, 0.0, 0.0, 0.0), - markerspace = :pixel, +@recipe Scatter positions begin + "Sets the color of the marker. If no color is set, multiple calls to `scatter!` will cycle through the axis color palette." + color = @inherit markercolor + "Sets the scatter marker." + marker = @inherit marker + "Sets the size of the marker." + markersize = @inherit markersize + "Sets the color of the outline around a marker." + strokecolor = @inherit markerstrokecolor + "Sets the width of the outline around a marker." + strokewidth = @inherit markerstrokewidth + "Sets the color of the glow effect around the marker." + glowcolor = (:black, 0.0) + "Sets the size of a glow effect around the marker." + glowwidth = 0.0 + + "Sets the rotation of the marker. A `Billboard` rotation is always around the depth axis." + rotation = Billboard() + "The offset of the marker from the given position in `markerspace` units. Default is centered around the position (markersize * -0.5)." + marker_offset = automatic + "Controls whether the model matrix (without translation) applies to the marker itself, rather than just the positions. (If this is true, `scale!` and `rotate!` will affect the marker." + transform_marker = false + "Optional distancefield used for e.g. font and bezier path rendering. Will get set automatically." + distancefield = nothing + uv_offset_width = (0.0, 0.0, 0.0, 0.0) + "Sets the space in which `markersize` is given. See `Makie.spaces()` for possible inputs" + markerspace = :pixel + "Sets which attributes to cycle when creating multiple plots" + cycle = [:color] + "Enables depth-sorting of markers which can improve border artifacts. Currently supported in GLMakie only." + depthsorting = false + mixin_generic_plot_attributes()... + mixin_colormap_attributes()... + fxaa = false +end - fxaa = false, - cycle = [:color], +function deprecated_attributes(::Type{<:Scatter}) + ( + (; attribute = :rotations, message = "`rotations` has been renamed to `rotation` for consistency in Makie v0.21.", error = true), ) - generic_plot_attributes!(attr) - return colormap_attributes!(attr, theme(scene, :colormap)) end """ @@ -430,39 +434,26 @@ end Plots a mesh for each element in `(x, y, z)`, `(x, y)`, or `positions` (similar to `scatter`). `markersize` is a scaling applied to the primitive passed as `marker`. - -## Attributes - -### Specific to `MeshScatter` - -- `color = theme(scene, :markercolor)` sets the color of the marker. If no color is set, multiple calls to `meshscatter!` will cycle through the axis color palette. - Otherwise, one can set one color per point by passing a `Vector{<:Colorant}`, or one colorant for the whole meshscatterplot. If color is a vector of numbers, the colormap args are used to map the numbers to colors. -- `cycle::Vector{Symbol} = [:color]` sets which attributes to cycle when creating multiple plots. -- `marker::Union{Symbol, GeometryBasics.GeometryPrimitive, GeometryBasics.Mesh}` sets the scattered mesh. -- `markersize::Union{<:Real, Vec3f} = 0.1` sets the scale of the mesh. This can be given as a Vector to apply to each scattered mesh individually. -- `rotations::Union{Real, Vec3f, Quaternion} = 0` sets the rotation of the mesh. A numeric rotation is around the z-axis, a `Vec3f` causes the mesh to rotate such that the the z-axis is now that vector, and a quaternion describes a general rotation. This can be given as a Vector to apply to each scattered mesh individually. - -$(Base.Docs.doc(shading_attributes!)) - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(MeshScatter, positions) do scene - attr = Attributes(; - color = theme(scene, :markercolor), - - marker = :Sphere, - markersize = 0.1, - rotations = 0.0, - space = :data, +@recipe MeshScatter positions begin + "Sets the color of the marker." + color = @inherit markercolor + "Sets the scattered mesh." + marker = :Sphere + "Sets the scale of the mesh. This can be given as a `Vector` to apply to each scattered mesh individually." + markersize = 0.1 + "Sets the rotation of the mesh. A numeric rotation is around the z-axis, a `Vec3f` causes the mesh to rotate such that the the z-axis is now that vector, and a quaternion describes a general rotation. This can be given as a Vector to apply to each scattered mesh individually." + rotation = 0.0 + cycle = [:color] + mixin_generic_plot_attributes()... + mixin_shading_attributes()... + mixin_colormap_attributes()... +end - fxaa = true, - cycle = [:color], +function deprecated_attributes(::Type{<:MeshScatter}) + ( + (; attribute = :rotations, message = "`rotations` has been renamed to `rotation` for consistency in Makie v0.21.", error = true), ) - shading_attributes!(attr) - generic_plot_attributes!(attr) - return colormap_attributes!(attr, theme(scene, :colormap)) end """ @@ -472,54 +463,118 @@ end Plots one or multiple texts passed via the `text` keyword. `Text` uses the `PointBased` conversion trait. +""" +@recipe Text positions begin + "Specifies one piece of text or a vector of texts to show, where the number has to match the number of positions given. Makie supports `String` which is used for all normal text and `LaTeXString` which layouts mathematical expressions using `MathTeXEngine.jl`." + text = "" + "Sets the color of the text. One can set one color per glyph by passing a `Vector{<:Colorant}`, or one colorant for the whole text. If color is a vector of numbers, the colormap args are used to map the numbers to colors." + color = @inherit textcolor + "Sets the font. Can be a `Symbol` which will be looked up in the `fonts` dictionary or a `String` specifying the (partial) name of a font or the file path of a font file" + font = @inherit font + "Used as a dictionary to look up fonts specified by `Symbol`, for example `:regular`, `:bold` or `:italic`." + fonts = @inherit fonts + "Sets the color of the outline around a marker." + strokecolor = (:black, 0.0) + "Sets the width of the outline around a marker." + strokewidth = 0 + "Sets the alignment of the string w.r.t. `position`. Uses `:left, :center, :right, :top, :bottom, :baseline` or fractions." + align = (:left, :bottom) + "Rotates text around the given position" + rotation = 0.0 + "The fontsize in units depending on `markerspace`." + fontsize = @inherit fontsize + "Deprecated: Specifies the position of the text. Use the positional argument to `text` instead." + position = (0.0, 0.0) + "Sets the alignment of text w.r.t its bounding box. Can be `:left, :center, :right` or a fraction. Will default to the horizontal alignment in `align`." + justification = automatic + "The lineheight multiplier." + lineheight = 1.0 + "Sets the space in which `fontsize` acts. See `Makie.spaces()` for possible inputs." + markerspace = :pixel + "Controls whether the model matrix (without translation) applies to the glyph itself, rather than just the positions. (If this is true, `scale!` and `rotate!` will affect the text glyphs.)" + transform_marker = false + "The offset of the text from the given position in `markerspace` units." + offset = (0.0, 0.0) + "Specifies a linewidth limit for text. If a word overflows this limit, a newline is inserted before it. Negative numbers disable word wrapping." + word_wrap_width = -1 + mixin_generic_plot_attributes()... + mixin_colormap_attributes()... + fxaa = false +end + +function deprecated_attributes(::Type{<:Text}) + ( + (; attribute = :textsize, message = "`textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.", error = true), + ) +end + +""" + voxels(x, y, z, chunk::Array{<:Real, 3}) + voxels(chunk::Array{<:Real, 3}) + +Plots a chunk of voxels centered at 0. Optionally the placement and scaling of +the chunk can be given as range-like x, y and z. (Only the extrema are +considered here. Voxels are always uniformly sized.) + +Internally voxels are represented as 8 bit unsigned integer, with `0x00` always +being an invisible "air" voxel. Passing a chunk with matching type will directly +set those values. Note that color handling is specialized for the internal +representation and may behave a bit differently than usual. ## Attributes -### Specific to `Text` - -- `color=theme(scene, :textcolor)` sets the color of the text. One can set one color per glyph by passing a `Vector{<:Colorant}`, or one colorant for the whole text. If color is a vector of numbers, the colormap args are used to map the numbers to colors. -- `text` specifies one piece of text or a vector of texts to show, where the number has to match the number of positions given. Makie supports `String` which is used for all normal text and `LaTeXString` which layouts mathematical expressions using `MathTeXEngine.jl`. -- `align::Tuple{Union{Symbol, Real}, Union{Symbol, Real}} = (:left, :bottom)` sets the alignment of the string w.r.t. `position`. Uses `:left, :center, :right, :top, :bottom, :baseline` or fractions. -- `font::Union{String, Vector{String}} = :regular` sets the font for the string or each character. -- `justification::Union{Real, Symbol} = automatic` sets the alignment of text w.r.t its bounding box. Can be `:left, :center, :right` or a fraction. Will default to the horizontal alignment in `align`. -- `rotation::Union{Real, Quaternion}` rotates text around the given position. -- `fontsize::Union{Real, Vec2f}` sets the size of each character. -- `markerspace::Symbol = :pixel` sets the space in which `fontsize` acts. See `Makie.spaces()` for possible inputs. -- `strokewidth::Real = 0` sets the width of the outline around a marker. -- `strokecolor::Union{Symbol, <:Colorant} = :black` sets the color of the outline around a marker. -- `glowwidth::Real = 0` sets the size of a glow effect around the marker. -- `glowcolor::Union{Symbol, <:Colorant} = (:black, 0)` sets the color of the glow effect. -- `word_wrap_width::Real = -1` specifies a linewidth limit for text. If a word overflows this limit, a newline is inserted before it. Negative numbers disable word wrapping. -- `transform_marker::Bool = false` controls whether the model matrix (without translation) applies to the glyph itself, rather than just the positions. (If this is true, `scale!` and `rotate!` will affect the text glyphs.) +### Specific to `Voxel` + +- `color = nothing` sets colors per voxel id, skipping `0x00`. This means that a voxel with id 1 will grab `plot.colors[1]` + and so on up to id 255. This can also be set to a Matrix of colors, i.e. an image for texture mapping. +- `is_air = x -> isnothing(x) || ismissing(x) || isnan(x)` controls which values in the input data are mapped to invisible + (air) voxels. +- `depthsorting = false` controls the render order of voxels. If set to `false` voxels close to the viewer are rendered + first which should reduce overdraw and yield better performance. If set to `true` voxels are rendered back to front + enabling correct order for transparent voxels. +- `uvmap = nothing` defines a map from voxel ids (and optionally sides) to uv coordinates. These uv coordinates are then + used to sample a 2D texture passed through `color` for texture mapping. +- `interpolate = false` controls whether the texture map is sampled with interpolation (i.e. smoothly) or not (i.e. pixelated). +- `gap = 0.0` sets the gap between adjacent voxels in units of the voxel size. This needs to be larger than 0.01 to take effect. +- `_limits`: Internal attribute for keeping track of `extrema(chunk)`. +- `_local_update`: Internal attribute for communicating updates to the backend. -$(Base.Docs.doc(colormap_attributes!)) +$(Base.Docs.doc(shading_attributes!)) + +### Color attributes + +- `colormap::Union{Symbol, Vector{<:Colorant}} = :viridis` sets the colormap that voxels sample to get their color. + Internally the colormap is always represented by 253 colors, matching voxel ids 2..254. Ids 1 and 255 are reserved + for `lowclip` and `highclip`. `PlotUtils.cgrad(...)`, `Makie.Reverse(any_colormap)` can be used as well, or any + symbol from ColorBrewer or PlotUtils. To see all available color gradients, you can call `Makie.available_gradients()`. +- `colorscale::Function = identity` color transform function. Can be any function, but only works well together with + `Colorbar` for `identity`, `log`, `log2`, `log10`, `sqrt`, `logit`, `Makie.pseudolog10` and `Makie.Symlog10`. +- `colorrange::Tuple{<:Real, <:Real}` sets the values representing the start and end points of `colormap`. + Internally this effects the voxel ids generated from the input data so that the colormap resolution remains at 253 colors. +- `lowclip::Union{Nothing, Symbol, <:Colorant} = nothing` sets a color for any value below the colorrange. (voxel id = 1) +- `highclip::Union{Nothing, Symbol, <:Colorant} = nothing` sets a color for any value above the colorrange. (voxel id = 255) +- `alpha = 1.0` sets the alpha value of the colormap or color attribute. Multiple alphas like in `plot(alpha=0.2, color=(:red, 0.5)` will get multiplied. $(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Text, positions) do scene - attr = Attributes(; - color = theme(scene, :textcolor), - - font = theme(scene, :font), - fonts = theme(scene, :fonts), - - strokecolor = (:black, 0.0), - strokewidth = 0, - align = (:left, :bottom), - rotation = 0.0, - fontsize = theme(scene, :fontsize), - position = (0.0, 0.0), - justification = automatic, - lineheight = 1.0, - markerspace = :pixel, - transform_marker = false, - offset = (0.0, 0.0), - word_wrap_width = -1, +@recipe(Voxels, chunk) do scene + attr = Attributes( + color = nothing, + is_air = x -> isnothing(x) || ismissing(x) || isnan(x), + uvmap = nothing, + interpolate = false, # texture + depthsorting = false, # false = fast, true = correct transparency + gap = 0.0, + # Internal: + _limits = (0.0, 1.0), + _local_update = (0:0, 0:0, 0:0), ) + shading_attributes!(attr) generic_plot_attributes!(attr) return colormap_attributes!(attr, theme(scene, :colormap)) end + """ poly(vertices, indices; kwargs...) poly(points; kwargs...) @@ -536,76 +591,93 @@ When a shape is given (essentially anything decomposable by `GeometryBasics`), i Plots polygons, which are defined by `coordinates` (the coordinates of the vertices) and `connectivity` (the edges between the vertices). - -## Attributes - -### Specific to `Poly` -- `color=theme(scene, :patchcolor)` sets the color of the poly. Can be a `Vector{<:Colorant}` for per vertex colors or a single `Colorant`. - A `Matrix{<:Colorant}` can be used to color the mesh with a texture, which requires the mesh to contain texture coordinates. - Vector or Matrices of numbers can be used as well, which will use the colormap arguments to map the numbers to colors. - One can also use `Makie.LinePattern`, to cover the poly with a regular stroke pattern. -- `strokecolor::Union{Symbol, <:Colorant} = :black` sets the color of the outline around a marker. -- `strokecolormap`::Union{Symbol, Vector{<:Colorant}} = :viridis` sets the colormap that is sampled for numeric `color`s. -- `strokewidth::Real = 0` sets the width of the outline around a marker. -- `linestyle::Union{Nothing, Symbol, Vector} = nothing` sets the pattern of the line (e.g. `:solid`, `:dot`, `:dashdot`) - -$(Base.Docs.doc(colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(Poly) do scene - attr = Attributes(; - color = theme(scene, :patchcolor), - - strokecolor = theme(scene, :patchstrokecolor), - strokecolormap = theme(scene, :colormap), - strokewidth = theme(scene, :patchstrokewidth), - linestyle = nothing, - - shading = NoShading, - fxaa = true, - - cycle = [:color => :patchcolor], - ) - generic_plot_attributes!(attr) - return colormap_attributes!(attr, theme(scene, :colormap)) +@recipe Poly begin + """ + Sets the color of the poly. Can be a `Vector{<:Colorant}` for per vertex colors or a single `Colorant`. + A `Matrix{<:Colorant}` can be used to color the mesh with a texture, which requires the mesh to contain texture coordinates. + Vector or Matrices of numbers can be used as well, which will use the colormap arguments to map the numbers to colors. + One can also use `Makie.LinePattern`, to cover the poly with a regular stroke pattern. + """ + color = @inherit patchcolor + "Sets the color of the outline around a marker." + strokecolor = @inherit patchstrokecolor + "Sets the colormap that is sampled for numeric `color`s." + strokecolormap = @inherit colormap + "Sets the width of the outline." + strokewidth = @inherit patchstrokewidth + "Sets the pattern of the line (e.g. `:solid`, `:dot`, `:dashdot`)" + linestyle = nothing + + shading = NoShading + + cycle = [:color => :patchcolor] + + mixin_generic_plot_attributes()... + mixin_colormap_attributes()... end -@recipe(Wireframe) do scene - attr = Attributes(; - depth_shift = -1f-5, - ) - return merge!(attr, default_theme(scene, LineSegments)) -end - -@recipe(Arrows, points, directions) do scene - attr = Attributes( - color = :black, - - arrowsize = automatic, - arrowhead = automatic, - arrowtail = automatic, - - linecolor = automatic, - linestyle = nothing, - align = :origin, - - normalize = false, - lengthscale = 1f0, - - colorscale = identity, - - quality = 32, - markerspace = :pixel, - ) +""" + wireframe(x, y, z) + wireframe(positions) + wireframe(mesh) - generic_plot_attributes!(attr) - shading_attributes!(attr) - colormap_attributes!(attr, theme(scene, :colormap)) +Draws a wireframe, either interpreted as a surface or as a mesh. +""" +@recipe Wireframe begin + documented_attributes(LineSegments)... + depth_shift = -1f-5 +end - attr[:fxaa] = automatic - attr[:linewidth] = automatic - # connect arrow + linecolor by default - get!(attr, :arrowcolor, attr[:linecolor]) - return attr +@recipe Arrows points directions begin + "Sets the color of arrowheads and lines. Can be overridden separately using `linecolor` and `arrowcolor`." + color = :black + """Scales the size of the arrow head. This defaults to + `0.3` in the 2D case and `Vec3f(0.2, 0.2, 0.3)` in the 3D case. For the latter + the first two components scale the radius (in x/y direction) and the last scales + the length of the cone. If the arrowsize is set to 1, the cone will have a + diameter and length of 1.""" + arrowsize = automatic + """Defines the marker (2D) or mesh (3D) that is used as + the arrow head. The default for is `'▲'` in 2D and a cone mesh in 3D. For the + latter the mesh should start at `Point3f(0)` and point in positive z-direction.""" + arrowhead = automatic + """Defines the mesh used to draw the arrow tail in 3D. + It should start at `Point3f(0)` and extend in negative z-direction. The default + is a cylinder. This has no effect on the 2D plot.""" + arrowtail = automatic + """Sets the color used for the arrow tail which is represented by a line in 2D. + Will copy `color` if set to `automatic`. + """ + linecolor = automatic + """Sets the linestyle used in 2D. Does not apply to 3D plots.""" + linestyle = nothing + """Sets how arrows are positioned. By default arrows start at + the given positions and extend along the given directions. If this attribute is + set to `:head`, `:lineend`, `:tailend`, `:headstart` or `:center` the given + positions will be between the head and tail of each arrow instead.""" + align = :origin + """By default the lengths of the directions given to `arrows` + are used to scale the length of the arrow tails. If this attribute is set to + true the directions are normalized, skipping this scaling.""" + normalize = false + """Scales the length of the arrow tail.""" + lengthscale = 1f0 + + """Defines the number of angle subdivisions used when generating + the arrow head and tail meshes. Consider lowering this if you have performance + issues. Only applies to 3D plots.""" + quality = 32 + markerspace = :pixel + + mixin_generic_plot_attributes()... + mixin_shading_attributes()... + mixin_colormap_attributes()... + + fxaa = automatic + """Scales the width/diameter of the arrow tail. + Defaults to `1` for 2D and `0.05` for the 3D case.""" + linewidth = automatic + """Sets the color of the arrow head. Will copy `color` if set to `automatic`.""" + arrowcolor = automatic end diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index 2e31672853c..422791220e8 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -189,6 +189,378 @@ macro recipe(theme_func, Tsym::Symbol, args::Symbol...) expr end +function attribute_names end +function documented_attributes end # this can be used for inheriting from other recipes + +attribute_names(_) = nothing + +Base.@kwdef struct AttributeMetadata + docstring::Union{Nothing,String} + default_expr::String # stringified expression, just needed for docs purposes +end + +update_metadata(am1::AttributeMetadata, am2::AttributeMetadata) = AttributeMetadata( + am2.docstring === nothing ? am1.docstring : am2.docstring, + am2.default_expr # TODO: should it be possible to overwrite only a docstring by not giving a default expr? +) + +struct DocumentedAttributes + d::Dict{Symbol,AttributeMetadata} + closure::Function +end + +macro DocumentedAttributes(expr::Expr) + if !(expr isa Expr && expr.head === :block) + throw(ArgumentError("Argument is not a begin end block")) + end + + metadata_exprs = [] + closure_exprs = [] + mixin_exprs = Expr[] + + for arg in expr.args + arg isa LineNumberNode && continue + + has_docs = arg isa Expr && arg.head === :macrocall && arg.args[1] isa GlobalRef + + if has_docs + docs = arg.args[3] + attr = arg.args[4] + else + docs = nothing + attr = arg + end + + is_attr_line = attr isa Expr && attr.head === :(=) && length(attr.args) == 2 + is_mixin_line = attr isa Expr && attr.head === :(...) && length(attr.args) == 1 + if !(is_attr_line || is_mixin_line) + error("$attr is neither a valid attribute line like `x = default_value` nor a mixin line like `some_mixin...`") + end + + if is_attr_line + sym = attr.args[1] + default = attr.args[2] + if !(sym isa Symbol) + error("$sym should be a symbol") + end + + push!(metadata_exprs, quote + am = AttributeMetadata(; docstring = $docs, default_expr = $(_default_expr_string(default))) + if haskey(d, $(QuoteNode(sym))) + d[$(QuoteNode(sym))] = update_metadata(d[$(QuoteNode(sym))], am) + else + d[$(QuoteNode(sym))] = am + end + end) + + if default isa Expr && default.head === :macrocall && default.args[1] === Symbol("@inherit") + if length(default.args) ∉ (3, 4) + error("@inherit works with 1 or 2 arguments, expression was $d") + end + if !(default.args[3] isa Symbol) + error("Argument 1 of @inherit must be a Symbol, got $(default.args[3])") + end + key = default.args[3] + _default = get(default.args, 4, :(error("Inherited key $($(QuoteNode(key))) not found in theme with no fallback given."))) + # first check scene theme + # then default value + d = :( + dict[$(QuoteNode(sym))] = if haskey(thm, $(QuoteNode(key))) + to_value(thm[$(QuoteNode(key))]) # only use value of theme entry + else + $(esc(_default)) + end + ) + push!(closure_exprs, d) + else + push!(closure_exprs, :( + dict[$(QuoteNode(sym))] = $(esc(default)) + )) + end + elseif is_mixin_line + # this intermediate variable is needed to evaluate each mixin only once + # and is inserted at the start of the final code block + gsym = gensym("mixin") + mixin = only(attr.args) + push!(mixin_exprs, quote + $gsym = $(esc(mixin)) + if !($gsym isa DocumentedAttributes) + error("Mixin was not a DocumentedAttributes but $($gsym)") + end + end) + + # the actual runtime values of the mixed in defaults + # are computed using the closure stored in the DocumentedAttributes + closure_exp = quote + # `scene` and `dict` here are defined below where this exp is interpolated into + merge!(dict, $gsym.closure(scene)) + end + push!(closure_exprs, closure_exp) + + # docstrings and default expressions of the mixed in + # DocumentedAttributes are inserted + metadata_exp = quote + for (key, value) in $gsym.d + if haskey(d, key) + error("Mixin `$($(QuoteNode(mixin)))` had the key :$key which already existed. It's not allowed for mixins to overwrite keys to avoid accidental overwrites. Drop those keys from the mixin first.") + end + d[key] = value + end + end + push!(metadata_exprs, metadata_exp) + else + error("Unreachable") + end + end + + quote + $(mixin_exprs...) + d = Dict{Symbol,AttributeMetadata}() + $(metadata_exprs...) + closure = function (scene) + thm = theme(scene) + dict = Dict{Symbol,Any}() + $(closure_exprs...) + return dict + end + DocumentedAttributes(d, closure) + end +end + +function is_attribute(T::Type{<:Plot}, sym::Symbol) + sym in attribute_names(T) +end + +function attribute_default_expressions(T::Type{<:Plot}) + Dict(k => v.default_expr for (k, v) in documented_attributes(T).d) +end + +function _attribute_docs(T::Type{<:Plot}) + Dict(k => v.docstring for (k, v) in documented_attributes(T).d) +end + + +macro recipe(Tsym::Symbol, args...) + + funcname_sym = to_func_name(Tsym) + funcname!_sym = Symbol("$(funcname_sym)!") + funcname! = esc(funcname!_sym) + PlotType = esc(Tsym) + funcname = esc(funcname_sym) + + syms = args[1:end-1] + for sym in syms + sym isa Symbol || throw(ArgumentError("Found argument that is not a symbol in the position where optional argument names should appear: $sym")) + end + attrblock = args[end] + if !(attrblock isa Expr && attrblock.head === :block) + throw(ArgumentError("Last argument is not a begin end block")) + end + # attrblock = expand_mixins(attrblock) + # attrs = [extract_attribute_metadata(arg) for arg in attrblock.args if !(arg isa LineNumberNode)] + + docs_placeholder = gensym() + + attr_placeholder = gensym() + + q = quote + # This part is as far as I know the only way to modify the docstring on top of the + # recipe, so that we can offer the convenience of automatic augmented docstrings + # but combine them with the simplicity of using a normal docstring. + # The trick is to mark some variable (in this case a gensymmed placeholder) with the + # Core.@__doc__ macro, which causes this variable to get assigned the docstring on top + # of the @recipe invocation. From there, it can then be retrieved, modified, and later + # attached to plotting function by using @doc again. We also delete the binding to the + # temporary variable so no unnecessary docstrings stay in place. + Core.@__doc__ $(esc(docs_placeholder)) = nothing + binding = Docs.Binding(@__MODULE__, $(QuoteNode(docs_placeholder))) + user_docstring = if haskey(Docs.meta(@__MODULE__), binding) + _docstring = @doc($docs_placeholder) + delete!(Docs.meta(@__MODULE__), binding) + _docstring + else + "No docstring defined.\n" + end + + + $(funcname)() = not_implemented_for($funcname) + const $(PlotType){$(esc(:ArgType))} = Plot{$funcname,$(esc(:ArgType))} + + # This weird syntax is so that the output of the macrocall can be escaped because it + # contains user expressions, without escaping what's passed to the macro because that + # messes with its transformation logic. Because we escape the whole block with the macro, + # we don't reference it by symbol but splice in the macro itself into the AST + # with `var"@DocumentedAttributes"` + const $attr_placeholder = $( + esc(Expr(:macrocall, var"@DocumentedAttributes", LineNumberNode(@__LINE__), attrblock)) + ) + + $(MakieCore).documented_attributes(::Type{<:$(PlotType)}) = $attr_placeholder + + $(MakieCore).plotsym(::Type{<:$(PlotType)}) = $(QuoteNode(Tsym)) + function ($funcname)(args...; kw...) + kwdict = Dict{Symbol, Any}(kw) + _create_plot($funcname, kwdict, args...) + end + function ($funcname!)(args...; kw...) + kwdict = Dict{Symbol, Any}(kw) + _create_plot!($funcname, kwdict, args...) + end + + function $(MakieCore).attribute_names(T::Type{<:$(PlotType)}) + keys(documented_attributes(T).d) + end + + function $(MakieCore).default_theme(scene, T::Type{<:$(PlotType)}) + Attributes(documented_attributes(T).closure(scene)) + end + + docstring_modified = make_recipe_docstring($PlotType, $(QuoteNode(Tsym)), $(QuoteNode(funcname_sym)),user_docstring) + @doc docstring_modified $funcname_sym + @doc "`$($(string(Tsym)))` is the plot type associated with plotting function `$($(string(funcname_sym)))`. Check the docstring for `$($(string(funcname_sym)))` for further information." $Tsym + @doc "`$($(string(funcname!_sym)))` is the mutating variant of plotting function `$($(string(funcname_sym)))`. Check the docstring for `$($(string(funcname_sym)))` for further information." $funcname!_sym + export $PlotType, $funcname, $funcname! + end + + if !isempty(syms) + push!( + q.args, + :( + $(esc(:($(MakieCore).argument_names)))(::Type{<:$PlotType}, len::Integer) = + $syms + ), + ) + end + + q +end + +function make_recipe_docstring(P::Type{<:Plot}, Tsym, funcname_sym, docstring) + io = IOBuffer() + + attr_docstrings = _attribute_docs(P) + + print(io, docstring) + + println(io, "## Plot type") + println(io, "The plot type alias for the `$funcname_sym` function is `$Tsym`.") + + println(io, "## Attributes") + println(io) + + names = sort(collect(attribute_names(P))) + exprdict = attribute_default_expressions(P) + for name in names + default = exprdict[name] + print(io, "**`", name, "`** = ", " `", default, "` — ") + println(io, something(attr_docstrings[name], "*No docs available.*")) + println(io) + end + + return String(take!(io)) +end + +# from MacroTools +isline(ex) = (ex isa Expr && ex.head === :line) || isa(ex, LineNumberNode) +rmlines(x) = x +function rmlines(x::Expr) + # Do not strip the first argument to a macrocall, which is + # required. + if x.head === :macrocall && length(x.args) >= 2 + Expr(x.head, x.args[1], nothing, filter(x->!isline(x), x.args[3:end])...) + else + Expr(x.head, filter(x->!isline(x), x.args)...) + end +end + +_default_expr_string(x) = string(rmlines(x)) +_default_expr_string(x::String) = repr(x) + +function extract_attribute_metadata(arg) + has_docs = arg isa Expr && arg.head === :macrocall && arg.args[1] isa GlobalRef + + if has_docs + docs = arg.args[3] + attr = arg.args[4] + else + docs = nothing + attr = arg + end + + if !(attr isa Expr && attr.head === :(=) && length(attr.args) == 2) + error("$attr is not a valid attribute line like :x[::Type] = default_value") + end + left = attr.args[1] + default = attr.args[2] + if left isa Symbol + attr_symbol = left + type = Any + else + if !(left isa Expr && left.head === :(::) && length(left.args) == 2) + error("$left is not a Symbol or an expression such as x::Type") + end + attr_symbol = left.args[1]::Symbol + type = left.args[2] + end + + (docs = docs, symbol = attr_symbol, type = type, default = default) +end + +function make_default_theme_expr(attrs, scenesym::Symbol) + + exprs = map(attrs) do a + + d = a.default + if d isa Expr && d.head === :macrocall && d.args[1] == Symbol("@inherit") + if length(d.args) != 4 + error("@inherit works with exactly 2 arguments, expression was $d") + end + if !(d.args[3] isa QuoteNode) + error("Argument 1 of @inherit must be a :symbol, got $(d.args[3])") + end + key, default = d.args[3:4] + # first check scene theme + # then default value + d = quote + if haskey(thm, $key) + to_value(thm[$key]) # only use value of theme entry + else + $default + end + end + end + + :(attr[$(QuoteNode(a.symbol))] = $d) + end + + quote + thm = theme($scenesym) + attr = Attributes() + $(exprs...) + attr + end +end + +function expand_mixins(attrblock::Expr) + Expr(:block, mapreduce(expand_mixin, vcat, attrblock.args)...) +end + +expand_mixin(x) = x +function expand_mixin(e::Expr) + if e.head === :macrocall && e.args[1] === Symbol("@mixin") + if length(e.args) != 3 && e.args[2] isa LineNumberNode && e.args[3] isa Symbol + error("Invalid mixin, needs to be of the format `@mixin some_mixin`, got $e") + end + mixin_ex = getproperty(MakieCore, e.args[3])()::Expr + if (mixin_ex.head !== :block) + error("Expected mixin to be a block expression (such as generated by `quote`)") + end + return mixin_ex.args + else + e + end +end + """ Plot(args::Vararg{<:DataType,N}) @@ -233,3 +605,102 @@ e.g.: ``` """ plottype(plot_args...) = Plot{plot} # default to dispatch to type recipes! + +# plot types can overload this to throw errors or show warnings when deprecated attributes are used. +# this is easier than if every plot type added manual checks in its `plot!` methods +deprecated_attributes(_) = () + +struct InvalidAttributeError <: Exception + plottype::Type + attributes::Set{Symbol} +end + +function print_columns(io::IO, v::Vector{String}; gapsize = 2, row_major = true, cols = displaysize(io)[2]) + lens = length.(v) # for unicode ligatures etc this won't work, but we don't use those for attribute names + function col_widths(ncols) + max_widths = zeros(Int, ncols) + for (i, len) in enumerate(lens) + j = mod1(i, ncols) + max_widths[j] = max(max_widths[j], len) + end + return max_widths + end + ncols = 1 + while true + widths = col_widths(ncols) + aggregated_width = (sum(widths) + (ncols-1) * gapsize) + if aggregated_width > cols + ncols = max(1, ncols-1) + break + end + ncols += 1 + end + widths = col_widths(ncols) + + for (i, (str, len)) in enumerate(zip(v, lens)) + j = mod1(i, ncols) + last_col = j == ncols + print(io, str) + remaining = widths[j] - len + !last_col * gapsize + for _ in 1:remaining + print(io, ' ') + end + if last_col + print(io, '\n') + end + end + + return +end + +function Base.showerror(io::IO, i::InvalidAttributeError) + n = length(i.attributes) + print(io, "Invalid attribute$(n > 1 ? "s" : "") ") + for (j, att) in enumerate(i.attributes) + j > 1 && print(io, j == length(i.attributes) ? " and " : ", ") + printstyled(io, att; color = :red, bold = true) + end + print(io, " for plot type ") + printstyled(io, i.plottype; color = :blue, bold = true) + println(io, ".") + nameset = sort(string.(collect(attribute_names(i.plottype)))) + println(io) + println(io, "The available plot attributes for $(i.plottype) are:") + println(io) + print_columns(io, nameset; cols = displaysize(stderr)[2]) + allowlist = attribute_name_allowlist() + println(io) + println(io) + println(io, "Generic attributes are:") + println(io) + print_columns(io, sort([string(a) for a in allowlist]); cols = displaysize(stderr)[2]) + println(io) +end + +function attribute_name_allowlist() + ( + :xautolimits, :yautolimits, :zautolimits, :label, :rasterize, :model, + :transformation, :userdata + ) +end + +function validate_attribute_keys(P::Type{<:Plot}, kw::Dict{Symbol}) + nameset = attribute_names(P) + nameset === nothing && return + allowlist = attribute_name_allowlist() + deprecations = deprecated_attributes(P)::Tuple{Vararg{NamedTuple{(:attribute, :message, :error), Tuple{Symbol, String, Bool}}}} + unknown = setdiff(keys(kw), nameset, allowlist, first.(deprecations)) + if !isempty(unknown) + throw(InvalidAttributeError(P, unknown)) + end + for (deprecated, message, should_error) in deprecations + if haskey(kw, deprecated) + full_message = "Keyword `$deprecated` is deprecated for plot type $P. $message" + if should_error + throw(ArgumentError(full_message)) + else + @warn full_message + end + end + end +end \ No newline at end of file diff --git a/MakieCore/src/types.jl b/MakieCore/src/types.jl index e80345d339c..d634647c2a4 100644 --- a/MakieCore/src/types.jl +++ b/MakieCore/src/types.jl @@ -79,6 +79,7 @@ mutable struct Plot{PlotFunc, T} <: ScenePlot{PlotFunc} parent::Union{AbstractScene,Plot} function Plot{Typ,T}(kw::Dict{Symbol, Any}, args::Vector{Any}, converted::NTuple{N, Observable}) where {Typ,T,N} + validate_attribute_keys(Plot{Typ}, kw) return new{Typ,T}(nothing, kw, args, converted, Attributes(), Plot[], Observables.ObserverFunction[]) end diff --git a/Project.toml b/Project.toml index 0dc70b8bbc1..39a6471701c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Makie" uuid = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" authors = ["Simon Danisch", "Julius Krumbiegel"] -version = "0.20.8" +version = "0.21.0" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" @@ -88,7 +88,7 @@ KernelDensity = "0.5, 0.6" LaTeXStrings = "1.2" LinearAlgebra = "1.0, 1.6" MacroTools = "0.5" -MakieCore = "=0.7.3" +MakieCore = "=0.8.0" Markdown = "1.0, 1.6" MathTeXEngine = "0.5" Observables = "0.5.5" diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 75ff2d58bbb..aa42fc055ee 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -1,7 +1,7 @@ name = "RPRMakie" uuid = "22d9f318-5e34-4b44-b769-6e3734a732a6" authors = ["Simon Danisch"] -version = "0.6.8" +version = "0.7.0" [deps] Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" @@ -17,7 +17,7 @@ Colors = "0.9, 0.10, 0.11, 0.12" FileIO = "1.6" GeometryBasics = "0.4.1" LinearAlgebra = "1.0, 1.6" -Makie = "=0.20.8" +Makie = "=0.21.0" Printf = "1.0, 1.6" RadeonProRender = "0.3.0" julia = "1.3" diff --git a/RPRMakie/src/meshes.jl b/RPRMakie/src/meshes.jl index 43bb3862d17..f86bbc60f50 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -112,7 +112,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) markersize end - rotations = Makie.to_rotation(plot.rotations[]) + rotations = Makie.to_rotation(plot.rotation[]) rotations = if rotations isa Makie.Quaternion Iterators.repeated(rotations, n_instances) @@ -129,6 +129,40 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) end +function to_rpr_object(context, matsys, scene, plot::Makie.Voxels) + # Potentially per instance attributes + positions = Makie.voxel_positions(plot) + m_mesh = normal_mesh(Rect3f(Point3f(-0.5), Vec3f(1))) + marker = RPR.Shape(context, m_mesh) + instances = [marker] + n_instances = length(positions) + RPR.rprShapeSetObjectID(marker, 0) + material = extract_material(matsys, plot) + set!(marker, material) + for i in 1:(n_instances-1) + inst = RPR.Shape(context, marker) + RPR.rprShapeSetObjectID(inst, i) + push!(instances, inst) + end + + color_from_num = Makie.voxel_colors(plot) + object_id = RPR.InputLookupMaterial(matsys) + object_id.value = RPR.RPR_MATERIAL_NODE_LOOKUP_OBJECT_ID + uv = object_id * Vec3f(0, 1/n_instances, 0) + tex = RPR.Texture(matsys, collect(color_from_num'); uv = uv) + material.color = tex + + scales = Iterators.repeated(Makie.voxel_size(plot), n_instances) + + for (instance, position, scale) in zip(instances, positions, scales) + mat = Makie.transformationmatrix(position, scale) + transform!(instance, mat) + end + + return instances +end + + function to_rpr_object(context, matsys, scene, plot::Makie.Surface) x = plot[1] y = plot[2] diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 014f156cff0..890ddde407d 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -55,7 +55,7 @@ end rot = qrotation(Vec3f(1, 0, 0), 0.5pi) * qrotation(Vec3f(0, 1, 0), 0.7pi) meshscatter( 1:3, 1:3, fill(0, 3, 3), - marker=catmesh, color=img, markersize=1, rotations=rot, + marker=catmesh, color=img, markersize=1, rotation=rot, axis=(type=LScene, show_axis=false) ) end @@ -131,8 +131,8 @@ end @reference_test "Ellipsoid marker sizes" begin # see PR #3722 pts = Point3f[[0, 0, 0], [1, 0, 0]] markersize = Vec3f[[0.5, 0.2, 0.5], [0.5, 0.2, 0.5]] - rotations = [qrotation(Vec3f(1, 0, 0), 0), qrotation(Vec3f(1, 1, 0), π / 4)] - meshscatter(pts; markersize, rotations, color=:white, diffuse=Vec3f(-2, 0, 4), specular=Vec3f(4, 0, -2)) + rotation = [qrotation(Vec3f(1, 0, 0), 0), qrotation(Vec3f(1, 1, 0), π / 4)] + meshscatter(pts; markersize, rotation, color=:white, diffuse=Vec3f(-2, 0, 4), specular=Vec3f(4, 0, -2)) end @reference_test "Record Video" begin @@ -355,7 +355,7 @@ end fig, ax, meshplot = meshscatter( pG[edges[:, 1]], color=colorsC, marker=meshC, - markersize=sizesC, rotations=rotationsC, + markersize=sizesC, rotation=rotationsC, ) meshscatter!( ax, pG, @@ -380,7 +380,7 @@ end @reference_test "image scatter" begin scatter( 1:10, 1:10, RNG.rand(10, 10) .* 10, - rotations=normalize.(RNG.rand(Quaternionf, 10 * 10)), + rotation=normalize.(RNG.rand(Quaternionf, 10 * 10)), markersize=20, # can also be an array of images for each point # need to be the same size for best performance, though diff --git a/ReferenceTests/src/tests/float32_conversion.jl b/ReferenceTests/src/tests/float32_conversion.jl new file mode 100644 index 00000000000..835edcba58e --- /dev/null +++ b/ReferenceTests/src/tests/float32_conversion.jl @@ -0,0 +1,100 @@ +@reference_test "Value range < eps(Float32)" begin + fig = Figure() + ax = Axis(fig[1, 1]) + xlims!(ax, 0, 12) + # no ylims! to check autolimits as well + ax.xticks[] = (0:12, string.(0:12)) + ax.yticks[] = (1e9 .+ (0:11), ["1e9 + $i" for i in 0:11]) + + # scatter + lines + scatterlines!(1:10, 1e9 .+ (1:10)) + linesegments!(2:11, 1e9 .+ (1:10)) + meshscatter!(3:12, 1e9 .+ (1:10)) + text!(Point2(1, 1e9 + 6), text = L"\frac{\sqrt{1+x}}{2}", fontsize = 20, align = (:left, :center)) + + image!(ax, 0 .. 3, (1e9 + 7) .. (1e9 + 10), [1 2; 3 4]) + heatmap!(ax, 10 .. 11, (1e9 + 2) .. (1e9 + 3), [1 2; 3 4]) + surface!(ax, 10 .. 11, (1e9 + 4) .. (1e9 + 5), [1 2; 3 4]; shading=NoShading) + + mesh!(ax, Circle(Point2(5, 1e9 + 8.5), 1.0); color=:red, shading=NoShading) + poly!(ax, [7, 9, 8], 1e9 .+ [2, 2, 3]; strokewidth=2) + + fig +end + +# TODO: PlotUtils tick finding fails with these ranges (and still fails with +# e40/e-40), resulting in only 3 ticks for the x axis and lots of warnings. +@reference_test "Below floatmin, above floatmax" begin + fig = Figure() + ax = Axis(fig[1, 1]) + + # scatter + lines + scatterlines!(1e-100 .* (1:10), 1e100 .* (1:10)) + linesegments!(1e-100 .* (2:11), 1e100 .* (1:10)) + # meshscatter!( 1e-100 .* (3:12), 1e100 .* (1:10)) # markersize does not match scales + text!(Point2(1e-100, 6e100), text = "Test", fontsize = 20, align = (:left, :center)) + + image!(ax, 1e-100 .. 3e-100, (7e100) .. (10e100), [1 2; 3 4]) + heatmap!(ax, 10e-100 .. 11e-100, (2e100) .. (3e100), [1 2; 3 4]) + surface!(ax, 10e-100 .. 11e-100, (4e100) .. (5e100), [1 2; 3 4]; shading=NoShading) + + mesh!(ax, Rect2d(Point2(5e-100, 8.5e100), Vec2d(1e-100, 1e100)); color=:red, shading=NoShading) + poly!(ax, 1e-100 .* [7, 9, 8], 1e100 .* [2, 2, 3]; strokewidth=2) + + fig +end + + +@reference_test "Model application with Float32 scaling" begin + fig = Figure() + ax = Axis(fig[1, 1]) + xlims!(ax, 0, 14) + ylims!(ax, 1e9, 1e9 + 12) + ax.xticks[] = (0:12, string.(0:12)) + ax.yticks[] = (1e9 .+ (0:11), ["1e9 + $i" for i in 0:11]) + + shift = Vec3(1, 1, 0) + + p1 = scatterlines!(1:10, 1e9 .+ (1:10)) + p2 = linesegments!(2:11, 1e9 .+ (1:10)) + p3 = meshscatter!(3:12, 1e9 .+ (1:10)) + p4 = text!(Point2(1, 1e9 + 6), text = "Test", fontsize = 20, align = (:left, :center)) + p5 = image!(ax, 0 .. 3, (1e9 + 7) .. (1e9 + 10), [1 2; 3 4]) + p6 = heatmap!(ax, 10 .. 11, (1e9 + 2) .. (1e9 + 3), [1 2; 3 4]) + p7 = surface!(ax, 10 .. 11, (1e9 + 4) .. (1e9 + 5), [1 2; 3 4]; shading=NoShading) + p8 = mesh!(ax, Circle(Point2(5, 1e9 + 8.5), 1.0); color=:red, shading=NoShading) + p9 = poly!(ax, [7, 9, 8], 1e9 .+ [2, 2, 3]; strokewidth=2) + + translate!.([p1, p2, p3, p4, p5, p6, p7, p8, p9], (shift,)) + + fig +end + +@reference_test "Float64 h/vspan + h/vlines + error/rangebars + ablines" begin + fig = Figure() + ax = Axis(fig[1, 1]) + + hspan!(ax, [1e9 + 4.5], [1e9 + 5.5], color = :yellow) + vspan!(ax, [1e9 + 4.5], [1e9 + 5.5], color = :yellow) + + hlines!(ax, [1e9 + 4.5, 1e9 + 5.5], color = :red, linewidth = 4) + vlines!(ax, [1e9 + 4.5, 1e9 + 5.5], color = :red, linewidth = 4) + + errorbars!(ax, 1e9 .+ (1:9), 1e9 .+ (1:9), 0.3, whiskerwidth = 10, direction = :x) + rangebars!(ax, 1e9 .+ (1:9), 1e9 .+ (0.7:8.7), 1e9 .+ (1.3:9.3), whiskerwidth = 10) + + ablines!(ax, 2 * (1e9 + 5), -1.0) + + fig +end + +@reference_test "Float64 hist" begin + fig = Figure() + ax = Axis(fig[1, 1]) + ylims!(ax, -1, 23) + p = hist!( + ax, 1e9 .+ cos.(range(0, pi, length = 100)), + strokewidth = 2, bins = 10, bar_labels = :y + ) + fig +end diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index d59149c3822..459ab08c701 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -36,6 +36,54 @@ end s end +# A test case for wide lines and mitering at joints +@reference_test "Miter Joints for line rendering" begin + scene = Scene() + cam2d!(scene) + r = 4 + sep = 4*r + scatter!(scene, (sep+2*r)*[-1,-1,1,1], (sep+2*r)*[-1,1,-1,1]) + + for i=-1:1 + for j=-1:1 + angle = pi/2 + pi/4*i + x = r*[-cos(angle/2),0,-cos(angle/2)] + y = r*[-sin(angle/2),0,sin(angle/2)] + + linewidth = 40 * 2.0^j + lines!(scene, x .+ sep*i, y .+ sep*j, color=RGBAf(0,0,0,0.5), linewidth=linewidth) + lines!(scene, x .+ sep*i, y .+ sep*j, color=:red) + end + end + center!(scene) + scene +end + +@reference_test "Lines from outside" begin + # This tests that lines that start or end in clipped regions are still + # rendered correctly. For perspective projections this can be tricky as + # points behind the camera get projected beyond far. + lps = let + ps1 = [Point3f(x, 0.2 * (z+1), z) for x in (-8, 0, 8) for z in (-9, -1, -1, 7)] + ps2 = [Point3f(x, 0.2 * (z+1), z) for z in (-9, -1, 7) for x in (-8, 0, 0, 8)] + vcat(ps1, ps2) + end + cs = [i for i in (1, 12, 2, 11, 3, 10, 4, 9, 5, 8, 6, 7) for _ in 1:2] + + fig = Figure() + + for (i, func) in enumerate((lines, linesegments)) + for (j, ls) in enumerate((:solid, :dot)) + a, p = func(fig[i, j], lps, color = cs, linewidth = 5, linestyle = ls) + cameracontrols(a).settings.center[] = false # avoid recenter on display + a.show_axis[] = false + update_cam!(a.scene, Vec3f(-0.2, 0.5, 0), Vec3f(-0.2, 0.2, -1), Vec3f(0, 1, 0)) + end + end + + fig +end + @reference_test "lines issue #3704" begin lines(1:10, sin, color = [fill(0, 9); fill(1, 1)], linewidth = 3, colormap = [:red, :cyan]) end @@ -76,7 +124,7 @@ end p, marker = m, markersize = 30, - rotations = rot, + rotation = rot, color=:black ) scatter!(s, p, color = :red, markersize = 6) @@ -165,7 +213,7 @@ end p, marker = marker, markersize = 75, - rotations = rot, + rotation = rot, ) end s @@ -479,6 +527,83 @@ end fig end +@reference_test "Voxel - texture mapping" begin + texture = let + w = RGBf(1, 1, 1) + r = RGBf(1, 0, 0) + g = RGBf(0, 1, 0) + b = RGBf(0, 0, 1) + o = RGBf(1, 1, 0) + c = RGBf(0, 1, 1) + m = RGBf(1, 0, 1) + k = RGBf(0, 0, 0) + [r w g w b w k w; + w w w w w w w w; + r k g k b k w k; + k k k k k k k k] + end + + # Use same uvs/texture-sections for every side of one voxel id + flat_uv_map = [Vec4f(x, x + 1 / 2, y, y + 1 / 4) + for x in range(0.0, 1.0; length=3)[1:(end - 1)] + for y in range(0.0, 1.0; length=5)[1:(end - 1)]] + + flat_voxels = UInt8[1, 0, 3, + 0, 0, 0, + 2, 0, 4, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 5, 0, 7, + 0, 0, 0, + 6, 0, 8] + + # Reshape the flat vector into a 3D array of dimensions 3x3x3. + voxels_3d = reshape(flat_voxels, (3, 3, 3)) + + fig = Figure(; size=(800, 400)) + a1 = LScene(fig[1, 1]; show_axis=false) + p1 = voxels!(a1, voxels_3d; uvmap=flat_uv_map, color=texture) + + # Use red for x, green for y, blue for z + sided_uv_map = Matrix{Vec4f}(undef, 1, 6) + sided_uv_map[1, 1:3] .= flat_uv_map[1:3] + sided_uv_map[1, 4:6] .= flat_uv_map[5:7] + + sided_voxels = reshape(UInt8[1], 1, 1, 1) + a2 = LScene(fig[1, 2]; show_axis=false) + p2 = voxels!(a2, sided_voxels; uvmap=sided_uv_map, color=texture) + + fig +end + +@reference_test "Voxel - colors and colormap" begin + # test direct mapping of ids to colors & upsampling of vector colormap + fig = Figure(size = (800, 400)) + chunk = reshape(UInt8[1, 0, 2, 0, 0, 0, 3, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0, 0, 0, 7, 0, 8], + (3, 3, 3)) + + cs = [:white, :red, :green, :blue, :black, :orange, :cyan, :magenta] + voxels(fig[1, 1], chunk, color = cs, axis=(show_axis = false,)) + a, p = voxels(fig[1, 2], Float32.(chunk), colormap = [:red, :blue], is_air = x -> x == 0.0, axis=(show_axis = false,)) + fig +end + +@reference_test "Voxel - lowclip and highclip" begin + # test that lowclip and highclip are visible for values just outside the colorrange + fig = Figure(size = (800, 400)) + chunk = reshape(collect(1:900), 30, 30, 1) + a1, _ = voxels(fig[1,1], chunk, lowclip = :red, highclip = :red, colorrange = (1.0, 900.0), shading = NoShading) + a2, _ = voxels(fig[1,2], chunk, lowclip = :red, highclip = :red, colorrange = (1.1, 899.9), shading = NoShading) + foreach(a -> update_cam!(a.scene, Vec3f(0, 0, 40), Vec3f(0), Vec3f(0,1,0)), (a1, a2)) + fig +end + +@reference_test "Voxel - gap attribute" begin + # test direct mapping of ids to colors & upsampling of vector colormap + voxels(RNG.rand(3,3,3), gap = 0.3) +end + @reference_test "Plot transform overwrite" begin # Tests that (primitive) plots can have different transform function to their # parent scene (identity in this case) @@ -504,4 +629,4 @@ end meshscatter!(ax, [0.1, 0.9], [0.6, 0.7], markersize = 0.05, color = :red, transformation = Transformation()) fig -end \ No newline at end of file +end diff --git a/ReferenceTests/src/tests/short_tests.jl b/ReferenceTests/src/tests/short_tests.jl index b28d814a8ee..6f408d355fc 100644 --- a/ReferenceTests/src/tests/short_tests.jl +++ b/ReferenceTests/src/tests/short_tests.jl @@ -22,7 +22,7 @@ end @reference_test "scatter rotation" begin angles = range(0, stop=2pi, length=20) pos = Point2f.(sin.(angles), cos.(angles)) - f, ax, pl = scatter(pos, markersize=0.2, markerspace=:data, rotations=-angles, marker='▲', axis=(;aspect = DataAspect())) + f, ax, pl = scatter(pos, markersize=0.2, markerspace=:data, rotation=-angles, marker='▲', axis=(;aspect = DataAspect())) scatter!(pos, markersize=10, color=:red) f end diff --git a/ReferenceTests/src/tests/text.jl b/ReferenceTests/src/tests/text.jl index 036f7af8ab1..a15a5c8e4f8 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 = boundingbox(t, :pixel) 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, boundingbox(t1, :data), 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, boundingbox(t2, :pixel), color = (:red, 0.3)) scene end @@ -149,7 +149,7 @@ end markerspace = :data ) - wireframe!(scene, boundingbox(t), color = (:blue, 0.3)) + wireframe!(scene, boundingbox(t, :data), 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, boundingbox(t2, :pixel), 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, boundingbox(t1, :data), 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, boundingbox(t2, :data), 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, boundingbox(t, :data), color=:black) s end diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index c6b510d3196..d5dc7bd9c24 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -1,7 +1,7 @@ name = "WGLMakie" uuid = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" authors = ["SimonDanisch "] -version = "0.9.8" +version = "0.10.0" [deps] Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" @@ -27,7 +27,7 @@ FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.1" Hyperscript = "0.0.3, 0.0.4, 0.0.5" LinearAlgebra = "1.0, 1.6" -Makie = "=0.20.8" +Makie = "=0.21.0" Observables = "0.5.1" PNGFiles = "0.3, 0.4" PrecompileTools = "1.0" diff --git a/WGLMakie/assets/lines.frag b/WGLMakie/assets/lines.frag deleted file mode 100644 index b146e66af6c..00000000000 --- a/WGLMakie/assets/lines.frag +++ /dev/null @@ -1,47 +0,0 @@ -precision mediump int; -precision mediump float; -precision mediump sampler2D; -precision mediump sampler3D; - -flat in vec2 f_uv_minmax; -in vec2 f_uv; -in vec4 f_color; -in float f_thickness; - -uniform float pattern_length; - -out vec4 fragment_color; - -// Half width of antialiasing smoothstep -#define ANTIALIAS_RADIUS 0.8 - -float aastep(float threshold1, float dist) { - return smoothstep(threshold1 - ANTIALIAS_RADIUS, threshold1 + ANTIALIAS_RADIUS, dist); -} - -float aastep(float threshold1, float threshold2, float dist) { - // We use 2x pixel space in the geometry shaders which passes through - // in uv.y, so we need to treat it here by using 2 * ANTIALIAS_RADIUS - float AA = 2.0f * ANTIALIAS_RADIUS; - return smoothstep(threshold1 - AA, threshold1 + AA, dist) - - smoothstep(threshold2 - AA, threshold2 + AA, dist); -} - -float aastep_scaled(float threshold1, float threshold2, float dist) { - float AA = ANTIALIAS_RADIUS / pattern_length; - return smoothstep(threshold1 - AA, threshold1 + AA, dist) - - smoothstep(threshold2 - AA, threshold2 + AA, dist); -} - -void main() { - vec4 color = vec4(f_color.rgb, 0.0f); - vec2 xy = f_uv; - - float alpha = aastep(0.0f, xy.x); - float alpha2 = aastep(-f_thickness, f_thickness, xy.y); - float alpha3 = aastep_scaled(f_uv_minmax.x, f_uv_minmax.y, f_uv.x); - - color = vec4(f_color.rgb, f_color.a * alpha * alpha2 * alpha3); - - fragment_color = color; -} diff --git a/WGLMakie/assets/lines.vert b/WGLMakie/assets/lines.vert deleted file mode 100644 index bb22d732b75..00000000000 --- a/WGLMakie/assets/lines.vert +++ /dev/null @@ -1,82 +0,0 @@ -in float position; -in vec2 linepoint_prev; -in vec2 linepoint_start; -in vec2 linepoint_end; -in vec2 linepoint_next; -in float linewidth_prev; -in float linewidth_start; -in float linewidth_end; -in float linewidth_next; - -uniform vec4 is_valid; -uniform vec4 color_end; -uniform vec4 color_start; -uniform mat4 model; -uniform mat4 projectionview; -uniform vec2 resolution; - -out vec2 f_uv; -out vec4 f_color; -out float f_thickness; - -vec3 screen_space(vec3 point) { - vec4 vertex = projectionview * model * vec4(point, 1); - return vec3(vertex.xy * resolution, vertex.z) / vertex.w; -} - -vec3 screen_space(vec2 point) { - return screen_space(vec3(point, 0)); -} - - -void emit_vertex(vec3 position, vec2 uv, bool is_start) { - - f_uv = uv; - - f_color = is_start ? color_start : color_end; - - gl_Position = vec4((position.xy / resolution), position.z, 1.0); - // linewidth scaling may shrink the effective linewidth - f_thickness = is_start ? linewidth_start : linewidth_end; -} - -void main() { - vec3 p1 = screen_space(linepoint_start); - vec3 p2 = screen_space(linepoint_end); - vec2 dir = p1.xy - p2.xy; - dir = normalize(dir); - vec2 line_normal = vec2(dir.y, -dir.x); - vec2 line_offset = line_normal * (linewidth_start / 2.0); - - // triangle 1 - vec3 v0 = vec3(p1.xy - line_offset, p1.z); - if (position == 0.0) { - emit_vertex(v0, vec2(0.0, 0.0), true); - return; - } - vec3 v2 = vec3(p2.xy - line_offset, p2.z); - if (position == 1.0) { - emit_vertex(v2, vec2(0.0, 0.0), false); - return; - } - vec3 v1 = vec3(p1.xy + line_offset, p1.z); - if (position == 2.0) { - emit_vertex(v1, vec2(0.0, 0.0), true); - return; - } - - // triangle 2 - if (position == 3.0) { - emit_vertex(v2, vec2(0.0, 0.0), false); - return; - } - vec3 v3 = vec3(p2.xy + line_offset, p2.z); - if (position == 4.0) { - emit_vertex(v3, vec2(0.0, 0.0), false); - return; - } - if (position == 5.0) { - emit_vertex(v1, vec2(0.0, 0.0), true); - return; - } -} diff --git a/WGLMakie/assets/particles.vert b/WGLMakie/assets/particles.vert index 13c86f2ec8e..495c475579d 100644 --- a/WGLMakie/assets/particles.vert +++ b/WGLMakie/assets/particles.vert @@ -31,7 +31,7 @@ void main(){ // those functions will get inserted by the shader creation pipeline vec3 vertex_position = get_markersize() * to_vec3(get_position()); vec3 N = get_normals() / get_markersize(); // see issue #3702 - rotate(get_rotations(), vertex_position, N); + rotate(get_rotation(), vertex_position, N); vertex_position = to_vec3(get_offset()) + vertex_position; vec4 position_world = model * vec4(vertex_position, 1); frag_normal = N; diff --git a/WGLMakie/assets/sprites.vert b/WGLMakie/assets/sprites.vert index 421a6ce7296..6b2e162eb72 100644 --- a/WGLMakie/assets/sprites.vert +++ b/WGLMakie/assets/sprites.vert @@ -76,7 +76,7 @@ void main(){ data_point = pview * data_point; // Compute transform for the offset vectors from the central point - trans = (get_billboard() ? projection : pview) * qmat(get_rotations()) * trans; + trans = (get_billboard() ? projection : pview) * qmat(get_rotation()) * trans; vec4 sprite_center = trans * vec4(sprite_bbox_centre, 0, 0); vec4 vclip = data_point + sprite_center; diff --git a/WGLMakie/assets/voxel.frag b/WGLMakie/assets/voxel.frag new file mode 100644 index 00000000000..3b2c4fead11 --- /dev/null +++ b/WGLMakie/assets/voxel.frag @@ -0,0 +1,130 @@ +// debug FLAGS +// #define DEBUG_RENDER_ORDER 2 // (0, 1, 2) - dimensions + +flat in vec3 o_normal; +in vec3 o_uvw; +flat in int o_side; +in vec2 o_tex_uv; + +in vec3 o_camdir; + +#ifdef DEBUG_RENDER_ORDER +flat in float plane_render_idx; // debug +flat in int plane_dim; +flat in int plane_front; +#endif + +vec4 debug_color(uint id) { + return vec4( + float((id & uint(225)) >> uint(5)) / 5.0, + float((id & uint(25)) >> uint(3)) / 3.0, + float((id & uint(7)) >> uint(1)) / 3.0, + 1.0 + ); +} +vec4 debug_color(int id) { return debug_color(uint(id)); } + +vec4 get_color(bool color, bool color_map, bool uv_map, int id) { + return debug_color(id); +} +vec4 get_color(bool color, sampler2D color_map, bool uv_map, int id) { + return texelFetch(color_map, ivec2(id-1, 0), 0); +} +vec4 get_color(sampler2D color, sampler2D color_map, bool uv_map, int id) { + return texelFetch(color, ivec2(id-1, 0), 0); +} +vec4 get_color(sampler2D color, bool color_map, bool uv_map, int id) { + return texelFetch(color, ivec2(id-1, 0), 0); +} +vec4 get_color(sampler2D color, sampler2D color_map, sampler2D uv_map, int id) { + vec4 lrbt = texelFetch(uv_map, ivec2(id-1, o_side), 0); + // compute uv normalized to voxel + // TODO: float precision causes this to wrap sometimes (e.g. 5.999..7.0002) + vec2 voxel_uv = mod(o_tex_uv, 1.0); + voxel_uv = mix(lrbt.xz, lrbt.yw, voxel_uv); + return texture(color, voxel_uv); +} +vec4 get_color(sampler2D color, bool color_map, sampler2D uv_map, int id) { + vec4 lrbt = texelFetch(uv_map, ivec2(id-1, o_side), 0); + // compute uv normalized to voxel + // TODO: float precision causes this to wrap sometimes (e.g. 5.999..7.0002) + vec2 voxel_uv = mod(o_tex_uv, 1.0); + voxel_uv = mix(lrbt.xz, lrbt.yw, voxel_uv); + return texture(color, voxel_uv); +} + +// Smoothes out edge around 0 light intensity, see GLMakie +float smooth_zero_max(float x) { + const float c = 0.00390625, xswap = 0.6406707120152759, yswap = 0.20508383900190955; + const float shift = 1.0 + xswap - yswap; + float pow8 = x + shift; + pow8 = pow8 * pow8; pow8 = pow8 * pow8; pow8 = pow8 * pow8; + return x < yswap ? c * pow8 : x; +} + +vec3 blinnphong(vec3 N, vec3 V, vec3 L, vec3 color){ + float diff_coeff = smooth_zero_max(dot(L, -N)); + + // specular coefficient + vec3 H = normalize(L + V); + + float spec_coeff = pow(max(dot(H, -N), 0.0), get_shininess()); + if (diff_coeff <= 0.0) + spec_coeff = 0.0; + + // final lighting model + return get_light_color() * vec3( + get_diffuse() * diff_coeff * color + + get_specular() * spec_coeff + ); +} + +flat in uint frag_instance_id; +vec4 pack_int(uint id, uint index) { + vec4 unpack; + unpack.x = float((id & uint(0xff00)) >> 8) / 255.0; + unpack.y = float((id & uint(0x00ff)) >> 0) / 255.0; + unpack.z = float((index & uint(0xff00)) >> 8) / 255.0; + unpack.w = float((index & uint(0x00ff)) >> 0) / 255.0; + return unpack; +} +void main() +{ + vec2 voxel_uv = mod(o_tex_uv, 1.0); + if (voxel_uv.x < 0.5 * gap || voxel_uv.x > 1.0 - 0.5 * gap || + voxel_uv.y < 0.5 * gap || voxel_uv.y > 1.0 - 0.5 * gap) + discard; + + // grab voxel id + int id = int(texture(voxel_id, o_uvw).x); + + // id is invisible so we simply discard + if (id == 0) { + discard; + } + + // otherwise we draw. For now just some color... + vec4 voxel_color = get_color(color, color_map, uv_map, id); + +#ifdef DEBUG_RENDER_ORDER + if (plane_dim != DEBUG_RENDER_ORDER) + discard; + voxel_color = vec4(plane_render_idx, 0, 0, id == 0 ? 0.01 : 1.0); +#endif + + if(get_shading()){ + vec3 L = get_light_direction(); + vec3 light = blinnphong(o_normal, normalize(o_camdir), L, voxel_color.rgb); + voxel_color.rgb = get_ambient() * voxel_color.rgb + light; + } + + if (picking) { + uvec3 size = uvec3(textureSize(voxel_id, 0).xyz); + uvec3 idx = uvec3(o_uvw * vec3(size)); + uint lin = idx.x + size.x * (idx.y + size.y * idx.z); + fragment_color = pack_int(object_id, lin); + return; + } + + fragment_color = voxel_color; +} \ No newline at end of file diff --git a/WGLMakie/assets/voxel.vert b/WGLMakie/assets/voxel.vert new file mode 100644 index 00000000000..967e440264e --- /dev/null +++ b/WGLMakie/assets/voxel.vert @@ -0,0 +1,171 @@ +// debug FLAGS +// #define DEBUG_RENDER_ORDER + +flat out vec3 o_normal; +out vec3 o_uvw; +flat out int o_side; +out vec2 o_tex_uv; + +#ifdef DEBUG_RENDER_ORDER +flat out float plane_render_idx; +flat out int plane_dim; +flat out int plane_front; +#endif + +out vec3 o_camdir; + +uniform mat4 projection, view; + +const vec3 unit_vecs[3] = vec3[]( vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1) ); +const mat2x3 orientations[3] = mat2x3[]( + mat2x3(0, 1, 0, 0, 0, 1), // xy -> _yz (x normal) + mat2x3(1, 0, 0, 0, 0, 1), // xy -> x_z (y normal) + mat2x3(1, 0, 0, 0, 1, 0) // xy -> xy_ (z normal) +); + +void main() { + get_dummy(); // otherwise this doesn't render :) + + /* How this works: + To simplify lets consider a 2d grid of pixel where the voxel surface would + be the square outline of around a data point x. + +---+---+---+ + | x | x | x | + +---+---+---+ + | x | x | x | + +---+---+---+ + | x | x | x | + +---+---+---+ + Naively we would draw 4 lines for each point x, coloring them based on the + data attached to x. This would result in 4 * N^2 lines with N^2 = number of + pixels. We can do much better though by drawing a line for each column and + row of pixels: + 1 +---+---+---+ + | x | x | x | + 2 +---+---+---+ + | x | x | x | + 3 +---+---+---+ + | x | x | x | + 4 +---+---+---+ + 5 6 7 8 + This results in 2 * (N+1) lines. We can adjust the color of the line by + sampling a Texture containing the information previously attached to vertices. + + Generalized to 3D voxels, lines become planes and the texture becomes 3D. + We draw the planes through instancing. So first we will need to map the + instance id to a dimension (xy, xz or yz plane) and an offset (in z, y or + x direction respectively). + */ + + // TODO: might be better for transparent rendering to alternate xyz? + // But how would we do this for non-cubic chunks? + + // Map instance id to dimension and index along dimension (0..N+1 or 0..2N) + ivec3 size = textureSize(voxel_id, 0); + int dim, id = gl_InstanceID, front = 1; + float gap = get_gap(); + if (gap > 0.01) { + front = 1 - 2 * int(gl_InstanceID & 1); + if (id < 2 * size.z) { + dim = 2; + id = id; + } else if (id < 2 * (size.z + size.y)) { + dim = 1; + id = id - 2 * size.z; + } else { // if (id > 2 * (size.z + size.y)) { + dim = 0; + id = id - 2 * (size.z + size.y); + } + } else { + if (id < size.z + 1) { + dim = 2; + id = id; + } else if (id < size.z + size.y + 2) { + dim = 1; + id = id - (size.z + 1); + } else { + dim = 0; + id = id - (size.z + size.y + 2); + } + } + +#ifdef DEBUG_RENDER_ORDER + plane_render_idx = float(id) / float(size[dim]-1); + plane_dim = dim; + plane_front = front; +#endif + + // plane placement + // Figure out which plane to start with + vec3 normal = get_normalmatrix() * unit_vecs[dim]; + int dir = int(sign(dot(get_view_direction(), normal))), start; + if (depthsorting) { + // TODO: depthsorted should start far away from viewer so every plane draws + start = int((0.5 + 0.5 * float(dir)) * float(size[dim])); + dir *= -1; + } else { + // otherwise we should start at viewer and expand in view direction so + // that the depth test can quickly eliminate unnecessary fragments + // Note that model can have rotations and (uneven) scaling + vec4 origin = model * vec4(0, 0, 0, 1); + vec4 back = model * vec4(size, 1); + float front_dist = dot(origin.xyz / origin.w, normal); + float back_dist = dot(back.xyz / back.w, normal); + float cam_dist = dot(eyeposition, normal); + float dist01 = (cam_dist - front_dist) / (back_dist - front_dist); + + // index of voxel closest to (and in front of) the camera + start = clamp(int(float(size[dim]) * dist01), 0, size[dim]); + } + + vec3 displacement; + if (gap > 0.01) { + // planes are doubled + // 2 * start + min(dir, 0) closest (camera facing) plane + // dir * id iterate away from first plane + // (idx + 2 * size[dim]) % 2 * size[dim] normalize to [0, 2size[dim]) + int plane_idx = (2 * start + min(dir, 0) + dir * id + 2 * size[dim]) % (2 * size[dim]); + // (plane_idx + 1) / 2 map to idx 0, 1, 2, 3, 4 -> displacements 0, 1, 1, 2, 2, ... + // 0.5 * dir * gap * front gap based offset from space filling placements + displacement = (float((plane_idx + 1) / 2) + 0.5 * float(dir * front) * gap) * unit_vecs[dim]; + } else { + // similar to above but with N+1 indices around N voxels + displacement = float((start + dir * id + size[dim] + 1) % (size[dim] + 1)) * unit_vecs[dim]; + } + + // place plane vertices + vec3 plane_vertex = vec3(size) * (orientations[dim] * get_position()) + displacement; + vec4 world_pos = get_model() * vec4(plane_vertex, 1.0f); + gl_Position = projection * view * world_pos; + gl_Position.z += gl_Position.w * get_depth_shift(); + + // For each plane the normal is constant in + // +- normal = +- world_normalmatrix * unit_vecs[dim] direction. We just need + // the correct prefactor + o_camdir = eyeposition - world_pos.xyz / world_pos.w; + float normal_dir; + if (gap > 0.01) { + // With a gap we render front and back faces. Since the gap always takes + // away from a voxel the normal should go in the opposite direction. + normal_dir = -float(dir * front); + } else { + // Without a gap we skip back facing faces so every normal faces the camera + normal_dir = sign(dot(o_camdir, normal)); + } + o_normal = normal_dir * normalize(normal); + + // The quad we render here is directly between two slices of voxels in our + // chunk. Each `plane_vertex` of the quad is in a 0 .. size scale, so they + // can be mapped directly to texture indices. We just need to figure out if + // the quad is associated with the "previous" or "next" slice of voxels. We + // can derive that from the normal direction, as the normal always points + // away from the voxel center. + o_uvw = (plane_vertex - 0.5 * (1.0 - gap) * o_normal) / vec3(size); + + // normal in: -x -y -z +x +y +z direction + o_side = dim + 3 * int(0.5 + 0.5 * normal_dir); + + // map plane_vertex (-w/2 .. w/2 scale) back to 2d (scaled 0 .. w) + // if the normal is negative invert range (w .. 0) + o_tex_uv = transpose(orientations[dim]) * (vec3(-normal_dir, normal_dir, 1.0) * plane_vertex); +} \ No newline at end of file diff --git a/WGLMakie/src/Lines.js b/WGLMakie/src/Lines.js index ab12e386ce3..ab32f8fb887 100644 --- a/WGLMakie/src/Lines.js +++ b/WGLMakie/src/Lines.js @@ -6,6 +6,7 @@ import { } from "./Shaders.js"; import { deserialize_uniforms } from "./Serialization.js"; +import { IntType } from "./THREE.js"; function filter_by_key(dict, keys, default_value = false) { const result = {}; @@ -26,88 +27,670 @@ function filter_by_key(dict, keys, default_value = false) { // https://www.khronos.org/assets/uploads/developers/presentations/Crazy_Panda_How_to_draw_lines_in_WebGL.pdf // https://github.com/gameofbombs/pixi-candles/tree/master/src // https://github.com/wwwtyro/instanced-lines-demos/tree/master -function linesegments_vertex_shader(uniforms, attributes) { +function lines_vertex_shader(uniforms, attributes, is_linesegments) { const attribute_decl = attributes_to_type_declaration(attributes); const uniform_decl = uniforms_to_type_declaration(uniforms); const color = attribute_type(attributes.color_start) || uniform_type(uniforms.color_start); - return `precision mediump int; - precision highp float; + if (is_linesegments) { + //////////////////////////////////////////////////////////////////////// + /// Linessegments + //////////////////////////////////////////////////////////////////////// - ${attribute_decl} - ${uniform_decl} - uniform int is_segments_multi; + // Note: + // If we run into problems with the number of varying vertex attributes + // used here, try splitting up f_quad_sdf1. The vec3 might be taking + // a full vec4 slot, while vec2 + float would not. + // Alternatively: + // - f_truncation could probably be traded for a float f_truncation_distance, + // with truncation happening at something like + // trunc_sdf = f_quad_sdf1.x - f_quad_sdf0 +- f_truncation_distance + // - f_quad_sdf1.x and .y could potentially be merged used abs like the + // normal direction (f_quad_sdf1.z). + // If those are not possible without degrading line quality we could + // also generate a simplified fragment shader for linesegments which + // drops all the unnecessary attributes to enable that as a workaround. - out vec2 f_uv; - out ${color} f_color; - flat out uint frag_instance_id; + return `precision mediump int; + precision highp float; - vec2 get_resolution() { - // 2 * px_per_unit doesn't make any sense, but works - // TODO, figure out what's going on! - return resolution / 2.0 * px_per_unit; - } + ${attribute_decl} - vec3 screen_space(vec3 point) { - vec4 vertex = projectionview * model * vec4(point, 1); - return vec3(vertex.xy * get_resolution(), vertex.z + vertex.w * depth_shift) / vertex.w; - } - vec3 screen_space(vec2 point) { - return screen_space(vec3(point, 0)); - } + out highp float f_quad_sdf0; // invalid / not needed + out highp vec3 f_quad_sdf1; + out highp float f_quad_sdf2; // invalid / not needed + out vec2 f_truncation; // invalid / not needed + out float f_linestart; // constant + out float f_linelength; - void main() { - vec3 p_a = screen_space(linepoint_start); - vec3 p_b = screen_space(linepoint_end); - float width = (px_per_unit * (position.x == 1.0 ? linewidth_end : linewidth_start)); - f_color = position.x == 1.0 ? color_end : color_start; - f_uv = vec2(position.x, position.y + 0.5); + flat out vec2 f_extrusion; // invalid / not needed + flat out float f_linewidth; + flat out vec4 f_pattern_overwrite; // invalid / not needed + flat out vec2 f_discard_limit; // invalid / not needed + flat out uint f_instance_id; + flat out ${color} f_color1; + flat out ${color} f_color2; + flat out float f_alpha_weight; + flat out float f_cumulative_length; - vec2 pointA = p_a.xy; - vec2 pointB = p_b.xy; + ${uniform_decl} - vec2 xBasis = pointB - pointA; - vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); - vec2 point = pointA + xBasis * position.x + yBasis * width * position.y; + // Constants + const float AA_RADIUS = 0.8; + const float AA_THICKNESS = 2.0 * AA_RADIUS; - gl_Position = vec4(point.xy / get_resolution(), position.x == 1.0 ? p_b.z : p_a.z, 1.0); - frag_instance_id = uint((gl_InstanceID * is_segments_multi) + int(position.x == 1.0)); - } + + //////////////////////////////////////////////////////////////////////// + // Geometry/Position Utils + //////////////////////////////////////////////////////////////////////// + + vec4 clip_space(vec3 point) { + return projectionview * model * vec4(point, 1); + } + vec4 clip_space(vec2 point) { return clip_space(vec3(point, 0)); } + + vec3 screen_space(vec4 vertex) { + return vec3( + (0.5 * vertex.xy / vertex.w + 0.5) * px_per_unit * resolution, + vertex.z / vertex.w + depth_shift + ); + } + + vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); } + vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); } + + + //////////////////////////////////////////////////////////////////////// + // Main + //////////////////////////////////////////////////////////////////////// + + + void main() { + bool is_end = position.x == 1.0; + + //////////////////////////////////////////////////////////////////// + // Handle line geometry (position, directions) + //////////////////////////////////////////////////////////////////// + + + float width = px_per_unit * (is_end ? linewidth_end : linewidth_start); + float halfwidth = 0.5 * max(AA_RADIUS, width); + + // restrict to visible area (see other shader) + vec3 p1, p2; + { + vec4 _p1 = clip_space(linepoint_start), _p2 = clip_space(linepoint_end); + vec4 v1 = _p2 - _p1; + + if (_p1.w < 0.0) + _p1 = _p1 + (-_p1.w - _p1.z) / (v1.z + v1.w) * v1; + if (_p2.w < 0.0) + _p2 = _p2 + (-_p2.w - _p2.z) / (v1.z + v1.w) * v1; + + p1 = screen_space(_p1); + p2 = screen_space(_p2); + } + + // line vector (xy-normalized vectors in line direction) + // Need z component for correct depth order + vec3 v1 = p2 - p1; + float segment_length = length(v1); + v1 /= segment_length; + + // line normal (i.e. in linewidth direction) + vec2 n1 = normal_vector(v1); + + + //////////////////////////////////////////////////////////////////// + // Static vertex data + //////////////////////////////////////////////////////////////////// + + + // invalid - no joints requiring pattern adjustments + f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); + + // invalid - no joints that need pixels discarded + f_discard_limit = vec2(10.0); + + // invalid - no joints requiring line sdfs to be extruded + f_extrusion = vec2(0.0); + + // used to compute width sdf + f_linewidth = halfwidth; + + f_instance_id = uint(2 * gl_InstanceID); + + // we restart patterns for each segment + f_cumulative_length = 0.0; + + + //////////////////////////////////////////////////////////////////// + // Varying vertex data + //////////////////////////////////////////////////////////////////// + + + // Vertex position (padded for joint & anti-aliasing) + float v_offset = position.x * (0.5 * segment_length + AA_THICKNESS); + float n_offset = (halfwidth + AA_THICKNESS) * position.y; + vec3 point = 0.5 * (p1 + p2) + v_offset * v1 + n_offset * vec3(n1, 0); + + // SDF's + vec2 VP1 = point.xy - p1.xy; + vec2 VP2 = point.xy - p2.xy; + + // invalid - no joint to compute overlap with + f_quad_sdf0 = 10.0; + + // sdf of this segment + f_quad_sdf1.x = dot(VP1, -v1.xy); + f_quad_sdf1.y = dot(VP2, v1.xy); + f_quad_sdf1.z = dot(VP1, n1); + + // invalid - no joint to compute overlap with + f_quad_sdf2 = 10.0; + + // invalid - no joint to truncate + f_truncation = vec2(-10.0); + + // simplified - no extrusion or joints means we just have: + f_linestart = 0.0; + f_linelength = segment_length; + + // for color sampling + f_color1 = color_start; + f_color2 = color_end; + f_alpha_weight = min(1.0, width / AA_RADIUS); + + // clip space position + gl_Position = vec4(2.0 * point.xy / (px_per_unit * resolution) - 1.0, point.z, 1.0); + } `; + + } else { + //////////////////////////////////////////////////////////////////////// + /// Lines + //////////////////////////////////////////////////////////////////////// + + return `precision mediump int; + precision highp float; + + ${attribute_decl} + + out highp float f_quad_sdf0; + out highp vec3 f_quad_sdf1; + out highp float f_quad_sdf2; + out vec2 f_truncation; + out float f_linestart; + out float f_linelength; + + flat out vec2 f_extrusion; + flat out float f_linewidth; + flat out vec4 f_pattern_overwrite; + flat out vec2 f_discard_limit; + flat out uint f_instance_id; + flat out ${color} f_color1; + flat out ${color} f_color2; + flat out float f_alpha_weight; + flat out float f_cumulative_length; + + ${uniform_decl} + + // Constants + const float MITER_LIMIT = -0.4; + const float AA_RADIUS = 0.8; + const float AA_THICKNESS = 2.0 * AA_RADIUS; + + + //////////////////////////////////////////////////////////////////////// + // Pattern handling + //////////////////////////////////////////////////////////////////////// + + + vec2 process_pattern(bool pattern, bool[4] isvalid, vec2 extrusion, float segment_length, float halfwidth) { + // do not adjust stuff + f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); + return vec2(0); + } + + vec2 process_pattern(sampler2D pattern, bool[4] isvalid, vec2 extrusion, float segment_length, float halfwidth) { + // samples: + // -ext1 p1 ext1 -ext2 p2 ext2 + // 1 2 3 4 5 6 + // prev | joint | this | joint | next + + // default to no overwrite + f_pattern_overwrite.x = -1e12; + f_pattern_overwrite.z = +1e12; + vec2 adjust = vec2(0); + float width = 2.0 * halfwidth; + float uv_scale = 1.0 / (width * pattern_length); + float left, center, right; + + if (isvalid[0]) { + float offset = abs(extrusion[0]); + left = width * texture(pattern, vec2(uv_scale * (lastlen_start - offset), 0.0)).x; + center = width * texture(pattern, vec2(uv_scale * (lastlen_start ), 0.0)).x; + right = width * texture(pattern, vec2(uv_scale * (lastlen_start + offset), 0.0)).x; + + // cases: + // ++-, +--, +-+ => elongate backwards + // -++, --+ => shrink forward + // +++, ---, -+- => freeze around joint + + if ((left > 0.0 && center > 0.0 && right > 0.0) || (left < 0.0 && right < 0.0)) { + // default/freeze + // overwrite until one AA gap past the corner/joint + f_pattern_overwrite.x = uv_scale * (lastlen_start + abs(extrusion[0]) + AA_RADIUS); + // using the sign of the center to decide between drawing or not drawing + f_pattern_overwrite.y = sign(center); + } else if (left > 0.0) { + // elongate backwards + adjust.x = -1.0; + } else if (right > 0.0) { + // shorten forward + adjust.x = 1.0; + } else { + // default - see above + f_pattern_overwrite.x = uv_scale * (lastlen_start + abs(extrusion[0]) + AA_RADIUS); + f_pattern_overwrite.y = sign(center); + } + + } // else there is no left segment, no left join, so no overwrite + + if (isvalid[3]) { + float offset = abs(extrusion[1]); + left = width * texture(pattern, vec2(uv_scale * (lastlen_start + segment_length - offset), 0.0)).x; + center = width * texture(pattern, vec2(uv_scale * (lastlen_start + segment_length ), 0.0)).x; + right = width * texture(pattern, vec2(uv_scale * (lastlen_start + segment_length + offset), 0.0)).x; + + if ((left > 0.0 && center > 0.0 && right > 0.0) || (left < 0.0 && right < 0.0)) { + // default/freeze + f_pattern_overwrite.z = uv_scale * (lastlen_start + segment_length - abs(extrusion[1]) - AA_RADIUS); + f_pattern_overwrite.w = sign(center); + } else if (left > 0.0) { + // shrink backwards + adjust.y = -1.0; + } else if (right > 0.0) { + // elongate forward + adjust.y = 1.0; + } else { + // default - see above + f_pattern_overwrite.z = uv_scale * (lastlen_start + segment_length - abs(extrusion[1]) - AA_RADIUS); + f_pattern_overwrite.w = sign(center); + } + } + + return adjust; + } + + + //////////////////////////////////////////////////////////////////////// + // Geometry/Position Utils + //////////////////////////////////////////////////////////////////////// + + vec4 clip_space(vec3 point) { + return projectionview * model * vec4(point, 1); + } + vec4 clip_space(vec2 point) { return clip_space(vec3(point, 0)); } + + vec3 screen_space(vec4 vertex) { + return vec3( + (0.5 * vertex.xy / vertex.w + 0.5) * px_per_unit * resolution, + vertex.z / vertex.w + depth_shift + ); + } + + vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); } + vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); } + + + //////////////////////////////////////////////////////////////////////// + // Main + //////////////////////////////////////////////////////////////////////// + + + void main() { + bool is_end = position.x == 1.0; + + + //////////////////////////////////////////////////////////////////// + // Handle line geometry (position, directions) + //////////////////////////////////////////////////////////////////// + + + float width = px_per_unit * (is_end ? linewidth_end : linewidth_start); + float halfwidth = 0.5 * max(AA_RADIUS, width); + + bool[4] isvalid = bool[4](true, true, true, true); + + // To apply pixel space linewidths we transform line vertices to pixel space + // here. This is dangerous with perspective projection as p.xyz / p.w sends + // points from behind the camera to beyond far (clip z > 1), causing lines + // to invert. To avoid this we translate points along the line direction, + // moving them to the edge of the visible area. + vec3 p0, p1, p2, p3; + { + // All in clip space + vec4 clip_p0 = clip_space(linepoint_prev); + vec4 clip_p1 = clip_space(linepoint_start); + vec4 clip_p2 = clip_space(linepoint_end); + vec4 clip_p3 = clip_space(linepoint_next); + + vec4 v1 = clip_p2 - clip_p1; + + // With our perspective projection matrix clip.w = -view.z with + // clip.w < 0.0 being behind the camera. + // Note that if the signs in the projectionmatrix change, this may become wrong. + if (clip_p1.w < 0.0) { + // the line connects outside the visible area so we may consider it disconnected + isvalid[0] = false; + // A clip position is visible if -w <= z <= w. To move the line along + // the line direction v to the start of the visible area, we solve: + // p.z + t * v.z = +-(p.w + t * v.w) + // where (-) gives us the result for the near clipping plane as p.z + // and p.w share the same sign and p.z/p.w = -1.0 is the near plane. + clip_p1 = clip_p1 + (-clip_p1.w - clip_p1.z) / (v1.z + v1.w) * v1; + } + if (clip_p2.w < 0.0) { + isvalid[3] = false; + clip_p2 = clip_p2 + (-clip_p2.w - clip_p2.z) / (v1.z + v1.w) * v1; + } + + // transform clip -> screen space, applying xyz / w normalization (which + // is now save as all vertices are in front of the camera) + p0 = screen_space(clip_p0); // start of previous segment + p1 = screen_space(clip_p1); // end of previous segment, start of current segment + p2 = screen_space(clip_p2); // end of current segment, start of next segment + p3 = screen_space(clip_p3); // end of next segment + } + + // doesn't work correctly with linepoint_x... + isvalid[0] = p0 != p1; + isvalid[3] = p2 != p3; + + // line vectors (xy-normalized vectors in line direction) + // Need z component here for correct depth order + vec3 v1 = p2 - p1; + float segment_length = length(v1); + v1 /= segment_length; + + // We don't need the z component for these + vec2 v0 = v1.xy, v2 = v1.xy; + bool[2] skip_joint; + if (isvalid[0]) + v0 = normalize(p1.xy - p0.xy); + if (isvalid[3]) + v2 = normalize(p3.xy - p2.xy); + + // line normals (i.e. in linewidth direction) + vec2 n0 = normal_vector(v0); + vec2 n1 = normal_vector(v1); + vec2 n2 = normal_vector(v2); + + + //////////////////////////////////////////////////////////////////// + // Handle joint geometry + //////////////////////////////////////////////////////////////////// + + + // joint information + + // Are we truncating the joint? + bool[2] is_truncated = bool[2]( + dot(v0.xy, v1.xy) < MITER_LIMIT, + dot(v1.xy, v2.xy) < MITER_LIMIT + ); + + // Miter normals (normal of truncated edge / vector to sharp corner) + // Note: n0 + n1 = vec(0) for a 180° change in direction. +-(v0 - v1) is the + // same direction, but becomes vec(0) at 0°, so we can use it instead + vec2 miter_n1 = is_truncated[0] ? normalize(v0.xy - v1.xy) : normalize(n0 + n1); + vec2 miter_n2 = is_truncated[1] ? normalize(v1.xy - v2.xy) : normalize(n1 + n2); + + // miter vectors (line vector matching miter normal) + vec2 miter_v1 = -normal_vector(miter_n1); + vec2 miter_v2 = -normal_vector(miter_n2); + + // distance between p1/2 and respective sharp corner + float miter_offset1 = dot(miter_n1, n1); // = dot(miter_v1, v1) + float miter_offset2 = dot(miter_n2, n1); // = dot(miter_v2, v1) + + // How far the line needs to extend to accomodate the joint. + // These are calculated as prefactors to v1 so that the line quad + // is given by: + // p1 + w * extrusion[0] * v1 ----- p2 + w * extrusion[1] * v1 + // | | + // p1 + w * extrusion[0] * v1 ----- p2 + w * extrusion[1] * v1 + // with w = halfwidth for drawn corners and w = halfwidth + AA_THICKNESS + // for the corners of quad. The sign difference due to miter joints + // is included based on the current vertex position (position.y). + // (truncated miter joints do not differ here) + vec2 extrusion; + + if (is_truncated[0]) { + // need to extend segment to include previous segments corners for truncated join + extrusion[0] = -abs(miter_offset1 / dot(miter_v1, n1)); + } else { + // shallow/spike join needs to include point where miter normal meets outer line edge + extrusion[0] = position.y * dot(miter_n1, v1.xy) / miter_offset1; + } + + if (is_truncated[1]) { + extrusion[1] = abs(miter_offset2 / dot(miter_n2, v1.xy)); + } else { + extrusion[1] = position.y * dot(miter_n2, v1.xy) / miter_offset2; + } + + + //////////////////////////////////////////////////////////////////// + // Joint adjustments + //////////////////////////////////////////////////////////////////// + + + // Miter joints can cause vertices to move past each other, e.g. + // _______ + // '. .' + // x + // '---' + // To avoid drawing the "inverted" section we move the relevant + // vertices to the crossing point (x) using this scaling factor. + float shape_factor = max( + 0.0, + segment_length / max( + segment_length, + (halfwidth + AA_THICKNESS) * (extrusion[0] - extrusion[1]) + ) + ); + + // If a pattern starts or stops drawing in a joint it will get + // fractured across the joint. To avoid this we either: + // - adjust the involved line segments so that the patterns ends + // on straight line quad (adjustment becomes +1.0 or -1.0) + // - or adjust the pattern to start/stop outside of the joint + // (f_pattern_overwrite is set, adjustment is 0.0) + vec2 adjustment = process_pattern( + pattern, isvalid, halfwidth * extrusion, segment_length, halfwidth + ); + + // If adjustment != 0.0 we replace a joint by an extruded line, + // so we no longer need to shrink the line for the joint to fit. + if (adjustment[0] != 0.0 || adjustment[1] != 0.0) + shape_factor = 1.0; + + //////////////////////////////////////////////////////////////////// + // Static vertex data + //////////////////////////////////////////////////////////////////// + + + // For truncated miter joints we discard overlapping sections of + // the two involved line segments. To avoid discarding far into + // the line segment we limit the range here. (Without this short + // segments can cut holes into longer sections.) + f_discard_limit = vec2( + is_truncated[0] ? 0.0 : 1e12, + is_truncated[1] ? 0.0 : 1e12 + ); + + // Used to elongate sdf to include joints + // if start/end elongate slightly so that there is no AA gap in loops + // if joint skipped elongate to new length + // if normal joint elongate a lot to let shape/truncation handle joint + f_extrusion = vec2( + !isvalid[0] ? min(AA_RADIUS, halfwidth) : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0])), + !isvalid[3] ? min(AA_RADIUS, halfwidth) : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1])) + ); + + // used to compute width sdf + f_linewidth = halfwidth; + + f_instance_id = uint(gl_InstanceID); + + f_cumulative_length = lastlen_start; + + + //////////////////////////////////////////////////////////////////// + // Varying vertex data + //////////////////////////////////////////////////////////////////// + + + vec3 offset; + int x = int(is_end); + if (adjustment[x] == 0.0) { + if (is_truncated[x] || !isvalid[3 * x]) { + // handle overlap in fragment shader via SDF comparison + offset = shape_factor * ( + (halfwidth * extrusion[x] + position.x * AA_THICKNESS) * v1 + + vec3(position.y * (halfwidth + AA_THICKNESS) * n1, 0) + ); + } else { + // handle overlap by adjusting geometry + // TODO: should this include z in miter_n? + offset = position.y * shape_factor * + (halfwidth + AA_THICKNESS) / + float[2](miter_offset1, miter_offset2)[x] * + vec3(vec2[2](miter_n1, miter_n2)[x], 0); + } + } else { + // discard joint for cleaner pattern handling + offset = + adjustment[x] * (halfwidth * abs(extrusion[x]) + AA_THICKNESS) * v1 + + vec3(position.y * (halfwidth + AA_THICKNESS) * n1, 0); + } + + // Vertex position (padded for joint & anti-aliasing) + vec3 point = vec3[2](p1, p2)[x] + offset; + + // SDF's + vec2 VP1 = point.xy - p1.xy; + vec2 VP2 = point.xy - p2.xy; + + // Signed distance of the previous segment from the shared point + // p1 in line direction. Used decide which segments renders + // which joint fragment/pixel for truncated joints. + if (isvalid[0] && (adjustment[0] == 0.0) && is_truncated[0]) + f_quad_sdf0 = dot(VP1, v0.xy); + else + f_quad_sdf0 = 1e12; + + // sdf of this segment + f_quad_sdf1.x = dot(VP1, -v1.xy); + f_quad_sdf1.y = dot(VP2, v1.xy); + f_quad_sdf1.z = dot(VP1, n1); + + // SDF for next segment, see quad_sdf0 + if (isvalid[3] && (adjustment[1] == 0.0) && is_truncated[1]) + f_quad_sdf2 = dot(VP2, -v2.xy); + else + f_quad_sdf2 = 1e12; + + // sdf for creating a flat cap on truncated joints + // (sign(dot(...)) detects if line bends left or right) + f_truncation.x = !is_truncated[0] ? -1.0 : + dot(VP1, sign(dot(miter_n1, -v1.xy)) * miter_n1) - halfwidth * abs(miter_offset1) + - abs(adjustment[0]) * 1e12; + f_truncation.y = !is_truncated[1] ? -1.0 : + dot(VP2, sign(dot(miter_n2, +v1.xy)) * miter_n2) - halfwidth * abs(miter_offset2) + - abs(adjustment[1]) * 1e12; + + // Colors should be sampled based on the normalized distance from the + // extruded edge (varies with offset in n direction) + // - correcting for this with per-vertex colors results visible face border + // - calculating normalized distance here will cause div 0/negative + // issues as (linelength +- (extrusion[0] + extrusion[1])) <= 0 is possible + // So defer color interpolation to fragment shader + f_linestart = shape_factor * halfwidth * extrusion[0]; + f_linelength = max(1.0, segment_length - shape_factor * halfwidth * (extrusion[0] - extrusion[1])); + + // for color sampling + f_color1 = color_start; + f_color2 = color_end; + f_alpha_weight = min(1.0, width / AA_RADIUS); + + // clip space position + gl_Position = vec4(2.0 * point.xy / (px_per_unit * resolution) - 1.0, point.z, 1.0); + } + `; + } } function lines_fragment_shader(uniforms, attributes) { - const color = - attribute_type(attributes.color_start) || - uniform_type(uniforms.color_start); const color_uniforms = filter_by_key(uniforms, [ - "colorrange", - "colormap", - "nan_color", - "highclip", - "lowclip", - "picking", + "picking", "pattern", "pattern_length", + "colorrange", "colormap", "nan_color", "highclip", "lowclip" ]); const uniform_decl = uniforms_to_type_declaration(color_uniforms); + const color = + attribute_type(attributes.color_start) || + uniform_type(uniforms.color_start); - return `#extension GL_OES_standard_derivatives : enable + return ` + // uncomment for debug rendering + // #define DEBUG precision mediump int; precision highp float; precision mediump sampler2D; precision mediump sampler3D; - in vec2 f_uv; - in ${color} f_color; + in highp float f_quad_sdf0; + in highp vec3 f_quad_sdf1; + in highp float f_quad_sdf2; + in vec2 f_truncation; + in float f_linestart; + in float f_linelength; + + flat in float f_linewidth; + flat in vec4 f_pattern_overwrite; + flat in vec2 f_extrusion; + flat in vec2 f_discard_limit; + flat in ${color} f_color1; + flat in ${color} f_color2; + flat in float f_alpha_weight; + flat in uint f_instance_id; + flat in float f_cumulative_length; + + uniform uint object_id; ${uniform_decl} out vec4 fragment_color; // Half width of antialiasing smoothstep - #define ANTIALIAS_RADIUS 0.7071067811865476 + const float AA_RADIUS = 0.8; + // space allocated for AA + const float AA_THICKNESS = 2.0 * AA_RADIUS; + + float aastep(float threshold, float value) { + return smoothstep(threshold-AA_RADIUS, threshold+AA_RADIUS, value); + } + + + //////////////////////////////////////////////////////////////////////// + // Color handling + //////////////////////////////////////////////////////////////////////// + vec4 get_color_from_cmap(float value, sampler2D colormap, vec2 colorrange) { float cmin = colorrange.x; @@ -142,17 +725,46 @@ function lines_fragment_shader(uniforms, attributes) { return vec4(color, 1.0); } - float aastep(float threshold, float value) { - float afwidth = length(vec2(dFdx(value), dFdy(value))) * ANTIALIAS_RADIUS; - return smoothstep(threshold-afwidth, threshold+afwidth, value); - } - float aastep(float threshold1, float threshold2, float dist) { - return aastep(threshold1, dist) * aastep(threshold2, 1.0 - dist); + //////////////////////////////////////////////////////////////////////// + // Pattern sampling + //////////////////////////////////////////////////////////////////////// + + + float get_pattern_sdf(sampler2D pattern, vec2 uv){ + + // f_pattern_overwrite.x + // v joint + // ---------------- + // | | + // ---------------- + // joint ^ + // f_pattern_overwrite.z + + float w = 2.0 * f_linewidth; + if (uv.x <= f_pattern_overwrite.x) { + // overwrite for pattern with "ON" to the right (positive uv.x) + float sdf_overwrite = w * pattern_length * (f_pattern_overwrite.x - uv.x); + // pattern value where we start overwriting + float edge_sample = w * texture(pattern, vec2(f_pattern_overwrite.x, 0.5)).x; + // offset for overwrite to smoothly connect between sampling and edge + float sdf_offset = max(f_pattern_overwrite.y * edge_sample, -AA_RADIUS); + // add offset and apply direction ("ON" to left or right) to overwrite + return f_pattern_overwrite.y * (sdf_overwrite + sdf_offset); + } else if (uv.x >= f_pattern_overwrite.z) { + // same as above (other than mirroring overwrite direction) + float sdf_overwrite = w * pattern_length * (uv.x - f_pattern_overwrite.z); + float edge_sample = w * texture(pattern, vec2(f_pattern_overwrite.z, 0.5)).x; + float sdf_offset = max(f_pattern_overwrite.w * edge_sample, -AA_RADIUS); + return f_pattern_overwrite.w * (sdf_overwrite + sdf_offset); + } else + // in allowed range + return w * texture(pattern, uv).x; } - flat in uint frag_instance_id; - uniform uint object_id; + float get_pattern_sdf(bool _, vec2 uv){ + return -10.0; + } vec4 pack_int(uint id, uint index) { vec4 unpack; @@ -162,14 +774,113 @@ function lines_fragment_shader(uniforms, attributes) { unpack.w = float((index & uint(0x00ff)) >> 0) / 255.0; return unpack; } + + void main(){ + vec4 color; + + // f_quad_sdf1.x is the distance from p1, negative in v1 direction. + vec2 uv = vec2( + (f_cumulative_length - f_quad_sdf1.x) / (2.0 * f_linewidth * pattern_length), + 0.5 + 0.5 * f_quad_sdf1.z / f_linewidth + ); + + #ifndef DEBUG + // discard fragments that are "more inside" the other segment to remove + // overlap between adjacent line segments. (truncated joints) + float dist_in_prev = max(f_quad_sdf0, - f_discard_limit.x); + float dist_in_next = max(f_quad_sdf2, - f_discard_limit.y); + if (dist_in_prev < f_quad_sdf1.x || dist_in_next < f_quad_sdf1.y) + discard; + + // SDF for inside vs outside along the line direction. extrusion adjusts + // the distance from p1/p2 for joints etc + float sdf = max(f_quad_sdf1.x - f_extrusion.x, f_quad_sdf1.y - f_extrusion.y); + + // distance in linewidth direction + sdf = max(sdf, abs(f_quad_sdf1.z) - f_linewidth); + + // truncation of truncated joints (creates flat cap) + sdf = max(sdf, f_truncation.x); + sdf = max(sdf, f_truncation.y); + + // inner truncation (AA for overlapping parts) + // min(a, b) keeps what is inside a and b + // where a is the smoothly cut of part just before discard triggers (i.e. visible) + // and b is the (smoothly) cut of part where the discard triggers + // 100.0x sdf makes the sdf much more sharply, avoiding overdraw in the center + sdf = max(sdf, min(f_quad_sdf1.x + 1.0, 100.0 * (f_quad_sdf1.x - f_quad_sdf0) - 1.0)); + sdf = max(sdf, min(f_quad_sdf1.y + 1.0, 100.0 * (f_quad_sdf1.y - f_quad_sdf2) - 1.0)); + + // pattern application + sdf = max(sdf, get_pattern_sdf(pattern, uv)); + + // draw + + // v- edge + // .--------------- + // '. + // p1 v1 + // '. ---> + // '---------- + // -f_quad_sdf1.x is the distance from p1, positive in v1 direction + // f_linestart is the distance between p1 and the left edge along v1 direction + // f_start_length.y is the distance between the edges of this segment, in v1 direction + // so this is 0 at the left edge and 1 at the right edge (with extrusion considered) + float factor = (-f_quad_sdf1.x - f_linestart) / f_linelength; + color = get_color(f_color1 + factor * (f_color2 - f_color1), colormap, colorrange); + + color.a *= aastep(0.0, -sdf) * f_alpha_weight; + #endif + + #ifdef DEBUG + // base color + color = vec4(0.5, 0.5, 0.5, 0.2); + color.rgb += (2.0 * mod(float(f_instance_id), 2.0) - 1.0) * 0.1; + + // show color interpolation as brightness gradient + // float factor = (-f_quad_sdf1.x - f_linestart) / f_linelength; + // color.rgb += (2.0 * factor - 1.0) * 0.2; + + // mark "outside" define by quad_sdf in black + float sdf = max(f_quad_sdf1.x - f_extrusion.x, f_quad_sdf1.y - f_extrusion.y); + sdf = max(sdf, abs(f_quad_sdf1.z) - f_linewidth); + color.rgb -= vec3(0.4) * step(0.0, sdf); + + // Mark discarded space in red/blue + float dist_in_prev = max(f_quad_sdf0, - f_discard_limit.x); + float dist_in_next = max(f_quad_sdf2, - f_discard_limit.y); + if (dist_in_prev < f_quad_sdf1.x) + color.r += 0.5; + if (dist_in_next <= f_quad_sdf1.y) { + color.b += 0.5; + } + + // remaining overlap as softer red/blue + if (f_quad_sdf1.x - f_quad_sdf0 - 1.0 > 0.0) + color.r += 0.2; + if (f_quad_sdf1.y - f_quad_sdf2 - 1.0 > 0.0) + color.b += 0.2; + + // Mark regions excluded via truncation in green + color.g += 0.5 * step(0.0, max(f_truncation.x, f_truncation.y)); + + // and inner truncation as soft green + if (min(f_quad_sdf1.x + 1.0, 100.0 * (f_quad_sdf1.x - f_quad_sdf0) - 1.0) > 0.0) + color.g += 0.2; + if (min(f_quad_sdf1.y + 1.0, 100.0 * (f_quad_sdf1.y - f_quad_sdf2) - 1.0) > 0.0) + color.g += 0.2; + + // mark pattern in white + color.rgb += vec3(0.3) * step(0.0, get_pattern_sdf(pattern, uv)); + #endif + + if (color.a <= 0.0) + discard; - float xalpha = aastep(0.0, 0.0, f_uv.x); - float yalpha = aastep(0.0, 0.0, f_uv.y); - vec4 color = get_color(f_color, colormap, colorrange); if (picking) { if (color.a > 0.1) { - fragment_color = pack_int(object_id, frag_instance_id); + fragment_color = pack_int(object_id, f_instance_id); } return; } @@ -178,40 +889,67 @@ function lines_fragment_shader(uniforms, attributes) { `; } -function create_line_material(scene, uniforms, attributes) { +function create_line_material(scene, uniforms, attributes, is_linesegments) { const uniforms_des = deserialize_uniforms(scene, uniforms); const mat = new THREE.RawShaderMaterial({ uniforms: uniforms_des, glslVersion: THREE.GLSL3, - vertexShader: linesegments_vertex_shader(uniforms_des, attributes), + vertexShader: lines_vertex_shader( + uniforms_des, + attributes, + is_linesegments + ), fragmentShader: lines_fragment_shader(uniforms_des, attributes), - transparent: true, + transparent: true, // Enable transparency + blending: THREE.CustomBlending, + blendSrc: THREE.SrcAlphaFactor, + blendDst: THREE.OneMinusSrcAlphaFactor, + blendSrcAlpha: THREE.ZeroFactor, + blendDstAlpha: THREE.OneFactor, + blendEquation: THREE.AddEquation, }); mat.uniforms.object_id = { value: 1 }; return mat; } -function attach_interleaved_line_buffer(attr_name, geometry, points, ndim, is_segments) { +function attach_interleaved_line_buffer(attr_name, geometry, data, ndim, is_segments, is_position) { + // Buffer required generated + // linepoint prev, start, end, next all + // color start, end start, end + // lastlen start start, end + // linewidth start, end* start, end + // * used but not strictly needed + const skip_elems = is_segments ? 2 * ndim : ndim; - const buffer = new THREE.InstancedInterleavedBuffer(points, skip_elems, 1); + const buffer = new THREE.InstancedInterleavedBuffer(data, skip_elems, 1); + buffer.count = Math.max(0, is_segments ? Math.floor(buffer.count - 1) : buffer.count - 3); + geometry.setAttribute( attr_name + "_start", - new THREE.InterleavedBufferAttribute(buffer, ndim, 0) + new THREE.InterleavedBufferAttribute(buffer, ndim, ndim) ); // xyz1 geometry.setAttribute( attr_name + "_end", - new THREE.InterleavedBufferAttribute(buffer, ndim, ndim) - ); // xyz1 + new THREE.InterleavedBufferAttribute(buffer, ndim, 2 * ndim) + ); // xyz2 + + if (is_position) { + geometry.setAttribute( + attr_name + "_prev", + new THREE.InterleavedBufferAttribute(buffer, ndim, 0) + ); // xyz0 + geometry.setAttribute( + attr_name + "_next", + new THREE.InterleavedBufferAttribute(buffer, ndim, 3 * ndim) + ); // xyz3 + } return buffer; } function create_line_instance_geometry() { const geometry = new THREE.InstancedBufferGeometry(); - const instance_positions = [ - 0, -0.5, 1, -0.5, 1, 0.5, - - 0, -0.5, 1, 0.5, 0, 0.5, - ]; + // (-1, -1) to (+1, +1) quad + const instance_positions = [-1,-1, 1,-1, 1,1, -1,-1, 1,1, -1,1]; geometry.setAttribute( "position", new THREE.Float32BufferAttribute(instance_positions, 2) @@ -223,7 +961,7 @@ function create_line_instance_geometry() { return geometry; } -function create_line_buffer(geometry, buffers, name, attr, is_segments) { +function create_line_buffer(geometry, buffers, name, attr, is_segments, is_position) { const flat_buffer = attr.value.flat; const ndims = attr.value.type_length; const linebuffer = attach_interleaved_line_buffer( @@ -231,7 +969,8 @@ function create_line_buffer(geometry, buffers, name, attr, is_segments) { geometry, flat_buffer, ndims, - is_segments + is_segments, + is_position ); buffers[name] = linebuffer; return flat_buffer; @@ -240,38 +979,25 @@ function create_line_buffer(geometry, buffers, name, attr, is_segments) { function create_line_buffers(geometry, buffers, attributes, is_segments) { for (let name in attributes) { const attr = attributes[name]; - create_line_buffer(geometry, buffers, name, attr, is_segments); + create_line_buffer(geometry, buffers, name, attr, is_segments, name == "linepoint"); } } function attach_updates(mesh, buffers, attributes, is_segments) { - let geometry = mesh.geometry; for (let name in attributes) { const attr = attributes[name]; - attr.on((new_points) => { + attr.on((new_vertex_data) => { let buff = buffers[name]; - const ndims = new_points.type_length; - const new_line_points = new_points.flat; - const old_count = buff.array.length; - const new_count = new_line_points.length / ndims; - if (old_count < new_line_points.length) { + const new_flat_data = new_vertex_data.flat; + const old_length = buff.array.length; + if (old_length != new_flat_data.length) { mesh.geometry.dispose(); - geometry = create_line_instance_geometry(); - buff = attach_interleaved_line_buffer( - name, - geometry, - new_line_points, - ndims, - is_segments - ); - mesh.geometry = geometry; - buffers[name] = buff; + mesh.geometry = create_line_instance_geometry(); + create_line_buffers(mesh.geometry, buffers, attributes, is_segments); + mesh.geometry.instanceCount = mesh.geometry.attributes.linepoint_start.count; } else { - buff.set(new_line_points); + buff.set(new_flat_data); } - const ls_factor = is_segments ? 2 : 1; - const offset = is_segments ? 0 : 1; - mesh.geometry.instanceCount = Math.max(0, (new_count / ls_factor) - offset); buff.needsUpdate = true; mesh.needsUpdate = true; }); @@ -290,14 +1016,17 @@ export function _create_line(scene, line_data, is_segments) { const material = create_line_material( scene, line_data.uniforms, - geometry.attributes + geometry.attributes, + is_segments ); - material.uniforms.is_segments_multi = {value: is_segments ? 2 : 1}; + material.depthTest = !line_data.overdraw.value; + material.depthWrite = !line_data.transparency.value; + + material.uniforms.is_linesegments = {value: is_segments}; const mesh = new THREE.Mesh(geometry, material); - const offset = is_segments ? 0 : 1; - const new_count = geometry.attributes.linepoint_start.count; - mesh.geometry.instanceCount = Math.max(0, new_count - offset); + mesh.geometry.instanceCount = geometry.attributes.linepoint_start.count; + attach_updates(mesh, buffers, line_data.attributes, is_segments); return mesh; } diff --git a/WGLMakie/src/WGLMakie.jl b/WGLMakie/src/WGLMakie.jl index 8eb0d8089b7..cba970079df 100644 --- a/WGLMakie/src/WGLMakie.jl +++ b/WGLMakie/src/WGLMakie.jl @@ -27,6 +27,7 @@ using Makie: attribute_per_char, layout_text using Makie: MouseButtonEvent, KeyEvent using Makie: apply_transform, transform_func_obs using Makie: spaces, is_data_space, is_pixel_space, is_relative_space, is_clip_space +using Makie: apply_transform_and_f32_conversion, f32_conversion_obs, f32_convert struct WebGL <: ShaderAbstractions.AbstractContext end @@ -42,6 +43,7 @@ include("lines.jl") include("meshes.jl") include("imagelike.jl") include("picking.jl") +include("voxel.jl") const LAST_INLINE = Base.RefValue{Union{Automatic, Bool}}(Makie.automatic) diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl index e5417f19fe7..fbac404ff42 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -9,10 +9,15 @@ nothing_or_color(c::Nothing) = RGBAf(0, 0, 0, 1) function create_shader(mscene::Scene, plot::Surface) # TODO OWN OPTIMIZED SHADER ... Or at least optimize this a bit more ... px, py, pz = plot[1], plot[2], plot[3] - grid(x, y, z, trans, space) = Makie.matrix_grid(p-> apply_transform(trans, p, space), x, y, z) - + function grid(x, y, z, f32c, trans, space) + Makie.matrix_grid(p -> f32_convert(f32c, apply_transform(trans, p, space), space), x, y, z) + end # TODO: Use Makie.surface2mesh - ps = lift(grid, plot, px, py, pz, transform_func_obs(plot), get(plot, :space, :data)) + ps = lift( + plot, px, py, pz, f32_conversion_obs(mscene), transform_func_obs(plot), get(plot, :space, :data) + ) do x, y, z, f32c, tf, space + return grid(x, y, z, f32c, tf, space) + end positions = Buffer(ps) rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), plot, pz) fs = lift(r -> decompose(QuadFace{Int}, r), plot, rect) @@ -96,9 +101,8 @@ end -xy_convert(x::AbstractArray{Float32}, n) = copy(x) -xy_convert(x::AbstractArray, n) = el32convert(x) -xy_convert(x, n) = Float32[LinRange(extrema(x)..., n + 1);] +xy_convert(x::AbstractArray, n) = copy(x) +xy_convert(x, n) = [LinRange(extrema(x)..., n + 1);] # TODO, speed up GeometryBasics function fast_faces(nvertices) @@ -135,21 +139,30 @@ function limits_to_uvmesh(plot) # TODO, this branch is only hit by Image, but not for Heatmap with stepranges # because convert_arguments converts x/y to Vector{Float32} - if px[] isa StepRangeLen && py[] isa StepRangeLen && Makie.is_identity_transform(t) + if px[] isa StepRangeLen && py[] isa StepRangeLen && Makie.is_identity_transform(t) && + isnothing(f32_conversion(plot)) rect = lift(plot, px, py) do x, y xmin, xmax = extrema(x) ymin, ymax = extrema(y) - return Rect2(xmin, ymin, xmax - xmin, ymax - ymin) + return Rect2f(xmin, ymin, xmax - xmin, ymax - ymin) end positions = Buffer(lift(rect -> decompose(Point2f, rect), plot, rect)) faces = Buffer(lift(rect -> decompose(GLTriangleFace, rect), plot, rect)) uv = Buffer(lift(decompose_uv, plot, rect)) else - function grid(x, y, trans, space) - return Makie.matrix_grid(p -> apply_transform(trans, p, space), x, y, zeros(length(x), length(y))) + # TODO: Use Makie.surface2mesh + function grid(x, y, f32c, trans, space) + return Makie.matrix_grid( + p -> f32_convert(f32c, apply_transform(trans, p, space), space), + x, y, zeros(length(x), length(y)) + ) end resolution = lift((x, y) -> (length(x), length(y)), plot, px, py; ignore_equal_values=true) - positions = Buffer(lift(grid, plot, px, py, t, get(plot, :space, :data))) + positions = Buffer(lift( + plot, px, py, f32_conversion_obs(plot), t, get(plot, :space, :data) + ) do x, y, f32c, tf, space + return grid(x, y, f32c, tf, space) + end) faces = Buffer(lift(fast_faces, plot, resolution)) uv = Buffer(lift(fast_uv, plot, resolution)) end diff --git a/WGLMakie/src/lines.jl b/WGLMakie/src/lines.jl index a69e160135b..ea0c0b2bb8a 100644 --- a/WGLMakie/src/lines.jl +++ b/WGLMakie/src/lines.jl @@ -1,11 +1,21 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) - Makie.@converted_attribute plot (linewidth,) + Makie.@converted_attribute plot (linewidth, linestyle) + uniforms = Dict( - :model => plot.model, + :model => map(Makie.patch_model, f32_conversion_obs(plot), plot.model), :depth_shift => plot.depth_shift, - :picking => false, + :picking => false ) + # TODO: maybe convert nothing to Sampler([-1.0]) to allowed dynamic linestyles? + if isnothing(to_value(linestyle)) + uniforms[:pattern] = false + uniforms[:pattern_length] = 1f0 + else + uniforms[:pattern] = Sampler(lift(Makie.linestyle_to_sdf, plot, linestyle); x_repeat=:repeat) + uniforms[:pattern_length] = lift(ls -> Float32(last(ls) - first(ls)), linestyle) + end + color = plot.calculated_colors if color[] isa Makie.ColorMapping uniforms[:colormap] = Sampler(color[].colormap) @@ -18,18 +28,105 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) for name in [:nan_color, :highclip, :lowclip] uniforms[name] = RGBAf(0, 0, 0, 0) end + get!(uniforms, :colormap, false) + get!(uniforms, :colorrange, false) + end + + # This is mostly NaN handling. The shader only draws a segment if each + # involved point are not NaN, i.e. p1 -- p2 is only drawn if all of + # (p0, p1, p2, p3) are not NaN. So if p3 is NaN we need to dublicate p2 to + # make the p1 -- p2 segment draw, which is what indices does. + indices = Observable(Int[]) + points_transformed = lift( + plot, f32_conversion_obs(scene), transform_func_obs(plot), plot[1], plot.space + ) do f32c, tf, ps, space + + transformed_points = apply_transform_and_f32_conversion(f32c, tf, ps, space) + # TODO: Do this in javascript? + if isempty(transformed_points) + empty!(indices[]) + notify(indices) + return transformed_points + else + sizehint!(empty!(indices[]), length(transformed_points) + 2) + was_nan = true + for i in eachindex(transformed_points) + # dublicate first and last element of line selection + if isnan(transformed_points[i]) + if !was_nan + push!(indices[], i-1) # end of line dublication + end + was_nan = true + elseif was_nan + push!(indices[], i) # start of line dublication + was_nan = false + end + + push!(indices[], i) + end + push!(indices[], length(transformed_points)) + notify(indices) + + return transformed_points[indices[]] + end end - points_transformed = lift(apply_transform, plot, transform_func_obs(plot), plot[1], plot.space) positions = lift(serialize_buffer_attribute, plot, points_transformed) attributes = Dict{Symbol, Any}(:linepoint => positions) + + # TODO: in Javascript + # NOTE: clip.w needs to be available in shaders to avoid line inversion problems + # if transformations are done on the CPU (compare with GLMakie) + # This calculates the cumulative pixel-space distance of each point from the + # last start point of a line. (I.e. from either the first point or the first + # point after the last NaN) + if plot isa Lines && to_value(linestyle) isa Vector + cam = Makie.parent_scene(plot).camera + pvm = lift(plot, cam.projectionview, cam.pixel_space, plot.space, uniforms[:model]) do _, _, space, model + return Makie.space_to_clip(cam, space, true) * model + end + attributes[:lastlen] = map(plot, points_transformed, pvm, cam.resolution) do ps, pvm, res + output = Vector{Float32}(undef, length(ps)) + + if !isempty(ps) + # clip -> pixel, but we can skip offset + scale = Vec2f(0.5 * res[1], 0.5 * res[2]) + # Initial position + clip = pvm * to_ndim(Point4f, to_ndim(Point3f, ps[1], 0f0), 1f0) + prev = scale .* Point2f(clip) ./ clip[4] + + # calculate cumulative pixel scale length + output[1] = 0f0 + for i in 2:length(ps) + clip = pvm * to_ndim(Point4f, to_ndim(Point3f, ps[i], 0f0), 1f0) + current = scale .* Point2f(clip) ./ clip[4] + l = norm(current - prev) + output[i] = ifelse(isnan(l), 0f0, output[i-1] + l) + prev = current + end + end + + return serialize_buffer_attribute(output) + end + else + attributes[:lastlen] = map(plot, points_transformed) do ps + return serialize_buffer_attribute(zeros(Float32, length(ps))) + end + end + for (name, attr) in [:color => color, :linewidth => linewidth] if Makie.is_scalar_attribute(to_value(attr)) uniforms[Symbol("$(name)_start")] = attr uniforms[Symbol("$(name)_end")] = attr else - attributes[name] = lift(serialize_buffer_attribute, plot, attr) + # TODO: to js? + # dublicates per vertex attributes to match positional dublication + # min(idxs, end) avoids update order issues here + attributes[name] = lift(plot, indices, attr) do idxs, vals + serialize_buffer_attribute(vals[min.(idxs, end)]) + end end end + attr = Dict( :name => string(Makie.plotkey(plot)) * "-" * string(objectid(plot)), :visible => plot.visible, @@ -38,7 +135,9 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) :cam_space => plot.space[], :uniforms => serialize_uniforms(uniforms), :uniform_updater => uniform_updater(plot, uniforms), - :attributes => attributes + :attributes => attributes, + :transparency => plot.transparency, + :overdraw => plot.overdraw ) return attr end diff --git a/WGLMakie/src/mapbox-lines.vert b/WGLMakie/src/mapbox-lines.vert deleted file mode 100644 index d2912bc4d67..00000000000 --- a/WGLMakie/src/mapbox-lines.vert +++ /dev/null @@ -1,74 +0,0 @@ -#version 300 es -precision highp float; -// floor(127 / 2) == 63.0 -// the maximum allowed miter limit is 2.0 at the moment. the extrude normal is -// stored in a byte (-128..127). we scale regular normals up to length 63, but -// there are also "special" normals that have a bigger length (of up to 126 in -// this case). -// #define scale 63.0 -#define EXTRUDE_SCALE 0.015873016 - -in vec2 a_pos_normal; -in vec4 a_data; - -uniform mat4 u_matrix; -uniform mat2 u_pixels_to_tile_units; -uniform vec2 u_units_to_pixels; -uniform lowp float u_device_pixel_ratio; - -out vec2 v_normal; -out vec2 v_width2; -out float v_gamma_scale; - -lowp float floorwidth = 1.0; -mediump float gapwidth = 0.0; -lowp float offset = 0.0; -float width = 1.0; - -void main() { - - // the distance over which the line edge fades out. - // Retina devices need a smaller distance to avoid aliasing. - float ANTIALIASING = 1.0 / u_device_pixel_ratio / 2.0; - - vec2 a_extrude = a_data.xy - 128.0; - float a_direction = mod(a_data.z, 4.0) - 1.0; - vec2 pos = floor(a_pos_normal * 0.5); - - // x is 1 if it's a round cap, 0 otherwise - // y is 1 if the normal points up, and -1 if it points down - // We store these in the least significant bit of a_pos_normal - mediump vec2 normal = a_pos_normal - 2.0 * pos; - normal.y = normal.y * 2.0 - 1.0; - v_normal = normal; - - // these transformations used to be applied in the JS and native code bases. - // moved them into the shader for clarity and simplicity. - gapwidth = gapwidth / 2.0; - float halfwidth = width / 2.0; - offset = -1.0 * offset; - - float inset = gapwidth + (gapwidth > 0.0 ? ANTIALIASING : 0.0); - float outset = gapwidth + halfwidth * (gapwidth > 0.0 ? 2.0 : 1.0) + (halfwidth == 0.0 ? 0.0 : ANTIALIASING); - - // Scale the extrusion vector down to a normal and then up by the line width - // of this vertex. - mediump vec2 dist = outset * a_extrude * EXTRUDE_SCALE; - - // Calculate the offset when drawing a line that is to the side of the actual line. - // We do this by creating a vector that points towards the extrude, but rotate - // it when we're drawing round end points (a_direction = -1 or 1) since their - // extrude vector points in another direction. - mediump float u = 0.5 * a_direction; - mediump float t = 1.0 - abs(u); - mediump vec2 offset2 = offset * a_extrude * EXTRUDE_SCALE * normal.y * mat2(t, -u, u, t); - - vec4 projected_extrude = u_matrix * vec4(dist * u_pixels_to_tile_units, 0.0, 0.0); - gl_Position = u_matrix * vec4(pos + offset2 * u_pixels_to_tile_units, 0.0, 1.0) + projected_extrude; - - - v_gamma_scale = 1.0; - - v_width2 = vec2(outset, inset); - -} diff --git a/WGLMakie/src/meshes.jl b/WGLMakie/src/meshes.jl index ded9dee2ab7..207ae02a05e 100644 --- a/WGLMakie/src/meshes.jl +++ b/WGLMakie/src/meshes.jl @@ -1,10 +1,10 @@ -function vertexbuffer(x, trans, space) +function vertexbuffer(x, f32c, trans, space) pos = decompose(Point, x) - return apply_transform(trans, pos, space) + return apply_transform_and_f32_conversion(f32c, trans, pos, space) end function vertexbuffer(x::Observable, @nospecialize(p)) - return Buffer(lift(vertexbuffer, p, x, transform_func_obs(p), get(p, :space, :data))) + return Buffer(lift(vertexbuffer, p, x, f32_conversion_obs(p), transform_func_obs(p), get(p, :space, :data))) end facebuffer(x) = faces(x) @@ -63,18 +63,19 @@ function draw_mesh(mscene::Scene, per_vertex, plot, uniforms; permute_tex=true) handle_color!(plot, uniforms, per_vertex; permute_tex=permute_tex) get!(uniforms, :pattern, false) - get!(uniforms, :model, plot.model) get!(uniforms, :ambient, Vec3f(1)) get!(uniforms, :light_direction, Vec3f(1)) get!(uniforms, :light_color, Vec3f(1)) + uniforms[:model] = map(Makie.patch_model, f32_conversion_obs(plot), plot.model) + uniforms[:interpolate_in_fragment_shader] = get(plot, :interpolate_in_fragment_shader, true) get!(uniforms, :shading, to_value(get(plot, :shading, NoShading)) != NoShading) - uniforms[:normalmatrix] = map(plot.model) do m + uniforms[:normalmatrix] = map(uniforms[:model]) do m i = Vec(1, 2, 3) - return transpose(inv(m[i, i])) + return Mat3f(transpose(inv(m[i, i]))) end diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index ae087c43a02..e873b02f533 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -36,21 +36,22 @@ function handle_color_getter!(uniform_dict, per_instance) end const IGNORE_KEYS = Set([ - :shading, :overdraw, :rotation, :distancefield, :space, :markerspace, :fxaa, + :shading, :overdraw, :distancefield, :space, :markerspace, :fxaa, :visible, :transformation, :alpha, :linewidth, :transparency, :marker, :light_direction, :light_color, :cycle, :label, :inspector_clear, :inspector_hover, - :inspector_label, :axis_cycler + :inspector_label, :axis_cycler, :userdata + # TODO add model here since we generally need to apply patch_model? ]) function create_shader(scene::Scene, plot::MeshScatter) # Potentially per instance attributes - per_instance_keys = (:rotations, :markersize, :intensity) + per_instance_keys = (:rotation, :markersize, :intensity) per_instance = filter(plot.attributes.attributes) do (k, v) return k in per_instance_keys && !(isscalar(v[])) end - per_instance[:offset] = lift(apply_transform, plot, transform_func_obs(plot), plot[1], plot.space) + per_instance[:offset] = apply_transform_and_f32_conversion(scene, plot, plot[1]) for (k, v) in per_instance per_instance[k] = Buffer(lift_convert(k, v, plot)) @@ -92,6 +93,8 @@ function create_shader(scene::Scene, plot::MeshScatter) uniform_dict[:object_id] = UInt32(0) uniform_dict[:shading] = map(x -> x != NoShading, plot.shading) + uniform_dict[:model] = map(Makie.patch_model, f32_conversion_obs(plot), plot.model) + return InstancedProgram(WebGL(), lasset("particles.vert"), lasset("particles.frag"), instance, VertexArray(; per_instance...), uniform_dict) end @@ -123,7 +126,7 @@ end function scatter_shader(scene::Scene, attributes, plot) # Potentially per instance attributes - per_instance_keys = (:pos, :rotations, :markersize, :color, :intensity, + per_instance_keys = (:pos, :rotation, :markersize, :color, :intensity, :uv_offset_width, :quad_offset, :marker_offset) uniform_dict = Dict{Symbol,Any}() uniform_dict[:image] = false @@ -193,7 +196,7 @@ function scatter_shader(scene::Scene, attributes, plot) to_value(per_instance[:color]) isa Bool && delete!(per_instance, :color) end - instance = uv_mesh(Rect2(-0.5f0, -0.5f0, 1f0, 1f0)) + instance = uv_mesh(Rect2f(-0.5f0, -0.5f0, 1f0, 1f0)) # Don't send obs, since it's overwritten in JS to be updated by the camera uniform_dict[:resolution] = to_value(scene.camera.resolution) uniform_dict[:px_per_unit] = 1f0 @@ -217,13 +220,13 @@ function create_shader(scene::Scene, plot::Scatter) attributes = copy(plot.attributes.attributes) space = get(attributes, :space, :data) attributes[:preprojection] = Mat4f(I) # calculate this in JS - attributes[:pos] = lift(apply_transform, plot, transform_func_obs(plot), plot[1], space) + attributes[:pos] = apply_transform_and_f32_conversion(scene, plot, plot[1], space) quad_offset = get(attributes, :marker_offset, Observable(Vec2f(0))) attributes[:marker_offset] = Vec3f(0) attributes[:quad_offset] = quad_offset - attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotations) - attributes[:model] = plot.model + attributes[:billboard] = lift(rot -> isa(rot, Billboard), plot, plot.rotation) + attributes[:model] = map(Makie.patch_model, f32_conversion_obs(plot), plot.model) attributes[:depth_shift] = get(plot, :depth_shift, Observable(0f0)) delete!(attributes, :uv_offset_width) @@ -238,14 +241,15 @@ value_or_first(x) = x function create_shader(scene::Scene, plot::Makie.Text{<:Tuple{<:Union{<:Makie.GlyphCollection, <:AbstractVector{<:Makie.GlyphCollection}}}}) glyphcollection = plot[1] - transfunc = Makie.transform_func_obs(plot) + f32c = f32_conversion_obs(plot) + transfunc = transform_func_obs(plot) pos = plot.position space = plot.space offset = plot.offset atlas = wgl_texture_atlas() - glyph_data = lift(plot, pos, glyphcollection, offset, transfunc, space; ignore_equal_values=true) do pos, gc, offset, transfunc, space - Makie.text_quads(atlas, pos, to_value(gc), offset, transfunc, space) + glyph_data = lift(plot, pos, glyphcollection, offset, f32c, transfunc, space; ignore_equal_values=true) do pos, gc, offset, f32c, transfunc, space + Makie.text_quads(atlas, pos, to_value(gc), offset, f32c, transfunc, space) end # unpack values from the one signal: @@ -274,9 +278,9 @@ function create_shader(scene::Scene, plot::Makie.Text{<:Tuple{<:Union{<:Makie.Gl plot_attributes = copy(plot.attributes) plot_attributes.attributes[:calculated_colors] = uniform_color uniforms = Dict( - :model => plot.model, + :model => map(Makie.patch_model, f32_conversion_obs(plot), plot.model), :shape_type => Observable(Cint(3)), - :rotations => uniform_rotation, + :rotation => uniform_rotation, :pos => positions, :marker_offset => char_offset, :quad_offset => quad_offset, diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl index 0bd6ad2d261..3769e3b1f53 100644 --- a/WGLMakie/src/serialization.jl +++ b/WGLMakie/src/serialization.jl @@ -71,13 +71,20 @@ function serialize_three(p::Makie.AbstractPattern) return serialize_three(Makie.to_image(p)) end +three_format(::Type{<:Integer}) = "RedIntegerFormat" three_format(::Type{<:Real}) = "RedFormat" three_format(::Type{<:RGB}) = "RGBFormat" three_format(::Type{<:RGBA}) = "RGBAFormat" +three_format(::Type{<: Makie.VecTypes{1}}) = "RedFormat" +three_format(::Type{<: Makie.VecTypes{2}}) = "RGFormat" +three_format(::Type{<: Makie.VecTypes{3}}) = "RGBFormat" +three_format(::Type{<: Makie.VecTypes{4}}) = "RGBAFormat" + three_type(::Type{Float16}) = "FloatType" three_type(::Type{Float32}) = "FloatType" three_type(::Type{N0f8}) = "UnsignedByteType" +three_type(::Type{UInt8}) = "UnsignedByteType" function three_filter(sym::Symbol) sym === :linear && return "LinearFilter" diff --git a/WGLMakie/src/voxel.jl b/WGLMakie/src/voxel.jl new file mode 100644 index 00000000000..743bee5e5a1 --- /dev/null +++ b/WGLMakie/src/voxel.jl @@ -0,0 +1,104 @@ +function create_shader(scene::Scene, plot::Makie.Voxels) + uniform_dict = Dict{Symbol, Any}() + uniform_dict[:voxel_id] = Sampler(plot.converted[end], minfilter = :nearest) + # for plane sorting + uniform_dict[:depthsorting] = plot.depthsorting + uniform_dict[:eyeposition] = Vec3f(1) + uniform_dict[:view_direction] = camera(scene).view_direction + # lighting + uniform_dict[:diffuse] = lift(x -> convert_attribute(x, Key{:diffuse}()), plot, plot.diffuse) + uniform_dict[:specular] = lift(x -> convert_attribute(x, Key{:specular}()), plot, plot.specular) + uniform_dict[:shininess] = lift(x -> convert_attribute(x, Key{:shininess}()), plot, plot.shininess) + uniform_dict[:depth_shift] = get(plot, :depth_shift, Observable(0.0f0)) + uniform_dict[:light_direction] = Vec3f(1) + uniform_dict[:light_color] = Vec3f(1) + uniform_dict[:ambient] = Vec3f(1) + # picking + uniform_dict[:picking] = false + uniform_dict[:object_id] = UInt32(0) + # other + uniform_dict[:normalmatrix] = map(plot.model) do m + # should be fine to ignore placement matrix here because + # translation is ignored and scale shouldn't matter + i = Vec(1, 2, 3) + return Mat3f(transpose(inv(m[i, i]))) + end + uniform_dict[:shading] = to_value(get(plot, :shading, NoShading)) != NoShading + uniform_dict[:gap] = lift(x -> convert_attribute(x, Key{:gap}(), Key{:voxels}()), plot.gap) + + # TODO: localized update + # buffer = Vector{UInt8}(undef, 1) + on(plot, plot._local_update) do (is, js, ks) + # required_length = length(is) * length(js) * length(ks) + # if length(buffer) < required_length + # resize!(buffer, required_length) + # end + # idx = 1 + # for k in ks, j in js, i in is + # buffer[idx] = plot.converted[end].val[i, j, k] + # idx += 1 + # end + # GLAbstraction.texsubimage(tex, buffer, is, js, ks) + notify(plot.converted[end]) + return + end + + # adjust model matrix with placement matrix + uniform_dict[:model] = map( + plot, plot.converted..., plot.model + ) do xs, ys, zs, chunk, model + mini = minimum.((xs, ys, zs)) + width = maximum.((xs, ys, zs)) .- mini + # with f32convert applying to 3D plots patch_model should apply to all of this + return Mat4f(model) * + Makie.scalematrix(Vec3f(width ./ size(chunk))) * + Makie.translationmatrix(Vec3f(mini)) + end + + maybe_color_mapping = plot.calculated_colors[] + uv_map = plot.uvmap + if maybe_color_mapping isa Makie.ColorMapping + uniform_dict[:color_map] = Sampler(maybe_color_mapping.colormap, minfilter = :nearest) + uniform_dict[:uv_map] = false + uniform_dict[:color] = false + elseif !isnothing(to_value(uv_map)) + uniform_dict[:color_map] = false + # WebGL doesn't have sampler1D so we need to pad id -> uv mappings to + # (id, side) -> uv mappings + wgl_uv_map = map(plot, uv_map) do uv_map + if uv_map isa Vector + new_map = Matrix{Vec4f}(undef, length(uv_map), 6) + for col in 1:6 + new_map[:, col] .= uv_map + end + return new_map + else + return uv_map + end + end + uniform_dict[:uv_map] = Sampler(wgl_uv_map, minfilter = :nearest) + interp = to_value(plot.interpolate) ? :linear : :nearest + uniform_dict[:color] = Sampler(maybe_color_mapping, minfilter = interp) + else + uniform_dict[:color_map] = false + uniform_dict[:uv_map] = false + uniform_dict[:color] = Sampler(maybe_color_mapping, minfilter = :nearest) + end + + # TODO: this is a waste + dummy_data = Observable(Float32[]) + onany(plot, plot.gap, plot.converted[end]) do gap, chunk + N = sum(size(chunk)) + N_instances = ifelse(gap > 0.01, 2 * N, N + 3) + if N_instances != length(dummy_data[]) # avoid updating unneccesarily + dummy_data[] = [0f0 for _ in 1:N_instances] + end + return + end + notify(plot.gap) + + instance = GeometryBasics.mesh(Rect2(0f0, 0f0, 1f0, 1f0)) + + return InstancedProgram(WebGL(), lasset("voxel.vert"), lasset("voxel.frag"), + instance, VertexArray(dummy = dummy_data), uniform_dict) +end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index 4a279a8440e..54e895a3672 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -21316,81 +21316,646 @@ function deserialize_uniforms(scene, data) { } return result; } -function linesegments_vertex_shader(uniforms, attributes) { +function lines_vertex_shader(uniforms, attributes, is_linesegments) { const attribute_decl = attributes_to_type_declaration(attributes); const uniform_decl = uniforms_to_type_declaration(uniforms); const color = attribute_type(attributes.color_start) || uniform_type(uniforms.color_start); - return `precision mediump int; - precision highp float; + if (is_linesegments) { + return `precision mediump int; + precision highp float; - ${attribute_decl} - ${uniform_decl} - uniform int is_segments_multi; + ${attribute_decl} - out vec2 f_uv; - out ${color} f_color; - flat out uint frag_instance_id; - vec2 get_resolution() { - // 2 * px_per_unit doesn't make any sense, but works - // TODO, figure out what's going on! - return resolution / 2.0 * px_per_unit; - } + out highp float f_quad_sdf0; // invalid / not needed + out highp vec3 f_quad_sdf1; + out highp float f_quad_sdf2; // invalid / not needed + out vec2 f_truncation; // invalid / not needed + out float f_linestart; // constant + out float f_linelength; - vec3 screen_space(vec3 point) { - vec4 vertex = projectionview * model * vec4(point, 1); - return vec3(vertex.xy * get_resolution(), vertex.z + vertex.w * depth_shift) / vertex.w; - } + flat out vec2 f_extrusion; // invalid / not needed + flat out float f_linewidth; + flat out vec4 f_pattern_overwrite; // invalid / not needed + flat out vec2 f_discard_limit; // invalid / not needed + flat out uint f_instance_id; + flat out ${color} f_color1; + flat out ${color} f_color2; + flat out float f_alpha_weight; + flat out float f_cumulative_length; - vec3 screen_space(vec2 point) { - return screen_space(vec3(point, 0)); - } + ${uniform_decl} - void main() { - vec3 p_a = screen_space(linepoint_start); - vec3 p_b = screen_space(linepoint_end); - float width = (px_per_unit * (position.x == 1.0 ? linewidth_end : linewidth_start)); - f_color = position.x == 1.0 ? color_end : color_start; - f_uv = vec2(position.x, position.y + 0.5); + // Constants + const float AA_RADIUS = 0.8; + const float AA_THICKNESS = 2.0 * AA_RADIUS; - vec2 pointA = p_a.xy; - vec2 pointB = p_b.xy; - vec2 xBasis = pointB - pointA; - vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); - vec2 point = pointA + xBasis * position.x + yBasis * width * position.y; + //////////////////////////////////////////////////////////////////////// + // Geometry/Position Utils + //////////////////////////////////////////////////////////////////////// - gl_Position = vec4(point.xy / get_resolution(), position.x == 1.0 ? p_b.z : p_a.z, 1.0); - frag_instance_id = uint((gl_InstanceID * is_segments_multi) + int(position.x == 1.0)); - } + vec4 clip_space(vec3 point) { + return projectionview * model * vec4(point, 1); + } + vec4 clip_space(vec2 point) { return clip_space(vec3(point, 0)); } + + vec3 screen_space(vec4 vertex) { + return vec3( + (0.5 * vertex.xy / vertex.w + 0.5) * px_per_unit * resolution, + vertex.z / vertex.w + depth_shift + ); + } + + vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); } + vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); } + + + //////////////////////////////////////////////////////////////////////// + // Main + //////////////////////////////////////////////////////////////////////// + + + void main() { + bool is_end = position.x == 1.0; + + //////////////////////////////////////////////////////////////////// + // Handle line geometry (position, directions) + //////////////////////////////////////////////////////////////////// + + + float width = px_per_unit * (is_end ? linewidth_end : linewidth_start); + float halfwidth = 0.5 * max(AA_RADIUS, width); + + // restrict to visible area (see other shader) + vec3 p1, p2; + { + vec4 _p1 = clip_space(linepoint_start), _p2 = clip_space(linepoint_end); + vec4 v1 = _p2 - _p1; + + if (_p1.w < 0.0) + _p1 = _p1 + (-_p1.w - _p1.z) / (v1.z + v1.w) * v1; + if (_p2.w < 0.0) + _p2 = _p2 + (-_p2.w - _p2.z) / (v1.z + v1.w) * v1; + + p1 = screen_space(_p1); + p2 = screen_space(_p2); + } + + // line vector (xy-normalized vectors in line direction) + // Need z component for correct depth order + vec3 v1 = p2 - p1; + float segment_length = length(v1); + v1 /= segment_length; + + // line normal (i.e. in linewidth direction) + vec2 n1 = normal_vector(v1); + + + //////////////////////////////////////////////////////////////////// + // Static vertex data + //////////////////////////////////////////////////////////////////// + + + // invalid - no joints requiring pattern adjustments + f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); + + // invalid - no joints that need pixels discarded + f_discard_limit = vec2(10.0); + + // invalid - no joints requiring line sdfs to be extruded + f_extrusion = vec2(0.0); + + // used to compute width sdf + f_linewidth = halfwidth; + + f_instance_id = uint(2 * gl_InstanceID); + + // we restart patterns for each segment + f_cumulative_length = 0.0; + + + //////////////////////////////////////////////////////////////////// + // Varying vertex data + //////////////////////////////////////////////////////////////////// + + + // Vertex position (padded for joint & anti-aliasing) + float v_offset = position.x * (0.5 * segment_length + AA_THICKNESS); + float n_offset = (halfwidth + AA_THICKNESS) * position.y; + vec3 point = 0.5 * (p1 + p2) + v_offset * v1 + n_offset * vec3(n1, 0); + + // SDF's + vec2 VP1 = point.xy - p1.xy; + vec2 VP2 = point.xy - p2.xy; + + // invalid - no joint to compute overlap with + f_quad_sdf0 = 10.0; + + // sdf of this segment + f_quad_sdf1.x = dot(VP1, -v1.xy); + f_quad_sdf1.y = dot(VP2, v1.xy); + f_quad_sdf1.z = dot(VP1, n1); + + // invalid - no joint to compute overlap with + f_quad_sdf2 = 10.0; + + // invalid - no joint to truncate + f_truncation = vec2(-10.0); + + // simplified - no extrusion or joints means we just have: + f_linestart = 0.0; + f_linelength = segment_length; + + // for color sampling + f_color1 = color_start; + f_color2 = color_end; + f_alpha_weight = min(1.0, width / AA_RADIUS); + + // clip space position + gl_Position = vec4(2.0 * point.xy / (px_per_unit * resolution) - 1.0, point.z, 1.0); + } + `; + } else { + return `precision mediump int; + precision highp float; + + ${attribute_decl} + + out highp float f_quad_sdf0; + out highp vec3 f_quad_sdf1; + out highp float f_quad_sdf2; + out vec2 f_truncation; + out float f_linestart; + out float f_linelength; + + flat out vec2 f_extrusion; + flat out float f_linewidth; + flat out vec4 f_pattern_overwrite; + flat out vec2 f_discard_limit; + flat out uint f_instance_id; + flat out ${color} f_color1; + flat out ${color} f_color2; + flat out float f_alpha_weight; + flat out float f_cumulative_length; + + ${uniform_decl} + + // Constants + const float MITER_LIMIT = -0.4; + const float AA_RADIUS = 0.8; + const float AA_THICKNESS = 2.0 * AA_RADIUS; + + + //////////////////////////////////////////////////////////////////////// + // Pattern handling + //////////////////////////////////////////////////////////////////////// + + + vec2 process_pattern(bool pattern, bool[4] isvalid, vec2 extrusion, float segment_length, float halfwidth) { + // do not adjust stuff + f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); + return vec2(0); + } + + vec2 process_pattern(sampler2D pattern, bool[4] isvalid, vec2 extrusion, float segment_length, float halfwidth) { + // samples: + // -ext1 p1 ext1 -ext2 p2 ext2 + // 1 2 3 4 5 6 + // prev | joint | this | joint | next + + // default to no overwrite + f_pattern_overwrite.x = -1e12; + f_pattern_overwrite.z = +1e12; + vec2 adjust = vec2(0); + float width = 2.0 * halfwidth; + float uv_scale = 1.0 / (width * pattern_length); + float left, center, right; + + if (isvalid[0]) { + float offset = abs(extrusion[0]); + left = width * texture(pattern, vec2(uv_scale * (lastlen_start - offset), 0.0)).x; + center = width * texture(pattern, vec2(uv_scale * (lastlen_start ), 0.0)).x; + right = width * texture(pattern, vec2(uv_scale * (lastlen_start + offset), 0.0)).x; + + // cases: + // ++-, +--, +-+ => elongate backwards + // -++, --+ => shrink forward + // +++, ---, -+- => freeze around joint + + if ((left > 0.0 && center > 0.0 && right > 0.0) || (left < 0.0 && right < 0.0)) { + // default/freeze + // overwrite until one AA gap past the corner/joint + f_pattern_overwrite.x = uv_scale * (lastlen_start + abs(extrusion[0]) + AA_RADIUS); + // using the sign of the center to decide between drawing or not drawing + f_pattern_overwrite.y = sign(center); + } else if (left > 0.0) { + // elongate backwards + adjust.x = -1.0; + } else if (right > 0.0) { + // shorten forward + adjust.x = 1.0; + } else { + // default - see above + f_pattern_overwrite.x = uv_scale * (lastlen_start + abs(extrusion[0]) + AA_RADIUS); + f_pattern_overwrite.y = sign(center); + } + + } // else there is no left segment, no left join, so no overwrite + + if (isvalid[3]) { + float offset = abs(extrusion[1]); + left = width * texture(pattern, vec2(uv_scale * (lastlen_start + segment_length - offset), 0.0)).x; + center = width * texture(pattern, vec2(uv_scale * (lastlen_start + segment_length ), 0.0)).x; + right = width * texture(pattern, vec2(uv_scale * (lastlen_start + segment_length + offset), 0.0)).x; + + if ((left > 0.0 && center > 0.0 && right > 0.0) || (left < 0.0 && right < 0.0)) { + // default/freeze + f_pattern_overwrite.z = uv_scale * (lastlen_start + segment_length - abs(extrusion[1]) - AA_RADIUS); + f_pattern_overwrite.w = sign(center); + } else if (left > 0.0) { + // shrink backwards + adjust.y = -1.0; + } else if (right > 0.0) { + // elongate forward + adjust.y = 1.0; + } else { + // default - see above + f_pattern_overwrite.z = uv_scale * (lastlen_start + segment_length - abs(extrusion[1]) - AA_RADIUS); + f_pattern_overwrite.w = sign(center); + } + } + + return adjust; + } + + + //////////////////////////////////////////////////////////////////////// + // Geometry/Position Utils + //////////////////////////////////////////////////////////////////////// + + vec4 clip_space(vec3 point) { + return projectionview * model * vec4(point, 1); + } + vec4 clip_space(vec2 point) { return clip_space(vec3(point, 0)); } + + vec3 screen_space(vec4 vertex) { + return vec3( + (0.5 * vertex.xy / vertex.w + 0.5) * px_per_unit * resolution, + vertex.z / vertex.w + depth_shift + ); + } + + vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); } + vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); } + + + //////////////////////////////////////////////////////////////////////// + // Main + //////////////////////////////////////////////////////////////////////// + + + void main() { + bool is_end = position.x == 1.0; + + + //////////////////////////////////////////////////////////////////// + // Handle line geometry (position, directions) + //////////////////////////////////////////////////////////////////// + + + float width = px_per_unit * (is_end ? linewidth_end : linewidth_start); + float halfwidth = 0.5 * max(AA_RADIUS, width); + + bool[4] isvalid = bool[4](true, true, true, true); + + // To apply pixel space linewidths we transform line vertices to pixel space + // here. This is dangerous with perspective projection as p.xyz / p.w sends + // points from behind the camera to beyond far (clip z > 1), causing lines + // to invert. To avoid this we translate points along the line direction, + // moving them to the edge of the visible area. + vec3 p0, p1, p2, p3; + { + // All in clip space + vec4 clip_p0 = clip_space(linepoint_prev); + vec4 clip_p1 = clip_space(linepoint_start); + vec4 clip_p2 = clip_space(linepoint_end); + vec4 clip_p3 = clip_space(linepoint_next); + + vec4 v1 = clip_p2 - clip_p1; + + // With our perspective projection matrix clip.w = -view.z with + // clip.w < 0.0 being behind the camera. + // Note that if the signs in the projectionmatrix change, this may become wrong. + if (clip_p1.w < 0.0) { + // the line connects outside the visible area so we may consider it disconnected + isvalid[0] = false; + // A clip position is visible if -w <= z <= w. To move the line along + // the line direction v to the start of the visible area, we solve: + // p.z + t * v.z = +-(p.w + t * v.w) + // where (-) gives us the result for the near clipping plane as p.z + // and p.w share the same sign and p.z/p.w = -1.0 is the near plane. + clip_p1 = clip_p1 + (-clip_p1.w - clip_p1.z) / (v1.z + v1.w) * v1; + } + if (clip_p2.w < 0.0) { + isvalid[3] = false; + clip_p2 = clip_p2 + (-clip_p2.w - clip_p2.z) / (v1.z + v1.w) * v1; + } + + // transform clip -> screen space, applying xyz / w normalization (which + // is now save as all vertices are in front of the camera) + p0 = screen_space(clip_p0); // start of previous segment + p1 = screen_space(clip_p1); // end of previous segment, start of current segment + p2 = screen_space(clip_p2); // end of current segment, start of next segment + p3 = screen_space(clip_p3); // end of next segment + } + + // doesn't work correctly with linepoint_x... + isvalid[0] = p0 != p1; + isvalid[3] = p2 != p3; + + // line vectors (xy-normalized vectors in line direction) + // Need z component here for correct depth order + vec3 v1 = p2 - p1; + float segment_length = length(v1); + v1 /= segment_length; + + // We don't need the z component for these + vec2 v0 = v1.xy, v2 = v1.xy; + bool[2] skip_joint; + if (isvalid[0]) + v0 = normalize(p1.xy - p0.xy); + if (isvalid[3]) + v2 = normalize(p3.xy - p2.xy); + + // line normals (i.e. in linewidth direction) + vec2 n0 = normal_vector(v0); + vec2 n1 = normal_vector(v1); + vec2 n2 = normal_vector(v2); + + + //////////////////////////////////////////////////////////////////// + // Handle joint geometry + //////////////////////////////////////////////////////////////////// + + + // joint information + + // Are we truncating the joint? + bool[2] is_truncated = bool[2]( + dot(v0.xy, v1.xy) < MITER_LIMIT, + dot(v1.xy, v2.xy) < MITER_LIMIT + ); + + // Miter normals (normal of truncated edge / vector to sharp corner) + // Note: n0 + n1 = vec(0) for a 180° change in direction. +-(v0 - v1) is the + // same direction, but becomes vec(0) at 0°, so we can use it instead + vec2 miter_n1 = is_truncated[0] ? normalize(v0.xy - v1.xy) : normalize(n0 + n1); + vec2 miter_n2 = is_truncated[1] ? normalize(v1.xy - v2.xy) : normalize(n1 + n2); + + // miter vectors (line vector matching miter normal) + vec2 miter_v1 = -normal_vector(miter_n1); + vec2 miter_v2 = -normal_vector(miter_n2); + + // distance between p1/2 and respective sharp corner + float miter_offset1 = dot(miter_n1, n1); // = dot(miter_v1, v1) + float miter_offset2 = dot(miter_n2, n1); // = dot(miter_v2, v1) + + // How far the line needs to extend to accomodate the joint. + // These are calculated as prefactors to v1 so that the line quad + // is given by: + // p1 + w * extrusion[0] * v1 ----- p2 + w * extrusion[1] * v1 + // | | + // p1 + w * extrusion[0] * v1 ----- p2 + w * extrusion[1] * v1 + // with w = halfwidth for drawn corners and w = halfwidth + AA_THICKNESS + // for the corners of quad. The sign difference due to miter joints + // is included based on the current vertex position (position.y). + // (truncated miter joints do not differ here) + vec2 extrusion; + + if (is_truncated[0]) { + // need to extend segment to include previous segments corners for truncated join + extrusion[0] = -abs(miter_offset1 / dot(miter_v1, n1)); + } else { + // shallow/spike join needs to include point where miter normal meets outer line edge + extrusion[0] = position.y * dot(miter_n1, v1.xy) / miter_offset1; + } + + if (is_truncated[1]) { + extrusion[1] = abs(miter_offset2 / dot(miter_n2, v1.xy)); + } else { + extrusion[1] = position.y * dot(miter_n2, v1.xy) / miter_offset2; + } + + + //////////////////////////////////////////////////////////////////// + // Joint adjustments + //////////////////////////////////////////////////////////////////// + + + // Miter joints can cause vertices to move past each other, e.g. + // _______ + // '. .' + // x + // '---' + // To avoid drawing the "inverted" section we move the relevant + // vertices to the crossing point (x) using this scaling factor. + float shape_factor = max( + 0.0, + segment_length / max( + segment_length, + (halfwidth + AA_THICKNESS) * (extrusion[0] - extrusion[1]) + ) + ); + + // If a pattern starts or stops drawing in a joint it will get + // fractured across the joint. To avoid this we either: + // - adjust the involved line segments so that the patterns ends + // on straight line quad (adjustment becomes +1.0 or -1.0) + // - or adjust the pattern to start/stop outside of the joint + // (f_pattern_overwrite is set, adjustment is 0.0) + vec2 adjustment = process_pattern( + pattern, isvalid, halfwidth * extrusion, segment_length, halfwidth + ); + + // If adjustment != 0.0 we replace a joint by an extruded line, + // so we no longer need to shrink the line for the joint to fit. + if (adjustment[0] != 0.0 || adjustment[1] != 0.0) + shape_factor = 1.0; + + //////////////////////////////////////////////////////////////////// + // Static vertex data + //////////////////////////////////////////////////////////////////// + + + // For truncated miter joints we discard overlapping sections of + // the two involved line segments. To avoid discarding far into + // the line segment we limit the range here. (Without this short + // segments can cut holes into longer sections.) + f_discard_limit = vec2( + is_truncated[0] ? 0.0 : 1e12, + is_truncated[1] ? 0.0 : 1e12 + ); + + // Used to elongate sdf to include joints + // if start/end elongate slightly so that there is no AA gap in loops + // if joint skipped elongate to new length + // if normal joint elongate a lot to let shape/truncation handle joint + f_extrusion = vec2( + !isvalid[0] ? min(AA_RADIUS, halfwidth) : (adjustment[0] == 0.0 ? 1e12 : halfwidth * abs(extrusion[0])), + !isvalid[3] ? min(AA_RADIUS, halfwidth) : (adjustment[1] == 0.0 ? 1e12 : halfwidth * abs(extrusion[1])) + ); + + // used to compute width sdf + f_linewidth = halfwidth; + + f_instance_id = uint(gl_InstanceID); + + f_cumulative_length = lastlen_start; + + + //////////////////////////////////////////////////////////////////// + // Varying vertex data + //////////////////////////////////////////////////////////////////// + + + vec3 offset; + int x = int(is_end); + if (adjustment[x] == 0.0) { + if (is_truncated[x] || !isvalid[3 * x]) { + // handle overlap in fragment shader via SDF comparison + offset = shape_factor * ( + (halfwidth * extrusion[x] + position.x * AA_THICKNESS) * v1 + + vec3(position.y * (halfwidth + AA_THICKNESS) * n1, 0) + ); + } else { + // handle overlap by adjusting geometry + // TODO: should this include z in miter_n? + offset = position.y * shape_factor * + (halfwidth + AA_THICKNESS) / + float[2](miter_offset1, miter_offset2)[x] * + vec3(vec2[2](miter_n1, miter_n2)[x], 0); + } + } else { + // discard joint for cleaner pattern handling + offset = + adjustment[x] * (halfwidth * abs(extrusion[x]) + AA_THICKNESS) * v1 + + vec3(position.y * (halfwidth + AA_THICKNESS) * n1, 0); + } + + // Vertex position (padded for joint & anti-aliasing) + vec3 point = vec3[2](p1, p2)[x] + offset; + + // SDF's + vec2 VP1 = point.xy - p1.xy; + vec2 VP2 = point.xy - p2.xy; + + // Signed distance of the previous segment from the shared point + // p1 in line direction. Used decide which segments renders + // which joint fragment/pixel for truncated joints. + if (isvalid[0] && (adjustment[0] == 0.0) && is_truncated[0]) + f_quad_sdf0 = dot(VP1, v0.xy); + else + f_quad_sdf0 = 1e12; + + // sdf of this segment + f_quad_sdf1.x = dot(VP1, -v1.xy); + f_quad_sdf1.y = dot(VP2, v1.xy); + f_quad_sdf1.z = dot(VP1, n1); + + // SDF for next segment, see quad_sdf0 + if (isvalid[3] && (adjustment[1] == 0.0) && is_truncated[1]) + f_quad_sdf2 = dot(VP2, -v2.xy); + else + f_quad_sdf2 = 1e12; + + // sdf for creating a flat cap on truncated joints + // (sign(dot(...)) detects if line bends left or right) + f_truncation.x = !is_truncated[0] ? -1.0 : + dot(VP1, sign(dot(miter_n1, -v1.xy)) * miter_n1) - halfwidth * abs(miter_offset1) + - abs(adjustment[0]) * 1e12; + f_truncation.y = !is_truncated[1] ? -1.0 : + dot(VP2, sign(dot(miter_n2, +v1.xy)) * miter_n2) - halfwidth * abs(miter_offset2) + - abs(adjustment[1]) * 1e12; + + // Colors should be sampled based on the normalized distance from the + // extruded edge (varies with offset in n direction) + // - correcting for this with per-vertex colors results visible face border + // - calculating normalized distance here will cause div 0/negative + // issues as (linelength +- (extrusion[0] + extrusion[1])) <= 0 is possible + // So defer color interpolation to fragment shader + f_linestart = shape_factor * halfwidth * extrusion[0]; + f_linelength = max(1.0, segment_length - shape_factor * halfwidth * (extrusion[0] - extrusion[1])); + + // for color sampling + f_color1 = color_start; + f_color2 = color_end; + f_alpha_weight = min(1.0, width / AA_RADIUS); + + // clip space position + gl_Position = vec4(2.0 * point.xy / (px_per_unit * resolution) - 1.0, point.z, 1.0); + } `; + } } function lines_fragment_shader(uniforms, attributes) { - const color = attribute_type(attributes.color_start) || uniform_type(uniforms.color_start); const color_uniforms = filter_by_key(uniforms, [ + "picking", + "pattern", + "pattern_length", "colorrange", "colormap", "nan_color", "highclip", - "lowclip", - "picking" + "lowclip" ]); const uniform_decl = uniforms_to_type_declaration(color_uniforms); - return `#extension GL_OES_standard_derivatives : enable + const color = attribute_type(attributes.color_start) || uniform_type(uniforms.color_start); + return ` + // uncomment for debug rendering + // #define DEBUG precision mediump int; precision highp float; precision mediump sampler2D; precision mediump sampler3D; - in vec2 f_uv; - in ${color} f_color; + in highp float f_quad_sdf0; + in highp vec3 f_quad_sdf1; + in highp float f_quad_sdf2; + in vec2 f_truncation; + in float f_linestart; + in float f_linelength; + + flat in float f_linewidth; + flat in vec4 f_pattern_overwrite; + flat in vec2 f_extrusion; + flat in vec2 f_discard_limit; + flat in ${color} f_color1; + flat in ${color} f_color2; + flat in float f_alpha_weight; + flat in uint f_instance_id; + flat in float f_cumulative_length; + + uniform uint object_id; ${uniform_decl} out vec4 fragment_color; // Half width of antialiasing smoothstep - #define ANTIALIAS_RADIUS 0.7071067811865476 + const float AA_RADIUS = 0.8; + // space allocated for AA + const float AA_THICKNESS = 2.0 * AA_RADIUS; + + float aastep(float threshold, float value) { + return smoothstep(threshold-AA_RADIUS, threshold+AA_RADIUS, value); + } + + + //////////////////////////////////////////////////////////////////////// + // Color handling + //////////////////////////////////////////////////////////////////////// + vec4 get_color_from_cmap(float value, sampler2D colormap, vec2 colorrange) { float cmin = colorrange.x; @@ -21425,17 +21990,46 @@ function lines_fragment_shader(uniforms, attributes) { return vec4(color, 1.0); } - float aastep(float threshold, float value) { - float afwidth = length(vec2(dFdx(value), dFdy(value))) * ANTIALIAS_RADIUS; - return smoothstep(threshold-afwidth, threshold+afwidth, value); - } - float aastep(float threshold1, float threshold2, float dist) { - return aastep(threshold1, dist) * aastep(threshold2, 1.0 - dist); + //////////////////////////////////////////////////////////////////////// + // Pattern sampling + //////////////////////////////////////////////////////////////////////// + + + float get_pattern_sdf(sampler2D pattern, vec2 uv){ + + // f_pattern_overwrite.x + // v joint + // ---------------- + // | | + // ---------------- + // joint ^ + // f_pattern_overwrite.z + + float w = 2.0 * f_linewidth; + if (uv.x <= f_pattern_overwrite.x) { + // overwrite for pattern with "ON" to the right (positive uv.x) + float sdf_overwrite = w * pattern_length * (f_pattern_overwrite.x - uv.x); + // pattern value where we start overwriting + float edge_sample = w * texture(pattern, vec2(f_pattern_overwrite.x, 0.5)).x; + // offset for overwrite to smoothly connect between sampling and edge + float sdf_offset = max(f_pattern_overwrite.y * edge_sample, -AA_RADIUS); + // add offset and apply direction ("ON" to left or right) to overwrite + return f_pattern_overwrite.y * (sdf_overwrite + sdf_offset); + } else if (uv.x >= f_pattern_overwrite.z) { + // same as above (other than mirroring overwrite direction) + float sdf_overwrite = w * pattern_length * (uv.x - f_pattern_overwrite.z); + float edge_sample = w * texture(pattern, vec2(f_pattern_overwrite.z, 0.5)).x; + float sdf_offset = max(f_pattern_overwrite.w * edge_sample, -AA_RADIUS); + return f_pattern_overwrite.w * (sdf_overwrite + sdf_offset); + } else + // in allowed range + return w * texture(pattern, uv).x; } - flat in uint frag_instance_id; - uniform uint object_id; + float get_pattern_sdf(bool _, vec2 uv){ + return -10.0; + } vec4 pack_int(uint id, uint index) { vec4 unpack; @@ -21445,14 +22039,113 @@ function lines_fragment_shader(uniforms, attributes) { unpack.w = float((index & uint(0x00ff)) >> 0) / 255.0; return unpack; } + + void main(){ + vec4 color; + + // f_quad_sdf1.x is the distance from p1, negative in v1 direction. + vec2 uv = vec2( + (f_cumulative_length - f_quad_sdf1.x) / (2.0 * f_linewidth * pattern_length), + 0.5 + 0.5 * f_quad_sdf1.z / f_linewidth + ); + + #ifndef DEBUG + // discard fragments that are "more inside" the other segment to remove + // overlap between adjacent line segments. (truncated joints) + float dist_in_prev = max(f_quad_sdf0, - f_discard_limit.x); + float dist_in_next = max(f_quad_sdf2, - f_discard_limit.y); + if (dist_in_prev < f_quad_sdf1.x || dist_in_next < f_quad_sdf1.y) + discard; + + // SDF for inside vs outside along the line direction. extrusion adjusts + // the distance from p1/p2 for joints etc + float sdf = max(f_quad_sdf1.x - f_extrusion.x, f_quad_sdf1.y - f_extrusion.y); + + // distance in linewidth direction + sdf = max(sdf, abs(f_quad_sdf1.z) - f_linewidth); + + // truncation of truncated joints (creates flat cap) + sdf = max(sdf, f_truncation.x); + sdf = max(sdf, f_truncation.y); + + // inner truncation (AA for overlapping parts) + // min(a, b) keeps what is inside a and b + // where a is the smoothly cut of part just before discard triggers (i.e. visible) + // and b is the (smoothly) cut of part where the discard triggers + // 100.0x sdf makes the sdf much more sharply, avoiding overdraw in the center + sdf = max(sdf, min(f_quad_sdf1.x + 1.0, 100.0 * (f_quad_sdf1.x - f_quad_sdf0) - 1.0)); + sdf = max(sdf, min(f_quad_sdf1.y + 1.0, 100.0 * (f_quad_sdf1.y - f_quad_sdf2) - 1.0)); + + // pattern application + sdf = max(sdf, get_pattern_sdf(pattern, uv)); + + // draw + + // v- edge + // .--------------- + // '. + // p1 v1 + // '. ---> + // '---------- + // -f_quad_sdf1.x is the distance from p1, positive in v1 direction + // f_linestart is the distance between p1 and the left edge along v1 direction + // f_start_length.y is the distance between the edges of this segment, in v1 direction + // so this is 0 at the left edge and 1 at the right edge (with extrusion considered) + float factor = (-f_quad_sdf1.x - f_linestart) / f_linelength; + color = get_color(f_color1 + factor * (f_color2 - f_color1), colormap, colorrange); + + color.a *= aastep(0.0, -sdf) * f_alpha_weight; + #endif + + #ifdef DEBUG + // base color + color = vec4(0.5, 0.5, 0.5, 0.2); + color.rgb += (2.0 * mod(float(f_instance_id), 2.0) - 1.0) * 0.1; + + // show color interpolation as brightness gradient + // float factor = (-f_quad_sdf1.x - f_linestart) / f_linelength; + // color.rgb += (2.0 * factor - 1.0) * 0.2; + + // mark "outside" define by quad_sdf in black + float sdf = max(f_quad_sdf1.x - f_extrusion.x, f_quad_sdf1.y - f_extrusion.y); + sdf = max(sdf, abs(f_quad_sdf1.z) - f_linewidth); + color.rgb -= vec3(0.4) * step(0.0, sdf); + + // Mark discarded space in red/blue + float dist_in_prev = max(f_quad_sdf0, - f_discard_limit.x); + float dist_in_next = max(f_quad_sdf2, - f_discard_limit.y); + if (dist_in_prev < f_quad_sdf1.x) + color.r += 0.5; + if (dist_in_next <= f_quad_sdf1.y) { + color.b += 0.5; + } + + // remaining overlap as softer red/blue + if (f_quad_sdf1.x - f_quad_sdf0 - 1.0 > 0.0) + color.r += 0.2; + if (f_quad_sdf1.y - f_quad_sdf2 - 1.0 > 0.0) + color.b += 0.2; + + // Mark regions excluded via truncation in green + color.g += 0.5 * step(0.0, max(f_truncation.x, f_truncation.y)); + + // and inner truncation as soft green + if (min(f_quad_sdf1.x + 1.0, 100.0 * (f_quad_sdf1.x - f_quad_sdf0) - 1.0) > 0.0) + color.g += 0.2; + if (min(f_quad_sdf1.y + 1.0, 100.0 * (f_quad_sdf1.y - f_quad_sdf2) - 1.0) > 0.0) + color.g += 0.2; + + // mark pattern in white + color.rgb += vec3(0.3) * step(0.0, get_pattern_sdf(pattern, uv)); + #endif + + if (color.a <= 0.0) + discard; - float xalpha = aastep(0.0, 0.0, f_uv.x); - float yalpha = aastep(0.0, 0.0, f_uv.y); - vec4 color = get_color(f_color, colormap, colorrange); if (picking) { if (color.a > 0.1) { - fragment_color = pack_int(object_id, frag_instance_id); + fragment_color = pack_int(object_id, f_instance_id); } return; } @@ -21460,42 +22153,53 @@ function lines_fragment_shader(uniforms, attributes) { } `; } -function create_line_material(scene, uniforms, attributes) { +function create_line_material(scene, uniforms, attributes, is_linesegments) { const uniforms_des = deserialize_uniforms(scene, uniforms); const mat = new THREE.RawShaderMaterial({ uniforms: uniforms_des, glslVersion: THREE.GLSL3, - vertexShader: linesegments_vertex_shader(uniforms_des, attributes), + vertexShader: lines_vertex_shader(uniforms_des, attributes, is_linesegments), fragmentShader: lines_fragment_shader(uniforms_des, attributes), - transparent: true + transparent: true, + blending: THREE.CustomBlending, + blendSrc: THREE.SrcAlphaFactor, + blendDst: THREE.OneMinusSrcAlphaFactor, + blendSrcAlpha: THREE.ZeroFactor, + blendDstAlpha: THREE.OneFactor, + blendEquation: THREE.AddEquation }); mat.uniforms.object_id = { value: 1 }; return mat; } -function attach_interleaved_line_buffer(attr_name, geometry, points, ndim, is_segments) { +function attach_interleaved_line_buffer(attr_name, geometry, data, ndim, is_segments, is_position) { const skip_elems = is_segments ? 2 * ndim : ndim; - const buffer = new THREE.InstancedInterleavedBuffer(points, skip_elems, 1); - geometry.setAttribute(attr_name + "_start", new THREE.InterleavedBufferAttribute(buffer, ndim, 0)); - geometry.setAttribute(attr_name + "_end", new THREE.InterleavedBufferAttribute(buffer, ndim, ndim)); + const buffer = new THREE.InstancedInterleavedBuffer(data, skip_elems, 1); + buffer.count = Math.max(0, is_segments ? Math.floor(buffer.count - 1) : buffer.count - 3); + geometry.setAttribute(attr_name + "_start", new THREE.InterleavedBufferAttribute(buffer, ndim, ndim)); + geometry.setAttribute(attr_name + "_end", new THREE.InterleavedBufferAttribute(buffer, ndim, 2 * ndim)); + if (is_position) { + geometry.setAttribute(attr_name + "_prev", new THREE.InterleavedBufferAttribute(buffer, ndim, 0)); + geometry.setAttribute(attr_name + "_next", new THREE.InterleavedBufferAttribute(buffer, ndim, 3 * ndim)); + } return buffer; } function create_line_instance_geometry() { const geometry = new THREE.InstancedBufferGeometry(); const instance_positions = [ - 0, - -0.5, + -1, + -1, 1, - -0.5, + -1, 1, - 0.5, - 0, - -0.5, 1, - 0.5, - 0, - 0.5 + -1, + -1, + 1, + 1, + -1, + 1 ]; geometry.setAttribute("position", new THREE.Float32BufferAttribute(instance_positions, 2)); geometry.boundingSphere = new THREE.Sphere(); @@ -21503,41 +22207,34 @@ function create_line_instance_geometry() { geometry.frustumCulled = false; return geometry; } -function create_line_buffer(geometry, buffers, name, attr, is_segments) { +function create_line_buffer(geometry, buffers, name, attr, is_segments, is_position) { const flat_buffer = attr.value.flat; const ndims = attr.value.type_length; - const linebuffer = attach_interleaved_line_buffer(name, geometry, flat_buffer, ndims, is_segments); + const linebuffer = attach_interleaved_line_buffer(name, geometry, flat_buffer, ndims, is_segments, is_position); buffers[name] = linebuffer; return flat_buffer; } function create_line_buffers(geometry, buffers, attributes, is_segments) { for(let name in attributes){ const attr = attributes[name]; - create_line_buffer(geometry, buffers, name, attr, is_segments); + create_line_buffer(geometry, buffers, name, attr, is_segments, name == "linepoint"); } } function attach_updates(mesh, buffers, attributes, is_segments) { - let geometry = mesh.geometry; for(let name in attributes){ const attr = attributes[name]; - attr.on((new_points)=>{ + attr.on((new_vertex_data)=>{ let buff = buffers[name]; - const ndims = new_points.type_length; - const new_line_points = new_points.flat; - const old_count = buff.array.length; - const new_count = new_line_points.length / ndims; - if (old_count < new_line_points.length) { + const new_flat_data = new_vertex_data.flat; + const old_length = buff.array.length; + if (old_length != new_flat_data.length) { mesh.geometry.dispose(); - geometry = create_line_instance_geometry(); - buff = attach_interleaved_line_buffer(name, geometry, new_line_points, ndims, is_segments); - mesh.geometry = geometry; - buffers[name] = buff; + mesh.geometry = create_line_instance_geometry(); + create_line_buffers(mesh.geometry, buffers, attributes, is_segments); + mesh.geometry.instanceCount = mesh.geometry.attributes.linepoint_start.count; } else { - buff.set(new_line_points); + buff.set(new_flat_data); } - const ls_factor = is_segments ? 2 : 1; - const offset = is_segments ? 0 : 1; - mesh.geometry.instanceCount = Math.max(0, new_count / ls_factor - offset); buff.needsUpdate = true; mesh.needsUpdate = true; }); @@ -21547,14 +22244,14 @@ function _create_line(scene, line_data, is_segments) { const geometry = create_line_instance_geometry(); const buffers = {}; create_line_buffers(geometry, buffers, line_data.attributes, is_segments); - const material = create_line_material(scene, line_data.uniforms, geometry.attributes); - material.uniforms.is_segments_multi = { - value: is_segments ? 2 : 1 + const material = create_line_material(scene, line_data.uniforms, geometry.attributes, is_segments); + material.depthTest = !line_data.overdraw.value; + material.depthWrite = !line_data.transparency.value; + material.uniforms.is_linesegments = { + value: is_segments }; const mesh = new THREE.Mesh(geometry, material); - const offset = is_segments ? 0 : 1; - const new_count = geometry.attributes.linepoint_start.count; - mesh.geometry.instanceCount = Math.max(0, new_count - offset); + mesh.geometry.instanceCount = geometry.attributes.linepoint_start.count; attach_updates(mesh, buffers, line_data.attributes, is_segments); return mesh; } @@ -22211,7 +22908,10 @@ function threejs_module(canvas) { antialias: true, canvas: canvas, context: context, - powerPreference: "high-performance" + powerPreference: "high-performance", + precision: "highp", + alpha: true, + logarithmicDepthBuffer: true }); renderer.debug.onShaderError = on_shader_error; renderer.setClearColor("#ffffff"); diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index 6fce954f2f1..d023b735243 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -354,6 +354,9 @@ function threejs_module(canvas) { canvas: canvas, context: context, powerPreference: "high-performance", + precision: "highp", + alpha: true, + logarithmicDepthBuffer: true }); renderer.debug.onShaderError = on_shader_error; diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index f3b9b4161b8..f7e4bd786b6 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -19,17 +19,7 @@ import Electron end excludes = Set([ - "Streamplot animation", - "Transforming lines", "image scatter", - "Line GIF", - "surface + contour3d", - # Hm weird, looks like some internal Bonito error missing an Observable: - "Errorbars x y low high", - "Rangebars x y low high", - # These are a bit sad, since it's just missing interpolations - "FEM mesh 2D", - "FEM polygon 2D", # missing transparency & image "Image on Surface Sphere", # Marker size seems wrong in some occasions: @@ -39,14 +29,11 @@ excludes = Set([ "Test heatmap + image overlap", # "heatmaps & surface", # TODO: fix direct NaN -> nancolor conversion "Order Independent Transparency", - "Record Video", "fast pixel marker", "Array of Images Scatter", "Image Scatter different sizes", - "lines and linestyles", "Textured meshscatter", # not yet implemented "3D Contour with 2D contour slices", # looks like a z-fighting issue - "colorscale (lines)", # also z-fighting ]) Makie.inline!(Makie.automatic) diff --git a/assets/voxel_spritesheet.png b/assets/voxel_spritesheet.png new file mode 100644 index 00000000000..b1030b995d0 Binary files /dev/null and b/assets/voxel_spritesheet.png differ diff --git a/docs/explanations/conversion_pipeline.md b/docs/explanations/conversion_pipeline.md new file mode 100644 index 00000000000..374402b7dd4 --- /dev/null +++ b/docs/explanations/conversion_pipeline.md @@ -0,0 +1,249 @@ +# Conversion Pipeline and Coordinate spaces + +## Conversion Pipeline + +The following graphic sketches out the conversion pipeline of data given to a plot with `space = :data` for GL backends. + +\begin{examplefigure}{} +```julia +#hideall +using GLMakie, LinearAlgebra + +function myarrows!(scene, ps; kwargs...) + ends = map(ps) do ps + output = Int[] + for i in eachindex(ps) + isnan(ps[i]) && push!(output, i-1) + end + push!(output, length(ps)) + output + end + + dict = Dict(kwargs) + endpoints = map((ps, is) -> ps[is], ps, ends) + dirs = map( + (ps, is) -> -Makie.quaternion_to_2d_angle.(Makie.to_rotation(normalize.(ps[is] .- ps[is .- 1]))), + ps, ends) + cols = map(is -> dict[:color] isa Vector ? dict[:color][is] : dict[:color], ends) + + lines!(scene, ps; kwargs...) + scatter!( + scene, endpoints, marker = Makie.BezierUTriangle, color = cols, + rotation = dirs + ) +end + +scene = Scene(size = (1220, 200)) +campixel!(scene) + +# init space label data +spacing = 80 +y0 = 80 +ps = Observable(Point2f.(1:8, y0)) +# spaces = [":data", "transformed64", "world", "eye", ":clip", "screen"] +spaces = ["plot.args", "plot.converted", "transformed64", "transformed32", "world", "eye", "clip", "screen"] + +# plot space labels +p = text!( + scene, ps, text = spaces, align = (:left, :center), + fontsize = 20, + color = [:black, :black, :gray, :gray, :gray, :gray, :gray, :gray] +) + +# update space label data & derive arrows +centers = Observable(Point2f[]) +text_centers = Observable(Point2f[]) +map!(ps, p.plots[1][1]) do gcs + edge = -spacing + 10 + xvals = Float64[] + xcenters = Float64[] + + for gc in gcs + bb = Makie.string_boundingbox(gc, Quaternionf(0,0,0,1)) + left = spacing + edge - minimum(bb)[1] + push!(xvals, left) + push!(xcenters, left + 0.5 * widths(bb)[1]) + edge = left + widths(bb)[1] + end + + text_centers[] = Point2f.(xcenters, y0) + centers[] = Point2f.(xvals[2:end] .- 0.5spacing, y0) + + return Point2f.(xvals, y0) +end + +arrowpos = map(centers) do centers + half = 0.5 * spacing - 5 + ps = Point2f[] + lws = Float64[] + for center in centers + x, y = center + push!(ps, Point2f(x-half, y), Point2f(x+half-5, y), Point2f(NaN)) + # push!(ps, Point2f(x-half, y), Point2f(x+half-8, y)) + # push!(ps, Point2f(x+half-10, y), Point2f(x+half, y)) + # push!(lws, 2, 2, 12, 0) + end + ps +end + +# plot arrows +myarrows!( + scene, arrowpos, linewidth = 2, + color = [c for c in [:red, :red, :red, :green, :green, :green, :gray] for _ in 1:3] +) + +# transformation labels +transformations = ["convert_arguments", "transform_func", "Float32Convert", "model", "view", "projection", "viewport"] +trans_offset = 25 +text!( + scene, centers, text = transformations, align = (:center, :center), + offset = (0, trans_offset), + color = [:black, :orange, :black, :orange, :cyan, :cyan, :gray] +) + +# Transformation +bracket_offset = Point2f(0, trans_offset + 15) +trans_bracket_pos = map(centers) do cs + [cs[2] + bracket_offset, cs[4] + bracket_offset] +end +text!( + scene, trans_bracket_pos, text = ["Transformation" for _ in 1:2], + color = :orange, align = (:center, :bottom)) + +# Camera +cam_bracket_pos = map(centers) do cs + (cs[5] + bracket_offset, cs[6] + bracket_offset) +end +bracket!( + scene, + cam_bracket_pos, + text = "Camera", + color = :cyan, textcolor = :cyan +) + +# CPU +dx = 10; dy = -20 +cpu_bracket_pos = map(text_centers) do ps + (ps[1] .+ (dx, dy), ps[4] .+ (-dx, dy)) +end +bracket!( + scene, + cpu_bracket_pos, + text = "CPU", + color = :red, textcolor = :red, + orientation = :down +) + +# GPU +gpu_bracket_pos = map(text_centers) do ps + (ps[4] .+ (dx, dy), ps[7] .+ (-dx, dy)) +end +bracket!( + scene, + gpu_bracket_pos, + text = "GPU", + color = :green, textcolor = :green, + orientation = :down +) + +# Internal +internal_bracket_pos = map(text_centers) do ps + (ps[7] .+ (dx, dy), ps[8] .+ (-dx, dy)) +end +bracket!( + scene, + internal_bracket_pos, + text = "GPU Internal", + color = :gray, textcolor = :gray, + orientation = :down +) + +# Float32Convert +f32_ps = map(text_centers) do cs + x1, y1 = cs[2] + x5, y5 = cs[3] + x2, y2 = 0.5 * (cs[3] .+ cs[4]) + x3, y3 = cs[6] + x4, y4 = 0.5 * (cs[4] .+ cs[5]) + Point2f[ + (x1, y1+15), (x1, y1+65), (NaN, NaN), + (x1+45, y1+83), (x5, y5+83), (x5, y2+25), (x5+45, y2+25), (NaN, NaN), + (x5-50, y2+25), (x5-10, y2+25), (NaN, NaN), + (x2+20, y2+40), (x2+20, y2+100), (x3, y3+100), (x3, y3+80), (NaN, NaN), + (x2+60, y2+25), (x4-15, y4+25) + ] +end +myarrows!(scene, f32_ps, color = :gray) +text!( + scene, map(ps -> ps[2], f32_ps), text = "ax.finallimits", + align = (:center, :bottom), offset = Vec2f(0, 10) +) + +scene +``` +\end{examplefigure} + +### Argument Conversions + +When calling a plot function, e.g. `scatter!(axis_or_scene, args...)` a new plot object is constructed. +The plot object keeps track of the input arguments in `plot.args`, promoting them to observables if need be. +It also keeps track of a type normalized set of inputs in `plot.converted` which are generated from `plot.args` using various `convert_arguments()` functions. +Generally speaking these functions either dispatch on the plot type or the result of `conversion_trait(PlotType, args...)`, i.e. `convert_arguments(type_or_traint, args...)`. +They are expected to generalize and simplify the structure of data given to a plot while leaving the numeric type as either a Float32 or Float64 as appropriate. + +### Transformation Objects + +The remaining transformed versions of data are not accessible, but rather abstract representations which the data goes through. +As such they are named based on the coordinate space they are in and grayed out. +Note as well that these representations are only relevant to primitive plots like lines or mesh. +Ignoring `Float32Convert` for now, the next two transformations are summarized under the `Transformation` object present in `plot.transformation` and `scene.transformation`. + +The first transformation is `transformation.transform_func`, which holds a function which is applied to a `Vector{Point{N, T}}` element by element. +It is meant to resolve transformations that cannot be represented as a matrix operations, for example moving data into a logarithmic space or into Polar coordinates. +They are implemented using the `apply_transform(func, data)` methods. +Generally we also expect transform function to be (partially) invertable and their inverse to be returned by `inverse_transform(func)`. + +The second transformation is `transformation.model`, which combines `translate!(plot, ...)`, `scale!(plot, ...)` and `rotate!(plot, ...)` into a matrix. +The order of operations here is fixed - rotations apply first, then scaling and finally translations. +As a matrix operation they can and are handled on the GPU. + +### Float32Convert + +Nested between `transform_func` and `model` is the application of `scene.float32convert`. +Its job is to bring the transformed data into a range acceptable for `Float32`, which is used on the GPU. + +Currently only `Axis` actually defines this transformation. +When calling `plot!(axis, ...)` it takes a snapshot of the limits of the plot using `data_limits(plot)` and updates its internal limits. +These are combined with other sources to generate `axis.finallimits`. +When setting the camera matrices `axis.finallimits` gets transformed by `transform_func` and processed by `scene.float32convert` to generate a valid Float32 range for the camera. +This processing will update the `Float32Convert` if needed. + +With respect to the conversion pipeline the `Float32Convert` is a linear function applied to transformed data using `f32_convert(scene, data)`. +After the transformation, data strictly uses Float32 as a numeric type. + +Note that since the `Float32Convert` is based on and transforms the limits used to create the camera (matrices), it should technically act between `model` and `view`. +In fact, this order is used for CairoMakie and some CPU projection code. +For the GPU however, we want to avoid applying `model` on the CPU. +To do that we calculate a new model matrix using `new_model = patch_model(scene, model)`, which acts after `Float32Convert`. + +### Camera + +Next in our conversion pipeline are the camera matrices tracked in `scene.camera`. +Their job is to transform plot data to a normalized "clip" space. +While not consistently followed, the `view` matrix is supposed to adjust the coordinate system to that of the viewer and the `projection` matrix is supposed to apply scaling and perspective projection if applicable. +The viewers position and orientation is set by either the the camera controller of the scene or the parent Block. + + + +## Coordinate spaces + +Currently `Makie` defines 4 coordinate spaces: :data, :clip, :relative and :pixel. +The example above shows te conversion pipeline for `space = :data`. + +For `space = :clip` we consider `plot.converted` to be in clip space, meaning that `transform_func`, `model`, `view` and `projection` can be skipped, and `Float32Convert` only does a cast to Float32. +The x and y direction correspond to right and up, with z increasing towards the viewer. +All coordinates are limited to a -1 .. 1 range. + +The other two spaces each include one matrix transformation to clip space. +For `space = :relative` this simply rescales the x and y dimension to a 0 .. 1 range. +And for `space = :pixel` the `camera.pixel_space` matrix is used to set the x and y range the size of the scene and the z range to -10_000 .. 10_000, with z facing away from the viewer. diff --git a/docs/reference/plots/arrows.md b/docs/reference/plots/arrows.md index 54cef0bb62b..c3a47e2b394 100644 --- a/docs/reference/plots/arrows.md +++ b/docs/reference/plots/arrows.md @@ -2,38 +2,6 @@ {{doc arrows}} -### Attributes - -- `arrowhead = automatic`: Defines the marker (2D) or mesh (3D) that is used as - the arrow head. The default for is `'▲'` in 2D and a cone mesh in 3D. For the - latter the mesh should start at `Point3f(0)` and point in positive z-direction. -- `arrowtail = automatic`: Defines the mesh used to draw the arrow tail in 3D. - It should start at `Point3f(0)` and extend in negative z-direction. The default - is a cylinder. This has no effect on the 2D plot. -- `quality = 32`: Defines the number of angle subdivisions used when generating - the arrow head and tail meshes. Consider lowering this if you have performance - issues. Only applies to 3D plots. -- `linecolor = :black`: Sets the color used for the arrow tail which is - represented by a line in 2D. -- `arrowcolor = linecolor`: Sets the color of the arrow head. -- `arrowsize = automatic`: Scales the size of the arrow head. This defaults to - `0.3` in the 2D case and `Vec3f(0.2, 0.2, 0.3)` in the 3D case. For the latter - the first two components scale the radius (in x/y direction) and the last scales - the length of the cone. If the arrowsize is set to 1, the cone will have a - diameter and length of 1. -- `linewidth = automatic`: Scales the width/diameter of the arrow tail. - Defaults to `1` for 2D and `0.05` for the 3D case. -- `lengthscale = 1f0`: Scales the length of the arrow tail. -- `linestyle = nothing`: Sets the linestyle used in 2D. Does not apply to 3D - plots. -- `normalize = false`: By default the lengths of the directions given to `arrows` - are used to scale the length of the arrow tails. If this attribute is set to - true the directions are normalized, skipping this scaling. -- `align = :origin`: Sets how arrows are positioned. By default arrows start at - the given positions and extend along the given directions. If this attribute is - set to `:head`, `:lineend`, `:tailend`, `:headstart` or `:center` the given - positions will be between the head and tail of each arrow instead. - ### Examples \begin{examplefigure}{} diff --git a/docs/reference/plots/lines.md b/docs/reference/plots/lines.md index 83c3e4f82e8..a41d6688d52 100644 --- a/docs/reference/plots/lines.md +++ b/docs/reference/plots/lines.md @@ -69,13 +69,13 @@ ps = rand(Point3f, 500) cs = rand(500) f = Figure(size = (600, 650)) Label(f[1, 1], "base", tellwidth = false) -lines(f[2, 1], ps, color = cs, markersize = 20, fxaa = false) +lines(f[2, 1], ps, color = cs, fxaa = false) Label(f[1, 2], "fxaa = true", tellwidth = false) -lines(f[2, 2], ps, color = cs, markersize = 20, fxaa = true) +lines(f[2, 2], ps, color = cs, fxaa = true) Label(f[3, 1], "transparency = true", tellwidth = false) -lines(f[4, 1], ps, color = cs, markersize = 20, transparency = true) +lines(f[4, 1], ps, color = cs, transparency = true) Label(f[3, 2], "overdraw = true", tellwidth = false) -lines(f[4, 2], ps, color = cs, markersize = 20, overdraw = true) +lines(f[4, 2], ps, color = cs, overdraw = true) f ``` \end{examplefigure} \ No newline at end of file diff --git a/docs/reference/plots/linesegments.md b/docs/reference/plots/linesegments.md index 2f336f9e2d4..d76ed361122 100644 --- a/docs/reference/plots/linesegments.md +++ b/docs/reference/plots/linesegments.md @@ -41,13 +41,13 @@ ps = rand(Point3f, 500) cs = rand(500) f = Figure(size = (600, 650)) Label(f[1, 1], "base", tellwidth = false) -linesegments(f[2, 1], ps, color = cs, markersize = 20, fxaa = false) +linesegments(f[2, 1], ps, color = cs, fxaa = false) Label(f[1, 2], "fxaa = true", tellwidth = false) -linesegments(f[2, 2], ps, color = cs, markersize = 20, fxaa = true) +linesegments(f[2, 2], ps, color = cs, fxaa = true) Label(f[3, 1], "transparency = true", tellwidth = false) -linesegments(f[4, 1], ps, color = cs, markersize = 20, transparency = true) +linesegments(f[4, 1], ps, color = cs, transparency = true) Label(f[3, 2], "overdraw = true", tellwidth = false) -linesegments(f[4, 2], ps, color = cs, markersize = 20, overdraw = true) +linesegments(f[4, 2], ps, color = cs, overdraw = true) f ``` \end{examplefigure} \ No newline at end of file diff --git a/docs/reference/plots/pie.md b/docs/reference/plots/pie.md index 78cee3f26f0..bec0f1e6868 100644 --- a/docs/reference/plots/pie.md +++ b/docs/reference/plots/pie.md @@ -2,27 +2,6 @@ {{doc pie}} - - -## Attributes - -### Generic - -- `normalize = true` sets whether the data will be normalized to the range [0, 2π]. -- `color` sets the color of the pie segments. It can be given as a single named color or a vector of the same length as the input data -- `strokecolor = :black` sets the color of the outline around the segments. -- `strokewidth = 1` sets the width of the outline around the segments. -- `vertex_per_deg = 1` defines the number of vertices per degree that are used to create the pie plot with polys. Increase if smoother circles are needed. -- `radius = 1` sets the radius for the pie plot. -- `inner_radius = 0` sets the inner radius of the plot. Choose as a value between 0 and `radius` to create a donut chart. -- `offset = 0` rotates the pie plot counterclockwise as given in radians. -- `transparency = false` adjusts how the plot deals with transparency. In GLMakie `transparency = true` results in using Order Independent Transparency. -- `inspectable = true` sets whether this plot should be seen by `DataInspector`. - -### Other - -Set the axis properties `autolimitaspect = 1` or `aspect = DataAspect()` to ensure that the pie chart looks like a circle and not an ellipsoid. - ## Examples \begin{examplefigure}{} diff --git a/docs/reference/plots/rainclouds.md b/docs/reference/plots/rainclouds.md index 2a5f434bcee..2233f2a3030 100644 --- a/docs/reference/plots/rainclouds.md +++ b/docs/reference/plots/rainclouds.md @@ -67,7 +67,7 @@ category_labels, data_array = mockup_categories_and_data_array(3) colors = Makie.wong_colors() rainclouds(category_labels, data_array; - xlabel = "Categories of Distributions", ylabel = "Samples", title = "My Title", + axis = (; xlabel = "Categories of Distributions", ylabel = "Samples", title = "My Title"), plot_boxplots = false, cloud_width=0.5, clouds=hist, hist_bins=50, color = colors[indexin(category_labels, unique(category_labels))]) ``` @@ -77,8 +77,8 @@ rainclouds(category_labels, data_array; \begin{examplefigure}{} ```julia rainclouds(category_labels, data_array; - ylabel = "Categories of Distributions", - xlabel = "Samples", title = "My Title", + axis = (; ylabel = "Categories of Distributions", + xlabel = "Samples", title = "My Title"), orientation = :horizontal, plot_boxplots = true, cloud_width=0.5, clouds=hist, color = colors[indexin(category_labels, unique(category_labels))]) @@ -88,8 +88,11 @@ rainclouds(category_labels, data_array; \begin{examplefigure}{} ```julia rainclouds(category_labels, data_array; - xlabel = "Categories of Distributions", - ylabel = "Samples", title = "My Title", + axis = (; + xlabel = "Categories of Distributions", + ylabel = "Samples", + title = "My Title" + ), plot_boxplots = true, cloud_width=0.5, clouds=hist, color = colors[indexin(category_labels, unique(category_labels))]) ``` @@ -99,7 +102,11 @@ rainclouds(category_labels, data_array; \begin{examplefigure}{} ```julia rainclouds(category_labels, data_array; - xlabel = "Categories of Distributions", ylabel = "Samples", title = "My Title", + axis = (; + xlabel = "Categories of Distributions", + ylabel = "Samples", + title = "My Title" + ), plot_boxplots = true, cloud_width=0.5, side = :right, violin_limits = extrema, color = colors[indexin(category_labels, unique(category_labels))]) ``` @@ -108,7 +115,11 @@ rainclouds(category_labels, data_array; \begin{examplefigure}{} ```julia rainclouds(category_labels, data_array; - xlabel = "Categories of Distributions", ylabel = "Samples", title = "My Title", + axis = (; + xlabel = "Categories of Distributions", + ylabel = "Samples", + title = "My Title", + ), plot_boxplots = true, cloud_width=0.5, side = :right, color = colors[indexin(category_labels, unique(category_labels))]) ``` @@ -119,7 +130,11 @@ rainclouds(category_labels, data_array; more_category_labels, more_data_array = mockup_categories_and_data_array(6) rainclouds(more_category_labels, more_data_array; - xlabel = "Categories of Distributions", ylabel = "Samples", title = "My Title", + axis = (; + xlabel = "Categories of Distributions", + ylabel = "Samples", + title = "My Title", + ), plot_boxplots = true, cloud_width=0.5, color = colors[indexin(more_category_labels, unique(more_category_labels))]) ``` @@ -129,8 +144,11 @@ rainclouds(more_category_labels, more_data_array; ```julia category_labels, data_array = mockup_categories_and_data_array(6) rainclouds(category_labels, data_array; - xlabel = "Categories of Distributions", - ylabel = "Samples", title = "My Title", + axis = (; + xlabel = "Categories of Distributions", + ylabel = "Samples", + title = "My Title", + ), plot_boxplots = true, cloud_width=0.5, color = colors[indexin(category_labels, unique(category_labels))]) ``` @@ -146,26 +164,30 @@ fig = Figure(size = (800*2, 600*5)) colors = [Makie.wong_colors(); Makie.wong_colors()] category_labels, data_array = mockup_categories_and_data_array(3) -rainclouds!(Axis(fig[1, 1]), category_labels, data_array; - title = "Left Side, with Box Plot", +rainclouds!( + Axis(fig[1, 1], title = "Left Side, with Box Plot"), + category_labels, data_array; side = :left, plot_boxplots = true, color = colors[indexin(category_labels, unique(category_labels))]) -rainclouds!(Axis(fig[2, 1]), category_labels, data_array; - title = "Left Side, without Box Plot", +rainclouds!( + Axis(fig[2, 1], title = "Left Side, without Box Plot"), + category_labels, data_array; side = :left, plot_boxplots = false, color = colors[indexin(category_labels, unique(category_labels))]) -rainclouds!(Axis(fig[1, 2]), category_labels, data_array; - title = "Right Side, with Box Plot", +rainclouds!( + Axis(fig[1, 2], title = "Right Side, with Box Plot"), + category_labels, data_array; side = :right, plot_boxplots = true, color = colors[indexin(category_labels, unique(category_labels))]) -rainclouds!(Axis(fig[2, 2]), category_labels, data_array; - title = "Right Side, without Box Plot", +rainclouds!( + Axis(fig[2, 2], title = "Right Side, without Box Plot"), + category_labels, data_array; side = :right, plot_boxplots = false, color = colors[indexin(category_labels, unique(category_labels))]) @@ -175,25 +197,27 @@ rainclouds!(Axis(fig[2, 2]), category_labels, data_array; # with and without clouds category_labels, data_array = mockup_categories_and_data_array(12) -rainclouds!(Axis(fig[3, 1:2]), category_labels, data_array; - title = "More categories. Default spacing.", +rainclouds!( + Axis(fig[3, 1:2], title = "More categories. Default spacing."), + category_labels, data_array; plot_boxplots = true, - dist_between_categories = 1.0, + gap = 1.0, color = colors[indexin(category_labels, unique(category_labels))]) -rainclouds!(Axis(fig[4, 1:2]), category_labels, data_array; - title = "More categories. Adjust space. (smaller cloud widths and smaller category distances)", +rainclouds!( + Axis(fig[4, 1:2], title = "More categories. Adjust space. (smaller cloud widths and smaller category distances)"), + category_labels, data_array; plot_boxplots = true, cloud_width = 0.3, - dist_between_categories = 0.5, + gap = 0.5, color = colors[indexin(category_labels, unique(category_labels))]) - -rainclouds!(Axis(fig[5, 1:2]), category_labels, data_array; - title = "More categories. Adjust space. No clouds.", +rainclouds!( + Axis(fig[5, 1:2], title = "More categories. Adjust space. No clouds."), + category_labels, data_array; plot_boxplots = true, clouds = nothing, - dist_between_categories = 0.5, + gap = 0.5, color = colors[indexin(category_labels, unique(category_labels))]) supertitle = Label(fig[0, :], "Cloud Plot Testing (Scatter, Violin, Boxplot)", fontsize=30) diff --git a/docs/reference/plots/scatter.md b/docs/reference/plots/scatter.md index b91750eb1cb..579172fc55c 100644 --- a/docs/reference/plots/scatter.md +++ b/docs/reference/plots/scatter.md @@ -206,7 +206,7 @@ arrow_path = BezierPath([ scatter(1:5, marker = arrow_path, markersize = range(20, 50, length = 5), - rotations = range(0, 2pi, length = 6)[1:end-1], + rotation = range(0, 2pi, length = 6)[1:end-1], ) ``` \end{examplefigure} @@ -287,7 +287,7 @@ scatter(1:4, fill(0, 4), marker=Polygon(p_big, [p_small]), markersize=100, color ### Marker rotation -Markers can be rotated using the `rotations` attribute, which also allows to pass a vector. +Markers can be rotated using the `rotation` attribute, which also allows to pass a vector. \begin{examplefigure}{svg = true} ```julia @@ -298,7 +298,7 @@ CairoMakie.activate!() # hide points = [Point2f(x, y) for y in 1:10 for x in 1:10] rotations = range(0, 2pi, length = length(points)) -scatter(points, rotations = rotations, markersize = 20, marker = '↑') +scatter(points, rotation = rotations, markersize = 20, marker = '↑') ``` \end{examplefigure} diff --git a/docs/reference/plots/stephist.md b/docs/reference/plots/stephist.md index cd4d6238045..43573242d38 100644 --- a/docs/reference/plots/stephist.md +++ b/docs/reference/plots/stephist.md @@ -14,7 +14,7 @@ data = randn(1000) f = Figure() stephist(f[1, 1], data, bins = 10) -stephist(f[1, 2], data, bins = 20, color = :red, strokewidth = 1, strokecolor = :black) +stephist(f[1, 2], data, bins = 20, color = :red, linewidth = 3) stephist(f[2, 1], data, bins = [-5, -2, -1, 0, 1, 2, 5], color = :gray) stephist(f[2, 2], data, normalization = :pdf) f diff --git a/docs/reference/plots/text.md b/docs/reference/plots/text.md index 103c497002c..7f1ed5a6821 100644 --- a/docs/reference/plots/text.md +++ b/docs/reference/plots/text.md @@ -115,7 +115,7 @@ for ((justification, halign), point) in zip(Iterators.product(symbols, symbols), align = (halign, :center), justification = justification) - bb = boundingbox(t) + bb = boundingbox(t, :pixel) wireframe!(scene, bb, color = (:red, 0.2)) end diff --git a/docs/reference/plots/voxel.md b/docs/reference/plots/voxel.md new file mode 100644 index 00000000000..90985ff2976 --- /dev/null +++ b/docs/reference/plots/voxel.md @@ -0,0 +1,220 @@ +# voxels + +{{doc voxels}} + +### Examples + + + +#### Basic Example + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +# Same as volume example +r = LinRange(-1, 1, 100) +cube = [(x.^2 + y.^2 + z.^2) for x = r, y = r, z = r] +cube_with_holes = cube .* (cube .> 1.4) + +# To match the volume example with isovalue=1.7 and isorange=0.05 we map all +# values outside the range (1.65..1.75) to invisible air blocks with is_air +f, a, p = voxels(-1..1, -1..1, -1..1, cube_with_holes, is_air = x -> !(1.65 <= x <= 1.75)) +``` +\end{examplefigure} + + +#### Gap Attribute + +The `gap` attribute allows you to specify a gap size between adjacent voxels. +It is given in units of the voxel size (at `gap = 0`) so that `gap = 0` creates no gaps and `gap = 1` reduces the voxel size to 0. +Note that this attribute only takes effect at values `gap > 0.01`. + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +chunk = reshape(collect(1:27), 3, 3, 3) +voxels(chunk, gap = 0.33) +``` +\end{examplefigure} + + +#### Color and the internal representation + +Voxels are represented as an `Array{UInt8, 3}` of voxel ids internally. +In this representation the voxel id `0x00` is defined as an invisible air block. +All other ids (0x01 - 0xff or 1 - 255) are visible and derive their color from the various color attributes. +For `plot.color` specifically the voxel id acts as an index into an array of colors: + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +chunk = UInt8[ + 1 0 2; 0 0 0; 3 0 4;;; + 0 0 0; 0 0 0; 0 0 0;;; + 5 0 6; 0 0 0; 7 0 8;;; +] +f, a, p = voxels(chunk, color = [:white, :red, :green, :blue, :black, :orange, :cyan, :magenta]) +``` +\end{examplefigure} + + +#### Colormaps + +With non `UInt8` inputs, colormap attributes (colormap, colorrange, highclip, lowclip and colorscale) work as usual, with the exception of `nan_color` which is not applicable: + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +chunk = reshape(collect(1:512), 8, 8, 8) + +f, a, p = voxels(chunk, + colorrange = (65, 448), colorscale = log10, + lowclip = :red, highclip = :orange, + colormap = [:blue, :green] +) +``` +\end{examplefigure} + +When passing voxel ids directly (i.e. an `Array{UInt8, 3}`) they are used to index a vector `[lowclip; sampled_colormap; highclip]`. +This means id 1 maps to lowclip, 2..254 to colors of the colormap and 255 to highclip. +`colorrange` and `colorscale` are ignored in this case. + + +#### Texturemaps + +You can also map a texture to voxels based on their id (and optionally the direction the face is facing). +For this `plot.color` needs to be an image (matrix of colors) and `plot.uvmap` needs to be defined. +The `uvmap` can take two forms here. +The first is a `Vector{Vec4f}` which maps voxel ids (starting at 1) to normalized uv coordinates, formatted left-right-bottom-top. + +\begin{examplefigure}{} +```julia +using GLMakie, FileIO +GLMakie.activate!() # hide + +# load a sprite sheet with 10 x 9 textures +texture = FileIO.load(Makie.assetpath("voxel_spritesheet.png")) + +# create a map idx -> LRBT coordinate of the textures, normalized to a 0..1 range +uv_map = [ + Vec4f(x, x+1/10, y, y+1/9) + for x in range(0.0, 1.0, length = 11)[1:end-1] + for y in range(0.0, 1.0, length = 10)[1:end-1] +] + +# Define which textures/uvs apply to which voxels (0 is invisible/air) +chunk = UInt8[ + 1 0 2; 0 0 0; 3 0 4;;; + 0 0 0; 0 0 0; 0 0 0;;; + 5 0 6; 0 0 0; 7 0 9;;; +] + +# draw +f, a, p = voxels(chunk, uvmap = uv_map, color = texture) +``` +\end{examplefigure} + +The second format allows you define sides in the second dimension of the uvmap. +The order of sides is: -x, -y, -z, +x, +y, +z. + +\begin{examplefigure}{} +```julia +using GLMakie, FileIO +GLMakie.activate!() # hide + +texture = FileIO.load(Makie.assetpath("voxel_spritesheet.png")) + +# idx -> uv LRBT map for convenience. Note the change in order loop order +uvs = [ + Vec4f(x, x+1/10, y, y+1/9) + for y in range(0.0, 1.0, length = 10)[1:end-1] + for x in range(0.0, 1.0, length = 11)[1:end-1] +] + +# Create uvmap with sides (-x -y -z x y z) in second dimension +uv_map = Matrix{Vec4f}(undef, 4, 6) +uv_map[1, :] = [uvs[9], uvs[9], uvs[8], uvs[9], uvs[9], uvs[8]] # 1 -> birch +uv_map[2, :] = [uvs[11], uvs[11], uvs[10], uvs[11], uvs[11], uvs[10]] # 2 -> oak +uv_map[3, :] = [uvs[2], uvs[2], uvs[2], uvs[2], uvs[2], uvs[18]] # 3 -> crafting table +uv_map[4, :] = [uvs[1], uvs[1], uvs[1], uvs[1], uvs[1], uvs[1]] # 4 -> planks + +chunk = UInt8[ + 1 0 1; 0 0 0; 1 0 1;;; + 0 0 0; 0 0 0; 0 0 0;;; + 2 0 2; 0 0 0; 3 0 4;;; +] + +f, a, p = voxels(chunk, uvmap = uv_map, color = texture) +``` +\end{examplefigure} + +The textures used in these examples are from [Kenney's Voxel Pack](https://www.kenney.nl/assets/voxel-pack). + + + +#### Updating Voxels + +The voxel plot is a bit different from other plot types which affects how you can and should update its data. + +First you *can* pass your data as an `Observable` and update that observable as usual: + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +chunk = Observable(ones(8,8,8)) +f, a, p = voxels(chunk, colorrange = (0, 1)) +chunk[] = rand(8,8,8) +f +``` +\end{examplefigure} + +You can also update the data contained in the plot object. +For this you can't index into the plot though, since that will return the converted voxel id data. +Instead you need to index into `p.args`. + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +f, a, p = voxels(ones(8,8,8), colorrange = (0, 1)) +p.args[end][] = rand(8,8,8) +f +``` +\end{examplefigure} + +Both of these solutions triggers a full replacement of the input array (i.e. `chunk`), the internal representation (`plot.converted[4]`) and the texture on gpu. +This can be quite slow and wasteful if you only want to update a small section of a large chunk. +In that case you should instead update your input data without triggering an update (using `obs.val`) and then call `local_update(plot, is, js, ks)` to process the update: + +\begin{examplefigure}{} +```julia +using GLMakie +GLMakie.activate!() # hide + +chunk = Observable(rand(64, 64, 64)) +f, a, p = voxels(chunk, colorrange = (0, 1)) +chunk.val[30:34, :, :] .= NaN # or p.args[end].val +Makie.local_update(p, 30:34, :, :) +f +``` +\end{examplefigure} + + + +#### Picking Voxels + +The `pick` function is able to pick individual voxels in a voxel plot. +The returned index is a flat index into the array passed to `voxels`, i.e. `plt.args[end][][idx]` will return the relevant data. +One important thing to note here is that the returned index is a `UInt32` internally and thus has limited range. +Very large voxel plots (~4.3 billion voxels or 2048 x 2048 x 1024) can reach this limit and trigger an integer overflow. diff --git a/precompile/shared-precompile.jl b/precompile/shared-precompile.jl index ecb28bde129..e19a57c993a 100644 --- a/precompile/shared-precompile.jl +++ b/precompile/shared-precompile.jl @@ -5,6 +5,7 @@ using GeometryBasics @compile poly(Recti(0, 0, 200, 200), strokewidth=20, strokecolor=:red, color=(:black, 0.4)) +@compile scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) @compile scatter(0..1, rand(10), markersize=rand(10) .* 20) @compile scatter(LinRange(0, 1, 10), rand(10)) @compile scatter(-1..1, x -> x^2) diff --git a/src/Makie.jl b/src/Makie.jl index 0859d41f837..927f0846069 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -88,8 +88,8 @@ using MakieCore: not_implemented_for import MakieCore: plot, plot!, theme, plotfunc, plottype, merge_attributes!, calculated_attributes!, get_attribute, plotsym, plotkey, attributes, used_attributes import MakieCore: create_axis_like, create_axis_like!, figurelike_return, figurelike_return! -import MakieCore: arrows, heatmap, image, lines, linesegments, mesh, meshscatter, poly, scatter, surface, text, volume -import MakieCore: arrows!, heatmap!, image!, lines!, linesegments!, mesh!, meshscatter!, poly!, scatter!, surface!, text!, volume! +import MakieCore: arrows, heatmap, image, lines, linesegments, mesh, meshscatter, poly, scatter, surface, text, volume, voxels +import MakieCore: arrows!, heatmap!, image!, lines!, linesegments!, mesh!, meshscatter!, poly!, scatter!, surface!, text!, volume!, voxels! import MakieCore: convert_arguments, convert_attribute, default_theme, conversion_trait export @L_str, @colorant_str @@ -97,11 +97,46 @@ export ConversionTrait, NoConversion, PointBased, GridBased, VertexGrid, CellGri export Pixel, px, Unit, plotkey, attributes, used_attributes export Linestyle -const RealVector{T} = AbstractVector{T} where T <: Number +const RealArray{T, N} = AbstractArray{T, N} where {T<:Real} +const RealVector{T} = RealArray{1} +const RealMatrix{T} = RealArray{2} + 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 Mat2d = Mat2{Float64} +const Mat3d = Mat3{Float64} +const Mat4d = Mat4{Float64} +export Point2d, Point3d, Point4d, Vec2d, Vec3d, Vec4d, Rect2d, Rect3d + +# TODO: move to GeometryBasics? +function Base.convert(::Type{Rect{N, T}}, r::Rect{N}) where {N, T} + return Rect{N, T}(r) +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...)) @@ -119,6 +154,7 @@ include("utilities/utilities.jl") # need Makie.AbstractPattern include("lighting.jl") # Basic scene/plot/recipe interfaces + types include("scenes.jl") +include("float32-scaling.jl") include("interfaces.jl") include("conversions.jl") @@ -168,6 +204,7 @@ include("basic_recipes/tricontourf.jl") include("basic_recipes/triplot.jl") include("basic_recipes/volumeslices.jl") include("basic_recipes/voronoiplot.jl") +include("basic_recipes/voxels.jl") include("basic_recipes/waterfall.jl") include("basic_recipes/wireframe.jl") include("basic_recipes/tooltip.jl") @@ -175,8 +212,10 @@ include("basic_recipes/tooltip.jl") # layouting of plots include("layouting/transformation.jl") include("layouting/data_limits.jl") -include("layouting/layouting.jl") +include("layouting/text_layouting.jl") include("layouting/boundingbox.jl") +include("layouting/text_boundingbox.jl") +include("layouting/maybe_unused.jl") # Declaritive SpecApi include("specapi.jl") @@ -356,9 +395,9 @@ include("basic_recipes/text.jl") include("basic_recipes/raincloud.jl") include("deprecated.jl") -export Arrows , Heatmap , Image , Lines , LineSegments , Mesh , MeshScatter , Poly , Scatter , Surface , Text , Volume , Wireframe -export arrows , heatmap , image , lines , linesegments , mesh , meshscatter , poly , scatter , surface , text , volume , wireframe -export arrows! , heatmap! , image! , lines! , linesegments! , mesh! , meshscatter! , poly! , scatter! , surface! , text! , volume! , wireframe! +export Arrows , Heatmap , Image , Lines , LineSegments , Mesh , MeshScatter , Poly , Scatter , Surface , Text , Volume , Wireframe, Voxels +export arrows , heatmap , image , lines , linesegments , mesh , meshscatter , poly , scatter , surface , text , volume , wireframe, voxels +export arrows! , heatmap! , image! , lines! , linesegments! , mesh! , meshscatter! , poly! , scatter! , surface! , text! , volume! , wireframe!, voxels! export AmbientLight, PointLight, DirectionalLight, SpotLight, EnvironmentLight, RectLight, SSAO diff --git a/src/basic_recipes/ablines.jl b/src/basic_recipes/ablines.jl index 3c2268b241e..ad01cdf3921 100644 --- a/src/basic_recipes/ablines.jl +++ b/src/basic_recipes/ablines.jl @@ -3,16 +3,9 @@ Creates a line defined by `f(x) = slope * x + intercept` crossing a whole `Scene` with 2D projection at its current limits. You can pass one or multiple intercepts or slopes. - -All style attributes are the same as for `LineSegments`. """ -@recipe(ABLines) do scene - Theme(; - xautolimits = false, - yautolimits = false, - default_theme(LineSegments, scene)..., - cycle = :color, - ) +@recipe ABLines begin + MakieCore.documented_attributes(LineSegments)... end function Makie.plot!(p::ABLines) @@ -21,9 +14,9 @@ function Makie.plot!(p::ABLines) is_identity_transform(transf) || throw(ArgumentError("ABLines is only defined for the identity transform, not $(typeof(transf)).")) - limits = lift(projview_to_2d_limits, p, scene.camera.projectionview) + limits = projview_to_2d_limits(p) - points = Observable(Point2f[]) + points = Observable(Point2d[]) onany(p, limits, p[1], p[2]) do lims, intercept, slope empty!(points[]) @@ -31,8 +24,8 @@ function Makie.plot!(p::ABLines) broadcast_foreach(intercept, slope) do intercept, slope f(x) = intercept + slope * x xmin, xmax = first.(extrema(lims)) - push!(points[], Point2f(xmin, f(xmin))) - push!(points[], Point2f(xmax, f(xmax))) + push!(points[], Point2d(xmin, f(xmin))) + push!(points[], Point2d(xmax, f(xmax))) end notify(points) end @@ -43,6 +36,9 @@ function Makie.plot!(p::ABLines) p end +data_limits(::ABLines) = Rect3f(Point3f(NaN), Vec3f(NaN)) +boundingbox(::ABLines, space::Symbol = :data) = Rect3f(Point3f(NaN), Vec3f(NaN)) + function abline!(args...; kwargs...) Base.depwarn("abline! is deprecated and will be removed in the future. Use ablines / ablines! instead." , :abline!, force = true) ablines!(args...; kwargs...) diff --git a/src/basic_recipes/annotations.jl b/src/basic_recipes/annotations.jl index 941614d2301..6f0906ad954 100644 --- a/src/basic_recipes/annotations.jl +++ b/src/basic_recipes/annotations.jl @@ -2,19 +2,16 @@ annotations(strings::Vector{String}, positions::Vector{Point}) Plots an array of texts at each position in `positions`. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Annotations, text, position) do scene - default_theme(scene, Text) +@recipe Annotations text position begin + MakieCore.documented_attributes(Text)... end function convert_arguments(::Type{<: Annotations}, strings::AbstractVector{<: AbstractString}, - text_positions::AbstractVector{<: Point{N}}) where N + text_positions::AbstractVector{<: Point{N, T}}) where {N, T} return (map(strings, text_positions) do str, pos - (String(str), Point{N, Float32}(pos)) + (String(str), Point{N, float_type(T)}(pos)) end,) end diff --git a/src/basic_recipes/arc.jl b/src/basic_recipes/arc.jl index b13042c402d..4445db15cf8 100644 --- a/src/basic_recipes/arc.jl +++ b/src/basic_recipes/arc.jl @@ -10,15 +10,11 @@ Examples: `arc(Point2f(0), 1, 0.0, π)` `arc(Point2f(1, 2), 0.3. π, -π)` - -## Attributes -$(ATTRIBUTES) """ -@recipe(Arc, origin, radius, start_angle, stop_angle) do scene - Attributes(; - default_theme(scene, Lines)..., - resolution = 361, - ) +@recipe Arc origin radius start_angle stop_angle begin + MakieCore.documented_attributes(Lines)... + "The number of line points approximating the arc." + resolution = 361 end function plot!(p::Arc) diff --git a/src/basic_recipes/arrows.jl b/src/basic_recipes/arrows.jl index c9a04553ea6..af82c2eeb5d 100644 --- a/src/basic_recipes/arrows.jl +++ b/src/basic_recipes/arrows.jl @@ -22,9 +22,6 @@ grid. If a `Function` is provided in place of `u, v, [w]`, then it must accept a `Point` as input, and return an appropriately dimensioned `Point`, `Vec`, or other array-like output. - -## Attributes -$(ATTRIBUTES) """ arrows @@ -106,11 +103,15 @@ function _circle(origin, r, normal, N) GeometryBasics.Mesh(meta(coords; normals=normals), faces) end -convert_arguments(::Type{<: Arrows}, x, y, u, v) = (Point2f.(x, y), Vec2f.(u, v)) +function convert_arguments(::Type{<: Arrows}, x, y, u, v) + return (Point2{float_type(x, y)}.(x, y), Vec2{float_type(u, v)}.(u, v)) +end function convert_arguments(::Type{<: Arrows}, x::AbstractVector, y::AbstractVector, u::AbstractMatrix, v::AbstractMatrix) - (vec(Point2f.(x, y')), vec(Vec2f.(u, v))) + return (vec(Point2{float_type(x, y)}.(x, y')), vec(Vec2{float_type(u, v)}.(u, v))) +end +function convert_arguments(::Type{<: Arrows}, x, y, z, u, v, w) + return (Point3{float_type(x, y, z)}.(x, y, z), Vec3{float_type(u, v, w)}.(u, v, w)) end -convert_arguments(::Type{<: Arrows}, x, y, z, u, v, w) = (Point3f.(x, y, z), Vec3f.(u, v, w)) function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) where {N, V} @extract arrowplot ( @@ -122,8 +123,8 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher fxaa, ssao, transparency, visible, inspectable ) - arrow_c = map((a, c)-> a === automatic ? c : a , arrowplot, arrowcolor, color) line_c = map((a, c)-> a === automatic ? c : a , arrowplot, linecolor, color) + arrow_c = map((a, c)-> a === automatic ? c : a , arrowplot, arrowcolor, color) fxaa_bool = lift(fxaa -> fxaa == automatic ? N == 3 : fxaa, arrowplot, fxaa) # automatic == fxaa for 3D marker_head = lift((ah, q) -> arrow_head(N, ah, q), arrowplot, arrowhead, quality) @@ -176,7 +177,7 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher lift(x-> last.(x), arrowplot, headstart), marker=marker_head, markersize = lift(as-> as === automatic ? theme(scene, :markersize)[] : as, arrowplot, arrowsize), - color = arrow_c, rotations = rotations, strokewidth = 0.0, + color = arrow_c, rotation = rotations, strokewidth = 0.0, colormap=colormap, markerspace=arrowplot.markerspace, colorrange=arrowplot.colorrange, fxaa = fxaa_bool, inspectable = inspectable, transparency = transparency, visible = visible @@ -210,7 +211,7 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher marker_tail = lift((at, q) -> arrow_tail(3, at, q), arrowplot, arrowtail, quality) meshscatter!( arrowplot, - start, rotations = directions, markersize = msize, + start, rotation = directions, markersize = msize, marker = marker_tail, color = line_c, colormap = colormap, colorscale = colorscale, colorrange = arrowplot.colorrange, fxaa = fxaa_bool, ssao = ssao, shading = shading, @@ -219,7 +220,7 @@ function plot!(arrowplot::Arrows{<: Tuple{AbstractVector{<: Point{N}}, V}}) wher ) meshscatter!( arrowplot, - start, rotations = directions, markersize = markersize, + start, rotation = directions, markersize = markersize, marker = marker_head, color = arrow_c, colormap = colormap, colorscale = colorscale, colorrange = arrowplot.colorrange, fxaa = fxaa_bool, ssao = ssao, shading = shading, diff --git a/src/basic_recipes/axis.jl b/src/basic_recipes/axis.jl index 82db813c536..6336234dfd2 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/band.jl b/src/basic_recipes/band.jl index 9a8de39b48d..05c3a23aefe 100644 --- a/src/basic_recipes/band.jl +++ b/src/basic_recipes/band.jl @@ -4,20 +4,15 @@ Plots a band from `ylower` to `yupper` along `x`. The form `band(lower, upper)` plots a [ruled surface](https://en.wikipedia.org/wiki/Ruled_surface) between the points in `lower` and `upper`. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Band, lowerpoints, upperpoints) do scene - attr = Attributes(; - default_theme(scene, Mesh)..., - colorrange = automatic, - ) - attr[:shading][] = NoShading - attr +@recipe Band lowerpoints upperpoints begin + MakieCore.documented_attributes(Mesh)... + shading = NoShading end -convert_arguments(::Type{<: Band}, x, ylower, yupper) = (Point2f.(x, ylower), Point2f.(x, yupper)) +function convert_arguments(::Type{<: Band}, x, ylower, yupper) + return (Point2{float_type(x, ylower)}.(x, ylower), Point2{float_type(x, yupper)}.(x, yupper)) +end function band_connect(n) ns = 1:n-1 diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index e08fede2e9e..8d9e941bc35 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -35,54 +35,53 @@ function bar_default_fillto(tf::Tuple, ys, offset, in_y_direction) end """ - barplot(x, y; kwargs...) + barplot(positions, heights; kwargs...) -Plots a barplot; `y` defines the height. `x` and `y` should be 1 dimensional. -Bar width is determined by the attribute `width`, shrunk by `gap` in the following way: -`width -> width * (1 - gap)`. - -## Attributes -$(ATTRIBUTES) +Plots a barplot. """ -@recipe(BarPlot, x, y) do scene - Attributes(; - fillto = automatic, - offset = 0.0, - color = theme(scene, :patchcolor), - alpha = 1.0, - colormap = theme(scene, :colormap), - colorscale = identity, - colorrange = automatic, - lowclip = automatic, - highclip = automatic, - nan_color = :transparent, - dodge = automatic, - n_dodge = automatic, - gap = 0.2, - dodge_gap = 0.03, - marker = Rect, - stack = automatic, - strokewidth = theme(scene, :patchstrokewidth), - strokecolor = theme(scene, :patchstrokecolor), - width = automatic, - direction = :y, - visible = theme(scene, :visible), - inspectable = theme(scene, :inspectable), - cycle = [:color => :patchcolor], - - bar_labels = nothing, - flip_labels_at = Inf, - label_rotation = 0π, - label_color = theme(scene, :textcolor), - color_over_background = automatic, - color_over_bar = automatic, - label_offset = 5, - label_font = theme(scene, :font), - label_size = theme(scene, :fontsize), - label_formatter = bar_label_formatter, - label_align = automatic, - transparency = false - ) +@recipe BarPlot x y begin + """Controls the baseline of the bars. This is zero in the default `automatic` case unless the barplot is in a log-scaled `Axis`. + With a log scale, the automatic default is half the minimum value because zero is an invalid value for a log scale. + """ + fillto = automatic + offset = 0.0 + color = @inherit patchcolor + MakieCore.mixin_generic_plot_attributes()... + MakieCore.mixin_colormap_attributes()... + dodge = automatic + n_dodge = automatic + """ + The final width of the bars is calculated as `w * (1 - gap)` where `w` is the width of each bar + as determined with the `width` attribute. + """ + gap = 0.2 + dodge_gap = 0.03 + stack = automatic + strokewidth = @inherit patchstrokewidth + strokecolor = @inherit patchstrokecolor + """ + The gapless width of the bars. If `automatic`, the width `w` is calculated as `minimum(diff(sort(unique(positions)))`. + The actual width of the bars is calculated as `w * (1 - gap)`. + """ + width = automatic + "Controls the direction of the bars, can be `:y` (vertical) or `:x` (horizontal)." + direction = :y + cycle = [:color => :patchcolor] + "Labels added at the end of each bar." + bar_labels = nothing + flip_labels_at = Inf + label_rotation = 0π + label_color = @inherit textcolor + color_over_background = automatic + color_over_bar = automatic + "The distance of the labels from the bar ends in screen units." + label_offset = 5 + "The font of the bar labels." + label_font = @inherit font + "The font size of the bar labels." + label_size = @inherit fontsize + label_formatter = bar_label_formatter + label_align = automatic end conversion_trait(::Type{<: BarPlot}) = PointBased() @@ -92,7 +91,7 @@ function bar_rectangle(x, y, width, fillto, in_y_direction) ymin = min(fillto, y) ymax = max(fillto, y) w = abs(width) - rect = Rectf(x - (w / 2f0), ymin, w, ymax - ymin) + rect = Rectd(x - (w / 2f0), ymin, w, ymax - ymin) return in_y_direction ? rect : flip(rect) end @@ -174,8 +173,8 @@ end function text_attributes(values, in_y_direction, flip_labels_at, color_over_background, color_over_bar, label_offset, label_rotation, label_align) - aligns = Vec2f[] - offsets = Vec2f[] + aligns = Vec2d[] + offsets = Vec2d[] text_colors = RGBAf[] swap(x, y) = in_y_direction ? (x, y) : (y, x) geti(x::AbstractArray, i) = x[i] @@ -226,7 +225,7 @@ function barplot_labels(xpositions, ypositions, offset, bar_labels, in_y_directi attributes = text_attributes(ypositions, in_y_direction, flip_labels_at, color_over_background, color_over_bar, label_offset, label_rotation, label_align) label_pos = broadcast(xpositions, ypositions, offset, bar_labels) do x, y, off, l - return (string(label_formatter(l)), in_y_direction ? Point2f(x, y+off) : Point2f(y, x+off)) + return (string(label_formatter(l)), in_y_direction ? Point2d(x, y+off) : Point2d(y, x+off)) end return (label_pos, attributes...) else @@ -242,9 +241,9 @@ function Makie.plot!(p::BarPlot) if !(eltype(bar_points[]) <: Point2) error("barplot only accepts x/y coordinates. Use `barplot(x, y)` or `barplot(xy::Vector{<:Point2})`.") end - labels = Observable(Tuple{Union{String,LaTeXStrings.LaTeXString}, Point2f}[]) - label_aligns = Observable(Vec2f[]) - label_offsets = Observable(Vec2f[]) + labels = Observable(Tuple{Union{String,LaTeXStrings.LaTeXString}, Point2d}[]) + label_aligns = Observable(Vec2d[]) + label_offsets = Observable(Vec2d[]) label_colors = Observable(RGBAf[]) function calculate_bars(xy, fillto, offset, transformation, width, dodge, n_dodge, gap, dodge_gap, stack, dir, bar_labels, flip_labels_at, label_color, color_over_background, diff --git a/src/basic_recipes/bracket.jl b/src/basic_recipes/bracket.jl index 9a76143e342..18d96e4e6df 100644 --- a/src/basic_recipes/bracket.jl +++ b/src/basic_recipes/bracket.jl @@ -7,40 +7,46 @@ Draws a bracket between each pair of points (x1, y1) and (x2, y2) with a text label at the midpoint. By default each label is rotated parallel to the line between the bracket points. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Bracket) do scene - Theme( - offset = 0, - width = 15, - text = "", - font = theme(scene, :font), - orientation = :up, - align = (:center, :center), - textoffset = automatic, - fontsize = theme(scene, :fontsize), - rotation = automatic, - color = theme(scene, :linecolor), - textcolor = theme(scene, :textcolor), - linewidth = theme(scene, :linewidth), - linestyle = :solid, - justification = automatic, - style = :curly, - ) +@recipe Bracket begin + "The offset of the bracket perpendicular to the line from start to end point in screen units. + The direction depends on the `orientation` attribute." + offset = 0 + """ + The width of the bracket (perpendicularly away from the line from start to end point) in screen units. + """ + width = 15 + text = "" + font = @inherit font + "Which way the bracket extends relative to the line from start to end point. Can be `:up` or `:down`." + orientation = :up + align = (:center, :center) + textoffset = automatic + fontsize = @inherit fontsize + rotation = automatic + color = @inherit linecolor + textcolor = @inherit textcolor + linewidth = @inherit linewidth + linestyle = :solid + justification = automatic + style = :curly end -Makie.convert_arguments(::Type{<:Bracket}, point1::VecTypes, point2::VecTypes) = ([(Point2f(point1), Point2f(point2))],) -Makie.convert_arguments(::Type{<:Bracket}, x1::Real, y1::Real, x2::Real, y2::Real) = ([(Point2f(x1, y1), Point2f(x2, y2))],) -function Makie.convert_arguments(::Type{<:Bracket}, x1::AbstractVector{<:Real}, y1::AbstractVector{<:Real}, x2::AbstractVector{<:Real}, y2::AbstractVector{<:Real}) +function convert_arguments(::Type{<:Bracket}, point1::VecTypes{2, T1}, point2::VecTypes{2, T2}) where {T1, T2} + return ([(Point2{float_type(T1)}(point1), Point2{float_type(T2)}(point2))],) +end +function convert_arguments(::Type{<:Bracket}, x1::Real, y1::Real, x2::Real, y2::Real) + return ([(Point2{float_type(x1, y1)}(x1, y1), Point2{float_type(x2, y2)}(x2, y2))],) +end +function convert_arguments(::Type{<:Bracket}, x1::AbstractVector{<:Real}, y1::AbstractVector{<:Real}, x2::AbstractVector{<:Real}, y2::AbstractVector{<:Real}) + T1 = float_type(x1, y1); T2 = float_type(x2, y2) points = broadcast(x1, y1, x2, y2) do x1, y1, x2, y2 - (Point2f(x1, y1), Point2f(x2, y2)) + (Point2{T1}(x1, y1), Point2{T2}(x2, y2)) end return (points,) end -function Makie.plot!(pl::Bracket) +function plot!(pl::Bracket) points = pl[1] @@ -111,7 +117,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, space::Symbol = :data) = transform_bbox(pl, data_limits(pl)) bracket_bezierpath(style::Symbol, args...) = bracket_bezierpath(Val(style), args...) diff --git a/src/basic_recipes/contourf.jl b/src/basic_recipes/contourf.jl index c2a15ddbe34..b4961f279bc 100644 --- a/src/basic_recipes/contourf.jl +++ b/src/basic_recipes/contourf.jl @@ -3,43 +3,43 @@ Plots a filled contour of the height information in `zs` at horizontal grid positions `xs` and vertical grid positions `ys`. - -The attribute `levels` can be either -- an `Int` that produces n equally wide levels or bands -- an `AbstractVector{<:Real}` that lists n consecutive edges from low to high, which result in n-1 levels or bands - -You can also set the `mode` attribute to `:relative`. -In this mode you specify edges by the fraction between minimum and maximum value of `zs`. -This can be used for example to draw bands for the upper 90% while excluding the lower 10% with `levels = 0.1:0.1:1.0, mode = :relative`. - -In :normal mode, if you want to show a band from `-Inf` to the low edge, -set `extendlow` to `:auto` for the same color as the first level, -or specify a different color (default `nothing` means no extended band) -If you want to show a band from the high edge to `Inf`, set `extendhigh` -to `:auto` for the same color as the last level, or specify a different color -(default `nothing` means no extended band). - -If `levels` is an `Int`, the contour plot will be rectangular as all `zs` will be covered. -This is why `Axis` defaults to tight limits for such contourf plots. -If you specify `levels` as an `AbstractVector{<:Real}`, however, note that the axis limits include the default margins because the contourf plot can have an irregular shape. -You can use `tightlimits!(ax)` to tighten the limits similar to the `Int` behavior. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Contourf) do scene - Theme( - levels = 10, - mode = :normal, - colormap = theme(scene, :colormap), - colorscale = identity, - extendlow = nothing, - extendhigh = nothing, - # TODO, Isoband doesn't seem to support nans? - nan_color = :transparent, - inspectable = theme(scene, :inspectable), - transparency = false - ) +@recipe Contourf begin + """ + Can be either + - an `Int` that produces n equally wide levels or bands + - an `AbstractVector{<:Real}` that lists n consecutive edges from low to high, which result in n-1 levels or bands + + If `levels` is an `Int`, the contourf plot will be rectangular as all `zs` values will be covered edge to edge. + This is why `Axis` defaults to tight limits for such contourf plots. + If you specify `levels` as an `AbstractVector{<:Real}`, however, note that the axis limits include the default margins because the contourf plot can have an irregular shape. + You can use `tightlimits!(ax)` to tighten the limits similar to the `Int` behavior. + """ + levels = 10 + """ + Determines how the `levels` attribute is interpreted, either `:normal` or `:relative`. + In `:normal` mode, the levels correspond directly to the z values. + In `:relative` mode, you specify edges by the fraction between minimum and maximum value of `zs`. + This can be used for example to draw bands for the upper 90% while excluding the lower 10% with `levels = 0.1:0.1:1.0, mode = :relative`. + """ + mode = :normal + colormap = @inherit colormap + colorscale = identity + """ + In `:normal` mode, if you want to show a band from `-Inf` to the low edge, + set `extendlow` to `:auto` to give the extension the same color as the first level, + or specify a color directly (default `nothing` means no extended band). + """ + extendlow = nothing + """ + In `:normal` mode, if you want to show a band from the high edge to `Inf`, set `extendhigh` + to `:auto` to give the extension the same color as the last level, or specify a color directly + (default `nothing` means no extended band). + """ + extendhigh = nothing + # TODO, Isoband doesn't seem to support nans? + nan_color = :transparent + MakieCore.mixin_generic_plot_attributes()... end # these attributes are computed dynamically and needed for colorbar e.g. diff --git a/src/basic_recipes/contours.jl b/src/basic_recipes/contours.jl index 861b9e249ed..e02e43806f7 100644 --- a/src/basic_recipes/contours.jl +++ b/src/basic_recipes/contours.jl @@ -9,39 +9,39 @@ end Creates a contour plot of the plane spanning `x::Vector`, `y::Vector`, `z::Matrix`. If only `z::Matrix` is supplied, the indices of the elements in `z` will be used as the `x` and `y` locations when plotting the contour. - -The attribute levels can be either - - an Int that produces n equally wide levels or bands - - an AbstractVector{<:Real} that lists n consecutive edges from low to high, which result in n-1 levels or bands - -To add contour labels, use `labels = true`, and pass additional label attributes such as `labelcolor`, `labelsize`, `labelfont` or `labelformatter`. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Contour) do scene - attr = Attributes(; - color = nothing, - levels = 5, - linewidth = 1.0, - linestyle = nothing, - enable_depth = true, - transparency = false, - labels = false, - - labelfont = theme(scene, :font), - labelcolor = nothing, # matches color by default - labelformatter = contour_label_formatter, - labelsize = 10, # arbitrary - ) - - - MakieCore.colormap_attributes!(attr, theme(scene, :colormap)) - MakieCore.generic_plot_attributes!(attr) - - return attr +@recipe Contour begin + """ + The color of the contour lines. If `nothing`, the color is determined by the numerical values of the + contour levels in combination with `colormap` and `colorrange`. + """ + color = nothing + """ + Controls the number and location of the contour lines. Can be either + + - an `Int` that produces n equally wide levels or bands + - an `AbstractVector{<:Real}` that lists n consecutive edges from low to high, which result in n-1 levels or bands + """ + levels = 5 + linewidth = 1.0 + linestyle = nothing + enable_depth = true + """ + If `true`, adds text labels to the contour lines. + """ + labels = false + "The font of the contour labels." + labelfont = @inherit font + "Color of the contour labels, if `nothing` it matches `color` by default." + labelcolor = nothing # matches color by default + """ + Formats the numeric values of the contour levels to strings. + """ + labelformatter = contour_label_formatter + "Font size of the contour labels" + labelsize = 10 # arbitrary + MakieCore.mixin_colormap_attributes()... + MakieCore.mixin_generic_plot_attributes()... end """ @@ -49,12 +49,9 @@ end Creates a 3D contour plot of the plane spanning x::Vector, y::Vector, z::Matrix, with z-elevation for each level. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Contour3d) do scene - default_theme(scene, Contour) +@recipe Contour3d begin + MakieCore.documented_attributes(Contour)... end angle(p1::Union{Vec2f,Point2f}, p2::Union{Vec2f,Point2f})::Float32 = @@ -160,6 +157,9 @@ function plot!(plot::Contour{<: Tuple{X, Y, Z, Vol}}) where {X, Y, Z, Vol} pop!(attr, :labelsize) pop!(attr, :labelcolor) pop!(attr, :labelformatter) + pop!(attr, :color) + pop!(attr, :linestyle) + pop!(attr, :linewidth) volume!(plot, attr, x, y, z, volume) end @@ -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}}, space::Symbol = :data) where {X, Y, Z} + return transform_bbox(plot, data_limits(plot)) end +# TODO: should this have a data_limits overload? +function boundingbox(plot::Contour3d, space::Symbol = :data) + 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 d9bf42b2244..f4ae09fe851 100644 --- a/src/basic_recipes/datashader.jl +++ b/src/basic_recipes/datashader.jl @@ -273,7 +273,7 @@ end !!! warning This feature might change outside breaking releases, since the API is not yet finalized. - Please be vary of bugs in the implementation and open issues if you encounter odd behaviour. + Please be wary of bugs in the implementation and open issues if you encounter odd behaviour. Points can be any array type supporting iteration & getindex, including memory mapped arrays. If you have separate arrays for x and y coordinates and want to avoid conversion and copy, consider using: @@ -282,60 +282,61 @@ using Makie.StructArrays points = StructArray{Point2f}((x, y)) datashader(points) ``` -Do pay attention though, that if x and y don't have a fast iteration/getindex implemented, this might be slower then just copying it into a new array. +Do pay attention though, that if x and y don't have a fast iteration/getindex implemented, this might be slower than just copying the data into a new array. For best performance, use `method=Makie.AggThreads()` and make sure to start julia with `julia -tauto` or have the environment variable `JULIA_NUM_THREADS` set to the number of cores you have. - -## Attributes - -### Specific to `DataShader` - -- `agg = AggCount()` can be `AggCount()`, `AggAny()` or `AggMean()`. User extendable by overloading: - - - ```Julia - struct MyAgg{T} <: Makie.AggOp end - MyAgg() = MyAgg{Float64}() - Makie.Aggregation.null(::MyAgg{T}) where {T} = zero(T) - Makie.Aggregation.embed(::MyAgg{T}, x) where {T} = convert(T, x) - Makie.Aggregation.merge(::MyAgg{T}, x::T, y::T) where {T} = x + y - Makie.Aggregation.value(::MyAgg{T}, x::T) where {T} = x - ``` - -- `method = AggThreads()` can be `AggThreads()` or `AggSerial()`. -- `async::Bool = true` will calculate get_aggregation in a task, and skip any zoom/pan updates while busy. Great for interaction, but must be disabled for saving to e.g. png or when inlining in documenter. - -- `operation::Function = automatic` Defaults to `Makie.equalize_histogram` function which gets called on the whole get_aggregation array before display (`operation(final_aggregation_result)`). -- `local_operation::Function = identity` function which gets call on each element after the aggregation (`map!(x-> local_operation(x), final_aggregation_result)`). - -- `point_transform::Function = identity` function which gets applied to every point before aggregating it. -- `binsize::Number = 1` factor defining how many bins one wants per screen pixel. Set to n > 1 if you want a corser image. -- `show_timings::Bool = false` show how long it takes to aggregate each frame. -- `interpolate::Bool = true` If the resulting image should be displayed interpolated. - -$(Base.Docs.doc(MakieCore.colormap_attributes!)) - -$(Base.Docs.doc(MakieCore.generic_plot_attributes!)) """ -@recipe(DataShader, points) do scene - attr = Theme( - - agg = AggCount(), - method = AggThreads(), - async = true, - # Defaults to equalize_histogram - # just set to automatic, so that if one sets local_operation, one doesn't do equalize_histogram on top of things. - operation=automatic, - local_operation=identity, - - point_transform = identity, - binsize = 1, - show_timings = false, - - interpolate = true - ) - MakieCore.generic_plot_attributes!(attr) - return MakieCore.colormap_attributes!(attr, theme(scene, :colormap)) +@recipe DataShader points begin + """ + Can be `AggCount()`, `AggAny()` or `AggMean()`. User-extensible by overloading: + + ```julia + struct MyAgg{T} <: Makie.AggOp end + MyAgg() = MyAgg{Float64}() + Makie.Aggregation.null(::MyAgg{T}) where {T} = zero(T) + Makie.Aggregation.embed(::MyAgg{T}, x) where {T} = convert(T, x) + Makie.Aggregation.merge(::MyAgg{T}, x::T, y::T) where {T} = x + y + Makie.Aggregation.value(::MyAgg{T}, x::T) where {T} = x + ``` + """ + agg = AggCount() + """ + Can be `AggThreads()` or `AggSerial()` for threaded vs. serial aggregation. + """ + method = AggThreads() + """ + Will calculate `get_aggregation` in a task, and skip any zoom/pan updates while busy. Great for interaction, but must be disabled for saving to e.g. png or when inlining in Documenter. + """ + async = true + # Defaults to equalize_histogram + # just set to automatic, so that if one sets local_operation, one doesn't do equalize_histogram on top of things. + """ + Defaults to `Makie.equalize_histogram` function which gets called on the whole get_aggregation array before display (`operation(final_aggregation_result)`). + """ + operation=automatic + """ + Function which gets called on each element after the aggregation (`map!(x-> local_operation(x), final_aggregation_result)`). + """ + local_operation=identity + + """ + Function which gets applied to every point before aggregating it. + """ + point_transform = identity + """ + Factor defining how many bins one wants per screen pixel. Set to n > 1 if you want a coarser image. + """ + binsize = 1 + """ + Set to `true` to show how long it takes to aggregate each frame. + """ + show_timings = false + """ + If the resulting image should be displayed interpolated. + """ + interpolate = true + MakieCore.mixin_generic_plot_attributes()... + MakieCore.mixin_colormap_attributes()... end function fast_bb(points, f) @@ -376,7 +377,7 @@ end function Makie.plot!(p::DataShader{<: Tuple{<: AbstractVector{<: Point}}}) scene = parent_scene(p) - limits = lift(projview_to_2d_limits, p, scene.camera.projectionview; ignore_equal_values=true) + limits = projview_to_2d_limits(p) viewport = lift(identity, p, scene.viewport; ignore_equal_values=true) canvas = canvas_obs(limits, viewport, p.agg, p.binsize) p._boundingbox = lift(fast_bb, p.points, p.point_transform) @@ -400,8 +401,8 @@ function Makie.plot!(p::DataShader{<: Tuple{<: AbstractVector{<: Point}}}) return end p.raw_colorrange = colorrange - image!(p, canvas_with_aggregation; - operation=p.operation, local_operation=p.local_operation, interpolate=p.interpolate, + image!(p, canvas_with_aggregation, p.operation, p.local_operation; + interpolate=p.interpolate, MakieCore.generic_plot_attributes(p)..., MakieCore.colormap_attributes(p)...) return p @@ -431,7 +432,7 @@ end function Makie.plot!(p::DataShader{<:Tuple{Dict{String, Vector{Point{2, Float32}}}}}) scene = parent_scene(p) - limits = lift(projview_to_2d_limits, p, scene.camera.projectionview; ignore_equal_values=true) + limits = projview_to_2d_limits(p) viewport = lift(identity, p, scene.viewport; ignore_equal_values=true) canvas = canvas_obs(limits, viewport, Observable(AggCount{Float32}()), p.binsize) p._boundingbox = lift(p.points, p.point_transform) do cats, func @@ -458,20 +459,19 @@ function Makie.plot!(p::DataShader{<:Tuple{Dict{String, Vector{Point{2, Float32} colors = Dict(k => Makie.wong_colors()[i] for (i, (k, v)) in enumerate(categories)) p._categories = colors op = map(total -> (x -> log10(x + 1) / log10(total + 1)), toal_value) - for (k, canvas) in canvases + + for (k, canv) in canvases color = colors[k] cmap = [(color, 0.0), (color, 1.0)] - image!(p, canvas; colorrange=Vec2f(0, 1), colormap=cmap, operation=identity, local_operation=op) + image!(p, canv, identity, op; colorrange=Vec2f(0, 1), colormap=cmap) end return p end data_limits(p::DataShader) = p._boundingbox[] +boundingbox(p::DataShader, space::Symbol = :data) = transform_bbox(p, p._boundingbox[]) -used_attributes(::Canvas) = (:operation, :local_operation) - -function convert_arguments(P::Type{<:Union{MeshScatter,Image,Surface,Contour,Contour3d}}, canvas::Canvas; - operation=automatic, local_operation=identity) +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) (xmin, ymin), (xmax, ymax) = extrema(canvas.bounds) return convert_arguments(P, xmin .. xmax, ymin .. ymax, pixel) diff --git a/src/basic_recipes/error_and_rangebars.jl b/src/basic_recipes/error_and_rangebars.jl index 95dc2ec39e1..8d0cb954a1c 100644 --- a/src/basic_recipes/error_and_rangebars.jl +++ b/src/basic_recipes/error_and_rangebars.jl @@ -13,24 +13,19 @@ Plots errorbars at xy positions, extending by errors in the given `direction`. If you want to plot intervals from low to high values instead of relative errors, use `rangebars`. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Errorbars) do scene - Theme( - whiskerwidth = 0, - color = theme(scene, :linecolor), - linewidth = theme(scene, :linewidth), - direction = :y, - visible = theme(scene, :visible), - colormap = theme(scene, :colormap), - colorscale = identity, - colorrange = automatic, - inspectable = theme(scene, :inspectable), - transparency = false, - cycle = [:color] - ) +@recipe Errorbars begin + "The width of the whiskers or line caps in screen units." + whiskerwidth = 0 + "The color of the lines. Can be an array to color each bar separately." + color = @inherit linecolor + "The thickness of the lines in screen units." + linewidth = @inherit linewidth + "The direction in which the bars are drawn. Can be `:x` or `:y`." + direction = :y + cycle = [:color] + MakieCore.mixin_colormap_attributes()... + MakieCore.mixin_generic_plot_attributes()... end @@ -43,86 +38,90 @@ Plots rangebars at `val` in one dimension, extending from `low` to `high` in the given the chosen `direction`. If you want to plot errors relative to a reference value, use `errorbars`. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Rangebars) do scene - Theme( - whiskerwidth = 0, - color = theme(scene, :linecolor), - linewidth = theme(scene, :linewidth), - direction = :y, - visible = theme(scene, :visible), - colormap = theme(scene, :colormap), - colorscale = identity, - colorrange = automatic, - inspectable = theme(scene, :inspectable), - transparency = false, - cycle = [:color] - ) +@recipe Rangebars begin + "The width of the whiskers or line caps in screen units." + whiskerwidth = 0 + "The color of the lines. Can be an array to color each bar separately." + color = @inherit linecolor + "The thickness of the lines in screen units." + linewidth = @inherit linewidth + "The direction in which the bars are drawn. Can be `:x` or `:y`." + direction = :y + cycle = [:color] + MakieCore.mixin_colormap_attributes()... + MakieCore.mixin_generic_plot_attributes()... end ### conversions for errorbars -function Makie.convert_arguments(::Type{<:Errorbars}, x, y, error_both) +function convert_arguments(::Type{<:Errorbars}, x, y, error_both) + T = float_type(x, y, error_both) xyerr = broadcast(x, y, error_both) do x, y, e - Vec4f(x, y, e, e) + Vec4{T}(x, y, e, e) end (xyerr,) end -function Makie.convert_arguments(::Type{<:Errorbars}, x, y, error_low, error_high) - xyerr = broadcast(Vec4f, x, y, error_low, error_high) +function convert_arguments(::Type{<:Errorbars}, x, y, error_low, error_high) + T = float_type(x, y, error_low, error_high) + xyerr = broadcast(Vec4{T}, x, y, error_low, error_high) (xyerr,) end -function Makie.convert_arguments(::Type{<:Errorbars}, x, y, error_low_high::AbstractVector{<:VecTypes{2}}) +function convert_arguments(::Type{<:Errorbars}, x, y, error_low_high::AbstractVector{<:VecTypes{2, T}}) where T + T_out = float_type(float_type(x, y), T) xyerr = broadcast(x, y, error_low_high) do x, y, (el, eh) - Vec4f(x, y, el, eh) + Vec4{T_out}(x, y, el, eh) end (xyerr,) end -function Makie.convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2}}, error_both) +function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T}}, error_both) where T + T_out = float_type(T, float_type(error_both)) xyerr = broadcast(xy, error_both) do (x, y), e - Vec4f(x, y, e, e) + Vec4{T_out}(x, y, e, e) end (xyerr,) end -function Makie.convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2}}, error_low, error_high) +function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T}}, error_low, error_high) where T + T_out = float_type(T, float_type(error_low, error_high)) xyerr = broadcast(xy, error_low, error_high) do (x, y), el, eh - Vec4f(x, y, el, eh) + Vec4{T_out}(x, y, el, eh) end (xyerr,) end -function Makie.convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2}}, error_low_high::AbstractVector{<:VecTypes{2}}) +function convert_arguments(::Type{<:Errorbars}, xy::AbstractVector{<:VecTypes{2, T1}}, error_low_high::AbstractVector{<:VecTypes{2, T2}}) where {T1, T2} + T_out = float_type(T1, T2) xyerr = broadcast(xy, error_low_high) do (x, y), (el, eh) - Vec4f(x, y, el, eh) + Vec4{T_out}(x, y, el, eh) end (xyerr,) end -function Makie.convert_arguments(::Type{<:Errorbars}, xy_error_both::AbstractVector{<:VecTypes{3}}) +function convert_arguments(::Type{<:Errorbars}, xy_error_both::AbstractVector{<:VecTypes{3, T}}) where T + T_out = float_type(T) xyerr = broadcast(xy_error_both) do (x, y, e) - Vec4f(x, y, e, e) + Vec4{T_out}(x, y, e, e) end (xyerr,) end ### conversions for rangebars -function Makie.convert_arguments(::Type{<:Rangebars}, val, low, high) - val_low_high = broadcast(Vec3f, val, low, high) +function convert_arguments(::Type{<:Rangebars}, val, low, high) + T = float_type(val, low, high) + val_low_high = broadcast(Vec3{T}, val, low, high) (val_low_high,) end -function Makie.convert_arguments(::Type{<:Rangebars}, val, low_high) +function convert_arguments(::Type{<:Rangebars}, val, low_high::AbstractVector{<:VecTypes{2, T}}) where T + T_out = float_type(float_type(val), T) val_low_high = broadcast(val, low_high) do val, (low, high) - Vec3f(val, low, high) + Vec3{T_out}(val, low, high) end (val_low_high,) end @@ -145,12 +144,12 @@ function Makie.plot!(plot::Errorbars{T}) where T <: Tuple{AbstractVector{<:VecTy end linesegpairs = lift(plot, x_y_low_high, is_in_y_direction) do x_y_low_high, in_y - output = sizehint!(Point2f[], 2length(x_y_low_high)) + output = sizehint!(Point2d[], 2length(x_y_low_high)) for (x, y, l, h) in x_y_low_high if in_y - push!(output, Point2f(x, y - l), Point2f(x, y + h)) + push!(output, Point2d(x, y - l), Point2d(x, y + h)) else - push!(output, Point2f(x - l, y), Point2f(x + h, y)) + push!(output, Point2d(x - l, y), Point2d(x + h, y)) end end return output @@ -175,12 +174,12 @@ function Makie.plot!(plot::Rangebars{T}) where T <: Tuple{AbstractVector{<:VecTy end linesegpairs = lift(plot, val_low_high, is_in_y_direction) do vlh, in_y - output = sizehint!(Point2f[], 2length(vlh)) + output = sizehint!(Point2d[], 2length(vlh)) for (v, l, h) in vlh if in_y - push!(output, Point2f(v, l), Point2f(v, h)) + push!(output, Point2d(v, l), Point2d(v, h)) else - push!(output, Point2f(l, v), Point2f(h, v)) + push!(output, Point2d(l, v), Point2d(h, v)) end end return output @@ -248,11 +247,12 @@ end function plot_to_screen(plot, points::AbstractVector) cam = parent_scene(plot).camera space = to_value(get(plot, :space, :data)) - spvm = clip_to_space(cam, :pixel) * space_to_clip(cam, space) * transformationmatrix(plot)[] + spvm = clip_to_space(cam, :pixel) * space_to_clip(cam, space) * + f32_convert_matrix(plot, space) * transformationmatrix(plot)[] return map(points) do p transformed = apply_transform(transform_func(plot), p, space) - p4d = spvm * to_ndim(Point4f, to_ndim(Point3f, transformed, 0), 1) + p4d = spvm * to_ndim(Point4d, to_ndim(Point3d, transformed, 0), 1) return Point2f(p4d) / p4d[4] end end @@ -260,21 +260,23 @@ end function plot_to_screen(plot, p::VecTypes) cam = parent_scene(plot).camera space = to_value(get(plot, :space, :data)) - spvm = clip_to_space(cam, :pixel) * space_to_clip(cam, space) * transformationmatrix(plot)[] + spvm = clip_to_space(cam, :pixel) * space_to_clip(cam, space) * + f32_convert_matrix(plot, space) * transformationmatrix(plot)[] transformed = apply_transform(transform_func(plot), p, space) - p4d = spvm * to_ndim(Point4f, to_ndim(Point3f, transformed, 0), 1) + p4d = spvm * to_ndim(Point4d, to_ndim(Point3d, transformed, 0), 1) return Point2f(p4d) / p4d[4] end function screen_to_plot(plot, points::AbstractVector) cam = parent_scene(plot).camera space = to_value(get(plot, :space, :data)) - mvps = inv(transformationmatrix(plot)[]) * clip_to_space(cam, space) * space_to_clip(cam, :pixel) + mvps = inv(transformationmatrix(plot)[]) * inv_f32_convert_matrix(plot, space) * + clip_to_space(cam, space) * space_to_clip(cam, :pixel) itf = inverse_transform(transform_func(plot)) return map(points) do p - pre_transform = mvps * to_ndim(Vec4f, to_ndim(Vec3f, p, 0.0), 1.0) - p3 = Point3f(pre_transform) / pre_transform[4] + pre_transform = mvps * to_ndim(Vec4d, to_ndim(Vec3d, p, 0.0), 1.0) + p3 = Point3d(pre_transform) / pre_transform[4] return apply_transform(itf, p3, space) end end @@ -282,13 +284,13 @@ end function screen_to_plot(plot, p::VecTypes) cam = parent_scene(plot).camera space = to_value(get(plot, :space, :data)) - mvps = inv(transformationmatrix(plot)[]) * clip_to_space(cam, space) * space_to_clip(cam, :pixel) - pre_transform = mvps * to_ndim(Vec4f, to_ndim(Vec3f, p, 0.0), 1.0) - p3 = Point3f(pre_transform) / pre_transform[4] + mvps = inv(transformationmatrix(plot)[]) * inv_f32_convert_matrix(plot, space) * + clip_to_space(cam, space) * space_to_clip(cam, :pixel) + pre_transform = mvps * to_ndim(Vec4d, to_ndim(Vec3d, p, 0.0), 1.0) + p3 = Point3d(pre_transform) / pre_transform[4] return apply_transform(itf, p3, space) 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}, space::Symbol = :data) = transform_bbox(bars, data_limits(bars)) diff --git a/src/basic_recipes/hvlines.jl b/src/basic_recipes/hvlines.jl index a919629d6b6..38d12802463 100644 --- a/src/basic_recipes/hvlines.jl +++ b/src/basic_recipes/hvlines.jl @@ -5,17 +5,14 @@ Create horizontal lines across a `Scene` with 2D projection. The lines will be placed at `ys` in data coordinates and `xmin` to `xmax` in scene coordinates (0 to 1). All three of these can have single or multiple values because they are broadcast to calculate the final line segments. - -All style attributes are the same as for `LineSegments`. """ -@recipe(HLines) do scene - Theme(; - xautolimits = false, - xmin = 0, - xmax = 1, - default_theme(scene, LineSegments)..., - cycle = :color, - ) +@recipe HLines begin + "The start of the lines in relative axis units (0 to 1) along the x dimension." + xmin = 0 + "The end of the lines in relative axis units (0 to 1) along the x dimension." + xmax = 1 + MakieCore.documented_attributes(LineSegments)... + cycle = [:color] end """ @@ -25,33 +22,33 @@ Create vertical lines across a `Scene` with 2D projection. The lines will be placed at `xs` in data coordinates and `ymin` to `ymax` in scene coordinates (0 to 1). All three of these can have single or multiple values because they are broadcast to calculate the final line segments. - -All style attributes are the same as for `LineSegments`. """ -@recipe(VLines) do scene - Theme(; - yautolimits = false, - ymin = 0, - ymax = 1, - default_theme(scene, LineSegments)..., - cycle = :color, - ) +@recipe VLines begin + "The start of the lines in relative axis units (0 to 1) along the y dimension." + ymin = 0 + "The start of the lines in relative axis units (0 to 1) along the y dimension." + ymax = 1 + MakieCore.documented_attributes(LineSegments)... + cycle = [:color] end -function projview_to_2d_limits(pv) - xmin, xmax = minmax((((-1, 1) .- pv[1, 4]) ./ pv[1, 1])...) - ymin, ymax = minmax((((-1, 1) .- pv[2, 4]) ./ pv[2, 2])...) - origin = Vec2f(xmin, ymin) - return Rect2f(origin, Vec2f(xmax, ymax) - origin) +function projview_to_2d_limits(plot::AbstractPlot) + scene = parent_scene(plot) + lift(plot, f32_conversion_obs(scene), scene.camera.projectionview, ignore_equal_values = true + ) do f32c, pv + xmin, xmax = minmax((((-1, 1) .- pv[1, 4]) ./ pv[1, 1])...) + ymin, ymax = minmax((((-1, 1) .- pv[2, 4]) ./ pv[2, 2])...) + origin = Vec2d(xmin, ymin) + return inv_f32_convert(f32c, Rect2d(origin, Vec2d(xmax, ymax) - origin)) + end end function Makie.plot!(p::Union{HLines, VLines}) scene = parent_scene(p) transf = transform_func_obs(scene) + limits = projview_to_2d_limits(p) - limits = lift(projview_to_2d_limits, p, scene.camera.projectionview) - - points = Observable(Point2f[]) + points = Observable(Point2d[]) mi = p isa HLines ? p.xmin : p.ymin ma = p isa HLines ? p.xmax : p.ymax @@ -65,14 +62,14 @@ function Makie.plot!(p::Union{HLines, VLines}) x_mi = min_x + (max_x - min_x) * mi x_ma = min_x + (max_x - min_x) * ma val = _apply_y_transform(transf, val) - push!(points[], Point2f(x_mi, val)) - push!(points[], Point2f(x_ma, val)) + push!(points[], Point2d(x_mi, val)) + push!(points[], Point2d(x_ma, val)) elseif p isa VLines y_mi = min_y + (max_y - min_y) * mi y_ma = min_y + (max_y - min_y) * ma val = _apply_x_transform(transf, val) - push!(points[], Point2f(val, y_mi)) - push!(points[], Point2f(val, y_ma)) + push!(points[], Point2d(val, y_mi)) + push!(points[], Point2d(val, y_ma)) end end notify(points) @@ -89,19 +86,13 @@ function Makie.plot!(p::Union{HLines, VLines}) end function data_limits(p::HLines) - scene = parent_scene(p) - limits = projview_to_2d_limits(scene.camera.projectionview[]) - 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(NaN, ymin, 0), Vec3d(NaN, ymax - ymin, 0)) end function data_limits(p::VLines) - scene = parent_scene(p) - limits = projview_to_2d_limits(scene.camera.projectionview[]) - 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, NaN, 0), Vec3d(xmax - xmin, NaN, 0)) +end + +boundingbox(p::Union{HLines, VLines}, space::Symbol = :data) = 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 cb082347705..3823a73ddc2 100644 --- a/src/basic_recipes/hvspan.jl +++ b/src/basic_recipes/hvspan.jl @@ -5,18 +5,15 @@ Create horizontal bands spanning across a `Scene` with 2D projection. The bands will be placed from `ys_low` to `ys_high` in data coordinates and `xmin` to `xmax` in scene coordinates (0 to 1). All four of these can have single or multiple values because they are broadcast to calculate the final spans. - -All style attributes are the same as for `Poly`. """ -@recipe(HSpan) do scene - Theme(; - xautolimits = false, - xmin = 0, - xmax = 1, - default_theme(Poly, scene)..., - cycle = [:color => :patchcolor], - ) - end +@recipe HSpan begin + "The start of the bands in relative axis units (0 to 1) along the x dimension." + xmin = 0 + "The end of the bands in relative axis units (0 to 1) along the x dimension." + xmax = 1 + MakieCore.documented_attributes(Poly)... + cycle = [:color => :patchcolor] +end """ vspan(xs_low, xs_high; ymin = 0.0, ymax = 1.0, attrs...) @@ -25,26 +22,22 @@ Create vertical bands spanning across a `Scene` with 2D projection. The bands will be placed from `xs_low` to `xs_high` in data coordinates and `ymin` to `ymax` in scene coordinates (0 to 1). All four of these can have single or multiple values because they are broadcast to calculate the final spans. - -All style attributes are the same as for `Poly`. """ -@recipe(VSpan) do scene - Theme(; - yautolimits = false, - ymin = 0, - ymax = 1, - default_theme(Poly, scene)..., - cycle = [:color => :patchcolor], - ) +@recipe VSpan begin + "The start of the bands in relative axis units (0 to 1) along the y dimension." + ymin = 0 + "The end of the bands in relative axis units (0 to 1) along the y dimension." + ymax = 1 + MakieCore.documented_attributes(Poly)... + cycle = [:color => :patchcolor] end function Makie.plot!(p::Union{HSpan, VSpan}) scene = Makie.parent_scene(p) transf = transform_func_obs(scene) + limits = projview_to_2d_limits(p) - limits = lift(projview_to_2d_limits, scene.camera.projectionview) - - rects = Observable(Rect2f[]) + rects = Observable(Rect2d[]) mi = p isa HSpan ? p.xmin : p.ymin ma = p isa HSpan ? p.xmax : p.ymax @@ -59,13 +52,13 @@ function Makie.plot!(p::Union{HSpan, VSpan}) x_ma = min_x + (max_x - min_x) * ma low = _apply_y_transform(transf, low) high = _apply_y_transform(transf, high) - push!(rects[], Rect2f(Point2f(x_mi, low), Vec2f(x_ma - x_mi, high - low))) + push!(rects[], Rect2d(Point2(x_mi, low), Vec2(x_ma - x_mi, high - low))) elseif p isa VSpan y_mi = min_y + (max_y - min_y) * mi y_ma = min_y + (max_y - min_y) * ma low = _apply_x_transform(transf, low) high = _apply_x_transform(transf, high) - push!(rects[], Rect2f(Point2f(low, y_mi), Vec2f(high - low, y_ma - y_mi))) + push!(rects[], Rect2d(Point2(low, y_mi), Vec2(high - low, y_ma - y_mi))) end end notify(rects) @@ -91,21 +84,15 @@ _apply_y_transform(::typeof(identity), v) = v function data_limits(p::HSpan) - scene = parent_scene(p) - limits = projview_to_2d_limits(scene.camera.projectionview[]) - itf = inverse_transform(p.transformation.transform_func[]) - 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(NaN, ymin, 0), Vec3d(NaN, ymax - ymin, 0)) end function data_limits(p::VSpan) - scene = parent_scene(p) - limits = projview_to_2d_limits(scene.camera.projectionview[]) - itf = inverse_transform(p.transformation.transform_func[]) 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, NaN, 0), Vec3d(xmax - xmin, NaN, 0)) +end + +boundingbox(p::Union{HSpan, VSpan}, space::Symbol = :data) = transform_bbox(p, data_limits(p)) diff --git a/src/basic_recipes/pie.jl b/src/basic_recipes/pie.jl index e13033d39e9..43f0e1454c8 100644 --- a/src/basic_recipes/pie.jl +++ b/src/basic_recipes/pie.jl @@ -1,25 +1,23 @@ """ - pie(fractions; kwargs...) + pie(values; kwargs...) -Creates a pie chart with the given `fractions`. - -## Attributes -$(ATTRIBUTES) +Creates a pie chart from the given `values`. """ -@recipe(Pie, values) do scene - Theme( - normalize = true, - color = :gray, - strokecolor = :black, - strokewidth = 1, - vertex_per_deg = 1, - radius = 1, - inner_radius = 0, - offset = 0, - inspectable = theme(scene, :inspectable), - visible = true, - transparency = false - ) +@recipe Pie values begin + "If `true`, the sum of all values is normalized to 2π (a full circle)." + normalize = true + color = :gray + strokecolor = :black + strokewidth = 1 + "Controls how many polygon vertices are used for one degree of rotation." + vertex_per_deg = 1 + "The outer radius of the pie segments." + radius = 1 + "The inner radius of the pie segments. If this is larger than zero, the pie pieces become ring sections." + inner_radius = 0 + "The angular offset of the first pie segment from the (1, 0) vector in radians." + offset = 0 + MakieCore.mixin_generic_plot_attributes()... end function plot!(plot::Pie) diff --git a/src/basic_recipes/poly.jl b/src/basic_recipes/poly.jl index 8792eadfb0c..71faaebc21e 100644 --- a/src/basic_recipes/poly.jl +++ b/src/basic_recipes/poly.jl @@ -4,14 +4,21 @@ convert_arguments(::Type{<: Poly}, v::AbstractVector{<: PolyElements}) = (v,) convert_arguments(::Type{<: Poly}, v::Union{Polygon, MultiPolygon}) = (v,) convert_arguments(::Type{<: Poly}, args...) = ([convert_arguments(Scatter, args...)[1]],) -convert_arguments(::Type{<: Poly}, vertices::AbstractArray, indices::AbstractArray) = convert_arguments(Mesh, vertices, indices) +function convert_arguments(::Type{<:Poly}, vertices::AbstractArray, indices::AbstractArray) + return convert_arguments(Mesh, vertices, indices) +end + +function convert_arguments(::Type{<:Poly}, x::RealVector, y::RealVector) + return convert_arguments(PointBased(), x, y) +end + convert_arguments(::Type{<: Poly}, m::GeometryBasics.Mesh) = (m,) convert_arguments(::Type{<: Poly}, m::GeometryBasics.GeometryPrimitive) = (m,) function plot!(plot::Poly{<: Tuple{Union{GeometryBasics.Mesh, GeometryPrimitive}}}) mesh!( - plot, lift(triangle_mesh, plot, plot[1]), + plot, lift(m -> convert_arguments(Mesh, m)[1], plot, plot[1]), color = plot.color, colormap = plot.colormap, colorscale = plot.colorscale, @@ -38,18 +45,20 @@ end # Poly conversion function poly_convert(geometries::AbstractVector, transform_func=identity) + # TODO is this a problem with Float64 meshes? isempty(geometries) && return typeof(GeometryBasics.Mesh(Point2f[], GLTriangleFace[]))[] return poly_convert.(geometries, (transform_func,)) end -function poly_convert(geometry::AbstractGeometry, transform_func=identity) - return GeometryBasics.triangle_mesh(geometry) +function poly_convert(geometry::AbstractGeometry{N, T}, transform_func=identity) where {N, T} + return GeometryBasics.mesh(geometry; pointtype=Point{N,float_type(T)}, facetype=GLTriangleFace) end poly_convert(meshes::AbstractVector{<:AbstractMesh}, transform_func=identity) = poly_convert.(meshes, (transform_func,)) function poly_convert(polys::AbstractVector{<:Polygon}, transform_func=identity) # GLPlainMesh2D is not concrete? + # TODO is this a problem with Float64 meshes? T = GeometryBasics.Mesh{2, Float32, GeometryBasics.Ngon{2, Float32, 3, Point2f}, SimpleFaceView{2, Float32, 3, GLIndex, Point2f, GLTriangleFace}} return isempty(polys) ? T[] : poly_convert.(polys, (transform_func,)) end @@ -62,8 +71,10 @@ poly_convert(mesh::GeometryBasics.Mesh, transform_func=identity) = mesh function poly_convert(polygon::Polygon, transform_func=identity) outer = metafree(coordinates(polygon.exterior)) - points = Vector{Point2f}[apply_transform(transform_func, outer)] - points_flat = Point2f[outer;] + # TODO consider applying f32 convert here too. We would need to identify this though... + PT = float_type(outer) + points = Vector{PT}[apply_transform(transform_func, outer)] + points_flat = PT[outer;] for inner in polygon.interiors inner_points = metafree(coordinates(inner)) append!(points_flat, inner_points) @@ -77,12 +88,12 @@ function poly_convert(polygon::Polygon, transform_func=identity) return GeometryBasics.Mesh(points_flat, faces) end -function poly_convert(polygon::AbstractVector{<:VecTypes}, transform_func=identity) - point2f = convert(Vector{Point2f}, polygon) - points_transformed = apply_transform(transform_func, point2f) +function poly_convert(polygon::AbstractVector{<:VecTypes{2, T}}, transform_func=identity) where {T} + points = convert(Vector{Point2{float_type(T)}}, polygon) + points_transformed = apply_transform(transform_func, points) faces = GeometryBasics.earcut_triangulate([points_transformed]) # TODO, same as above! - return GeometryBasics.Mesh(point2f, faces) + return GeometryBasics.Mesh(points, faces) end function poly_convert(polygons::AbstractVector{<:AbstractVector{<:VecTypes}}, transform_func=identity) @@ -96,21 +107,21 @@ to_lines(polygon) = convert_arguments(Lines, polygon)[1] to_lines(polygon::GeometryBasics.Mesh) = convert_arguments(PointBased(), polygon)[1] function to_lines(meshes::AbstractVector) - line = Point2f[] + line = Point2d[] for (i, mesh) in enumerate(meshes) points = to_lines(mesh) append!(line, points) # push!(line, points[1]) # dont need to separate the last line segment if i != length(meshes) - push!(line, Point2f(NaN)) + push!(line, Point2d(NaN)) end end return line end function to_lines(polygon::AbstractVector{<: VecTypes}) - result = Point2f.(polygon) + result = Point2d.(polygon) isempty(result) || push!(result, polygon[1]) return result end @@ -164,7 +175,7 @@ end function plot!(plot::Mesh{<: Tuple{<: AbstractVector{P}}}) where P <: Union{AbstractMesh, Polygon} meshes = plot[1] - attributes = Attributes( + attrs = Attributes( visible = plot.visible, shading = plot.shading, fxaa = plot.fxaa, inspectable = plot.inspectable, transparency = plot.transparency, space = plot.space, ssao = plot.ssao, @@ -183,6 +194,8 @@ function plot!(plot::Mesh{<: Tuple{<: AbstractVector{P}}}) where P <: Union{Abst mesh_colors = Observable{Union{AbstractPattern, Matrix{RGBAf}, RGBColors, Float32}}() + interpolate_in_fragment_shader = Observable(false) + map!(plot, mesh_colors, plot.color, num_meshes) do colors, num_meshes # one mesh per color if colors isa AbstractVector && length(colors) == length(num_meshes) @@ -196,23 +209,27 @@ function plot!(plot::Mesh{<: Tuple{<: AbstractVector{P}}}) where P <: Union{Abst end end # For GLMakie (right now), to not interpolate between the colors (which are meant to be per mesh) - attributes[:interpolate_in_fragment_shader] = false + interpolate_in_fragment_shader[] = false return result else # If we have colors per vertex, we need to interpolate in fragment shader - attributes[:interpolate_in_fragment_shader] = true + interpolate_in_fragment_shader[] = true return to_color(colors) end end - attributes[:color] = mesh_colors + attrs[:color] = mesh_colors transform_func = plot.transformation.transform_func bigmesh = lift(plot, meshes, transform_func) do meshes, tf if isempty(meshes) + # TODO: Float64 return GeometryBasics.Mesh(Point2f[], GLTriangleFace[]) else triangle_meshes = map(mesh -> poly_convert(mesh, tf), meshes) return merge(triangle_meshes) end end - return mesh!(plot, attributes, bigmesh) + mpl = mesh!(plot, attrs, bigmesh) + # splice in internal attribute after creation to avoid validation + attributes(mpl)[:interpolate_in_fragment_shader] = interpolate_in_fragment_shader + return mpl end diff --git a/src/basic_recipes/raincloud.jl b/src/basic_recipes/raincloud.jl index 93b47b07fbc..c99e54eed7d 100644 --- a/src/basic_recipes/raincloud.jl +++ b/src/basic_recipes/raincloud.jl @@ -28,83 +28,100 @@ between each. - `data_array`: Typically `Vector{Float64}` used for to represent the datapoints to plot. # Keywords -- `gap=0.2`: Distance between elements of x-axis. -- `side=:left`: Can take values of `:left`, `:right`, determines where the violin plot will - be, relative to the scatter points -- `dodge`: vector of `Integer`` (length of data) of grouping variable to create multiple - side-by-side boxes at the same x position -- `dodge_gap = 0.03`: spacing between dodged boxes -- `n_dodge`: the number of categories to dodge (defaults to maximum(dodge)) -- `color`: a single color, or a vector of colors, one for each point ## Violin/Histogram Plot Specific Keywords -- `clouds=violin`: [violin, hist, nothing] to show cloud plots either as violin or histogram - plot, or no cloud plot. -- `hist_bins=30`: if `clouds=hist`, this passes down the number of bins to the histogram - call. -- `cloud_width=1.0`: Determines size of violin plot. Corresponds to `width` keyword arg in -`violin`. -- `orientation=:vertical` orientation of raindclouds (`:vertical` or `:horizontal`) -- `violin_limits=(-Inf, Inf)`: specify values to trim the `violin`. Can be a `Tuple` or a - `Function` (e.g. `datalimits=extrema`) - -## Box Plot Specific Keywords -- `plot_boxplots=true`: Boolean to show boxplots to summarize distribution of data. -- `boxplot_width=0.1`: Width of the boxplot in category x-axis absolute terms. -- `center_boxplot=true`: Determines whether or not to have the boxplot be centered in the - category. -- `whiskerwidth=0.5`: The width of the Q1, Q3 whisker in the boxplot. Value as a portion of - the `boxplot_width`. -- `strokewidth=1.0`: Determines the stroke width for the outline of the boxplot. -- `show_median=true`: Determines whether or not to have a line should the median value in - the boxplot. -- `boxplot_nudge=0.075`: Determines the distance away the boxplot should be placed from the - center line when `center_boxplot` is `false`. This is the value used to recentering the - boxplot. -- `show_boxplot_outliers`: show outliers in the boxplot as points (usually confusing when -paired with the scatter plot so the default is to not show them) ## Scatter Plot Specific Keywords - `side_nudge`: Default value is 0.02 if `plot_boxplots` is true, otherwise `0.075` default. - `jitter_width=0.05`: Determines the width of the scatter-plot bar in category x-axis absolute terms. -- `markersize=2`: Size of marker used for the scatter plot. - -## Axis General Keywords -- `title` -- `xlabel` -- `ylabel` """ -@recipe(RainClouds, category_labels, data_array) do scene - return Attributes( - side = :left, - orientation = :vertical, - center_boxplot = true, - # Cloud plot - cloud_width = 0.75, - violin_limits = (-Inf, Inf), - # Box Plot Settings - boxplot_width = 0.1, - whiskerwidth = 0.5, - strokewidth = 1.0, - show_median = true, - boxplot_nudge = 0.075, - - gap = 0.2, - - markersize = 2.0, - dodge = automatic, - n_dodge = automatic, - dodge_gap = 0.01, - - plot_boxplots = true, - show_boxplot_outliers = false, - clouds = violin, - hist_bins = 30, - - color = theme(scene, :patchcolor), - cycle = [:color => :patchcolor], - ) +@recipe RainClouds category_labels data_array begin + """ + Can take values of `:left`, `:right`, determines where the violin plot will be, + relative to the scatter points + """ + side = :left + """ + Orientation of rainclouds (`:vertical` or `:horizontal`) + """ + orientation = :vertical + """ + Whether or not to center the boxplot on the category. + """ + center_boxplot = true + # Cloud plot + """ + Determines size of violin plot. Corresponds to `width` keyword arg in `violin`. + """ + cloud_width = 0.75 + """ + Specify values to trim the `violin`. Can be a `Tuple` or a + `Function` (e.g. `datalimits=extrema`) + """ + violin_limits = (-Inf, Inf) + # Box Plot Settings + """ + Width of the boxplot on the category axis. + """ + boxplot_width = 0.1 + "The width of the Q1, Q3 whisker in the boxplot. Value as a portion of the `boxplot_width`." + whiskerwidth = 0.5 + "Determines the stroke width for the outline of the boxplot." + strokewidth = 1.0 + """ + Determines whether or not to have a line for the median value in the boxplot. + """ + show_median = true + """ + Determines the distance away the boxplot should be placed from the + center line when `center_boxplot` is `false`. This is the value used to recentering the + boxplot. + """ + boxplot_nudge = 0.075 + + "Distance between elements on the main axis (depending on `orientation`)." + gap = 0.2 + + """ + Size of marker used for the scatter plot. + """ + markersize = 2.0 + + """ + Vector of `Integer` (length of data) of grouping variable to create multiple + side-by-side boxes at the same x position + """ + dodge = automatic + """ + The number of categories to dodge (defaults to `maximum(dodge)`) + """ + n_dodge = automatic + "Spacing between dodged boxes." + dodge_gap = 0.01 + + "Whether to show boxplots to summarize distribution of data." + plot_boxplots = true + """ + Show outliers in the boxplot as points (usually confusing when + paired with the scatter plot so the default is to not show them) + """ + show_boxplot_outliers = false + """ + [`violin`, `hist`, `nothing`] how to show cloud plots, either as violin or histogram + plots, or not at all. + """ + clouds = violin + """ + If `clouds=hist`, this passes down the number of bins to the histogram call. + """ + hist_bins = 30 + + """ + A single color, or a vector of colors, one for each point. + """ + color = @inherit patchcolor + cycle = [:color => :patchcolor] end # create_jitter_array(length_data_array; jitter_width = 0.1, clamped_portion = 0.1) diff --git a/src/basic_recipes/scatterlines.jl b/src/basic_recipes/scatterlines.jl index cc2e02d4ec8..286c9ef5644 100644 --- a/src/basic_recipes/scatterlines.jl +++ b/src/basic_recipes/scatterlines.jl @@ -2,30 +2,28 @@ scatterlines(xs, ys, [zs]; kwargs...) Plots `scatter` markers and `lines` between them. - -## Attributes -$(ATTRIBUTES) """ -@recipe(ScatterLines) do scene - s_theme = default_theme(scene, Scatter) - l_theme = default_theme(scene, Lines) - Attributes( - color = l_theme.color, - colormap = l_theme.colormap, - colorscale = l_theme.colorscale, - colorrange = get(l_theme.attributes, :colorrange, automatic), - linestyle = l_theme.linestyle, - linewidth = l_theme.linewidth, - markercolor = automatic, - markercolormap = automatic, - markercolorrange = automatic, - markersize = s_theme.markersize, - strokecolor = s_theme.strokecolor, - strokewidth = s_theme.strokewidth, - marker = s_theme.marker, - inspectable = theme(scene, :inspectable), - cycle = [:color], - ) +@recipe ScatterLines begin + "The color of the line, and by default also of the scatter markers." + color = @inherit linecolor + "Sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`" + linestyle = nothing + "Sets the width of the line in screen units" + linewidth = @inherit linewidth + markercolor = automatic + markercolormap = automatic + markercolorrange = automatic + "Sets the size of the marker." + markersize = @inherit markersize + "Sets the color of the outline around a marker." + strokecolor = @inherit markerstrokecolor + "Sets the width of the outline around a marker." + strokewidth = @inherit markerstrokewidth + "Sets the scatter marker." + marker = @inherit marker + MakieCore.mixin_generic_plot_attributes()... + MakieCore.mixin_colormap_attributes()... + cycle = [:color] end conversion_trait(::Type{<: ScatterLines}) = PointBased() diff --git a/src/basic_recipes/series.jl b/src/basic_recipes/series.jl index a0375a0790a..d65bdc8779f 100644 --- a/src/basic_recipes/series.jl +++ b/src/basic_recipes/series.jl @@ -1,17 +1,7 @@ """ - series(curves; - linewidth=2, - color=:lighttest, - solid_color=nothing, - labels=nothing, - # scatter arguments, if any is set != nothing, a scatterplot is added - marker=nothing, - markersize=nothing, - markercolor=automatic, - strokecolor=nothing, - strokewidth=nothing) + series(curves) Curves can be: * `AbstractVector{<: AbstractVector{<: Point2}}`: the native representation of a series as a vector of lines @@ -19,21 +9,20 @@ Curves can be: * `AbstractVector, AbstractMatrix`: the same as the above, but the first argument sets the x values for all lines * `AbstractVector{<: Tuple{X<: AbstractVector, Y<: AbstractVector}}`: A vector of tuples, where each tuple contains a vector for the x and y coordinates +If any of `marker`, `markersize`, `markercolor`, `strokecolor` or `strokewidth` is set != nothing, a scatterplot is added. """ -@recipe(Series, curves) do scene - Attributes( - linewidth=2, - color=:lighttest, - solid_color=nothing, - labels=nothing, - linestyle=:solid, - marker=nothing, - markersize=nothing, - markercolor=automatic, - strokecolor=nothing, - strokewidth=nothing, - space = :data, - ) +@recipe Series curves begin + linewidth=2 + color=:lighttest + solid_color=nothing + labels=nothing + linestyle=:solid + marker=nothing + markersize=nothing + markercolor=automatic + strokecolor=nothing + strokewidth=nothing + space = :data end replace_missing(x) = ismissing(x) ? NaN : x @@ -43,14 +32,17 @@ function convert_arguments(T::Type{<: Series}, y::AbstractMatrix) end function convert_arguments(::Type{<: Series}, x::AbstractVector, ys::AbstractMatrix) + T = float_type(x, ys) return (map(1:size(ys, 1)) do i - Point2f.(replace_missing.(x), replace_missing.(view(ys, i, :))) + Point2{T}.(replace_missing.(x), replace_missing.(view(ys, i, :))) end,) end function convert_arguments(::Type{<: Series}, arg::AbstractVector{<: Tuple{X, Y}}) where {X, Y} + # TODO: is this problematic with varying tuple types? return (map(arg) do (x, y) - Point2f.(replace_missing.(x), replace_missing.(y)) + T = float_type(x, y) + Point2{T}.(replace_missing.(x), replace_missing.(y)) end,) end @@ -60,7 +52,8 @@ end function convert_arguments(::Type{<: Series}, arg::AbstractVector{<: AbstractVector{<:Point2}}) return (map(arg) do points - Point2f.(replace_missing.(first.(points)), replace_missing.(last.(points))) + T = float_type(points) + T.(replace_missing.(first.(points)), replace_missing.(last.(points))) end,) end diff --git a/src/basic_recipes/spy.jl b/src/basic_recipes/spy.jl index eda01af1237..feab9efc753 100644 --- a/src/basic_recipes/spy.jl +++ b/src/basic_recipes/spy.jl @@ -10,21 +10,14 @@ spy(x) # or if you want to specify the range of x and y: spy(0..1, 0..1, x) ``` -## Attributes -$(ATTRIBUTES) """ -@recipe(Spy, x, y, z) do scene - Attributes( - marker = automatic, - markersize = automatic, - colormap = theme(scene, :colormap), - colorscale = identity, - colorrange = automatic, - framecolor = :black, - framesize = 1, - inspectable = theme(scene, :inspectable), - visible = theme(scene, :visible) - ) +@recipe Spy x y z begin + marker = automatic + markersize = automatic + framecolor = :black + framesize = 1 + MakieCore.mixin_generic_plot_attributes()... + MakieCore.mixin_colormap_attributes()... end function convert_arguments(::Type{<: Spy}, x::SparseArrays.AbstractSparseArray) diff --git a/src/basic_recipes/stairs.jl b/src/basic_recipes/stairs.jl index 4d74024c9b1..543cb9e2fba 100644 --- a/src/basic_recipes/stairs.jl +++ b/src/basic_recipes/stairs.jl @@ -3,21 +3,17 @@ Plot a stair function. -The `step` parameter can take the following values: -- `:pre`: horizontal part of step extends to the left of each value in `xs`. -- `:post`: horizontal part of step extends to the right of each value in `xs`. -- `:center`: horizontal part of step extends halfway between the two adjacent values of `xs`. - -The conversion trait of stem is `PointBased`. - -## Attributes -$(ATTRIBUTES) +The conversion trait of `stairs` is `PointBased`. """ -@recipe(Stairs) do scene - a = Attributes( - step = :pre, # :center :post - ) - merge(a, default_theme(scene, Lines)) +@recipe Stairs begin + """ + The `step` parameter can take the following values: + - `:pre`: horizontal part of step extends to the left of each value in `xs`. + - `:post`: horizontal part of step extends to the right of each value in `xs`. + - `:center`: horizontal part of step extends halfway between the two adjacent values of `xs`. + """ + step = :pre + MakieCore.documented_attributes(Lines)... end conversion_trait(::Type{<:Stairs}) = PointBased() diff --git a/src/basic_recipes/stem.jl b/src/basic_recipes/stem.jl index 682478762e5..91ab22462ef 100644 --- a/src/basic_recipes/stem.jl +++ b/src/basic_recipes/stem.jl @@ -3,40 +3,35 @@ Plots markers at the given positions extending from `offset` along stem lines. -`offset` can be a number, in which case it sets y for 2D, and z for 3D stems. -It can be a Point2 for 2D plots, as well as a Point3 for 3D plots. -It can also be an iterable of any of these at the same length as xs, ys, zs. - -The conversion trait of stem is `PointBased`. - -## Attributes -$(ATTRIBUTES) +The conversion trait of `stem` is `PointBased`. """ -@recipe(Stem) do scene - Attributes( - stemcolor = theme(scene, :linecolor), - stemcolormap = theme(scene, :colormap), - stemcolorrange = automatic, - stemwidth = theme(scene, :linewidth), - stemlinestyle = nothing, - trunkwidth = theme(scene, :linewidth), - trunklinestyle = nothing, - trunkcolor = theme(scene, :linecolor), - trunkcolormap = theme(scene, :colormap), - trunkcolorrange = automatic, - offset = 0, - marker = :circle, - markersize = theme(scene, :markersize), - color = theme(scene, :markercolor), - colormap = theme(scene, :colormap), - colorscale = identity, - colorrange = automatic, - strokecolor = theme(scene, :markerstrokecolor), - strokewidth = theme(scene, :markerstrokewidth), - visible = true, - inspectable = theme(scene, :inspectable), - cycle = [[:stemcolor, :color, :trunkcolor] => :color], - ) +@recipe Stem begin + stemcolor = @inherit linecolor + stemcolormap = @inherit colormap + stemcolorrange = automatic + stemwidth = @inherit linewidth + stemlinestyle = nothing + trunkwidth = @inherit linewidth + trunklinestyle = nothing + trunkcolor = @inherit linecolor + trunkcolormap = @inherit colormap + trunkcolorrange = automatic + """ + Can be a number, in which case it sets `y` for 2D, and `z` for 3D stems. + It can be a `Point2` for 2D plots, as well as a `Point3` for 3D plots. + It can also be an iterable of any of these at the same length as `xs`, `ys`, `zs`. + """ + offset = 0 + marker = :circle + markersize = @inherit markersize + color = @inherit markercolor + colormap = @inherit colormap + colorscale = identity + colorrange = automatic + strokecolor = @inherit markerstrokecolor + strokewidth = @inherit markerstrokewidth + MakieCore.mixin_generic_plot_attributes()... + cycle = [[:stemcolor, :color, :trunkcolor] => :color] end diff --git a/src/basic_recipes/streamplot.jl b/src/basic_recipes/streamplot.jl index 6855c9858c5..58bf3fb90e7 100644 --- a/src/basic_recipes/streamplot.jl +++ b/src/basic_recipes/streamplot.jl @@ -11,34 +11,29 @@ v(x::Point2{T}) where T = Point2f(x[2], 4*x[1]) streamplot(v, -2..2, -2..2) ``` -One can choose the color of the lines by passing a function `color_func(dx::Point)` to the `color` attribute. -By default this is set to `norm`, but can be set to any function or composition of functions. -The `dx` which is passed to `color_func` is the output of `f` at the point being colored. - -## Attributes -$(ATTRIBUTES) - ## Implementation See the function `Makie.streamplot_impl` for implementation details. """ -@recipe(StreamPlot, f, limits) do scene - attr = Attributes( - stepsize = 0.01, - gridsize = (32, 32, 32), - maxsteps = 500, - color = norm, - - arrow_size = automatic, - arrow_head = automatic, - density = 1.0, - quality = 16, - - linewidth = theme(scene, :linewidth), - linestyle = nothing, - ) - MakieCore.colormap_attributes!(attr, theme(scene, :colormap)) - MakieCore.generic_plot_attributes!(attr) - return attr +@recipe StreamPlot f limits begin + stepsize = 0.01 + gridsize = (32, 32, 32) + maxsteps = 500 + """ + One can choose the color of the lines by passing a function `color_func(dx::Point)` to the `color` attribute. + This can be set to any function or composition of functions. + The `dx` which is passed to `color_func` is the output of `f` at the point being colored. + """ + color = norm + + arrow_size = automatic + arrow_head = automatic + density = 1.0 + quality = 16 + + linewidth = @inherit linewidth + linestyle = nothing + MakieCore.mixin_colormap_attributes()... + MakieCore.mixin_generic_plot_attributes()... end function convert_arguments(::Type{<: StreamPlot}, f::Function, xrange, yrange) @@ -51,8 +46,8 @@ function convert_arguments(::Type{<: StreamPlot}, f::Function, xrange, yrange, z xmin, xmax = extrema(xrange) ymin, ymax = extrema(yrange) zmin, zmax = extrema(zrange) - mini = Vec3f(xmin, ymin, zmin) - maxi = Vec3f(xmax, ymax, zmax) + mini = Vec3(xmin, ymin, zmin) + maxi = Vec3(xmax, ymax, zmax) return (f, Rect(mini, maxi .- mini)) end @@ -228,7 +223,7 @@ function plot!(p::StreamPlot) scatterfun(N)( p, lift(first, p, data); - markersize=arrow_size, rotations=rotations, + markersize=arrow_size, rotation=rotations, color=lift(x -> x[4], p, data), marker = lift((ah, q) -> arrow_head(N, ah, q), p, p.arrow_head, p.quality), colormap_args..., diff --git a/src/basic_recipes/text.jl b/src/basic_recipes/text.jl index 1b42770ede3..70ede844dcb 100644 --- a/src/basic_recipes/text.jl +++ b/src/basic_recipes/text.jl @@ -1,11 +1,10 @@ function check_textsize_deprecation(@nospecialize(dictlike)) if haskey(dictlike, :textsize) - throw(ArgumentError("The attribute `textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.")) + throw(ArgumentError("`textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.")) end end function plot!(plot::Text) - check_textsize_deprecation(plot) positions = plot[1] # attach a function to any text that calculates the glyph layout and stores it glyphcollections = Observable(GlyphCollection[]; ignore_equal_values=true) @@ -60,8 +59,8 @@ function plot!(plot::Text) sc = parent_scene(plot) - onany(plot, linesegs, positions, sc.camera.projectionview, sc.viewport, - transform_func_obs(sc), get(plot, :space, :data)) do segs, pos, _, _, transf, space + onany(plot, linesegs, positions, sc.camera.projectionview, sc.viewport, f32_conversion_obs(sc), + transform_func_obs(sc), get(plot, :space, :data)) do segs, pos, _, _, _, transf, space pos_transf = plot_to_screen(plot, pos) linesegs_shifted[] = map(segs, lineindices[]) do seg, index seg + attr_broadcast_getindex(pos_transf, index) @@ -76,11 +75,13 @@ function plot!(plot::Text) pop!(attrs, :text) pop!(attrs, :align) pop!(attrs, :color) + pop!(attrs, :calculated_colors) t = text!(plot, glyphcollections; attrs..., position = positions) # remove attributes that the backends will choke on pop!(t.attributes, :font) pop!(t.attributes, :fonts) + pop!(t.attributes, :text) linesegments!(plot, linesegs_shifted; linewidth = linewidths, color = linecolors, space = :pixel) plot @@ -124,7 +125,9 @@ function _get_glyphcollection_and_linesegments(latexstring::LaTeXString, index, end function plot!(plot::Text{<:Tuple{<:AbstractString}}) - text!(plot, plot.position; text = plot[1], plot.attributes...) + attrs = copy(plot.attributes) + pop!(attrs, :calculated_colors) + text!(plot, plot.position; attrs..., text = plot[1]) plot end @@ -142,7 +145,9 @@ plot!(plot::Text{<:Tuple{<:GlyphCollection}}) = plot plot!(plot::Text{<:Tuple{<:AbstractArray{<:GlyphCollection}}}) = plot function plot!(plot::Text{<:Tuple{<:AbstractArray{<:AbstractString}}}) - text!(plot, plot.position; text = plot[1], plot.attributes...) + attrs = copy(plot.attributes) + pop!(attrs, :calculated_colors) + text!(plot, plot.position; attrs..., text = plot[1]) plot end @@ -153,18 +158,20 @@ function plot!(plot::Text{<:Tuple{<:AbstractArray{<:Tuple{<:Any, <:Point}}}}) strings = Observable{Vector{Any}}(first.(strings_and_positions[])) positions = Observable( - Point3f[to_ndim(Point3f, last(x), 0) for x in strings_and_positions[]] # avoid Any for zero elements + Point3d[to_ndim(Point3d, last(x), 0) for x in strings_and_positions[]] # avoid Any for zero elements ) attrs = plot.attributes pop!(attrs, :position) + pop!(attrs, :calculated_colors) + pop!(attrs, :text) - text!(plot, positions; text = strings, attrs...) + text!(plot, positions; attrs..., text = strings) # update both text and positions together on(plot, strings_and_positions) do str_pos strs = first.(str_pos) - poss = to_ndim.(Ref(Point3f), last.(str_pos), 0) + poss = to_ndim.(Ref(Point3d), last.(str_pos), 0) strings_unequal = strings.val != strs pos_unequal = positions.val != poss diff --git a/src/basic_recipes/timeseries.jl b/src/basic_recipes/timeseries.jl index 7e7fc9ca8ed..510f2d2abd1 100644 --- a/src/basic_recipes/timeseries.jl +++ b/src/basic_recipes/timeseries.jl @@ -2,6 +2,7 @@ timeseries(x::Observable{{Union{Number, Point2}}}) Plots a sampled signal. + Usage: ```julia signal = Observable(1.0) @@ -20,11 +21,9 @@ end ``` """ -@recipe(TimeSeries, signal) do scene - Attributes( - history = 100; - default_theme(scene, Lines)... - ) +@recipe TimeSeries signal begin + history = 100 + MakieCore.documented_attributes(Lines)... end signal2point(signal::Number, start) = Point2f(time() - start, signal) diff --git a/src/basic_recipes/tooltip.jl b/src/basic_recipes/tooltip.jl index b94b557a74b..b22b2321035 100644 --- a/src/basic_recipes/tooltip.jl +++ b/src/basic_recipes/tooltip.jl @@ -2,77 +2,62 @@ tooltip(position, string) tooltip(x, y, string) -Creates a tooltip pointing at `position` displaying the given `string` - -## Attributes - -### Generic - -- `visible::Bool = true` sets whether the plot will be rendered or not. -- `overdraw::Bool = false` sets whether the plot will draw over other plots. This specifically means ignoring depth checks in GL backends. -- `transparency::Bool = false` adjusts how the plot deals with transparency. In GLMakie `transparency = true` results in using Order Independent Transparency. -- `inspectable::Bool = true` sets whether this plot should be seen by `DataInspector`. -- `depth_shift::Float32 = 0f0` adjusts the depth value of a plot after all other transformations, i.e. in clip space, where `0 <= depth <= 1`. This only applies to GLMakie and WGLMakie and can be used to adjust render order (like a tunable overdraw). -- `space::Symbol = :data` sets the transformation space for positions of markers. See `Makie.spaces()` for possible inputs. - -### Tooltip specific - -- `offset = 10` sets the offset between the given `position` and the tip of the triangle pointing at that position. -- `placement = :above` sets where the tooltipü should be placed relative to `position`. Can be `:above`, `:below`, `:left`, `:right`. -- `align = 0.5` sets the alignment of the tooltip relative `position`. With `align = 0.5` the tooltip is centered above/below/left/right the `position`. -- `backgroundcolor = :white` sets the background color of the tooltip. -- `triangle_size = 10` sets the size of the triangle pointing at `position`. -- `outline_color = :black` sets the color of the tooltip outline. -- `outline_linewidth = 2f0` sets the linewidth of the tooltip outline. -- `outline_linestyle = nothing` sets the linestyle of the tooltip outline. - -- `textpadding = (4, 4, 4, 4)` sets the padding around text in the tooltip. This is given as `(left, right, bottom top)` offsets. -- `textcolor = theme(scene, :textcolor)` sets the text color. -- `fontsize = 16` sets the text size. -- `font = theme(scene, :font)` sets the font. -- `strokewidth = 0`: Gives text an outline if set to a positive value. -- `strokecolor = :white` sets the text outline color. -- `justification = :left` sets whether text is aligned to the `:left`, `:center` or `:right` within its bounding box. +Creates a tooltip pointing at `position` displaying the given `string """ -@recipe(Tooltip, position) do scene - Attributes(; - # General - text = "", - offset = 10, - placement = :above, - align = 0.5, - xautolimits = false, - yautolimits = false, - zautolimits = false, - overdraw = false, - depth_shift = 0f0, - transparency = false, - visible = true, - inspectable = false, - space = :data, +@recipe Tooltip position begin + # General + text = "" + "Sets the offset between the given `position` and the tip of the triangle pointing at that position." + offset = 10 + "Sets where the tooltip should be placed relative to `position`. Can be `:above`, `:below`, `:left`, `:right`." + placement = :above + "Sets the alignment of the tooltip relative `position`. With `align = 0.5` the tooltip is centered above/below/left/right the `position`." + align = 0.5 + xautolimits = false + yautolimits = false + zautolimits = false - # Text - textpadding = (4, 4, 4, 4), # LRBT - textcolor = theme(scene, :textcolor), - fontsize = 16, - font = theme(scene, :font), - strokewidth = 0, - strokecolor = :white, - justification = :left, + # Text + "Sets the padding around text in the tooltip. This is given as `(left, right, bottom, top)` offsets." + textpadding = (4, 4, 4, 4) # LRBT + "Sets the text color." + textcolor = @inherit textcolor + "Sets the text size in screen units." + fontsize = 16 + "Sets the font." + font = @inherit font + "Gives text an outline if set to a positive value." + strokewidth = 0 + "Sets the text outline color." + strokecolor = :white + "Sets whether text is aligned to the `:left`, `:center` or `:right` within its bounding box." + justification = :left + + # Background + "Sets the background color of the tooltip." + backgroundcolor = :white + "Sets the size of the triangle pointing at `position`." + triangle_size = 10 - # Background - backgroundcolor = :white, - triangle_size = 10, + # Outline + "Sets the color of the tooltip outline." + outline_color = :black + "Sets the linewidth of the tooltip outline." + outline_linewidth = 2f0 + "Sets the linestyle of the tooltip outline." + outline_linestyle = nothing + + MakieCore.mixin_generic_plot_attributes()... + inspectable = false +end - # Outline - outline_color = :black, - outline_linewidth = 2f0, - outline_linestyle = nothing, - ) +function convert_arguments(::Type{<: Tooltip}, x::Real, y::Real, str::AbstractString) + return (Point2{float_type(x, y)}(x, y), str) +end +function convert_arguments(::Type{<: Tooltip}, x::Real, y::Real) + return (Point2{float_type(x, y)}(x, y),) end -convert_arguments(::Type{<: Tooltip}, x::Real, y::Real, str::AbstractString) = (Point2f(x, y), str) -convert_arguments(::Type{<: Tooltip}, x::Real, y::Real) = (Point2f(x, y),) function plot!(plot::Tooltip{<:Tuple{<:VecTypes, <:AbstractString}}) plot.attributes[:text] = plot[2] tooltip!(plot, plot[1]; plot.attributes...) @@ -154,7 +139,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 = boundingbox(tp, :pixel) + 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/tricontourf.jl b/src/basic_recipes/tricontourf.jl index e711345735d..1c38591bb9f 100644 --- a/src/basic_recipes/tricontourf.jl +++ b/src/basic_recipes/tricontourf.jl @@ -7,47 +7,50 @@ struct DelaunayTriangulation end Plots a filled tricontour of the height information in `zs` at the horizontal positions `xs` and vertical positions `ys`. A `Triangulation` from DelaunayTriangulation.jl can also be provided instead of `xs` and `ys` for specifying the triangles, otherwise an unconstrained triangulation of `xs` and `ys` is computed. - -## Attributes - -### Specific to `Tricontourf` - -- `levels = 10` can be either an `Int` which results in n bands delimited by n+1 equally spaced levels, or it can be an `AbstractVector{<:Real}` that lists n consecutive edges from low to high, which result in n-1 bands. -- `mode = :normal` sets the way in which a vector of levels is interpreted, if it's set to `:relative`, each number is interpreted as a fraction between the minimum and maximum values of `zs`. For example, `levels = 0.1:0.1:1.0` would exclude the lower 10% of data. -- `extendlow = nothing`. This sets the color of an optional additional band from `minimum(zs)` to the lowest value in `levels`. If it's `:auto`, the lower end of the colormap is picked and the remaining colors are shifted accordingly. If it's any color representation, this color is used. If it's `nothing`, no band is added. -- `extendhigh = nothing`. This sets the color of an optional additional band from the highest value of `levels` to `maximum(zs)`. If it's `:auto`, the high end of the colormap is picked and the remaining colors are shifted accordingly. If it's any color representation, this color is used. If it's `nothing`, no band is added. -- `triangulation = DelaunayTriangulation()`. The mode with which the points in `xs` and `ys` are triangulated. Passing `DelaunayTriangulation()` performs a Delaunay triangulation. You can also pass a preexisting triangulation as an `AbstractMatrix{<:Int}` with size (3, n), where each column specifies the vertex indices of one triangle, or as a `Triangulation` from DelaunayTriangulation.jl. - -### Generic - -- `visible::Bool = true` sets whether the plot will be rendered or not. -- `overdraw::Bool = false` sets whether the plot will draw over other plots. This specifically means ignoring depth checks in GL backends. -- `transparency::Bool = false` adjusts how the plot deals with transparency. In GLMakie `transparency = true` results in using Order Independent Transparency. -- `fxaa::Bool = false` adjusts whether the plot is rendered with fxaa (anti-aliasing). -- `inspectable::Bool = true` sets whether this plot should be seen by `DataInspector`. -- `depth_shift::Float32 = 0f0` adjusts the depth value of a plot after all other transformations, i.e. in clip space, where `0 <= depth <= 1`. This only applies to GLMakie and WGLMakie and can be used to adjust render order (like a tunable overdraw). -- `model::Makie.Mat4f` sets a model matrix for the plot. This replaces adjustments made with `translate!`, `rotate!` and `scale!`. -- `color` sets the color of the plot. It can be given as a named color `Symbol` or a `Colors.Colorant`. Transparency can be included either directly as an alpha value in the `Colorant` or as an additional float in a tuple `(color, alpha)`. The color can also be set for each scattered marker by passing a `Vector` of colors or be used to index the `colormap` by passing a `Real` number or `Vector{<: Real}`. -- `colormap::Union{Symbol, Vector{<:Colorant}} = :viridis` sets the colormap from which the band colors are sampled. -- `colorscale::Function = identity` color transform function. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Tricontourf) do scene - Theme( - levels = 10, - mode = :normal, - colormap = theme(scene, :colormap), - colorscale = identity, - extendlow = nothing, - extendhigh = nothing, - nan_color = :transparent, - inspectable = theme(scene, :inspectable), - transparency = false, - triangulation = DelaunayTriangulation(), - edges = nothing, - ) +@recipe Tricontourf begin + "Can be either an `Int` which results in n bands delimited by n+1 equally spaced levels, or it can be an `AbstractVector{<:Real}` that lists n consecutive edges from low to high, which result in n-1 bands." + levels = 10 + """ + Sets the way in which a vector of levels is interpreted, + if it's set to `:relative`, each number is interpreted as a fraction + between the minimum and maximum values of `zs`. + For example, `levels = 0.1:0.1:1.0` would exclude the lower 10% of data. + """ + mode = :normal + "Sets the colormap from which the band colors are sampled." + colormap = @inherit colormap + "Color transform function" + colorscale = identity + """ + This sets the color of an optional additional band from + `minimum(zs)` to the lowest value in `levels`. + If it's `:auto`, the lower end of the colormap is picked + and the remaining colors are shifted accordingly. + If it's any color representation, this color is used. + If it's `nothing`, no band is added. + """ + extendlow = nothing + """ + This sets the color of an optional additional band from + the highest value of `levels` to `maximum(zs)`. + If it's `:auto`, the high end of the colormap is picked + and the remaining colors are shifted accordingly. + If it's any color representation, this color is used. + If it's `nothing`, no band is added. + """ + extendhigh = nothing + nan_color = :transparent + """ + The mode with which the points in `xs` and `ys` are triangulated. + Passing `DelaunayTriangulation()` performs a Delaunay triangulation. + You can also pass a preexisting triangulation as an `AbstractMatrix{<:Int}` + with size (3, n), where each column specifies the vertex indices of one triangle, + or as a `Triangulation` from DelaunayTriangulation.jl. + """ + triangulation = DelaunayTriangulation() + edges = nothing + MakieCore.mixin_generic_plot_attributes()... end function Makie.used_attributes(::Type{<:Tricontourf}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}, ::AbstractVector{<:Real}) @@ -56,8 +59,9 @@ end function Makie.convert_arguments(::Type{<:Tricontourf}, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}; triangulation=DelaunayTriangulation()) - z = elconvert(Float32, z) - points = [x'; y'] + T = float_type(x, y, z) + z = elconvert(T, z) + points = [elconvert(T, x)'; elconvert(T, y)'] if triangulation isa DelaunayTriangulation tri = DelTri.triangulate(points) elseif !(triangulation isa DelTri.Triangulation) diff --git a/src/basic_recipes/triplot.jl b/src/basic_recipes/triplot.jl index 917e3ff4265..151d447bef4 100644 --- a/src/basic_recipes/triplot.jl +++ b/src/basic_recipes/triplot.jl @@ -4,72 +4,63 @@ triplot(triangles::Triangulation; kwargs...) Plots a triangulation based on the provided position or `Triangulation` from DelaunayTriangulation.jl. - -## Attributes - -- `show_points = false` determines whether to plot the individual points. Note that this will only plot points included in the triangulation. -- `show_convex_hull = false` determines whether to plot the convex hull. -- `show_ghost_edges = false` determines whether to plot the ghost edges. -- `show_constrained_edges = false` determines whether to plot the constrained edges. -- `recompute_centers = false` determines whether to recompute the representative points for the ghost edge orientation. Note that this will mutate `tri.representative_point_list` directly. - -- `markersize = 12` sets the size of the points. -- `marker = :circle` sets the shape of the points. -- `markercolor = :black` sets the color of the points. -- `strokecolor = :black` sets the color of triangle edges. -- `strokewidth = 1` sets the linewidth of triangle edges. -- `linestyle = :solid` sets the linestyle of triangle edges. -- `triangle_color = (:white, 0.0)` sets the color of the triangles. - -- `convex_hull_color = :red` sets the color of the convex hull. -- `convex_hull_linestyle = :dash` sets the linestyle of the convex hull. -- `convex_hull_linewidth = 1` sets the width of the convex hull. - -- `ghost_edge_color = :blue` sets the color of the ghost edges. -- `ghost_edge_linestyle = :solid` sets the linestyle of the ghost edges. -- `ghost_edge_linewidth = 1` sets the width of the ghost edges. -- `ghost_edge_extension_factor = 0.1` sets the extension factor for the rectangle that the exterior ghost edges are extended onto. -- `bounding_box::Union{Automatic, Rect2, Tuple} = automatic`: Sets the bounding box for truncating ghost edges which can be a `Rect2` (or `BBox`) or a tuple of the form `(xmin, xmax, ymin, ymax)`. By default, the rectangle will be given by `[a - eΔx, b + eΔx] × [c - eΔy, d + eΔy]` where `e` is the `ghost_edge_extension_factor`, `Δx = b - a` and `Δy = d - c` are the lengths of the sides of the rectangle, and `[a, b] × [c, d]` is the bounding box of the points in the triangulation. - -- `constrained_edge_color = :magenta` sets the color of the constrained edges. -- `constrained_edge_linestyle = :solid` sets the linestyle of the constrained edges. -- `constrained_edge_linewidth = 1` sets the width of the constrained edges. """ -@recipe(Triplot, triangles) do scene - sc = default_theme(scene, Scatter) - return Attributes(; - # Toggles - show_points=false, - show_convex_hull=false, - show_ghost_edges=false, - show_constrained_edges=false, - recompute_centers=false, - - # Mesh settings - markersize=theme(scene, :markersize), - marker=theme(scene, :marker), - markercolor=sc.color, - strokecolor=theme(scene, :patchstrokecolor), - strokewidth=1, - linestyle=:solid, - triangle_color=(:white, 0.0), - - # Convex hull settings - convex_hull_color=:red, - convex_hull_linestyle=:dash, - convex_hull_linewidth=theme(scene, :linewidth), - - # Ghost edge settings - ghost_edge_color=:blue, - ghost_edge_linestyle=theme(scene, :linestyle), - ghost_edge_linewidth=theme(scene, :linewidth), - ghost_edge_extension_factor=0.1, - bounding_box=automatic, - - # Constrained edge settings - constrained_edge_color=:magenta, - constrained_edge_linestyle=theme(scene, :linestyle), - constrained_edge_linewidth=theme(scene, :linewidth)) +@recipe Triplot triangles begin + # Toggles + "Determines whether to plot the individual points. Note that this will only plot points included in the triangulation." + show_points=false + "Determines whether to plot the convex hull." + show_convex_hull=false + "Determines whether to plot the ghost edges." + show_ghost_edges=false + "Determines whether to plot the constrained edges." + show_constrained_edges=false + "Determines whether to recompute the representative points for the ghost edge orientation. Note that this will mutate `tri.representative_point_list` directly." + recompute_centers=false + + # Mesh settings + "Sets the size of the points." + markersize= @inherit markersize + "Sets the shape of the points." + marker= @inherit marker + "Sets the color of the points." + markercolor= @inherit markercolor + "Sets the color of triangle edges." + strokecolor= @inherit patchstrokecolor + "Sets the linewidth of triangle edges." + strokewidth=1 + "Sets the linestyle of triangle edges." + linestyle=:solid + "Sets the color of the triangles." + triangle_color= :transparent + + # Convex hull settings + "Sets the color of the convex hull." + convex_hull_color=:red + "Sets the linestyle of the convex hull." + convex_hull_linestyle=:dash + "Sets the width of the convex hull." + convex_hull_linewidth= @inherit linewidth + + # Ghost edge settings + "Sets the color of the ghost edges." + ghost_edge_color=:blue + "Sets the linestyle of the ghost edges." + ghost_edge_linestyle= @inherit linestyle + "Sets the width of the ghost edges." + ghost_edge_linewidth= @inherit linewidth + "Sets the extension factor for the rectangle that the exterior ghost edges are extended onto." + ghost_edge_extension_factor=0.1 + "Sets the bounding box for truncating ghost edges which can be a `Rect2` (or `BBox`) or a tuple of the form `(xmin, xmax, ymin, ymax)`. By default, the rectangle will be given by `[a - eΔx, b + eΔx] × [c - eΔy, d + eΔy]` where `e` is the `ghost_edge_extension_factor`, `Δx = b - a` and `Δy = d - c` are the lengths of the sides of the rectangle, and `[a, b] × [c, d]` is the bounding box of the points in the triangulation." + bounding_box=automatic + + # Constrained edge settings + "Sets the color of the constrained edges." + constrained_edge_color=:magenta + "Sets the linestyle of the constrained edges." + constrained_edge_linestyle= @inherit linestyle + "Sets the width of the constrained edges." + constrained_edge_linewidth= @inherit linewidth end function get_all_triangulation_points!(points, tri) @@ -176,6 +167,7 @@ function get_triangulation_constrained_edges!(constrained_edges, tri) return constrained_edges end +# TODO: restrict to Point2? Makie.convert_arguments(::Type{<:Triplot}, ps) = convert_arguments(PointBased(), ps) Makie.convert_arguments(::Type{<:Triplot}, xs, ys) = convert_arguments(PointBased(), xs, ys) Makie.convert_arguments(::Type{<:Triplot}, x::DelTri.Triangulation) = (x,) @@ -245,11 +237,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}}}, space::Symbol = :data) = transform_bbox(p, data_limits(p)) \ No newline at end of file diff --git a/src/basic_recipes/volumeslices.jl b/src/basic_recipes/volumeslices.jl index a43598099b5..4a8b19bf3a2 100644 --- a/src/basic_recipes/volumeslices.jl +++ b/src/basic_recipes/volumeslices.jl @@ -5,16 +5,11 @@ VolumeSlices volumeslices(x, y, z, v) Draws heatmap slices of the volume v - -## Attributes -$(ATTRIBUTES) """ -@recipe(VolumeSlices, x, y, z, volume) do scene - Attributes(; - default_theme(scene, Heatmap)..., - bbox_visible = true, - bbox_color = RGBAf(0.5, 0.5, 0.5, 0.5) - ) +@recipe VolumeSlices x y z volume begin + MakieCore.documented_attributes(Heatmap)... + bbox_visible = true + bbox_color = RGBAf(0.5, 0.5, 0.5, 0.5) end function plot!(plot::VolumeSlices) diff --git a/src/basic_recipes/voronoiplot.jl b/src/basic_recipes/voronoiplot.jl index 7ce3d6f87b2..44c88606446 100644 --- a/src/basic_recipes/voronoiplot.jl +++ b/src/basic_recipes/voronoiplot.jl @@ -8,44 +8,32 @@ Generates and plots a Voronoi tessalation from `heatmap`- or point-like data. The tessellation can also be passed directly as a `VoronoiTessellation` from DelaunayTriangulation.jl. - -## Attributes - -- `show_generators = true` determines whether to plot the individual generators. - -- `markersize = 12` sets the size of the points. -- `marker = :circle` sets the shape of the points. -- `markercolor = :black` sets the color of the points. - -- `strokecolor = :black` sets the strokecolor of the polygons. -- `strokewidth = 1` sets the width of the polygon stroke. -- `color = automatic` sets the color of the polygons. If `automatic`, the polygons will be individually colored according to the colormap. -- `unbounded_edge_extension_factor = 0.1` sets the extension factor for the unbounded edges, used in `DelaunayTriangulation.polygon_bounds`. -- `clip::Union{Automatic, Rect2, Circle, Tuple} = automatic` sets the clipping area for the generated polygons which can be a `Rect2` (or `BBox`), `Tuple` with entries `(xmin, xmax, ymin, ymax)` or as a `Circle`. Anything outside the specified area will be removed. If the `clip` is not set it is automatically determined using `unbounded_edge_extension_factor` as a `Rect`. - -$(Base.Docs.doc(MakieCore.colormap_attributes!)) """ -@recipe(Voronoiplot, vorn) do scene - th = default_theme(scene, Mesh) - sc = default_theme(scene, Scatter) - attr = Attributes(; - # Toggles - show_generators=true, - smooth=false, - - # Point settings - markersize=sc.markersize, - marker=sc.marker, - markercolor=sc.color, - - # Polygon settings - strokecolor=theme(scene, :patchstrokecolor), - strokewidth=1.0, - color=automatic, - unbounded_edge_extension_factor=0.1, - clip=automatic) - MakieCore.colormap_attributes!(attr, theme(scene, :colormap)) - return attr +@recipe Voronoiplot vorn begin + "Determines whether to plot the individual generators." + show_generators=true + smooth=false + + # Point settings + "Sets the size of the points." + markersize= @inherit markersize + "Sets the shape of the points." + marker= @inherit marker + "Sets the color of the points." + markercolor= @inherit markercolor + + # Polygon settings + "Sets the strokecolor of the polygons." + strokecolor= @inherit patchstrokecolor + "Sets the width of the polygon stroke." + strokewidth=1.0 + "Sets the color of the polygons. If `automatic`, the polygons will be individually colored according to the colormap." + color=automatic + "Sets the extension factor for the unbounded edges, used in `DelaunayTriangulation.polygon_bounds`." + unbounded_edge_extension_factor=0.1 + "Sets the clipping area for the generated polygons which can be a `Rect2` (or `BBox`), `Tuple` with entries `(xmin, xmax, ymin, ymax)` or as a `Circle`. Anything outside the specified area will be removed. If the `clip` is not set it is automatically determined using `unbounded_edge_extension_factor` as a `Rect`." + clip=automatic + MakieCore.mixin_colormap_attributes()... end function _clip_polygon(poly::Polygon, circle::Circle) @@ -167,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}}}, space::Symbol = :data) = transform_bbox(p, data_limits(p)) function plot!(p::Voronoiplot{<:Tuple{<:DelTri.VoronoiTessellation}}) generators_2f = Observable(Point2f[]) diff --git a/src/basic_recipes/voxels.jl b/src/basic_recipes/voxels.jl new file mode 100644 index 00000000000..3d649ff9ea9 --- /dev/null +++ b/src/basic_recipes/voxels.jl @@ -0,0 +1,233 @@ +function convert_arguments(T::Type{<:Voxels}, chunk::Array{<: Real, 3}) + X, Y, Z = to_ndim(Vec3{Int}, size(chunk), 1) + return convert_arguments(T, -0.5X..0.5X, -0.5Y..0.5Y, -0.5Z..0.5Z, chunk) +end +function convert_arguments(T::Type{<:Voxels}, xs, ys, zs, chunk::Array{<: Real, 3}) + xi = Float32(minimum(xs))..Float32(maximum(xs)) + yi = Float32(minimum(ys))..Float32(maximum(ys)) + zi = Float32(minimum(zs))..Float32(maximum(zs)) + return convert_arguments(T, xi, yi, zi, chunk) +end +function convert_arguments(::Type{<:Voxels}, xs::ClosedInterval{Float32}, ys::ClosedInterval{Float32}, zs::ClosedInterval{Float32}, chunk::Array{<: Real, 3}) + return (xs, ys, zs, Array{UInt8, 3}(undef, to_ndim(Vec3{Int}, size(chunk), 1)...)) +end +function convert_arguments(::Type{<:Voxels}, xs::ClosedInterval{Float32}, ys::ClosedInterval{Float32}, zs::ClosedInterval{Float32}, chunk::Array{UInt8, 3}) + return (xs, ys, zs, chunk) +end + +function calculated_attributes!(::Type{<:Voxels}, plot) + if !isnothing(plot.color[]) + cc = lift(plot, plot.color, plot.alpha) do color, a + if color isa AbstractVector + output = Vector{RGBAf}(undef, 255) + @inbounds for i in 1:min(255, length(color)) + c = to_color(color[i]) + output[i] = RGBAf(Colors.color(c), Colors.alpha(c) * a) + end + for i in min(255, length(color))+1 : 255 + output[i] = RGBAf(0,0,0,0) + end + elseif color isa AbstractArray + output = similar(color, RGBAf) + @inbounds for i in eachindex(color) + c = to_color(color[i]) + output[i] = RGBAf(Colors.color(c), Colors.alpha(c) * a) + end + else + c = to_color(color) + output .= RGBAf(Colors.color(c), Colors.alpha(c) * a) + end + return output + end + attributes(plot.attributes)[:calculated_colors] = cc + + else + + # ... + dummy_data = Observable(UInt8[1, 255]) + + # Always sample N colors + cmap = map(plot.colormap, plot.lowclip, plot.highclip) do cmap, lowclip, highclip + cm = if cmap isa Vector && length(cmap) != 255 + resample_cmap(cmap, 253) + else + categorical_colors(cmap, 253) + end + lc = lowclip === automatic ? first(cm) : to_color(lowclip) + hc = highclip === automatic ? last(cm) : to_color(highclip) + return [lc; cm; hc] + end + + # always use 1..N + colorrange = Observable(Vec2f(1, 255)) + + # Needs to happen in voxel id generation + colorscale = Observable(identity) + + # We always treat nan as air, invalid + nan_color = Observable(:transparent) + + # TODO: categorical? + attributes(plot.attributes)[:calculated_colors] = ColorMapping( + dummy_data[], dummy_data, cmap, colorrange, colorscale, + plot.alpha, plot.lowclip, plot.highclip, nan_color + ) + + end + + return nothing +end + +""" + local_update(p::Voxels, i, j, k) + +Updates a section of the Voxel plot given by indices i, j, k (Integer, UnitRange +or Colon()) according to the data present in `p.args[end]`. + +This is used to avoid updating the whole chunk with a pattern such as +``` +p.args[end].val[20:30, 7:10, 8] = new_data +local_update(plot, 20:30, 7:10, 8) +``` +""" +function local_update(plot::Voxels, is, js, ks) + to_range(N, i::Integer) = i:i + to_range(N, r::UnitRange) = r + to_range(N, ::Colon) = 1:N + to_range(N, x::Any) = throw(ArgumentError("Indices can't be converted to a range representation ($x)")) + + _size = size(plot.converted[end].val) + is, js, ks = to_range.(_size, (is, js, ks)) + + mini, maxi = apply_scale(plot.colorscale[], plot._limits[]) + input = plot.args[end][] + for k in ks, j in js, i in is + idx = i + _size[1] * ((j-1) + _size[2] * (k-1)) + _update_voxel(plot.converted[end].val, input, idx, plot.is_air[], plot.colorscale[], mini, maxi) + end + plot._local_update[] = (is, js, ks) + return nothing +end + +Base.@propagate_inbounds function _update_voxel( + output::Array{UInt8, 3}, input::Array, i::Integer, + is_air::Function, scale, mini::Real, maxi::Real + ) + + @boundscheck checkbounds(Bool, output, i) && checkbounds(Bool, input, i) + # Rescale data to UInt8 range for voxel ids + c = 252.99998 + @inbounds begin + x = input[i] + if is_air(x) + output[i] = 0x00 + else + lin = clamp(c * (apply_scale(scale, x) - mini) / (maxi - mini) + 2, 1, 255) + output[i] = trunc(UInt8, lin) + end + end + return nothing +end + +Base.@propagate_inbounds function _update_voxel( + output::Array{UInt8, 3}, input::Array{UInt8, 3}, i::Integer, + is_air::Function, scale, mini::Real, maxi::Real + ) + return nothing +end + +function plot!(plot::Voxels) + # Disconnect automatic mapping + # I want to avoid recalculating limits every time the input is updated. + # Maybe this can be done with conversion kwargs...? + off(plot.args[end], plot.args[end].listeners[1][2]) + + # If a UInt8 Array is passed we don't do any mapping between plot.args and + # plot.converted. Instead we just set plot.converted = plot.args in + # convert_arguments + if eltype(plot.args[end][]) == UInt8 + plot._limits[] = (1, 255) + return + end + + + # Use new mapping that doesn't recalculate limits + onany(plot, plot._limits, plot.is_air, plot.colorscale) do lims, is_air, scale + # _limits always triggers after plot.args[1] + chunk = plot.args[end][] + output = plot.converted[end] + + # TODO: Julia doesn't allow this + # maybe resize + # if size(chunk) != size(output.val) + # resize!(output.val, size(chunk)) + # end + + # update voxel ids + mini, maxi = apply_scale(scale, lims) + maxi = max(mini + 10eps(float(mini)), maxi) + @inbounds for i in eachindex(chunk) + _update_voxel(output.val, chunk, i, is_air, scale, mini, maxi) + end + + # trigger converted + notify(output) + + return + end + + # Initial limits + map!(plot, plot._limits, plot.args[end], plot.colorrange) do data, colorrange + if colorrange !== automatic + return colorrange + end + + mini, maxi = (Inf, -Inf) + for elem in data + plot.is_air[](elem) && continue + mini = min(mini, elem) + maxi = max(maxi, elem) + end + if !(isfinite(mini) && isfinite(maxi) && isa(mini, Real)) + throw(ArgumentError("Voxel Chunk contains invalid data, resulting in invalid limits ($mini, $maxi).")) + end + return (mini, maxi) + end + + return +end + +function voxel_size(p::Voxels) + mini = minimum.(to_value.(p.converted[1:3])) + maxi = maximum.(to_value.(p.converted[1:3])) + _size = size(p.converted[4][]) + return Vec3f((maxi .- mini) ./ _size .- convert_attribute(p.gap[], key"gap"(), key"voxels"())) +end + +function voxel_positions(p::Voxels) + mini = minimum.(to_value.(p.converted[1:3])) + maxi = maximum.(to_value.(p.converted[1:3])) + voxel_id = p.converted[4][] + _size = size(voxel_id) + step = (maxi .- mini) ./ _size + return [ + Point3f(mini .+ step .* (i-0.5, j-0.5, k-0.5)) + for k in 1:_size[3] for j in 1:_size[2] for i in 1:_size[1] + if voxel_id[i, j, k] !== 0x00 + ] +end + +function voxel_colors(p::Voxels) + voxel_id = p.converted[4][] + colormapping = p.calculated_colors[] + uv_map = p.uvmap[] + if !isnothing(uv_map) + @warn "Voxel textures are not implemented in this backend!" + elseif colormapping isa ColorMapping + color = colormapping.colormap[] + else + color = colormapping + end + + return [color[id] for id in voxel_id if id !== 0x00] +end \ No newline at end of file diff --git a/src/basic_recipes/waterfall.jl b/src/basic_recipes/waterfall.jl index 1aa0bf787cb..562eb301ae4 100644 --- a/src/basic_recipes/waterfall.jl +++ b/src/basic_recipes/waterfall.jl @@ -4,30 +4,24 @@ Plots a [waterfall chart](https://en.wikipedia.org/wiki/Waterfall_chart) to visualize individual positive and negative components that add up to a net result as a barplot with stacked bars next to each other. - -## Attributes -$(ATTRIBUTES) - -Furthermore the same attributes as for `barplot` are supported. """ -@recipe(Waterfall, x, y) do scene - return Attributes(; - dodge=automatic, - n_dodge=automatic, - gap=0.2, - dodge_gap=0.03, - width=automatic, - cycle=[:color => :patchcolor], - stack=automatic, - show_direction=false, - marker_pos=:utriangle, - marker_neg=:dtriangle, - direction_color=theme(scene, :backgroundcolor), - show_final=false, - final_color=plot_color(:grey90, 0.5), - final_gap=automatic, - final_dodge_gap=0, - ) +@recipe Waterfall x y begin + color = @inherit patchcolor + dodge=automatic + n_dodge=automatic + gap=0.2 + dodge_gap=0.03 + width=automatic + cycle=[:color => :patchcolor] + stack=automatic + show_direction=false + marker_pos=:utriangle + marker_neg=:dtriangle + direction_color= @inherit backgroundcolor + show_final=false + final_color=plot_color(:grey90, 0.5) + final_gap=automatic + final_dodge_gap=0 end conversion_trait(::Type{<:Waterfall}) = PointBased() @@ -67,10 +61,20 @@ function Makie.plot!(p::Waterfall) ) end + bar_attrs = copy(p.attributes) + delete!(bar_attrs, :direction_color) + delete!(bar_attrs, :marker_pos) + delete!(bar_attrs, :final_color) + delete!(bar_attrs, :final_dodge_gap) + delete!(bar_attrs, :show_direction) + delete!(bar_attrs, :final_gap) + delete!(bar_attrs, :show_final) + delete!(bar_attrs, :marker_neg) + barplot!( p, lift(x -> x.xy, p, fromto); - p.attributes..., + bar_attrs..., fillto=lift(x -> x.fillto, p, fromto), stack=automatic, ) diff --git a/src/basic_recipes/wireframe.jl b/src/basic_recipes/wireframe.jl index bbf6d271e0d..d22fa051121 100644 --- a/src/basic_recipes/wireframe.jl +++ b/src/basic_recipes/wireframe.jl @@ -1,20 +1,3 @@ -""" - wireframe(x, y, z) - wireframe(positions) - wireframe(mesh) - -Draws a wireframe, either interpreted as a surface or as a mesh. - -## Attributes -$(ATTRIBUTES) -""" -wireframe - -""" -See [`wireframe`](@ref). -""" -wireframe! - function convert_arguments(::Type{<: Wireframe}, x::AbstractVector, y::AbstractVector, z::AbstractMatrix) (ngrid(x, y)..., z) end diff --git a/src/bezier.jl b/src/bezier.jl index b3e410b540f..b830ded0443 100644 --- a/src/bezier.jl +++ b/src/bezier.jl @@ -1,5 +1,3 @@ -const Point2d = Point2{Float64} - struct MoveTo p::Point2d end @@ -63,18 +61,18 @@ function elliptical_arc_to_beziers(arc::EllipticalArc) n_beziers = ceil(Int, delta_a / 0.5pi) angles = range(arc.a1, arc.a2; length=n_beziers + 1) - startpoint = Point2f(cos(arc.a1), sin(arc.a1)) + startpoint = Point2d(cos(arc.a1), sin(arc.a1)) curves = map(angles[1:(end - 1)], angles[2:end]) do start, stop theta = stop - start kappa = 4 / 3 * tan(theta / 4) - c1 = Point2f(cos(start) - kappa * sin(start), sin(start) + kappa * cos(start)) - c2 = Point2f(cos(stop) + kappa * sin(stop), sin(stop) - kappa * cos(stop)) - b = Point2f(cos(stop), sin(stop)) + c1 = Point2d(cos(start) - kappa * sin(start), sin(start) + kappa * cos(start)) + c2 = Point2d(cos(stop) + kappa * sin(stop), sin(stop) - kappa * cos(stop)) + b = Point2d(cos(stop), sin(stop)) return CurveTo(c1, c2, b) end path = BezierPath([LineTo(startpoint), curves...]) - path = scale(path, Vec2{Float64}(arc.r1, arc.r2)) + path = scale(path, Vec2d(arc.r1, arc.r2)) path = rotate(path, arc.angle) return translate(path, arc.c) end @@ -94,21 +92,21 @@ end function point_at_angle(e::EllipticalArc, theta) M = abs(e.r1) * cos(theta) N = abs(e.r2) * sin(theta) - return Point2f(e.c[1] + cos(e.angle) * M - sin(e.angle) * N, + return Point2d(e.c[1] + cos(e.angle) * M - sin(e.angle) * N, e.c[2] + sin(e.angle) * M + cos(e.angle) * N) end -function cleanup_bbox(bb::Rect2f) +function cleanup_bbox(bb::Rect2) if any(x -> x < 0, bb.widths) p = bb.origin .+ (bb.widths .< 0) .* bb.widths - return Rect2f(p, abs.(bb.widths)) + return Rect2d(p, abs.(bb.widths)) end return bb end struct BezierPath commands::Vector{PathCommand} - boundingbox::Rect2f + boundingbox::Rect2d hash::UInt32 function BezierPath(commands::Vector) c = convert(Vector{PathCommand}, commands) @@ -129,7 +127,7 @@ function Base.:+(pc::P, p::Point2) where P <: PathCommand return P(map(f -> getfield(pc, f) + p, fnames)...) end -scale(bp::BezierPath, s::Real) = BezierPath([scale(x, Vec2{Float64}(s, s)) for x in bp.commands]) +scale(bp::BezierPath, s::Real) = BezierPath([scale(x, Vec2d(s, s)) for x in bp.commands]) scale(bp::BezierPath, v::VecTypes{2}) = BezierPath([scale(x, v) for x in bp.commands]) translate(bp::BezierPath, v::VecTypes{2}) = BezierPath([translate(x, v) for x in bp.commands]) @@ -206,7 +204,7 @@ Base.:+(bp::BezierPath, p::Point2) = BezierPath(bp.commands .+ Ref(p)) function bezier_ngon(n, radius, angle) - points = [radius * Point2f(cos(a + angle), sin(a + angle)) + points = [radius * Point2d(cos(a + angle), sin(a + angle)) for a in range(0, 2pi, length = n+1)[1:end-1]] BezierPath([ MoveTo(points[1]); @@ -218,7 +216,7 @@ end function bezier_star(n, inner_radius, outer_radius, angle) points = [ (isodd(i) ? outer_radius : inner_radius) * - Point2f(cos(a + angle), sin(a + angle)) + Point2d(cos(a + angle), sin(a + angle)) for (i, a) in enumerate(range(0, 2pi, length = 2n+1)[1:end-1])] BezierPath([ MoveTo(points[1]); @@ -227,9 +225,9 @@ function bezier_star(n, inner_radius, outer_radius, angle) ]) end -function BezierPath(poly::Polygon) +function BezierPath(poly::Polygon{N, T}) where {N, T} commands = Makie.PathCommand[] - points = reinterpret(Point2f, poly.exterior) + points = reinterpret(Point{N, T}, poly.exterior) ext_direction = sign(area(points)) #signed area gives us clockwise / anti-clockwise push!(commands, MoveTo(points[1])) for i in 2:length(points) @@ -237,7 +235,7 @@ function BezierPath(poly::Polygon) end for inter in poly.interiors - points = reinterpret(Point2f, inter) + points = reinterpret(Point{N, T}, inter) # holes, in bezierpath, always need to have the opposite winding order if sign(area(points)) == ext_direction points = reverse(points) @@ -262,7 +260,7 @@ function BezierPath(svg::AbstractString; fit = false, bbox = nothing, flipy = fa end if fit if bbox === nothing - p = fit_to_bbox(p, Rect2f((-0.5, -0.5), (1.0, 1.0)), keep_aspect = keep_aspect) + p = fit_to_bbox(p, Rect2d((-0.5, -0.5), (1.0, 1.0)), keep_aspect = keep_aspect) else p = fit_to_bbox(p, bbox, keep_aspect = keep_aspect) end @@ -518,9 +516,9 @@ function render_path(path, bitmap_size_px = 256) path_size = widths(bbox(path)) / maximum(widths(bbox(path))) w = ceil(Int, 64 * path_size[1]) h = ceil(Int, 64 * path_size[2]) - path_size = Vec2f(w, h) / 64f0 + path_size = Vec2d(w, h) / 64.0 - path_unit_rect = fit_to_bbox(path_replaced, Rect2f(Point2f(0), path_size)) + path_unit_rect = fit_to_bbox(path_replaced, Rect2d(Point2d(0), path_size)) path_transformed = Makie.scale(path_unit_rect, scale_factor) @@ -585,20 +583,20 @@ Makie.convert_attribute(b::BezierPath, ::key"marker", ::key"scatter") = b Makie.convert_attribute(ab::AbstractVector{<:BezierPath}, ::key"marker", ::key"scatter") = ab struct BezierSegment - from::Point2f - c1::Point2f - c2::Point2f - to::Point2f + from::Point2d + c1::Point2d + c2::Point2d + to::Point2d end struct LineSegment - from::Point2f - to::Point2f + from::Point2d + to::Point2d end function bbox(ls::LineSegment) - return Rect2f(ls.from, ls.to - ls.from) + return Rect2d(ls.from, ls.to - ls.from) end function bbox(b::BezierSegment) @@ -652,7 +650,7 @@ function bbox(b::BezierSegment) end end - return Rect2f(Point(mi...), Point(ma...) - Point(mi...)) + return Rect2d(Point(mi...), Point(ma...) - Point(mi...)) end segment(p, l::LineTo) = LineSegment(p, l.p) @@ -701,7 +699,7 @@ const BezierCross = let first_three = Point2d[(r, ri), (ri, ri), (ri, r)] all = (x -> reduce(vcat, x))(map(0:(pi / 2):(3pi / 2)) do a - m = Mat2f(sin(a), cos(a), cos(a), -sin(a)) + m = Mat2d(sin(a), cos(a), cos(a), -sin(a)) return Ref(m) .* first_three end) diff --git a/src/camera/camera.jl b/src/camera/camera.jl index 2d75f3dc9ca..3d70c677bd3 100644 --- a/src/camera/camera.jl +++ b/src/camera/camera.jl @@ -18,8 +18,8 @@ function Base.show(io::IO, camera::Camera) println(io, " projection: ", camera.projection[]) println(io, " projectionview: ", camera.projectionview[]) println(io, " resolution: ", camera.resolution[]) - println(io, " lookat: ", camera.lookat[]) println(io, " eyeposition: ", camera.eyeposition[]) + println(io, " view direction: ", camera.view_direction[]) end function disconnect!(c::Camera) @@ -84,7 +84,7 @@ function Camera(viewport) proj, proj_view, lift(a-> Vec2f(widths(a)), viewport), - Observable(Vec3f(0)), + Observable(Vec3f(0, 0, -1)), Observable(Vec3f(1)), ObserverFunction[], Dict{Symbol, Observable}() diff --git a/src/camera/camera2d.jl b/src/camera/camera2d.jl index d9d354a180f..b73b2a000ce 100644 --- a/src/camera/camera2d.jl +++ b/src/camera/camera2d.jl @@ -38,6 +38,7 @@ function cam2d!(scene::SceneLike; kw_args...) cam = from_dict(Camera2D, cam_attributes) # remove previously connected camera disconnect!(camera(scene)) + camera(scene).view_direction[] = Vec3f(0, 0, -1) add_zoom!(scene, cam) add_pan!(scene, cam) correct_ratio!(scene, cam) @@ -343,6 +344,7 @@ controls. """ function campixel!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) disconnect!(camera(scene)) + camera(scene).view_direction[] = Vec3f(0, 0, -1) update_once = Observable(false) closure = UpdatePixelCam(camera(scene), nearclip, farclip) on(closure, camera(scene), viewport(scene)) @@ -364,6 +366,8 @@ Creates a camera for the given `scene` which maps the scene area to a 0..1 by 0..1 range. This camera does not feature controls. """ function cam_relative!(scene::Scene; nearclip=-10_000f0, farclip=10_000f0) + disconnect!(camera(scene)) + camera(scene).view_direction[] = Vec3f(0, 0, -1) projection = orthographicprojection(0f0, 1f0, 0f0, 1f0, nearclip, farclip) set_proj_view!(camera(scene), projection, Mat4f(I)) cam = RelativeCamera() diff --git a/src/camera/camera3d.jl b/src/camera/camera3d.jl index 6c8d72d0647..3f8ebe68e31 100644 --- a/src/camera/camera3d.jl +++ b/src/camera/camera3d.jl @@ -750,7 +750,7 @@ function update_cam!(scene::Scene, cam::Camera3D) set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] - scene.camera.lookat[] = cam.lookat[] + scene.camera.view_direction[] = normalize(cam.lookat[] - cam.eyeposition[]) end diff --git a/src/camera/old_camera3d.jl b/src/camera/old_camera3d.jl index 33d381d2e45..4d1ace33bcf 100644 --- a/src/camera/old_camera3d.jl +++ b/src/camera/old_camera3d.jl @@ -334,6 +334,7 @@ function update_cam!(scene::Scene, cam::OldCamera3D) view = Makie.lookat(eyeposition, lookat, upvector) set_proj_view!(camera(scene), proj, view) scene.camera.eyeposition[] = cam.eyeposition[] + scene.camera.view_direction[] = normalize(cam.lookat[] - cam.eyeposition[]) end function update_cam!(scene::Scene, camera::OldCamera3D, area3d::Rect) diff --git a/src/camera/projection_math.jl b/src/camera/projection_math.jl index eada9918c2e..d4f235a9b70 100644 --- a/src/camera/projection_math.jl +++ b/src/camera/projection_math.jl @@ -238,22 +238,23 @@ function rotation(u::Vec{3, T}, v::Vec{3, T}) where T return Quaternion(cross(u, half)..., dot(u, half)) end -function to_world(scene::Scene, point::T) where T <: StaticVector - cam = scene.camera - x = to_world( - point, - inv(transformationmatrix(scene)[]) * - inv(cam.view[]) * - inv(cam.projection[]), - T(size(scene)) +function to_world(scene::SceneLike, point::VecTypes{2}) + cam = camera(scene) + x = _to_world( + Point2d(point[1], point[2]), + # inv(transformationmatrix(scene)[]) * + inv(Mat4d(cam.projectionview[])), + Vec2d(cam.resolution[]) ) - Point2f(x[1], x[2]) + return inv_f32_convert(scene, Point2d(x[1], x[2])) end w_component(x::Point) = 1.0 w_component(x::Vec) = 0.0 -function to_world( +@deprecate to_world(p::VecTypes, M::Mat4, res::Vec2) to_world(scene::Scene, p::VecTypes) false + +function _to_world( p::StaticVector{N, T}, prj_view_inv::Mat4, cam_res::StaticVector @@ -266,10 +267,10 @@ function to_world( T(0), w_component(p) ) ws = prj_view_inv * pix_space - ws ./ ws[4] + return ws ./ ws[4] end -function to_world( +function _to_world( p::Vec{N, T}, prj_view_inv::Mat4, cam_res::StaticVector @@ -278,42 +279,56 @@ function to_world( to_world(zeros(Point{N, T}), prj_view_inv, cam_res) end -function project(scene::Scene, point::T) where T<:StaticVector + +# TODO: consider warning here to discourage risky functions +function project(matrix::Mat4{T1}, p::VT, dim4::Real = 1.0) where {N, T1 <: Real, T2 <: Real, VT <: VecTypes{N, T2}} + T = promote_type(Float32, T1, T2) + p = to_ndim(Vec4{T}, to_ndim(Vec3{T}, p, 0.0), dim4) + p = matrix * p + to_ndim(VT, p, 0.0) +end + +function project(scene::Scene, point::VecTypes) cam = scene.camera area = viewport(scene)[] # TODO, I think we need .+ minimum(area) # Which would be semi breaking at this point though, I suppose return project( cam.projectionview[] * + f32_convert_matrix(scene.float32convert, :data) * transformationmatrix(scene)[], Vec2f(widths(area)), Point(point) ) 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) +# TODO: consider warning here to discourage risky functions +function project(proj_view::Mat4{T1}, resolution::Vec2, point::Point{N, T2}) where {N, T1, T2} + T = promote_type(Float32, 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)) +# TODO: consider warning here to discourage risky functions +function project_point2(mat4::Mat4{T1}, point2::Point2{T2}) where {T1, T2} + T = promote_type(Float32, 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) +# TODO: consider warning here to discourage risky functions +function transform(model::Mat4{T1}, x::VT) where {T1, VT<:VecTypes} + T = promote_type(Float32, 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 +375,28 @@ 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) +# TODO: consider warning here to discourage risky functions +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 + +function project(scenelike::SceneLike, 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) + cam = camera(scenelike) + + input_f32c = f32_convert_matrix(scenelike, input_space) + clip_from_input = space_to_clip(cam, input_space) + output_from_clip = clip_to_space(cam, output_space) + output_f32c = inv_f32_convert_matrix(scenelike, output_space) + + p4d = to_ndim(Point4{T}, to_ndim(Point3{T}, transformed, 0), 1) + transformed = output_f32c * output_from_clip * clip_from_input * input_f32c * p4d + return Point3{T}(transformed[Vec(1, 2, 3)] ./ transformed[4]) end diff --git a/src/conversions.jl b/src/conversions.jl index 7b20101c507..6e196413942 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -1,8 +1,9 @@ ################################################################################ # Type Conversions # ################################################################################ -const RangeLike = Union{AbstractVector, ClosedInterval, Tuple{Any,Any}} + +const RangeLike = Union{AbstractVector, ClosedInterval, Tuple{Any,Any}} # if no plot type based conversion is defined, we try using a trait function convert_arguments(T::PlotFunc, args...; kw...) ct = conversion_trait(T, args...) @@ -14,19 +15,14 @@ function convert_arguments(T::PlotFunc, args...; kw...) convert_arguments_individually(T, args...) catch ee if ee isa MethodError - error( - """ - `Makie.convert_arguments` for the plot type $T and its conversion trait $ct was unsuccessful. - - The signature that could not be converted was: - $(join("::" .* string.(typeof.(args)), ", ")) - - Makie needs to convert all plot input arguments to types that can be consumed by the backends (typically Arrays with Float32 elements). - You can define a method for `Makie.convert_arguments` (a type recipe) for these types or their supertypes to make this set of arguments convertible (See http://docs.makie.org/stable/documentation/recipes/index.html). - - Alternatively, you can define `Makie.convert_single_argument` for single arguments which have types that are unknown to Makie but which can be converted to known types and fed back to the conversion pipeline. - """ - ) + error(""" + `Makie.convert_arguments` for the plot type $T and its conversion trait $ct was unsuccessful. + The signature that could not be converted was: + $(join("::" .* string.(typeof.(args)), ", ")) + Makie needs to convert all plot input arguments to types that can be consumed by the backends (typically Arrays with Float32 elements). + You can define a method for `Makie.convert_arguments` (a type recipe) for these types or their supertypes to make this set of arguments convertible (See http://docs.makie.org/stable/documentation/recipes/index.html). + Alternatively, you can define `Makie.convert_single_argument` for single arguments which have types that are unknown to Makie but which can be converted to known types and fed back to the conversion pipeline. + """) else rethrow(ee) end @@ -36,7 +32,6 @@ function convert_arguments(T::PlotFunc, args...; kw...) end end end - # in case no trait matches we try to convert each individual argument # and reconvert the whole tuple in order to handle missings centrally, e.g. function convert_arguments_individually(T::PlotFunc, args...) @@ -69,12 +64,13 @@ convert_single_argument(x) = x # replace missings with NaNs function convert_single_argument(a::AbstractArray{<:Union{Missing, <:Real}}) - [ismissing(x) ? NaN32 : convert(Float32, x) for x in a] + return float_convert(a) end # same for points -function convert_single_argument(a::AbstractArray{<:Union{Missing, <:Point{N}}}) where N - [ismissing(x) ? Point{N, Float32}(NaN32) : Point{N, Float32}(x) for x in a] +function convert_single_argument(a::AbstractArray{<:Union{Missing, <:Point{N, PT}}}) where {N, PT} + T = float_type(PT) + return Point{N,T}[ismissing(x) ? Point{N,T}(NaN) : Point{N,T}(x) for x in a] end ################################################################################ @@ -85,19 +81,21 @@ end Wrap a single point or equivalent object in a single-element array. """ function convert_arguments(::PointBased, x::Real, y::Real) - ([Point2f(x, y)],) + T = float_type(x, y) + return ([Point{2, T}(x, y)],) end function convert_arguments(::PointBased, x::Real, y::Real, z::Real) - ([Point3f(x, y, z)],) + T = float_type(x, y, z) + return ([Point{3, T}(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} + return ([Point{N,float_type(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} + return (float_convert(positions),) end function convert_arguments(::PointBased, positions::SubArray{<: VecTypes, 1}) @@ -109,16 +107,19 @@ end 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, y::AbstractVector, z::AbstractArray) - (vec(Point3f.(x, y', z)),) +function convert_arguments(::PointBased, x::RealArray, y::RealVector, z::RealArray) + T = float_type(x, y, z) + (vec(Point{3, T}.(x, y', z)),) end -function convert_arguments(p::PointBased, x::AbstractInterval, y::AbstractInterval, z::AbstractMatrix) - return convert_arguments(p, to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) +function convert_arguments(::PointBased, x::RealVector, y::RealVector, z::RealVector) + T = float_type(x, y, z) + return (Point{3,T}.(x, y, z),) end -function convert_arguments(::PointBased, x::AbstractArray, y::AbstractMatrix, z::AbstractArray) - (vec(Point3f.(x, y, z)),) + +function convert_arguments(p::PointBased, x::AbstractInterval, y::AbstractInterval, z::RealArray) + return convert_arguments(p, to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) end """ @@ -128,7 +129,15 @@ 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),) +function convert_arguments(::PointBased, x::RealArray, y::RealMatrix, z::RealArray) + T = float_type(x, y, z) + (vec(Point{3, T}.(x, y, z)),) +end + + +function convert_arguments(::PointBased, x::RealVector, y::RealVector) + return (Point{2,float_type(x, y)}.(x, y),) +end """ convert_arguments(P, x)::(Vector) @@ -136,16 +145,14 @@ convert_arguments(::PointBased, x::RealVector, y::RealVector, z::RealVector) = ( Takes an input GeometryPrimitive `x` and decomposes it to points. `P` is the plot Type (it is optional). """ -convert_arguments(p::PointBased, x::GeometryPrimitive) = convert_arguments(p, decompose(Point, x)) +function convert_arguments(p::PointBased, x::GeometryPrimitive{Dim, T}) where {Dim, T} + return convert_arguments(p, decompose(Point{Dim, float_type(T)}, x)) +end -function convert_arguments(::PointBased, pos::AbstractMatrix{<: Number}) +function convert_arguments(::PointBased, pos::AbstractMatrix{<: Real}) (to_vertices(pos),) end -convert_arguments(P::PointBased, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}) = (Point2f.(x, y),) - -convert_arguments(P::PointBased, x::AbstractVector{<:Real}, y::AbstractVector{<:Real}, z::AbstractVector{<:Real}) = (Point3f.(x, y, z),) - """ convert_arguments(P, y)::Vector Takes vector `y` and generates a range from 1 to the length of `y`, for plotting on @@ -174,13 +181,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{float_type(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, coordinates(mesh)) end function convert_arguments(PB::PointBased, linesegments::FaceView{<:Line, P}) where {P<:AbstractPoint} @@ -188,19 +195,20 @@ function convert_arguments(PB::PointBased, linesegments::FaceView{<:Line, P}) wh return convert_arguments(PB, collect(reinterpret(P, linesegments))) end -function convert_arguments(P::PointBased, rect::Rect3) - return (decompose(Point3f, rect),) +function convert_arguments(::PointBased, rect::Rect3{T}) where {T} + return (decompose(Point3{float_type(T)}, rect),) end -function convert_arguments(P::Type{<: LineSegments}, rect::Rect3) +function convert_arguments(P::Type{<: LineSegments}, rect::Rect3{T}) where {T} f = decompose(LineFace{Int}, rect) - p = connect(decompose(Point3f, rect), f) + p = connect(decompose(Point3{float_type(T)}, rect), f) return convert_arguments(P, p) end -function convert_arguments(::Type{<: Lines}, rect::Rect3) - points = unique(decompose(Point3f, rect)) - push!(points, Point3f(NaN)) # use to seperate linesegments +function convert_arguments(::Type{<: Lines}, rect::Rect3{T}) where {T} + PT = Point3{float_type(T)} + points = unique(decompose(PT, rect)) + push!(points, PT(NaN)) # use to seperate linesegments return (points[[1, 2, 3, 4, 1, 5, 6, 2, 9, 6, 8, 3, 9, 5, 7, 4, 9, 7, 8]],) end """ @@ -218,12 +226,13 @@ end Takes an input `Array{LineString}` or a `MultiLineString` and decomposes it to points. """ -function convert_arguments(PB::PointBased, linestring::Union{Array{<:LineString}, MultiLineString}) - arr = Point2f[]; n = length(linestring) +function convert_arguments(PB::PointBased, linestring::Union{<:AbstractVector{<:LineString{N, T}}, MultiLineString{N, T}}) where {N, T} + T_out = float_type(T) + arr = Point{N, T_out}[]; n = length(linestring) for idx in 1:n append!(arr, convert_arguments(PB, linestring[idx])[1]) if idx != n # don't add NaN at the end - push!(arr, Point2f(NaN)) + push!(arr, Point{N, T_out}(NaN)) end end return (arr,) @@ -242,7 +251,7 @@ function convert_arguments(PB::PointBased, pol::Polygon) push!(arr, arr[1]) # close exterior end for interior in pol.interiors - push!(arr, Point2f(NaN)) + push!(arr, Point2(NaN)) inter = convert_arguments(PB, interior)[1] # this should always be a Tuple{<: Vector{Point}} append!(arr, inter) if !isempty(inter) && inter[1] != inter[end] @@ -258,14 +267,14 @@ end Takes an input `Array{Polygon}` or a `MultiPolygon` and decomposes it to points. """ -function convert_arguments(PB::PointBased, mp::Union{Array{<:Polygon}, MultiPolygon}) - arr = Point2f[] +function convert_arguments(PB::PointBased, mp::Union{Array{<:Polygon{N, T}}, MultiPolygon{N, T}}) where {N, T} + arr = Point{N,float_type(T)}[] n = length(mp) for idx in 1:n converted = convert_arguments(PB, mp[idx])[1] # this should always be a Tuple{<: Vector{Point}} append!(arr, converted) if idx != n # don't add NaN at the end - push!(arr, Point2f(NaN)) + push!(arr, Point2(NaN)) end end return (arr,) @@ -273,12 +282,12 @@ end function convert_arguments(::PointBased, b::BezierPath) b2 = replace_nonfreetype_commands(b) - points = Point2f[] - last_point = Point2f(NaN) + points = Point2d[] + last_point = Point2d(NaN) last_moveto = false function poly3(t, p0, p1, p2, p3) - Point2f((1-t)^3 .* p0 .+ t*p1*(3*(1-t)^2) + p2*(3*(1-t)*t^2) .+ p3*t^3) + Point2d((1-t)^3 .* p0 .+ t*p1*(3*(1-t)^2) + p2*(3*(1-t)*t^2) .+ p3*t^3) end for command in b2.commands @@ -287,7 +296,7 @@ function convert_arguments(::PointBased, b::BezierPath) last_moveto = true elseif command isa LineTo if last_moveto - isempty(points) || push!(points, Point2f(NaN, NaN)) + isempty(points) || push!(points, Point2d(NaN, NaN)) push!(points, last_point) end push!(points, command.p) @@ -295,7 +304,7 @@ function convert_arguments(::PointBased, b::BezierPath) last_moveto = false elseif command isa CurveTo if last_moveto - isempty(points) || push!(points, Point2f(NaN, NaN)) + isempty(points) || push!(points, Point2d(NaN, NaN)) push!(points, last_point) end last_moveto = false @@ -313,15 +322,16 @@ end # GridBased # ################################################################################ -function edges(v::AbstractVector) +function edges(v::AbstractVector{T}) where T + T_out = float_type(T) l = length(v) if l == 1 - return [v[1] - 0.5, v[1] + 0.5] + return T_out[v[1] - 0.5, v[1] + 0.5] else # Equivalent to # mids = 0.5 .* (v[1:end-1] .+ v[2:end]) # borders = [2v[1] - mids[1]; mids; 2v[end] - mids[end]] - borders = [0.5 * (v[max(1, i)] + v[min(end, i+1)]) for i in 0:length(v)] + borders = T_out[0.5 * (v[max(1, i)] + v[min(end, i+1)]) for i in 0:length(v)] borders[1] = 2borders[1] - borders[2] borders[end] = 2borders[end] - borders[end-1] return borders @@ -345,11 +355,10 @@ whether they represent edges or centers of the heatmap bins. If they are centers, convert to edges. Convert eltypes to `Float32` and return outputs as a `Tuple`. """ -function convert_arguments(ct::GridBased, x::AbstractVecOrMat{<: Number}, y::AbstractVecOrMat{<: Number}, z::AbstractMatrix{<: Union{Number, Colorant}}) - return map(el32convert, adjust_axes(ct, x, y, z)) -end -function convert_arguments(ct::GridBased, x::AbstractVecOrMat{<: Number}, y::AbstractVecOrMat{<: Number}, z::AbstractMatrix{<:Number}) - return map(el32convert, adjust_axes(ct, x, y, z)) +function convert_arguments(ct::GridBased, x::AbstractVecOrMat{<:Real}, y::AbstractVecOrMat{<:Real}, + z::AbstractMatrix{<:Union{Real,Colorant}}) + nx, ny, nz = adjust_axes(ct, x, y, z) + return (float_convert(nx), float_convert(ny), el32convert(nz)) end convert_arguments(ct::VertexGrid, x::AbstractMatrix, y::AbstractMatrix) = convert_arguments(ct, x, y, zeros(size(y))) @@ -360,7 +369,7 @@ convert_arguments(ct::VertexGrid, x::AbstractMatrix, y::AbstractMatrix) = conver Takes one or two ClosedIntervals `x` and `y` and converts them to closed ranges with size(z, 1/2). """ -function convert_arguments(P::GridBased, x::RangeLike, y::RangeLike, z::AbstractMatrix{<: Union{Number, Colorant}}) +function convert_arguments(P::GridBased, x::RangeLike, y::RangeLike, z::AbstractMatrix{<: Union{Real, Colorant}}) convert_arguments(P, to_linspace(x, size(z, 1)), to_linspace(y, size(z, 2)), z) end @@ -369,35 +378,37 @@ end Generates `ClosedInterval`s of size `0 .. size(mat, 1/2)` as x and y values. """ -function convert_arguments(::ImageLike, data::AbstractMatrix) +function convert_arguments(::ImageLike, data::AbstractMatrix{<: Union{Real, Colorant}}) n, m = Float32.(size(data)) - return (0f0 .. n, 0f0 .. m, el32convert(data)) + return (Float32(0) .. n, Float32(0) .. m, el32convert(data)) end function print_range_warning(side::String, value) @warn "Encountered an `AbstractVector` with value $value on side $side in `convert_arguments` for the `ImageLike` trait. Using an `AbstractVector` to specify one dimension of an `ImageLike` is deprecated because `ImageLike` sides always need exactly two values, start and stop. Use interval notation `start .. stop` or a two-element tuple `(start, stop)` instead." end -function convert_arguments(::ImageLike, xs::RangeLike, ys::RangeLike, data::AbstractMatrix) +function convert_arguments(::ImageLike, xs::RangeLike, ys::RangeLike, + data::AbstractMatrix{<:Union{Real,Colorant}}) if xs isa AbstractVector print_range_warning("x", xs) end if ys isa AbstractVector print_range_warning("y", ys) end - _interval(v::Union{Interval,AbstractVector}) = Float32(minimum(v)) .. Float32(maximum(v)) # having minimum and maximum here actually invites bugs - _interval(t::Tuple{Any, Any}) = Float32(t[1]) .. Float32(t[2]) + # having minimum and maximum here actually invites bugs + _interval(v::Union{Interval,AbstractVector}) = float_convert(minimum(v)) .. float_convert(maximum(v)) + _interval(t::Tuple{Any, Any}) = float_convert(t[1]) .. float_convert(t[2]) x = _interval(xs) y = _interval(ys) return (x, y, el32convert(data)) end -function convert_arguments(ct::GridBased, data::AbstractMatrix) +function convert_arguments(ct::GridBased, data::AbstractMatrix{<:Union{Real,Colorant}}) n, m = Float32.(size(data)) convert_arguments(ct, 1f0 .. n, 1f0 .. m, el32convert(data)) end -function convert_arguments(ct::GridBased, x::AbstractVector{<:Number}, y::AbstractVector{<:Number}, z::AbstractVector{<:Number}) +function convert_arguments(ct::GridBased, x::RealVector, y::RealVector, z::RealVector) if !(length(x) == length(y) == length(z)) error("x, y and z need to have the same length. Lengths are $(length.((x, y, z)))") end @@ -419,7 +430,7 @@ function convert_arguments(ct::GridBased, x::AbstractVector{<:Number}, y::Abstra j = searchsortedfirst(y_centers, yi) @inbounds zs[i, j] = zi end - convert_arguments(ct, x_centers, y_centers, zs) + return convert_arguments(ct, x_centers, y_centers, zs) end @@ -430,14 +441,11 @@ Takes vectors `x` and `y` and the function `f`, and applies `f` on the grid that This is equivalent to `f.(x, y')`. `P` is the plot Type (it is optional). """ -function convert_arguments(ct::Union{GridBased, ImageLike}, x::AbstractVector{T1}, y::AbstractVector{T2}, f::Function) where {T1, T2} +function convert_arguments(ct::Union{GridBased, ImageLike}, x::AbstractVector, y::AbstractVector, f::Function) if !applicable(f, x[1], y[1]) - error("You need to pass a function with signature f(x::$T1, y::$T2). Found: $f") + error("You need to pass a function with signature f(x::$(eltype(x)), y::$(eltype(y))). Found: $f") end - T = typeof(f(x[1], y[1])) - z = similar(x, T, (length(x), length(y))) - z .= f.(x, y') - return convert_arguments(ct, x, y, z) + return convert_arguments(ct, x, y, f.(x, y')) end ################################################################################ @@ -452,13 +460,14 @@ and stores the `ClosedInterval` to `n`, `m` and `k`, plus the original array in `P` is the plot Type (it is optional). """ -function convert_arguments(::VolumeLike, data::AbstractArray{T, 3}) where T +function convert_arguments(::VolumeLike, data::RealArray{3}) n, m, k = Float32.(size(data)) return (0f0 .. n, 0f0 .. m, 0f0 .. k, el32convert(data)) end -function convert_arguments(::VolumeLike, x::RangeLike, y::RangeLike, z::RangeLike, data::AbstractArray{T, 3}) where T - return (x, y, z, el32convert(data)) +function convert_arguments(::VolumeLike, x::RangeLike, y::RangeLike, z::RangeLike, + data::RealArray{3}) + return (el32convert(x), el32convert(y), el32convert(z), el32convert(data)) end """ convert_arguments(P, x, y, z, i)::(Vector, Vector, Vector, Matrix) @@ -467,17 +476,17 @@ Takes 3 `AbstractVector` `x`, `y`, and `z` and the `AbstractMatrix` `i`, and put `P` is the plot Type (it is optional). """ -function convert_arguments(::VolumeLike, x::AbstractVector, y::AbstractVector, z::AbstractVector, i::AbstractArray{T, 3}) where T - (x, y, z, el32convert(i)) +function convert_arguments(::VolumeLike, x::RealVector, y::AbstractVector, z::RealVector, i::RealArray{3}) + (el32convert(x), el32convert(y), el32convert(z), el32convert(i)) end ################################################################################ # <:Lines # ################################################################################ -function convert_arguments(::Type{<: Lines}, x::Rect2) +function convert_arguments(::Type{<: Lines}, x::Rect2{T}) where T # TODO fix the order of decompose - points = decompose(Point2f, x) + points = decompose(Point2{float_type(T)}, x) return (points[[1, 2, 4, 3, 1]],) end @@ -490,27 +499,15 @@ Accepts a Vector of Pair of Points (e.g. `[Point(0, 0) => Point(1, 1), ...]`) to encode e.g. linesegments or directions. """ function convert_arguments(::Type{<: LineSegments}, positions::AbstractVector{E}) where E <: Union{Pair{A, A}, Tuple{A, A}} where A <: VecTypes{N, T} where {N, T} - (elconvert(Point{N, Float32}, reinterpret(Point{N, T}, positions)),) + return (float_convert(reinterpret(Point{N,T}, positions)),) end -function convert_arguments(::Type{<: LineSegments}, x::Rect2) +function convert_arguments(::Type{<: LineSegments}, x::Rect2{T}) where T # TODO fix the order of decompose - points = decompose(Point2f, x) + points = decompose(Point2{float_type(T)}, x) return (points[[1, 2, 2, 4, 4, 3, 3, 1]],) end -################################################################################ -# <:Text # -################################################################################ - -""" - convert_arguments(x)::(String) - -Takes an input `AbstractString` `x` and converts it to a string. -""" -# convert_arguments(::Type{<: Text}, x::AbstractString) = (String(x),) - - ################################################################################ # <:Mesh # ################################################################################ @@ -525,7 +522,7 @@ function convert_arguments( T::Type{<:Mesh}, x::RealVector, y::RealVector, z::RealVector ) - convert_arguments(T, Point3f.(x, y, z)) + convert_arguments(T, Point3{float_type(x, y, z)}.(x, y, z)) end """ convert_arguments(Mesh, xyz::AbstractVector)::GLNormalMesh @@ -542,7 +539,8 @@ function convert_arguments( return convert_arguments(MT, xyz, collect(faces)) end -function convert_arguments(::Type{<:Mesh}, mesh::GeometryBasics.Mesh{N}) where {N} +function convert_arguments(::Type{<:Mesh}, mesh::GeometryBasics.Mesh{N, T}) where {N, T} + T_out = float_type(T) # Make sure we have normals! if !hasproperty(mesh, :normals) n = normals(metafree(decompose(Point, mesh)), faces(mesh)) @@ -552,32 +550,41 @@ function convert_arguments(::Type{<:Mesh}, mesh::GeometryBasics.Mesh{N}) where { end end # If already correct eltypes for GL, we can pass the mesh through as is - if eltype(metafree(coordinates(mesh))) == Point{N, Float32} && eltype(faces(mesh)) == GLTriangleFace + if eltype(metafree(coordinates(mesh))) == Point{N, T_out} && eltype(faces(mesh)) == GLTriangleFace return (mesh,) else # Else, we need to convert it! - return (GeometryBasics.mesh(mesh, pointtype=Point{N, Float32}, facetype=GLTriangleFace),) + return (GeometryBasics.mesh(mesh, pointtype=Point{N, T_out}, facetype=GLTriangleFace),) end end function convert_arguments( - MT::Type{<:Mesh}, + ::Type{<:Mesh}, meshes::AbstractVector{<: Union{AbstractMesh, AbstractPolygon}} ) return (meshes,) end +function convert_arguments(MT::Type{<:Mesh}, xyz::AbstractPolygon) + m = GeometryBasics.mesh(xyz; pointtype=float_type(xyz), facetype=GLTriangleFace) + return convert_arguments(MT, m) +end + +# TODO GeometryBasics can't deal with this directly for Integer Points? function convert_arguments( MT::Type{<:Mesh}, - xyz::Union{AbstractPolygon, AbstractVector{<: AbstractPoint{2}}} + xyz::AbstractVector{<: AbstractPoint{2}} ) - return convert_arguments(MT, triangle_mesh(xyz)) + ps = float_convert(xyz) + m = GeometryBasics.mesh(ps; pointtype=eltype(ps), facetype=GLTriangleFace) + return convert_arguments(MT, m) end -function convert_arguments(MT::Type{<:Mesh}, geom::GeometryPrimitive) +function convert_arguments(::Type{<:Mesh}, geom::GeometryPrimitive{N, T}) where {N, T <: Real} # we convert to UV mesh as default, because otherwise the uv informations get lost # - we can still drop them, but we can't add them later on - return (GeometryBasics.uv_normal_mesh(geom),) + m = GeometryBasics.mesh(geom; pointtype=Point{N,float_type(T)}, uv=Vec2f, normaltype=Vec3f, facetype=GLTriangleFace) + return (m,) end """ @@ -591,7 +598,7 @@ function convert_arguments( x::RealVector, y::RealVector, z::RealVector, indices::AbstractVector ) - return convert_arguments(T, Point3f.(x, y, z), indices) + return convert_arguments(T, Point3{float_type(x, y, z)}.(x, y, z), indices) end """ @@ -609,7 +616,7 @@ function convert_arguments( vs = to_vertices(vertices) fs = to_triangles(indices) if eltype(vs) <: Point{3} - ns = normals(vs, fs) + ns = Vec3f.(normals(vs, fs)) m = GeometryBasics.Mesh(meta(vs; normals=ns), fs) else # TODO, we don't need to add normals here, but maybe nice for type stability? @@ -623,19 +630,20 @@ end # Function Conversions # ################################################################################ + # Allow the user to pass a function to `arrows` which determines the direction # and magnitude of the arrows. The function must accept `Point2f` as input. # and return Point2f or Vec2f or some array like structure as output. function convert_arguments(::Type{<:Arrows}, x::AbstractVector, y::AbstractVector, f::Function) - points = Point2f.(x, y') - f_out = Vec2f.(f.(points)) + points = Point2{float_type(x, y)}.(x, y') + f_out = Vec2{float_type(x, y)}.(f.(points)) return (vec(points), vec(f_out)) end function convert_arguments(::Type{<:Arrows}, x::AbstractVector, y::AbstractVector, z::AbstractVector, f::Function) - points = [Point3f(x, y, z) for x in x, y in y, z in z] - f_out = Vec3f.(f.(points)) + points = [Point3{float_type(x, y, z)}(x, y, z) for x in x, y in y, z in z] + f_out = Vec3{float_type(x, y, z)}.(f.(points)) return (vec(points), vec(f_out)) end @@ -655,7 +663,7 @@ function convert_arguments(::VolumeLike, x::AbstractVector, y::AbstractVector, z A = (x, y, z)[i] return reshape(A, ntuple(j -> j != i ? 1 : length(A), Val(3))) end - return (x, y, z, el32convert.(f.(_x, _y, _z))) + return (el32convert(x), el32convert(y), el32convert(z), el32convert(f.(_x, _y, _z))) end function convert_arguments(P::PlotFunc, r::AbstractVector, f::Function) @@ -667,31 +675,6 @@ function convert_arguments(P::PlotFunc, i::AbstractInterval, f::Function) return convert_arguments(P, x, y) end - - -# The following `tryrange` code was copied from Plots.jl -# https://github.com/MakieOrg/Plots.jl/blob/15dc61feb57cba1df524ce5d69f68c2c4ea5b942/src/series.jl#L399-L416 - -# try some intervals over which the function may be defined -function tryrange(F::AbstractArray, vec) - rets = [tryrange(f, vec) for f in F] # get the preferred for each - maxind = maximum(indexin(rets, vec)) # get the last attempt that succeeded (most likely to fit all) - rets .= [tryrange(f, vec[maxind:maxind]) for f in F] # ensure that all functions compute there - rets[1] -end - -function tryrange(F, vec) - for v in vec - try - tmp = F(v) - return v - catch - end - end - error("$F is not a Function, or is not defined at any of the values $vec") -end - - # OffsetArrays conversions function convert_arguments(sl::GridBased, wm::OffsetArray) x1, y1 = wm.offsets .+ 1 @@ -711,23 +694,63 @@ to_linspace(interval, N) = range(minimum(interval), stop = maximum(interval), le """ Converts the elemen array type to `T1` without making a copy if the element type matches """ -elconvert(::Type{T1}, x::AbstractArray{T2, N}) where {T1, T2, N} = convert(AbstractArray{T1, N}, x) -float32type(x::Type) = Float32 +function elconvert(::Type{T1}, x::AbstractArray{T2, N}) where {T1, T2, N} + return convert(AbstractArray{T1, N}, x) +end + +function elconvert(::Type{T}, x::AbstractArray{<: Union{Missing, <:Real}}) where {T} + return map(x) do elem + return (ismissing(elem) ? T(NaN) : convert(T, elem)) + end +end + +float_type(a, rest...) = float_type(typeof(a), map(typeof, rest)...) +float_type(a::AbstractArray, rest...) = float_type(float_type(a), map(float_type, rest)...) +float_type(a::AbstractPolygon, rest...) = float_type(float_type(a), map(float_type, rest)...) +float_type(a::Type, rest::Type...) = float_type(promote_type(a, rest...)) +float_type(::Type{Float64}) = Float64 +float_type(::Type{Float32}) = Float32 +float_type(::Type{<:Real}) = Float64 +float_type(::Type{<:Union{Int8,UInt8,Int16,UInt16}}) = Float32 +float_type(::Type{<:Union{Float16}}) = Float32 +float_type(::Type{Point{N,T}}) where {N,T} = Point{N,float_type(T)} +float_type(::Type{Vec{N,T}}) where {N,T} = Vec{N,float_type(T)} +float_type(::Type{NTuple{N, T}}) where {N,T} = Point{N,float_type(T)} +float_type(::Type{Tuple{T1, T2}}) where {T1,T2} = Point2{promote_type(float_type(T1), float_type(T2))} +float_type(::Type{Tuple{T1, T2, T3}}) where {T1,T2,T3} = Point3{promote_type(float_type(T1), float_type(T2), float_type(T3))} +float_type(::Type{Union{Missing, T}}) where {T} = float_type(T) +float_type(::Type{Union{Nothing, T}}) where {T} = float_type(T) +float_type(::AbstractArray{T}) where {T} = float_type(T) +float_type(::AbstractPolygon{N, T}) where {N, T} = Point{N, float_type(T)} + +float_convert(x) = convert(float_type(x), x) +float_convert(x::AbstractArray{Float32}) = x +float_convert(x::AbstractArray{Float64}) = x +float_convert(x::AbstractArray) = elconvert(float_type(x), x) +float_convert(x::Observable) = lift(float_convert, x) +float_convert(x::AbstractArray{<:Union{Missing, T}}) where {T<:Real} = elconvert(float_type(T), x) + +float32type(::Type{<:Real}) = Float32 +float32type(::Type{Point{N,T}}) where {N,T} = Point{N,float32type(T)} +float32type(::Type{Vec{N,T}}) where {N,T} = Vec{N,float32type(T)} + +# We may want to always use UInt8 for colors? float32type(::Type{<: RGB}) = RGB{Float32} float32type(::Type{<: RGBA}) = RGBA{Float32} float32type(::Type{<: Colorant}) = RGBA{Float32} -float32type(x::AbstractArray{T}) where T = float32type(T) -float32type(x::T) where T = float32type(T) +float32type(::AbstractArray{T}) where T = float32type(T) +float32type(::T) where {T} = float32type(T) + +el32convert(x::ClosedInterval) = Float32(minimum(x)) .. Float32(maximum(x)) el32convert(x::AbstractArray) = elconvert(float32type(x), x) +el32convert(x::AbstractArray{T}) where {T<:Real} = elconvert(float32type(T), x) +el32convert(x::AbstractArray{<:Union{Missing,T}}) where {T<:Real} = elconvert(float32type(T), x) el32convert(x::AbstractArray{Float32}) = x el32convert(x::Observable) = lift(el32convert, x) el32convert(x) = convert(float32type(x), x) +el32convert(x::Mat{X, Y, T}) where {X, Y, T} = Mat{X, Y, Float32}(x) + -function el32convert(x::AbstractArray{T, N}) where {T<:Union{Missing, <: Number}, N} - return map(x) do elem - return (ismissing(elem) ? NaN32 : convert(Float32, elem))::Float32 - end::Array{Float32, N} -end """ to_triangles(indices) @@ -774,12 +797,13 @@ Converts a representation of vertices `v` to its canonical representation as a - otherwise if `v` has 2 or 3 columns, it will treat each row as a vertex. """ function to_vertices(verts::AbstractVector{<: VecTypes{3, T}}) where T - vert3f0 = T != Float32 ? map(Point3f, verts) : verts - return reinterpret(Point3f, vert3f0) + T_out = float_type(T) + vert3 = T != T_out ? map(Point3{T_out}, verts) : verts + return reinterpret(Point3{T_out}, vert3) end -function to_vertices(verts::AbstractVector{<: VecTypes{N}}) where {N} - return map(Point{N, Float32}, verts) +function to_vertices(verts::AbstractVector{<: VecTypes{N, T}}) where {N, T} + return map(Point{N, float_type(T)}, verts) end function to_vertices(verts::AbstractMatrix{<: Number}) @@ -792,32 +816,66 @@ function to_vertices(verts::AbstractMatrix{<: Number}) end end -function to_vertices(verts::AbstractMatrix{T}, ::Val{1}) where T <: Number +function to_vertices(verts::AbstractMatrix{T}, ::Val{1}) where T <: Real N = size(verts, 1) - if T == Float32 && N == 3 + if T == float_type(T) && N == 3 reinterpret(Point{N, T}, elconvert(T, vec(verts))) else - let N = Val(N), lverts = verts + let N = Val(N); lverts = verts; T_out = float_type(T) broadcast(1:size(verts, 2), N) do vidx, n - Point(ntuple(i-> Float32(lverts[i, vidx]), n)) + Point(ntuple(i-> T_out(lverts[i, vidx]), n)) end end end end -function to_vertices(verts::AbstractMatrix{T}, ::Val{2}) where T <: Number - let N = Val(size(verts, 2)), lverts = verts +function to_vertices(verts::AbstractMatrix{T}, ::Val{2}) where T <: Real + let N = Val(size(verts, 2)); lverts = verts; T_out = float_type(T) broadcast(1:size(verts, 1), N) do vidx, n - Point(ntuple(i-> Float32(verts[vidx, i]), n)) + Point(ntuple(i-> T_out(lverts[vidx, i]), n)) + end + end +end + + +################################################################################ +### Unused? +################################################################################ + +# The following `tryrange` code was copied from Plots.jl +# https://github.com/MakieOrg/Plots.jl/blob/15dc61feb57cba1df524ce5d69f68c2c4ea5b942/src/series.jl#L399-L416 + +# try some intervals over which the function may be defined +function tryrange(F::AbstractArray, vec) + rets = [tryrange(f, vec) for f in F] # get the preferred for each + maxind = maximum(indexin(rets, vec)) # get the last attempt that succeeded (most likely to fit all) + rets .= [tryrange(f, vec[maxind:maxind]) for f in F] # ensure that all functions compute there + rets[1] +end + +function tryrange(F, vec) + for v in vec + try + tmp = F(v) + return v + catch end end + error("$F is not a Function, or is not defined at any of the values $vec") end + + + ################################################################################ # Attribute conversions # ################################################################################ + + + + convert_attribute(x, key::Key, ::Key) = convert_attribute(x, key) convert_attribute(s::SceneLike, x, key::Key, ::Key) = convert_attribute(s, x, key) convert_attribute(s::SceneLike, x, key::Key) = convert_attribute(x, key) @@ -826,7 +884,6 @@ convert_attribute(x, key::Key) = x convert_attribute(color, ::key"color") = to_color(color) convert_attribute(colormap, ::key"colormap") = to_colormap(colormap) -convert_attribute(rotation, ::key"rotation") = to_rotation(rotation) convert_attribute(font, ::key"font") = to_font(font) convert_attribute(align, ::key"align") = to_align(align) @@ -861,11 +918,11 @@ function to_color(c::Tuple{<: Any, <: Number}) return RGBAf(Colors.color(col), alpha(col) * c[2]) end -convert_attribute(b::Billboard{Float32}, ::key"rotations") = to_rotation(b.rotation) -convert_attribute(b::Billboard{Vector{Float32}}, ::key"rotations") = to_rotation.(b.rotation) -convert_attribute(r::AbstractArray, ::key"rotations") = to_rotation.(r) -convert_attribute(r::StaticVector, ::key"rotations") = to_rotation(r) -convert_attribute(r, ::key"rotations") = to_rotation(r) +convert_attribute(b::Billboard{Float32}, ::key"rotation") = to_rotation(b.rotation) +convert_attribute(b::Billboard{Vector{Float32}}, ::key"rotation") = to_rotation.(b.rotation) +convert_attribute(r::AbstractArray, ::key"rotation") = to_rotation.(r) +convert_attribute(r::StaticVector, ::key"rotation") = to_rotation(r) +convert_attribute(r, ::key"rotation") = to_rotation(r) convert_attribute(c, ::key"markersize", ::key"scatter") = to_2d_scale(c) convert_attribute(c, ::key"markersize", ::key"meshscatter") = to_3d_scale(c) @@ -1694,6 +1751,7 @@ end convert_attribute(value, ::key"marker", ::key"scatter") = to_spritemarker(value) convert_attribute(value, ::key"isovalue", ::key"volume") = Float32(value) convert_attribute(value, ::key"isorange", ::key"volume") = Float32(value) +convert_attribute(value, ::key"gap", ::key"voxels") = ifelse(value <= 0.01, 0f0, Float32(value)) function convert_attribute(value::Symbol, ::key"marker", ::key"meshscatter") if value === :Sphere diff --git a/src/float32-scaling.jl b/src/float32-scaling.jl new file mode 100644 index 00000000000..c181ffb03f5 --- /dev/null +++ b/src/float32-scaling.jl @@ -0,0 +1,255 @@ +################################################################################ +### LinearScaling +################################################################################ + +# For reference: +# 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 + + +@inline function f32_convert(ls::LinearScaling, p::VecTypes{N}) where N + # TODO Point{N, Float32}(::Point{N, Int}) doesn't work + return to_ndim(Point{N, Float32}, ls(p), 0) +end +@inline function f32_convert(ls::LinearScaling, ps::AbstractArray{<: VecTypes{N}}) where N + return [to_ndim(Point{N, Float32}, ls(p), 0) for p in ps] +end + +@inline f32_convert(ls::LinearScaling, x::Real, dim::Integer) = Float32(ls(x, dim)) +@inline function f32_convert(ls::LinearScaling, xs::AbstractArray{<: Real}, dim::Integer) + return [Float32(ls(x, dim)) for x in xs] +end + +@inline function f32_convert(ls::LinearScaling, r::Rect{N}) where {N} + mini = ls(minimum(r)) + maxi = ls(maximum(r)) + return Rect{N, Float32}(mini, maxi - mini) +end + +@inline function f32_convert(ls::LinearScaling, data, space::Symbol) + return space in (:data, :transformed) ? f32_convert(ls, data) : f32_convert(nothing, data) +end +@inline function f32_convert(ls::LinearScaling, data, dim::Integer, space::Symbol) + return space in (:data, :transformed) ? f32_convert(ls, data, dim) : f32_convert(nothing, data, dim) +end + + +Base.inv(ls::LinearScaling) = LinearScaling(1.0 ./ ls.scale, - ls.offset ./ ls.scale) + + +function inv_f32_scale(ls::LinearScaling, v::VecTypes{3}) + return Vec3d(v) ./ ls.scale +end + +@inline function inv_f32_convert(ls::LinearScaling, r::Rect{N}) where {N} + ils = inv(ls) + mini = ils(Vec{N, Float64}(minimum(r))) + maxi = ils(Vec{N, Float64}(maximum(r))) + return Rect{N, Float64}(mini, maxi - mini) +end + +# For CairoMakie +function f32_convert_matrix(ls::LinearScaling) + scale = to_ndim(Vec3d, ls.scale, 1) + translation = to_ndim(Vec3d, ls.offset, 0) + return transformationmatrix(translation, scale) +end +function f32_convert_matrix(ls::LinearScaling, space::Symbol) + # maybe :world? + return space in (:data, :transformed) ? f32_convert_matrix(ls) : Mat4d(I) +end +inv_f32_convert_matrix(ls::LinearScaling, space::Symbol) = f32_convert_matrix(inv(ls), space) + + +# returns Matrix R such that M * ls = ls * R +patch_model(::Nothing, M::Mat4d) = Mat4f(M) +function patch_model(ls::LinearScaling, M::Mat4d) + return Mat4f(f32_convert_matrix(ls) * M * f32_convert_matrix(inv(ls))) +end + + +################################################################################ +### Float32Convert +################################################################################ + +# For reference: +# struct Float32Convert +# scaling::Observable{LinearScaling} +# resolution::Float32 +# end + +""" + Float32Convert([resolution = 1e4]) + +Creates a Float32Convert which acts as an additional conversion step when +attached to a `scene` as `scene.float32convert`. The optional `resolution` +controls the minimum number of individual values that the conversion keeps +available. I.e. the conversion ensures that +`(max - min) > resolution * max(eps(min), eps(max))` whenever `update_limits!` +is called. Note that resolution must be smaller than `1 / eps(Float32)`. +""" +function Float32Convert(resolution = 1e4) + scaling = LinearScaling(Vec{3, Float64}(1.0), Vec{3, Float64}(0.0)) + return Float32Convert(Observable(scaling), resolution) +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 + +""" + update_limits!(c::Union{Float32Convert, Nothing}, lims::Rect) + update_limits!(c::Union{Float32Convert, Nothing}, min::VecTypes{3, Float64}, max::VecTypes{3, Float64}) + +This function is used to report a limit update to `Float32Convert`. If the +conversion applied to the given limits results in a range not representable +with Float32 to high enough precision, the conversion will update. After the +update update the converted range will be -1 .. 1. + +The function returns true if an update has occured. If `Nothing` is passed, the +function always returns false. +""" +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 = Float64(eps(Float32)) * max.(abs.(low), abs.(high)) + min_resolved = delta ./ max_eps + f32min = Float64(floatmin(Float32)) * c.resolution + f32max = Float64(floatmax(Float32)) / c.resolution + + # 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((abs.(low) .< f32min) .& (abs.(high) .< f32min)) || + any((abs.(low) .> f32max) .& (abs.(high) .> f32max)) + + 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 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::Nothing, data, ::Symbol) = f32_convert(c, data) +@inline f32_convert(c::Nothing, data, dim::Integer, ::Symbol) = f32_convert(c, data, dim) + +@inline f32_convert(c::Float32Convert, args...) = f32_convert(c.scaling[], args...) +@inline f32_convert(x::SceneLike, args...) = f32_convert(f32_conversion(x), args...) + +@inline inv_f32_convert(c::Nothing, args...) = f32_convert(c, args...) +@inline inv_f32_convert(c::Float32Convert, x::Real) = inv(c.scaling[])(Float64(x)) +@inline inv_f32_convert(c::Float32Convert, x::VecTypes{N}) where N = inv(c.scaling[])(to_ndim(Point{N, Float64}, x, 0)) +@inline inv_f32_convert(c::Float32Convert, x::AbstractArray) = inv_f32_convert.((c,), x) +@inline inv_f32_convert(ls::Float32Convert, r::Rect) = inv_f32_convert(ls.scaling[], r) +@inline inv_f32_convert(x::SceneLike, args...) = inv_f32_convert(f32_conversion(x), args...) + +@inline inv_f32_scale(c::Nothing, v::VecTypes{3}) = Vec3d(v) +@inline inv_f32_scale(c::Float32Convert, v::VecTypes{3}) = inv_f32_scale(c.scaling[], v) +@inline inv_f32_scale(x::SceneLike, args...) = inv_f32_scale(f32_conversion(x), args...) + + +# For CairoMakie & project +f32_convert_matrix(::Nothing, ::Symbol) = Mat4d(I) +f32_convert_matrix(c::Float32Convert, space::Symbol) = f32_convert_matrix(c.scaling[], space) +f32_convert_matrix(x, space::Symbol) = f32_convert_matrix(f32_conversion(x), space) + +inv_f32_convert_matrix(::Nothing, ::Symbol) = Mat4d(I) +inv_f32_convert_matrix(c::Float32Convert, space::Symbol) = f32_convert_matrix(inv(c.scaling[]), space) +inv_f32_convert_matrix(x, space::Symbol) = inv_f32_convert_matrix(f32_conversion(x), space) + +# For GLMakie, WGLMakie, maybe RPRMakie +function f32_conversion_obs(scene::Scene) + if isnothing(scene.float32convert) + return Observable(nothing) + else + return scene.float32convert.scaling + end +end +f32_conversion_obs(plot::AbstractPlot) = f32_conversion_obs(parent_scene(plot)) + +f32_conversion(plot::AbstractPlot) = f32_conversion(parent_scene(plot)) +f32_conversion(scene::Scene) = scene.float32convert + +patch_model(scene::SceneLike, M::Mat4d) = patch_model(f32_conversion(scene), M) + + +# 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)) + ) + return map( + apply_transform_and_f32_conversion, plot, + f32_conversion_obs(scene), transform_func_obs(plot), data, space + ) +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)) + ) + return map( + apply_transform_and_f32_conversion, plot, + f32_conversion_obs(scene), transform_func_obs(plot), data, dim, space + ) +end + +function apply_transform_and_f32_conversion( + float32convert::Union{Nothing, Float32Convert, LinearScaling}, + transform_func, data, space::Symbol + ) + tf = space == :data ? transform_func : identity + f32c = space in (:data, :transformed) ? float32convert : nothing + # avoid intermediate arrays. TODO: Is transform_func strictly per element? + return [Makie.f32_convert(f32c, apply_transform(tf, x)) for x in data] +end + +function apply_transform_and_f32_conversion( + float32convert::Union{Nothing, Float32Convert, LinearScaling}, + transform_func, data, dim::Integer, space::Symbol + ) + tf = space == :data ? transform_func : identity + f32c = space in (:data, :transformed) ? float32convert : nothing + if dim == 1 + return [Makie.f32_convert(f32c, apply_transform(tf, Point2(x, 0))[1], dim) for x in data] + elseif dim == 2 + return [Makie.f32_convert(f32c, apply_transform(tf, Point2(0, x))[2], dim) for x in data] + elseif dim == 3 + return [Makie.f32_convert(f32c, apply_transform(tf, Point3(0, 0, x))[3], dim) for x in data] + else + error("The transform_func and float32 conversion can only be applied along dimensions 1, 2 or 3, not $dim") + end +end diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index 5a536e4c56e..0c66f001b00 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -167,7 +167,7 @@ function shift_project(scene, pos) project( camera(scene).projectionview[], Vec2f(size(scene)), - pos + f32_convert(scene, pos), ) .+ Vec2f(origin(viewport(scene)[])) end @@ -313,7 +313,7 @@ function on_hover(inspector) if should_clear plot = inspector.selection - if haskey(plot, :inspector_clear) + if to_value(get(plot, :inspector_clear, automatic)) !== automatic plot[:inspector_clear][](inspector, plot) end inspector.plot.visible[] = false @@ -333,10 +333,10 @@ function show_data_recursion(inspector, plot, idx) # Some show_data methods use the current selection to tell whether the # temporary plots (indicator plots) are theirs or not, so we want to # reset after processing them. We also don't want to reset when the - processed = if haskey(plot, :inspector_hover) - plot[:inspector_hover][](inspector, plot, idx) - else + processed = if to_value(get(plot, :inspector_hover, automatic)) == automatic show_data(inspector, plot, idx) + else + plot[:inspector_hover][](inspector, plot, idx) end if processed && inspector.selection != plot @@ -355,10 +355,10 @@ function show_data_recursion(inspector, plot::AbstractPlot, idx, source) # Some show_data methods use the current selection to tell whether the # temporary plots (indicator plots) are theirs or not, so we want to # reset after processing them. We also don't want to reset when the - processed = if haskey(plot, :inspector_hover) - plot[:inspector_hover][](inspector, plot, idx, source) - else + processed = if to_value(get(plot, :inspector_hover, automatic)) == automatic show_data(inspector, plot, idx, source) + else + plot[:inspector_hover][](inspector, plot, idx, source) end if processed && inspector.selection != plot @@ -372,7 +372,7 @@ end # clears temporary plots (i.e. bboxes) and update selection function clear_temporary_plots!(inspector::DataInspector, plot) if inspector.selection !== plot - if haskey(inspector.selection, :inspector_clear) + if to_value(get(inspector.selection, :inspector_clear, automatic)) !== automatic inspector.selection[:inspector_clear][](inspector, inspector.selection) end inspector.selection = plot @@ -431,10 +431,10 @@ function show_data(inspector::DataInspector, plot::Scatter, idx) proj_pos = shift_project(scene, apply_transform_and_model(plot, pos)) update_tooltip_alignment!(inspector, proj_pos) - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, pos) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = position2string(pos) + else + tt.text[] = plot[:inspector_label][](plot, idx, pos) end tt.offset[] = ifelse( a.apply_tooltip_offset[], @@ -455,8 +455,18 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) if a.enable_indicators[] translation = apply_transform_and_model(plot, plot[1][][idx]) - rotation = _to_rotation(plot.rotations[], idx) - scale = _to_scale(plot.markersize[], idx) + rotation = to_rotation(_to_rotation(plot.rotation[], idx)) + scale = inv_f32_scale(plot, _to_scale(plot.markersize[], idx)) + + bbox = Rect3d(convert_attribute( + plot.marker[], Key{:marker}(), Key{Makie.plotkey(plot)}() + )) + + ps = convert_arguments(LineSegments, bbox)[1] + ps = map(ps) do p + p3d = to_ndim(Point3d, p, 0) + return rotation * (scale .* p3d) + translation + end if inspector.selection != plot clear_temporary_plots!(inspector, plot) @@ -468,15 +478,9 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) upvector = cc.upvector[] end - bbox = Rect{3, Float32}(convert_attribute( - plot.marker[], Key{:marker}(), Key{Makie.plotkey(plot)}() - )) - T = Transformation( - identity; translation = translation, rotation = rotation, scale = scale - ) - - p = wireframe!( - scene, bbox, transformation = T, color = a.indicator_color, + T = Transformation(identity) + p = linesegments!( + scene, ps, transformation = T, color = a.indicator_color, linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false ) @@ -486,8 +490,7 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) cc isa Camera3D && update_cam!(scene, eyeposition, lookat, upvector) elseif !isempty(inspector.temp_plots) - p = inspector.temp_plots[1] - transform!(p, translation = translation, scale = scale, rotation = rotation) + inspector.temp_plots[1][1][] = ps end @@ -498,10 +501,10 @@ function show_data(inspector::DataInspector, plot::MeshScatter, idx) proj_pos = shift_project(scene, apply_transform_and_model(plot, pos)) update_tooltip_alignment!(inspector, proj_pos) - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, pos) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = position2string(pos) + else + tt.text[] = plot[:inspector_label][](plot, idx, pos) end tt.visible[] = true @@ -525,10 +528,10 @@ function show_data(inspector::DataInspector, plot::Union{Lines, LineSegments}, i a.offset[] ) - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, eltype(plot[1][])(pos)) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = position2string(eltype(plot[1][])(pos)) + else + tt.text[] = plot[:inspector_label][](plot, idx, eltype(plot[1][])(pos)) end tt.visible[] = true a.indicator_visible[] && (a.indicator_visible[] = false) @@ -542,14 +545,7 @@ function show_data(inspector::DataInspector, plot::Mesh, idx) tt = inspector.plot scene = parent_scene(plot) - # Manual boundingbox including transfunc - bbox = let - points = point_iterator(plot) - trans_func = transform_func(plot) - model = plot.model[] - iter = iterate_transformed(points, model, to_value(get(plot, :space, :data)), trans_func) - limits_from_transformed_points(iter) - end + bbox = boundingbox(plot) proj_pos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, proj_pos) @@ -583,10 +579,10 @@ function show_data(inspector::DataInspector, plot::Mesh, idx) end tt[1][] = proj_pos - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, bbox) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = bbox2string(bbox) + else + tt.text[] = plot[:inspector_label][](plot, idx, bbox) end tt.visible[] = true @@ -605,10 +601,10 @@ function show_data(inspector::DataInspector, plot::Surface, idx) if !isnan(pos) tt[1][] = proj_pos - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, pos) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = position2string(pos) + else + tt.text[] = plot[:inspector_label][](plot, idx, pos) end tt.visible[] = true tt.offset[] = 0f0 @@ -657,11 +653,11 @@ function show_imagelike(inspector, plot, name, edge_based) return true end - if haskey(plot, :inspector_label) + if to_value(get(plot, :inspector_label, automatic)) == automatic + tt.text[] = color2text(name, x, y, z) + else ins_p = z isa Colorant ? (pos[1], pos[2], z) : Point3f(pos[1], pos[2], z) tt.text[] = plot[:inspector_label][](plot, (i, j), ins_p) - else - tt.text[] = color2text(name, x, y, z) end a._color[] = if z isa AbstractFloat @@ -705,7 +701,7 @@ function show_imagelike(inspector, plot, name, edge_based) clear_temporary_plots!(inspector, plot) p = wireframe!( scene, bbox, color = a.indicator_color, model = plot.model, - strokewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, + linewidth = a.indicator_linewidth, linestyle = a.indicator_linestyle, visible = a.indicator_visible, inspectable = false, depth_shift = -1f-3 ) @@ -777,11 +773,11 @@ function _pixelated_image_bbox(xs, ys, img, i::Integer, j::Integer, edge_based) x0, x1 = extrema(xs) y0, y1 = extrema(ys) nw, nh = ((x1 - x0), (y1 - y0)) ./ size(img) - Rect2f(x0 + nw * (i-1), y0 + nh * (j-1), nw, nh) + Rect2d(x0 + nw * (i-1), y0 + nh * (j-1), nw, nh) end function _pixelated_image_bbox(xs::Vector, ys::Vector, img, i::Integer, j::Integer, edge_based) if edge_based - Rect2f(xs[i], ys[j], xs[i+1] - xs[i], ys[j+1] - ys[j]) + Rect2d(xs[i], ys[j], xs[i+1] - xs[i], ys[j+1] - ys[j]) else _pixelated_image_bbox( minimum(xs)..maximum(xs), minimum(ys)..maximum(ys), @@ -842,10 +838,10 @@ function show_data(inspector::DataInspector, plot::BarPlot, idx) a.indicator_visible[] = true end - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, pos) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = position2string(pos) + else + tt.text[] = plot[:inspector_label][](plot, idx, pos) end tt.visible[] = true @@ -867,10 +863,10 @@ function show_data(inspector::DataInspector, plot::Arrows, idx, source) v = vec2string(plot[2][][idx]) tt[1][] = mpos - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, pos) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = "Position:\n $p\nDirection:\n $v" + else + tt.text[] = plot[:inspector_label][](plot, idx, pos) end tt.visible[] = true a.indicator_visible[] && (a.indicator_visible[] = false) @@ -888,10 +884,10 @@ function show_data(inspector::DataInspector, plot::Contourf, idx, source::Mesh) mpos = Point2f(mouseposition_px(inspector.root)) update_tooltip_alignment!(inspector, mpos) tt[1][] = mpos - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, idx, mpos) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = @sprintf("level = %0.3f", level) + else + tt.text[] = plot[:inspector_label][](plot, idx, mpos) end tt.visible[] = true @@ -962,13 +958,13 @@ function show_data(inspector::DataInspector, plot::VolumeSlices, idx, child::Hea world_pos = apply_transform_and_model(child, pos) - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, (i, j), world_pos) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic tt.text[] = @sprintf( "x: %0.6f\ny: %0.6f\nz: %0.6f\n%0.6f0", world_pos[1], world_pos[2], world_pos[3], val ) + else + tt.text[] = plot[:inspector_label][](plot, (i, j), world_pos) end tt.visible[] = true @@ -1026,12 +1022,12 @@ function show_data(inspector::DataInspector, plot::Band, idx::Integer, mesh::Mes # Update tooltip update_tooltip_alignment!(inspector, mouseposition_px(inspector.root)) - if haskey(plot, :inspector_label) - tt.text[] = plot[:inspector_label][](plot, right, (P1, P2)) - else + if to_value(get(plot, :inspector_label, automatic)) == automatic P1 = apply_transform_and_model(mesh, P1, Point2f) P2 = apply_transform_and_model(mesh, P2, Point2f) tt.text[] = @sprintf("(%0.3f, %0.3f) .. (%0.3f, %0.3f)", P1[1], P1[2], P2[1], P2[2]) + else + tt.text[] = plot[:inspector_label][](plot, right, (P1, P2)) end tt.visible[] = true else diff --git a/src/interaction/ray_casting.jl b/src/interaction/ray_casting.jl index 75f52c77e18..868d0148560 100644 --- a/src/interaction/ray_casting.jl +++ b/src/interaction/ray_casting.jl @@ -2,11 +2,21 @@ ### Ray Generation ################################################################################ -struct Ray - origin::Point3f - direction::Vec3f +struct Ray{T} + origin::Point3{T} + direction::Vec3{T} + + function Ray(pos::VecTypes{3, T1}, dir::VecTypes{3, T2}) where {T1, T2} + T = promote_type(Float32, T1, T2) + return new{T}(to_ndim(Point3{T}, pos, 0), to_ndim(Vec3{T}, dir, 0)) + end +end + +function Base.convert(::Type{Ray{Float32}}, ray::Ray) + return Ray(Point3f(ray.origin), Vec3f(ray.direction)) end + """ ray_at_cursor(fig/ax/scene) @@ -96,12 +106,24 @@ function ray_from_projectionview(scene::Scene, xy::VecTypes{2}) end -function transform(M::Mat4f, ray::Ray) - p4d = M * to_ndim(Point4f, ray.origin, 1f0) +function transform(M::Mat4{T}, ray::Ray) where {T} + p4d = M * to_ndim(Point4{T}, ray.origin, 1f0) dir = normalize(M[Vec(1,2,3), Vec(1,2,3)] * ray.direction) return Ray(p4d[Vec(1,2,3)] / p4d[4], dir) end +f32_convert(::Nothing, ray::Ray) = ray +function f32_convert(ls::LinearScaling, ray::Ray) + return Ray(f32_convert(ls, ray.origin), normalize(ls.scale .* ray.direction)) +end + +inv_f32_convert(::Nothing, ray::Ray) = ray +inv_f32_convert(c::Float32Convert, ray::Ray) = inv_f32_convert(c.scaling[], ray) +function inv_f32_convert(ls::LinearScaling, ray::Ray) + ils = inv(ls) + return Ray(ils(ray.origin), normalize(ils.scale .* ray.direction)) +end + ################################################################################ ### Ray - object intersections @@ -110,9 +132,9 @@ end # These work in 2D and 3D function closest_point_on_line(A::VecTypes, B::VecTypes, ray::Ray) - return closest_point_on_line(to_ndim(Point3f, A, 0), to_ndim(Point3f, B, 0), ray) + return closest_point_on_line(to_ndim(Point3d, A, 0), to_ndim(Point3d, B, 0), ray) end -function closest_point_on_line(A::Point3f, B::Point3f, ray::Ray) +function closest_point_on_line(A::Point3, B::Point3, ray::Ray) # See: # https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection AB_norm = norm(B .- A) @@ -126,12 +148,15 @@ end function ray_triangle_intersection(A::VecTypes, B::VecTypes, C::VecTypes, ray::Ray, ϵ = 1e-6) return ray_triangle_intersection( - to_ndim(Point3f, A, 0f0), to_ndim(Point3f, B, 0f0), to_ndim(Point3f, C, 0f0), + to_ndim(Point3d, A, 0), to_ndim(Point3d, B, 0), to_ndim(Point3d, C, 0), ray, ϵ ) end -function ray_triangle_intersection(A::VecTypes{3}, B::VecTypes{3}, C::VecTypes{3}, ray::Ray, ϵ = 1e-6) +function ray_triangle_intersection( + A::VecTypes{3, T1}, B::VecTypes{3, T2}, C::VecTypes{3, T3}, ray::Ray{T4}, ϵ = 1e-6 + ) where {T1, T2, T3, T4} + T = promote_type(T1, T2, T3, T4, Float32) # See: https://www.iue.tuwien.ac.at/phd/ertl/node114.html # Alternative: https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm AO = A .- ray.origin @@ -143,13 +168,13 @@ function ray_triangle_intersection(A::VecTypes{3}, B::VecTypes{3}, C::VecTypes{3 # all positive or all negative if (A1 > -ϵ && A2 > -ϵ && A3 > -ϵ) || (A1 < ϵ && A2 < ϵ && A3 < ϵ) - return Point3f((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) + return Point3{T}((A1 * A .+ A2 * B .+ A3 * C) / (A1 + A2 + A3)) else - return Point3f(NaN) + return Point3{T}(NaN) end end -function ray_rect_intersection(rect::Rect2f, ray::Ray) +function ray_rect_intersection(rect::Rect2, ray::Ray) possible_hit = ray.origin - ray.origin[3] / ray.direction[3] * ray.direction min = minimum(rect); max = maximum(rect) if all(min <= possible_hit[Vec(1,2)] <= max) @@ -159,7 +184,7 @@ function ray_rect_intersection(rect::Rect2f, ray::Ray) end -function ray_rect_intersection(rect::Rect3f, ray::Ray) +function ray_rect_intersection(rect::Rect3, ray::Ray) mins = (minimum(rect) - ray.origin) ./ ray.direction maxs = (maximum(rect) - ray.origin) ./ ray.direction x, y, z = min.(mins, maxs) @@ -170,9 +195,15 @@ function ray_rect_intersection(rect::Rect3f, ray::Ray) return Point3f(NaN) end -function is_point_on_ray(p::Point3f, ray::Ray) +function is_point_on_ray(p::Point3{T1}, ray::Ray{T2}) where {T1 <: Real, T2 <: Real} diff = ray.origin - p - return abs(dot(diff, ray.direction)) ≈ abs(norm(diff)) + return isapprox( + abs(dot(diff, ray.direction)), + abs(norm(diff)), + # use lower eps of the input types so we don't have to bother converting + # Float64 Rays to Float32 + rtol = sqrt(max(eps(T1), eps(T2))) + ) end @@ -241,11 +272,11 @@ end function position_on_plot(plot::Union{Scatter, MeshScatter}, idx, ray::Ray; apply_transform = true) point = plot[1][][idx] - point3f = to_ndim(Point3f, point, 0.0f0) - point_t = if apply_transform && !isnan(point3f) - apply_transform_and_model(plot, point3f) + point3 = to_ndim(Point3d, point, 0) + point_t = if apply_transform && !isnan(point3) + apply_transform_and_model(plot, point3) else - point3f + point3 end return to_ndim(typeof(point), point_t, 0.0f0) end @@ -256,12 +287,12 @@ function position_on_plot(plot::Union{Lines, LineSegments}, idx, ray::Ray; apply end p0, p1 = apply_transform_and_model(plot, plot[1][][(idx-1):idx]) - pos = closest_point_on_line(p0, p1, ray) + pos = closest_point_on_line(f32_convert(plot, p0), f32_convert(plot, p1), ray) if apply_transform - return pos + return inv_f32_convert(plot, Point3d(pos)) else - p4d = inv(plot.model[]) * to_ndim(Point4f, pos, 1f0) + p4d = inv(plot.model[]) * to_ndim(Point4d, inv_f32_convert(plot, pos), 1) p3d = p4d[Vec(1, 2, 3)] / p4d[4] itf = inverse_transform(transform_func(plot)) return Makie.apply_transform(itf, p3d, get(plot, :space, :data)) @@ -274,24 +305,24 @@ function position_on_plot(plot::Union{Heatmap, Image}, idx, ray::Ray; apply_tran # model matrix may add a z component to the Rect2f, which we can't represent. # So we instead inverse-transform the ray space = to_value(get(plot, :space, :data)) - p0, p1 = map(Point2f.(extrema(plot.x[]), extrema(plot.y[]))) do p + p0, p1 = map(Point2d.(extrema(plot.x[]), extrema(plot.y[]))) do p return Makie.apply_transform(transform_func(plot), p, space) end - ray = transform(inv(plot.model[]), ray) - pos = ray_rect_intersection(Rect2f(p0, p1 - p0), ray) + ray = transform(inv(plot.model[]), inv_f32_convert(plot, ray)) + pos = ray_rect_intersection(Rect2(p0, p1 - p0), ray) if apply_transform - p4d = plot.model[] * to_ndim(Point4f, to_ndim(Point3f, pos, 0), 1) + p4d = plot.model[] * to_ndim(Point4d, to_ndim(Point3d, pos, 0), 1) return p4d[Vec(1, 2, 3)] / p4d[4] else pos = Makie.apply_transform(inverse_transform(transform_func(plot)), pos, space) - return to_ndim(Point3f, pos, 0) + return to_ndim(Point3d, pos, 0) end end function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) positions = coordinates(plot.mesh[]) - ray = transform(inv(plot.model[]), ray) + ray = transform(inv(plot.model[]), inv_f32_convert(plot, ray)) tf = transform_func(plot) space = to_value(get(plot, :space, :data)) @@ -300,10 +331,10 @@ function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) p1, p2, p3 = positions[f] p1, p2, p3 = Makie.apply_transform.(tf, (p1, p2, p3), space) pos = ray_triangle_intersection(p1, p2, p3, ray) - if pos !== Point3f(NaN) + if !isnan(pos) if apply_transform - p4d = plot.model[] * to_ndim(Point4f, pos, 1) - return Point3f(p4d) / p4d[4] + p4d = plot.model[] * to_ndim(Point4d, pos, 1) + return Point3d(p4d) / p4d[4] else return Makie.apply_transform(inverse_transform(tf), pos, space) end @@ -313,7 +344,7 @@ function position_on_plot(plot::Mesh, idx, ray::Ray; apply_transform = true) @debug "Did not find intersection for index = $idx when casting a ray on mesh." - return Point3f(NaN) + return Point3d(NaN) end # Handling indexing into different surface input types @@ -327,7 +358,7 @@ surface_y(ys::AbstractMatrix, i, j, N) = ys[i, j] function surface_pos(xs, ys, zs, i, j) N, M = size(zs) - return Point3f(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) + return Point3d(surface_x(xs, i, j, N), surface_y(ys, i, j, M), zs[i, j]) end function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) @@ -337,7 +368,7 @@ function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) w, h = size(zs) _i = mod1(idx, w); _j = div(idx-1, w) - ray = transform(inv(plot.model[]), ray) + ray = transform(inv(plot.model[]), inv_f32_convert(plot, ray)) tf = transform_func(plot) space = to_value(get(plot, :space, :data)) @@ -352,8 +383,8 @@ function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) B = surface_pos(xs, ys, zs, i-1, j) C = surface_pos(xs, ys, zs, i, j+1) A, B, C = map((A, B, C)) do p - xy = Makie.apply_transform(tf, Point2f(p), space) - Point3f(xy[1], xy[2], p[3]) + xy = Makie.apply_transform(tf, Point2d(p), space) + Point3d(xy[1], xy[2], p[3]) end pos = ray_triangle_intersection(A, B, C, ray) end @@ -363,8 +394,8 @@ function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) B = surface_pos(xs, ys, zs, i, j+1) C = surface_pos(xs, ys, zs, i+1, j+1) A, B, C = map((A, B, C)) do p - xy = Makie.apply_transform(tf, Point2f(p), space) - Point3f(xy[1], xy[2], p[3]) + xy = Makie.apply_transform(tf, Point2d(p), space) + Point3d(xy[1], xy[2], p[3]) end pos = ray_triangle_intersection(A, B, C, ray) end @@ -373,29 +404,29 @@ function position_on_plot(plot::Surface, idx, ray::Ray; apply_transform = true) end if apply_transform - p4d = plot.model[] * to_ndim(Point4f, pos, 1) + p4d = plot.model[] * to_ndim(Point4d, pos, 1) return p4d[Vec(1, 2, 3)] / p4d[4] else - xy = Makie.apply_transform(inverse_transform(tf), Point2f(pos), space) - return Point3f(xy[1], xy[2], pos[3]) + xy = Makie.apply_transform(inverse_transform(tf), Point2d(pos), space) + return Point3d(xy[1], xy[2], pos[3]) end end function position_on_plot(plot::Volume, idx, ray::Ray; apply_transform = true) - min, max = Point3f.(extrema(plot.x[]), extrema(plot.y[]), extrema(plot.z[])) + min, max = Point3d.(extrema(plot.x[]), extrema(plot.y[]), extrema(plot.z[])) if apply_transform min = apply_transform_and_model(plot, min) max = apply_transform_and_model(plot, max) - return ray_rect_intersection(Rect3f(min, max .- min), ray) + return ray_rect_intersection(Rect3(min, max .- min), ray) else min = Makie.apply_transform(transform_func(plot), min, get(plot, :space, :data)) max = Makie.apply_transform(transform_func(plot), max, get(plot, :space, :data)) ray = transform(inv(plot.model[]), ray) - pos = ray_rect_intersection(Rect3f(min, max .- min), ray) + pos = ray_rect_intersection(Rect3(min, max .- min), ray) return Makie.apply_transform(inverse_transform(plot), pos, get(plot, :space, :data)) end end -position_on_plot(plot::Text, args...; kwargs...) = Point3f(NaN) -position_on_plot(plot::Nothing, args...; kwargs...) = Point3f(NaN) +position_on_plot(plot::Text, args...; kwargs...) = Point3d(NaN) +position_on_plot(plot::Nothing, args...; kwargs...) = Point3d(NaN) diff --git a/src/interfaces.jl b/src/interfaces.jl index bcd154eabf9..13dfeeac88e 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -101,7 +101,7 @@ end const atomic_functions = ( text, meshscatter, scatter, mesh, linesegments, - lines, surface, volume, heatmap, image + lines, surface, volume, heatmap, image, voxels ) const Atomic{Arg} = Union{map(x-> Plot{x, Arg}, atomic_functions)...} @@ -138,7 +138,7 @@ function Plot{Func}(args::Tuple, plot_attributes::Dict) where {Func} if used_attrs === () args_converted = convert_arguments(P, map(to_value, args)...) else - kw = [Pair(k, to_value(v)) for (k, v) in plot_attributes if k in used_attrs] + kw = [Pair(k, to_value(pop!(plot_attributes, k))) for k in used_attrs if haskey(plot_attributes, k)] args_converted = convert_arguments(P, map(to_value, args)...; kw...) end preconvert_attr = Attributes() diff --git a/src/layouting/boundingbox.jl b/src/layouting/boundingbox.jl index 0a9226d16b1..1fbd1a23781 100644 --- a/src/layouting/boundingbox.jl +++ b/src/layouting/boundingbox.jl @@ -1,163 +1,138 @@ -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 +# TODO: differentiate input space (plot.converted) and :world space (after +# transform_func and model) more clearly. +""" + boundingbox(scenelike[, exclude = plot -> false]) -function rotate_bbox(bb::Rect3f, rot) - points = decompose(Point3f, bb) - Rect3f(Ref(rot) .* points) -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 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 - ) +See also: [`data_limits`](@ref) +""" +function boundingbox(scenelike, exclude::Function = (p)-> false, space::Symbol = :data) + bb_ref = Base.RefValue(Rect3d()) + foreach_plot(scenelike) do plot + if !exclude(plot) + update_boundingbox!(bb_ref, boundingbox(plot, space)) + end end + return bb_ref[] 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 Rect2f((l, b), (w, h - b)) -end - -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)) -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) +""" +function boundingbox(plot::AbstractPlot, space::Symbol = :data) + # 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(boundingbox(plot.plots[1], space)) + for i in 2:length(plot.plots) + update_boundingbox!(bb_ref, boundingbox(plot.plots[i], space)) 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) - 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(x::Text{<:Tuple{<:AbstractArray{<:GlyphCollection}}}) - if x.space[] == x.markerspace[] - pos = to_ndim.(Point3f, x.position[], 0) +# same as data_limits except using iterate_transformed +function boundingbox(plot::MeshScatter, space::Symbol = :data) + # TODO: avoid mesh generation here if possible + @get_attribute plot (marker, markersize, rotation) + marker_bb = Rect3d(marker) + positions = iterate_transformed(plot) + scales = markersize + # fast path for constant markersize + if scales isa VecTypes{3} && rotation isa Quaternion + bb = Rect3d(positions) + marker_bb = rotation * (marker_bb * scales) + return Rect3d(minimum(bb) + minimum(marker_bb), widths(bb) + widths(marker_bb)) 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: optimize const scale, var rot and var scale, const rot + return limits_with_marker_transforms(positions, scales, rotation, marker_bb) end - return boundingbox(x[1][], pos, to_rotation(x.rotation[])) 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) +function boundingbox(plot::Scatter) + if plot.space[] == plot.markerspace[] + scale, offset = marker_attributes( + get_texture_atlas(), + plot.marker[], + plot.markersize[], + get(plot.attributes, :font, Observable(Makie.defaultfont())), + plot.marker_offset[], + plot + ) + rotations = convert_attribute(to_value(get(plot, :rotation, 0)), key"rotation"()) + model = plot.model[] + model33 = model[Vec(1,2,3), Vec(1,2,3)] + transform_marker = to_value(get(plot, :transform_marker, false))::Bool + + bb = Rect3d() + for (i, p) in enumerate(point_iterator(plot)) + marker_pos = apply_transform_and_model(plot, p) + quad_origin = to_ndim(Vec3d, sv_getindex(offset[], i), 0) + quad_size = Vec2d(sv_getindex(scale[], i)) + quad_rotation = sv_getindex(rotations, i) + + if transform_marker + p4d = model * to_ndim(Point4d, quad_origin, 1) + quad_origin = quad_rotation * p4d[Vec(1,2,3)] / p4d[4] + quad_v1 = quad_rotation * (model33 * Vec3d(quad_size[1], 0, 0)) + quad_v2 = quad_rotation * (model33 * Vec3d(0, quad_size[2], 0)) + else + quad_origin = quad_rotation * quad_origin + quad_v1 = quad_rotation * Vec3d(quad_size[1], 0, 0) + quad_v2 = quad_rotation * Vec3d(0, quad_size[2], 0) + end + + bb = update_boundingbox(bb, marker_pos + quad_origin) + bb = update_boundingbox(bb, marker_pos + quad_origin + quad_v1) + bb = update_boundingbox(bb, marker_pos + quad_origin + quad_v2) + bb = update_boundingbox(bb, marker_pos + quad_origin + quad_v1 + quad_v2) end + return bb + + else + return Rect3d(iterate_transformed(plot)) 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 +################################################################################ + + +@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..805ea6fa74c 100644 --- a/src/layouting/data_limits.jl +++ b/src/layouting/data_limits.jl @@ -1,268 +1,279 @@ -_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::Function = (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) +function data_limits(plot::Text) + if plot.space[] == plot.markerspace[] + return string_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]) +function data_limits(plot::Scatter) + if plot.space[] == plot.markerspace[] + scale, offset = marker_attributes( + get_texture_atlas(), + plot.marker[], + plot.markersize[], + get(plot.attributes, :font, Observable(Makie.defaultfont())), + plot.marker_offset[], + plot + ) + rotations = convert_attribute(to_value(get(plot, :rotation, 0)), key"rotation"()) + + bb = Rect3d() + for (i, p) in enumerate(point_iterator(plot)) + marker_pos = to_ndim(Point3d, p, 0) + quad_origin = to_ndim(Vec3d, sv_getindex(offset[], i), 0) + quad_size = Vec2d(sv_getindex(scale[], i)) + quad_rotation = sv_getindex(rotations, i) + + quad_origin = quad_rotation * quad_origin + quad_v1 = quad_rotation * Vec3d(quad_size[1], 0, 0) + quad_v2 = quad_rotation * Vec3d(0, quad_size[2], 0) + + bb = update_boundingbox(bb, marker_pos + quad_origin) + bb = update_boundingbox(bb, marker_pos + quad_origin + quad_v1) + bb = update_boundingbox(bb, marker_pos + quad_origin + quad_v2) + bb = update_boundingbox(bb, marker_pos + quad_origin + quad_v1 + quad_v2) + end + return bb + else + return Rect3d(point_iterator(plot)) + end end -point_iterator(mesh::GeometryBasics.Mesh) = decompose(Point, mesh) +function data_limits(plot::Voxels) + xyz = to_value.(plot.converted[1:3]) + return Rect3d(minimum.(xyz), maximum.(xyz) .- minimum.(xyz)) +end -function point_iterator(list::AbstractVector) - if length(list) == 1 - # save a copy! - return point_iterator(list[1]) +# includes markersize and rotation +function data_limits(plot::MeshScatter) + # TODO: avoid mesh generation here if possible + @get_attribute plot (marker, markersize, rotation) + marker_bb = Rect3d(marker) + positions = point_iterator(plot) + scales = markersize + # fast path for constant markersize + if scales isa VecTypes{3} && rotation isa Quaternion + bb = Rect3d(positions) + marker_bb = rotation * (marker_bb * scales) + return Rect3d(minimum(bb) + minimum(marker_bb), widths(bb) + widths(marker_bb)) else - points = Point3f[] - for elem in list - for point in point_iterator(elem) - push!(points, to_ndim(Point3f, point, 0)) - end - end - return points + # TODO: optimize const scale, var rot and var scale, const rot + return limits_with_marker_transforms(positions, scales, rotation, marker_bb) end end -point_iterator(plot::Mesh) = point_iterator(plot.mesh[]) +# include bbox from scaled markers +function limits_with_marker_transforms(positions, scales, rotation, element_bbox) + isempty(positions) && return Rect3d() -function br_getindex(vector::AbstractVector, idx::CartesianIndex, dim::Int) - return vector[Tuple(idx)[dim]] -end + first_scale = attr_broadcast_getindex(scales, 1) + first_rot = attr_broadcast_getindex(rotation, 1) + full_bbox = Ref(first_rot * (element_bbox * first_scale) + to_ndim(Point3d, first(positions), 0)) + for (i, pos) in enumerate(positions) + scale, rot = attr_broadcast_getindex(scales, i), attr_broadcast_getindex(rotation, i) + transformed_bbox = rot * (element_bbox * scale) + to_ndim(Point3d, pos, 0) + update_boundingbox!(full_bbox, transformed_bbox) + end -function br_getindex(matrix::AbstractMatrix, idx::CartesianIndex, dim::Int) - return matrix[idx] + return full_bbox[] end -function point_iterator(plot::Union{Image, Heatmap, Surface}) - rect = data_limits(plot) - return unique(decompose(Point3f, rect)) -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)) -end +################################################################################ +### point_iterator +################################################################################ -function foreach_plot(f, s::Scene) - foreach_plot(f, s.plots) - foreach(sub-> foreach_plot(f, sub), s.children) -end -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 +function point_iterator(plot::Union{Scatter, MeshScatter, Lines, LineSegments}) + return plot.positions[] end -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 +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_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 +point_iterator(mesh::GeometryBasics.Mesh) = decompose(Point, mesh) +point_iterator(plot::Mesh) = point_iterator(plot.mesh[]) -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) -end +# Fallback for other primitive plots, used in boundingbox +point_iterator(plot::AbstractPlot) = point_iterator(data_limits(plot)) -function iterate_transformed(points, model, space, trans_func) - (to_ndim(Point3f, project(model, apply_transform(trans_func, point, space)), 0f0) for point in points) -end +# For generic usage +point_iterator(bbox::Rect) = unique(decompose(Point3d, bbox)) -function update_boundingbox!(bb_ref, point) - if all(isfinite, point) - vec = to_ndim(Vec3f, point, 0.0) - bb_ref[] = update(bb_ref[], vec) - end -end -function update_boundingbox!(bb_ref, bb::Rect) - # ref is uninitialized, so just set it to the first bb - if !isfinite_rect(bb_ref[]) - bb_ref[] = bb - return +################################################################################ +### Utilities +################################################################################ + + +isfinite_rect(x::Rect) = all(isfinite, x.origin) && all(isfinite, x.widths) +function isfinite_rect(x::Rect{N}, dim::Int) where N + if 0 < dim <= N + return isfinite(origin(x)[dim]) && isfinite(widths(x)[dim]) + else + return false end - # don't update if not finite - !isfinite_rect(bb) && return - # ok, update! - bb_ref[] = union(bb_ref[], bb) - return end +_isfinite(x) = isfinite(x) +_isfinite(x::VecTypes) = all(isfinite, x) -# Default data_limits -function data_limits(plot::AbstractPlot) - # Assume primitive plot - if isempty(plot.plots) - return limits_from_transformed_points(iterate_transformed(plot)) - end +finite_min(a, b) = isfinite(a) ? (isfinite(b) ? min(a, b) : a) : (isfinite(b) ? b : a) +finite_min(a, b, c) = finite_min(finite_min(a, b), c) +finite_min(a, b, rest...) = finite_min(finite_min(a, b), rest...) - # 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 +finite_max(a, b) = isfinite(a) ? (isfinite(b) ? max(a, b) : a) : (isfinite(b) ? b : a) +finite_max(a, b, c) = finite_max(finite_max(a, b), c) +finite_max(a, b, rest...) = finite_max(finite_max(a, b), rest...) - return bb_ref[] -end +finite_minmax(a, b) = isfinite(a) ? (isfinite(b) ? minmax(a, b) : (a, a)) : (isfinite(b) ? (b, b) : (a, b)) +finite_minmax(a, b, c) = finite_minmax(finite_minmax(a, b), c) +finite_minmax(a, b, rest...) = finite_minmax(finite_minmax(a, b), rest...) -function _update_rect(rect::Rect{N, T}, point::VecTypes{N, T}) where {N, T} - mi = minimum(rect) - ma = maximum(rect) - mis_mas = map(mi, ma, point) do _mi, _ma, _p - (isnan(_mi) ? _p : _p < _mi ? _p : _mi), (isnan(_ma) ? _p : _p > _ma ? _p : _ma) +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 - new_o = map(first, mis_mas) - new_w = map(mis_mas) do (mi, ma) - ma - mi + while vs !== nothing + x, s = vs + vs = iterate(itr, s) + _isfinite(x) || continue + vmax = scalarmax(x, vmax) + vmin = scalarmin(x, vmin) end - typeof(rect)(new_o, new_w) + return (vmin, vmax) 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 +# 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 -# 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[] +# TODO: Consider deprecating Ref versions (performance is the same) +function update_boundingbox!(bb_ref::Base.RefValue, point) + bb_ref[] = update_boundingbox(bb_ref[], point) 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[] +function update_boundingbox(bb::Rect{N, T1}, point::VecTypes{M, T2}) where {N, T1, M, T2} + p = to_ndim(Vec{N, promote_type(T1, T2)}, point, 0.0) + mini = finite_min.(minimum(bb), p) + maxi = finite_max.(maximum(bb), p) + return Rect{N}(mini, maxi - mini) 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) +function update_boundingbox!(bb_ref::Base.RefValue, bb::Rect) + bb_ref[] = update_boundingbox(bb_ref[], bb) 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) +function update_boundingbox(a::Rect{N}, b::Rect{N}) where N + mini = finite_min.(minimum(a), minimum(b)) + maxi = finite_max.(maximum(a), maximum(b)) + return Rect{N}(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 +@deprecate _update_rect(rect, point) update_boundingbox(rect, point) false -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/maybe_unused.jl b/src/layouting/maybe_unused.jl new file mode 100644 index 00000000000..60e9768cd17 --- /dev/null +++ b/src/layouting/maybe_unused.jl @@ -0,0 +1,110 @@ +################################################################################ +### 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 + +_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..a4bc14dd588 --- /dev/null +++ b/src/layouting/text_boundingbox.jl @@ -0,0 +1,156 @@ +@deprecate boundingbox(plot::Text) boundingbox(plot, plot.markerspace[]) + +function boundingbox(plot::Text, target_space::Symbol) + # TODO: + # This is temporary prep work for the future. We should actually consider + # plot.space, markerspace, textsize, etc when computing the boundingbox in + # the target_space given to the function. + # We may also want a cheap version that only considers forward + # transformations (i.e. drops textsize etc when markerspace is not part of + # the plot.space -> target_space conversion chain) + if target_space == :data + if plot.space[] == plot.markerspace[] + # probably shouldn't transform... + return transform_bbox(plot, string_boundingbox(plot)) + else + return Rect3d(iterate_transformed(plot)) + end + elseif target_space == plot.markerspace[] + return string_boundingbox(plot) + else + error("`target_space = :$target_space` must be either :data or markerspace = :$(plot.markerspace[])") + end +end + + +# TODO: Naming: not px, it's whatever markerspace is... +function string_boundingbox(plot::Text) + bb = Rect3d() + for p in plot.plots + _bb = string_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 +string_boundingbox(x::LineSegments) = data_limits(x) + +function string_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 string_boundingbox(x[1][], pos, to_rotation(x.rotation[])) +end + +function string_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 string_boundingbox(x[1][], pos, to_rotation(x.rotation[])) +end + +function string_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 string_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) + glyphbb3 = Rect3d(to_ndim(Point3d, origin(glyphbb), 0), to_ndim(Point3d, widths(glyphbb), 0)) + charbb = rotate_bbox(glyphbb3, 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 = string_boundingbox(layout, pos, rot) + else + bb = union(bb, string_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 + +# tested but not used? +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 + +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/layouting.jl b/src/layouting/text_layouting.jl similarity index 98% rename from src/layouting/layouting.jl rename to src/layouting/text_layouting.jl index 4765bbc7cad..93f2bf9b42e 100644 --- a/src/layouting/layouting.jl +++ b/src/layouting/text_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/transformation.jl b/src/layouting/transformation.jl index 409328f6308..29a9f5cffad 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,29 +183,33 @@ 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 to_ndim(output_type, transformed, NaN) + end end -function apply_transform_and_model(model::Mat4f, f, positions::Vector, space = :data, output_type = Point3f) - return map(positions) do pos +function apply_transform_and_model(model::Mat4, f, positions::AbstractArray, space = :data, output_type = Point3d) + output = similar(positions, output_type) + return map!(output, positions) do pos apply_transform_and_model(model, f, pos, space, output_type) end end @@ -256,14 +265,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 +280,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 +296,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 +398,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 +439,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 +473,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.jl b/src/makielayout/blocks.jl index c6bf0bdb73f..a4646f5af7c 100644 --- a/src/makielayout/blocks.jl +++ b/src/makielayout/blocks.jl @@ -137,7 +137,7 @@ end function make_attr_dict_expr(attrs, sceneattrsym, curthemesym) - pairs = map(attrs) do a + exprs = map(attrs) do a d = a.default if d isa Expr && d.head === :macrocall && d.args[1] == Symbol("@inherit") @@ -162,10 +162,14 @@ function make_attr_dict_expr(attrs, sceneattrsym, curthemesym) end end - Expr(:call, :(=>), QuoteNode(a.symbol), d) + :(d[$(QuoteNode(a.symbol))] = $d) end - :(Dict($(pairs...))) + quote + d = Dict{Symbol,Any}() + $(exprs...) + d + end end @@ -205,44 +209,14 @@ function extract_attributes!(body) args = filter(x -> !(x isa LineNumberNode), attrs_block.args) - function extract_attr(arg) - has_docs = arg isa Expr && arg.head === :macrocall && arg.args[1] isa GlobalRef - - if has_docs - docs = arg.args[3] - attr = arg.args[4] - else - docs = nothing - attr = arg - end - - if !(attr isa Expr && attr.head === :(=) && length(attr.args) == 2) - error("$attr is not a valid attribute line like :x[::Type] = default_value") - end - left = attr.args[1] - default = attr.args[2] - if left isa Symbol - attr_symbol = left - type = Any - else - if !(left isa Expr && left.head === :(::) && length(left.args) == 2) - error("$left is not a Symbol or an expression such as x::Type") - end - attr_symbol = left.args[1]::Symbol - type = left.args[2] - end - - (docs = docs, symbol = attr_symbol, type = type, default = default) - end - - attrs = map(extract_attr, args) + attrs::Vector{Any} = map(MakieCore.extract_attribute_metadata, args) - lras = map(extract_attr, layout_related_attributes) + lras = map(MakieCore.extract_attribute_metadata, layout_related_attributes) for lra in lras i = findfirst(x -> x.symbol == lra.symbol, attrs) if i === nothing - push!(attrs, extract_attr(lra)) + push!(attrs, lra) end end diff --git a/src/makielayout/blocks/axis.jl b/src/makielayout/blocks/axis.jl index e43c2d6f74c..c31683776f3 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 + boundingbox(subtitlet, :data).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 = boundingbox(titlet, :data).widths[2] + titlegap + subtitleheight = boundingbox(subtitlet, :data).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 @@ -581,7 +588,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 @@ -597,7 +604,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 @@ -839,10 +846,31 @@ 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) + # Limits can be one dimensional (partially NaN) e.g. for hlines + # which results in every model * point to become NaN. For now we skip + # model application if the model matrix is identity to avoid this... + model = plot.model[][Vec(1,2,3), Vec(1,2,3)] + if !(model ≈ I) + bb = Rect3d(map(p -> model * to_ndim(Point3d, p, 0), coordinates(bb))) + end + 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 + isfinite_rect(boundingbox, dim) || return nothing # otherwise start with the first box mini, maxi = minimum(boundingbox), maximum(boundingbox) @@ -1230,7 +1258,6 @@ function Makie.xlims!(ax::Axis, xlims) ax.xreversed[] = false end mlims = convert_limit_attribute(ax.limits[]) - ax.limits.val = (xlims, mlims[2]) reset_limits!(ax, yauto = false) nothing @@ -1248,7 +1275,6 @@ function Makie.ylims!(ax::Axis, ylims) ax.yreversed[] = false end mlims = convert_limit_attribute(ax.limits[]) - ax.limits.val = (mlims[1], ylims) reset_limits!(ax, xauto = false) nothing @@ -1384,15 +1410,15 @@ 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) - BBox(xl..., yl...) + xl = Float64.(defaultlimits(userlimits[1], xscale)) + yl = Float64.(defaultlimits(userlimits[2], yscale)) + return BBox(xl..., yl...) end defaultlimits(limits::Nothing, scale) = defaultlimits(scale) diff --git a/src/makielayout/blocks/button.jl b/src/makielayout/blocks/button.jl index c52d71e7426..9fd77fefa41 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(boundingbox(labeltext, :data)) 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/colorbar.jl b/src/makielayout/blocks/colorbar.jl index 9032a15a888..20887a36605 100644 --- a/src/makielayout/blocks/colorbar.jl +++ b/src/makielayout/blocks/colorbar.jl @@ -75,6 +75,17 @@ function extract_colormap(plot::Union{Contourf,Tricontourf}) elow, ehigh, plot.nan_color) end +function extract_colormap(plot::Voxels) + limits = plot._limits + # TODO: does this need padding for lowclip and highclip? + discretized_values = map(lims -> range(lims[1], lims[2], length = 253), plot, limits) + + return ColorMapping( + discretized_values[], discretized_values, plot.colormap, limits, plot.colorscale, + plot.alpha, plot.lowclip, plot.highclip, Observable(:transparent) + ) +end + function extract_colormap_recursive(@nospecialize(plot::T)) where {T <: AbstractPlot} cmap = extract_colormap(plot) diff --git a/src/makielayout/blocks/label.jl b/src/makielayout/blocks/label.jl index adc7de7fbc7..dabb36cb830 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(boundingbox(t, :data)) 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..ceba42d36db 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 = boundingbox(selectiontext, :data) 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 -> string_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..44101b0aab4 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 = string_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 = string_boundingbox(gc, Quaternionf(0, 0, 0, 1)) # no rotation max_widths = max.(max_widths, widths(bbox)[Vec(1,2)]) end @@ -186,22 +186,22 @@ function polaraxis_bbox(rlims, thetalims, r0, dir, theta_0) # Initial bbox from corners p = polar2cartesian(rmin, thetamin) bb = Rect2f(p, Vec2f(0)) - bb = _update_rect(bb, polar2cartesian(rmax, thetamin)) - bb = _update_rect(bb, polar2cartesian(rmin, thetamax)) - bb = _update_rect(bb, polar2cartesian(rmax, thetamax)) + bb = update_boundingbox(bb, polar2cartesian(rmax, thetamin)) + bb = update_boundingbox(bb, polar2cartesian(rmin, thetamax)) + bb = update_boundingbox(bb, polar2cartesian(rmax, thetamax)) # only outer circle can update bb if thetamin < -3pi/2 < thetamax || thetamin < pi/2 < thetamax - bb = _update_rect(bb, polar2cartesian(rmax, pi/2)) + bb = update_boundingbox(bb, polar2cartesian(rmax, pi/2)) end if thetamin < -pi < thetamax || thetamin < pi < thetamax - bb = _update_rect(bb, polar2cartesian(rmax, pi)) + bb = update_boundingbox(bb, polar2cartesian(rmax, pi)) end if thetamin < -pi/2 < thetamax || thetamin < 3pi/2 < thetamax - bb = _update_rect(bb, polar2cartesian(rmax, 3pi/2)) + bb = update_boundingbox(bb, polar2cartesian(rmax, 3pi/2)) end if thetamin < 0 < thetamax - bb = _update_rect(bb, polar2cartesian(rmax, 0)) + bb = update_boundingbox(bb, polar2cartesian(rmax, 0)) end return bb @@ -558,8 +558,9 @@ function draw_axis!(po::PolarAxis) rtick_align = Observable{Point2f}() rtick_offset = Observable{Point2f}() rtick_rotation = Observable{Float32}() - rgridpoints = Observable{Vector{GeometryBasics.LineString}}() - rminorgridpoints = Observable{Vector{GeometryBasics.LineString}}() + LSType = typeof(GeometryBasics.LineString(Point2f[])) + rgridpoints = Observable{Vector{LSType}}() + rminorgridpoints = Observable{Vector{LSType}}() function default_rtickangle(rtickangle, direction, thetalims) if rtickangle === automatic 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 3945f0ba86e..bffb16a16f6 100644 --- a/src/makielayout/interactions.jl +++ b/src/makielayout/interactions.jl @@ -108,21 +108,21 @@ 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 function _selection_vertices(ax_scene, outer, inner) - _clamp(p, plow, phigh) = Point2f(clamp(p[1], plow[1], phigh[1]), clamp(p[2], plow[2], phigh[2])) + _clamp(p, plow, phigh) = Point2(clamp(p[1], plow[1], phigh[1]), clamp(p[2], plow[2], phigh[2])) proj(point) = project(ax_scene, point) .+ minimum(ax_scene.viewport[]) transf = Makie.transform_func(ax_scene) outer = positivize(Makie.apply_transform(transf, outer)) @@ -244,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)] .* @@ -276,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) @@ -346,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 471da012cda..c715dd10c08 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] + boundingbox(labeltext, :data).widths[horizontal[] ? 2 : 1] end labelspace::Float32 = (labelvisible && !label_is_empty) ? real_labelsize + labelpadding : 0f0 @@ -188,13 +188,13 @@ function update_tick_obs(tick_obs, horizontal::Observable{Bool}, flipped::Observ end # if labels are given manually, it's possible that some of them are outside the displayed limits -# we only check approximately because otherwise because of floating point errors, ticks can be dismissed sometimes -is_within_limits(tv, limits) = (limits[1] ≤ tv || limits[1] ≈ tv) && (tv ≤ limits[2] || tv ≈ limits[2]) +# we only check approximately because we want to keep ticks on the frame +is_within_limits(tv, limits) = (limits[1] - 100eps(limits[1]) < tv) && (tv < limits[2] + 100eps(limits[2])) 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(boundingbox(ticklabels, :data)))) : 0f0 else # width - ticklabelsvisible[] ? (ticklabels === nothing ? 0f0 : width(Rect2f(boundingbox(ticklabels)))) : 0f0 + ticklabelsvisible[] ? (ticklabels === nothing ? 0f0 : width(Rect2f(boundingbox(ticklabels, :data)))) : 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(boundingbox(labeltext, :data)) 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, limits, ticks, tickformat, attrs.scale) do (position, extents, horizontal), 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(boundingbox(tls, :data))) : 0f0 else # width - tls.visible[] ? width(Rect2f(boundingbox(tls))) : 0f0 + tls.visible[] ? width(Rect2f(boundingbox(tls, :data))) : 0f0 end la.attributes.ticklabelspace = maxwidth return Float64(maxwidth) diff --git a/src/makielayout/mousestatemachine.jl b/src/makielayout/mousestatemachine.jl index c7fe0ddadd1..b3fe3da35a7 100644 --- a/src/makielayout/mousestatemachine.jl +++ b/src/makielayout/mousestatemachine.jl @@ -55,10 +55,10 @@ Fields: struct MouseEvent type::MouseEventType t::Float64 - data::Point2f + data::Point2d px::Point2f prev_t::Float64 - prev_data::Point2f + prev_data::Point2d prev_px::Point2f end @@ -190,7 +190,7 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) dblclick_max_interval = 0.2 mouseevent = Observable{MouseEvent}( - MouseEvent(MouseEventTypes.out, 0.0, Point2f(0, 0), Point2f(0, 0), 0.0, Point2f(0, 0), Point2f(0, 0)) + MouseEvent(MouseEventTypes.out, 0.0, Point2d(0, 0), Point2f(0, 0), 0.0, Point2d(0, 0), Point2f(0, 0)) ) # initialize state variables last_mouseevent = Ref{Mouse.Action}(Mouse.release) @@ -210,7 +210,7 @@ function _addmouseevents!(scene, is_mouse_over_relevant_area, priority) consumed = false t = time() data = mouseposition(scene) - px = Makie.mouseposition_px(scene) + px = mouseposition_px(scene) mouse_inside = is_mouse_over_relevant_area() # last_mouseevent can only be up or down diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 7723db9bf9c..00f15cbec13 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/boxplot.jl b/src/stats/boxplot.jl index e6cbe36c2a0..eeb80996792 100644 --- a/src/stats/boxplot.jl +++ b/src/stats/boxplot.jl @@ -13,62 +13,58 @@ The boxplot has 3 components: median - an `errorbar` whose whiskers span `range * iqr` - points marking outliers, that is, data outside the whiskers -# Arguments +## Arguments - `x`: positions of the categories - `y`: variables within the boxes -# Keywords -- `weights`: vector of statistical weights (length of data). By default, each observation has weight `1`. -- `orientation=:vertical`: orientation of box (`:vertical` or `:horizontal`) -- `width=1`: width of the box before shrinking -- `gap=0.2`: shrinking factor, `width -> width * (1 - gap)` -- `show_notch=false`: draw the notch -- `notchwidth=0.5`: multiplier of `width` for narrowest width of notch -- `show_median=true`: show median as midline -- `range`: multiple of IQR controlling whisker length -- `whiskerwidth`: multiplier of `width` for width of T's on whiskers, or - `:match` to match `width` -- `show_outliers`: show outliers as points -- `dodge`: vector of `Integer` (length of data) of grouping variable to create multiple side-by-side boxes at the same `x` position -- `dodge_gap = 0.03`: spacing between dodged boxes """ -@recipe(BoxPlot, x, y) do scene - Theme( - weights = automatic, - color = theme(scene, :patchcolor), - colormap = theme(scene, :colormap), - colorscale=identity, - colorrange = automatic, - orientation = :vertical, - # box and dodging - width = automatic, - dodge = automatic, - n_dodge = automatic, - gap = 0.2, - dodge_gap = 0.03, - strokecolor = theme(scene, :patchstrokecolor), - strokewidth = theme(scene, :patchstrokewidth), - # notch - show_notch = false, - notchwidth = 0.5, - # median line - show_median = true, - mediancolor = theme(scene, :linecolor), - medianlinewidth = theme(scene, :linewidth), - # whiskers - range = 1.5, - whiskerwidth = 0.0, - whiskercolor = theme(scene, :linecolor), - whiskerlinewidth = theme(scene, :linewidth), - # outliers points - show_outliers = true, - marker = theme(scene, :marker), - markersize = theme(scene, :markersize), - outliercolor = automatic, - outlierstrokecolor = theme(scene, :markerstrokecolor), - outlierstrokewidth = theme(scene, :markerstrokewidth), - cycle = [:color => :patchcolor], - inspectable = theme(scene, :inspectable) - ) +@recipe BoxPlot x y begin + "Vector of statistical weights (length of data). By default, each observation has weight `1`." + weights = automatic + color = @inherit patchcolor + colormap = @inherit colormap + colorscale=identity + colorrange = automatic + "Orientation of box (`:vertical` or `:horizontal`)." + orientation = :vertical + # box and dodging + "Width of the box before shrinking." + width = automatic + "Vector of `Integer` (length of data) of grouping variable to create multiple side-by-side boxes at the same `x` position." + dodge = automatic + n_dodge = automatic + "Shrinking factor, `width -> width * (1 - gap)`." + gap = 0.2 + "Spacing between dodged boxes." + dodge_gap = 0.03 + strokecolor = @inherit patchstrokecolor + strokewidth = @inherit patchstrokewidth + # notch + "Draw the notch." + show_notch = false + "Multiplier of `width` for narrowest width of notch." + notchwidth = 0.5 + # median line + "Show median as midline." + show_median = true + mediancolor = @inherit linecolor + medianlinewidth = @inherit linewidth + # whiskers + "Multiple of IQR controlling whisker length." + range = 1.5 + "Multiplier of `width` for width of T's on whiskers, or `:match` to match `width`." + whiskerwidth = 0.0 + whiskercolor = @inherit linecolor + whiskerlinewidth = @inherit linewidth + # outliers points + "Show outliers as points." + show_outliers = true + marker = @inherit marker + markersize = @inherit markersize + outliercolor = automatic + outlierstrokecolor = @inherit markerstrokecolor + outlierstrokewidth = @inherit markerstrokewidth + cycle = [:color => :patchcolor] + inspectable = @inherit inspectable end conversion_trait(x::Type{<:BoxPlot}) = SampleBased() diff --git a/src/stats/crossbar.jl b/src/stats/crossbar.jl index 5fb77c29f0f..bd60b5abde4 100644 --- a/src/stats/crossbar.jl +++ b/src/stats/crossbar.jl @@ -6,49 +6,45 @@ The StatMakie.jl package is licensed under the MIT "Expat" License: crossbar(x, y, ymin, ymax; kwargs...) Draw a crossbar. A crossbar represents a range with a (potentially notched) box. It is most commonly used as part of the `boxplot`. -# Arguments +## Arguments - `x`: position of the box - `y`: position of the midline within the box - `ymin`: lower limit of the box - `ymax`: upper limit of the box -# Keywords -- `orientation=:vertical`: orientation of box (`:vertical` or `:horizontal`) -- `width=1`: width of the box before shrinking -- `gap=0.2`: shrinking factor, `width -> width * (1 - gap)` -- `show_notch=false`: draw the notch -- `notchmin=automatic`: lower limit of the notch -- `notchmax=automatic`: upper limit of the notch -- `notchwidth=0.5`: multiplier of `width` for narrowest width of notch -- `show_midline=true`: show midline """ -@recipe(CrossBar, x, y, ymin, ymax) do scene - t = Theme( - color=theme(scene, :patchcolor), - colormap=theme(scene, :colormap), - colorscale=identity, - colorrange=automatic, - orientation=:vertical, +@recipe CrossBar x y ymin ymax begin + color= @inherit patchcolor + colormap= @inherit colormap + colorscale=identity + colorrange=automatic + "Orientation of box (`:vertical` or `:horizontal`)." + orientation=:vertical # box and dodging - width = automatic, - dodge = automatic, - n_dodge = automatic, - gap = 0.2, - dodge_gap = 0.03, - strokecolor = theme(scene, :patchstrokecolor), - strokewidth = theme(scene, :patchstrokewidth), + "Width of the box before shrinking." + width = automatic + dodge = automatic + n_dodge = automatic + "Shrinking factor, `width -> width * (1 - gap)`." + gap = 0.2 + dodge_gap = 0.03 + strokecolor = @inherit patchstrokecolor + strokewidth = @inherit patchstrokewidth # notch - show_notch=false, - notchmin=automatic, - notchmax=automatic, - notchwidth=0.5, + "Whether to draw the notch." + show_notch=false + "Lower limit of the notch." + notchmin=automatic + "Upper limit of the notch." + notchmax=automatic + "Multiplier of `width` for narrowest width of notch." + notchwidth=0.5 # median line - show_midline=true, - midlinecolor=automatic, - midlinewidth=theme(scene, :linewidth), - inspectable = theme(scene, :inspectable), - cycle = [:color => :patchcolor], -) - t + "Show midline." + show_midline=true + midlinecolor=automatic + midlinewidth= @inherit linewidth + inspectable = @inherit inspectable + cycle = [:color => :patchcolor] end function Makie.plot!(plot::CrossBar) diff --git a/src/stats/density.jl b/src/stats/density.jl index 82034cce2e3..36d35bb8dc1 100644 --- a/src/stats/density.jl +++ b/src/stats/density.jl @@ -17,41 +17,38 @@ function convert_arguments(P::PlotFunc, d::KernelDensity.BivariateKDE) end """ - density(values; npoints = 200, offset = 0.0, direction = :x) + density(values) Plot a kernel density estimate of `values`. -`npoints` controls the resolution of the estimate, the baseline can be -shifted with `offset` and the `direction` set to `:x` or `:y`. -`bandwidth` and `boundary` are determined automatically by default. - -Statistical weights can be provided via the `weights` keyword argument. - -`color` is usually set to a single color, but can also be set to `:x` or -`:y` to color with a gradient. If you use `:y` when `direction = :x` (or vice versa), -note that only 2-element colormaps can work correctly. - -## Attributes -$(ATTRIBUTES) """ -@recipe(Density) do scene - Theme( - color = theme(scene, :patchcolor), - colormap = theme(scene, :colormap), - colorscale = identity, - colorrange = Makie.automatic, - strokecolor = theme(scene, :patchstrokecolor), - strokewidth = theme(scene, :patchstrokewidth), - linestyle = nothing, - strokearound = false, - npoints = 200, - offset = 0.0, - direction = :x, - boundary = automatic, - bandwidth = automatic, - weights = automatic, - cycle = [:color => :patchcolor], - inspectable = theme(scene, :inspectable) - ) +@recipe Density begin + """ + Usually set to a single color, but can also be set to `:x` or + `:y` to color with a gradient. If you use `:y` when `direction = :x` (or vice versa), + note that only 2-element colormaps can work correctly. + """ + color = @inherit patchcolor + colormap = @inherit colormap + colorscale = identity + colorrange = Makie.automatic + strokecolor = @inherit patchstrokecolor + strokewidth = @inherit patchstrokewidth + linestyle = nothing + strokearound = false + "The resolution of the estimated curve along the dimension set in `direction`." + npoints = 200 + "Shift the density baseline, for layering multiple densities on top of each other." + offset = 0.0 + "The dimension along which the `values` are distributed. Can be `:x` or `:y`." + direction = :x + "Boundary of the density estimation, determined automatically if `automatic`." + boundary = automatic + "Kernel density bandwidth, determined automatically if `automatic`." + bandwidth = automatic + "Assign a vector of statistical weights to `values`." + weights = automatic + cycle = [:color => :patchcolor] + inspectable = @inherit inspectable end function plot!(plot::Density{<:Tuple{<:AbstractVector}}) diff --git a/src/stats/distributions.jl b/src/stats/distributions.jl index 6f2221119f3..0586f590800 100644 --- a/src/stats/distributions.jl +++ b/src/stats/distributions.jl @@ -44,32 +44,19 @@ Possible values are the following. Broadly speaking, `qqline = :identity` is useful to see if `x` and `y` follow the same distribution, whereas `qqline = :fit` and `qqline = :fitrobust` are useful to see if the distribution of `y` can be obtained from the distribution of `x` via an affine transformation. - -Graphical attributes are -- `color` to control color of both line and markers (if `markercolor` is not specified) -- `linestyle` -- `linewidth` -- `markercolor` -- `strokecolor` -- `strokewidth` -- `marker` -- `markersize` """ -@recipe(QQPlot) do scene - s_theme = default_theme(scene, Scatter) - l_theme = default_theme(scene, Lines) - Attributes( - color = l_theme.color, - linestyle = l_theme.linestyle, - linewidth = l_theme.linewidth, - markercolor = automatic, - markersize = s_theme.markersize, - strokecolor = s_theme.strokecolor, - strokewidth = s_theme.strokewidth, - marker = s_theme.marker, - inspectable = theme(scene, :inspectable), - cycle = [:color], - ) +@recipe QQPlot begin + "Control color of both line and markers (if `markercolor` is not specified)." + color = @inherit linecolor + linestyle = nothing + linewidth = @inherit linewidth + markercolor = automatic + markersize = @inherit markersize + strokecolor = @inherit markerstrokecolor + strokewidth = @inherit markerstrokewidth + marker = @inherit marker + MakieCore.mixin_generic_plot_attributes()... + cycle = [:color] end """ @@ -78,8 +65,8 @@ end Shorthand for `qqplot(Normal(0,1), y)`, i.e., draw a Q-Q plot of `y` against the standard normal distribution. See `qqplot` for more details. """ -@recipe(QQNorm) do scene - default_theme(scene, QQPlot) +@recipe QQNorm begin + MakieCore.documented_attributes(QQPlot)... end # Compute points and line for the qqplot diff --git a/src/stats/ecdf.jl b/src/stats/ecdf.jl index f8038456a9e..9c89e3cc5b7 100644 --- a/src/stats/ecdf.jl +++ b/src/stats/ecdf.jl @@ -44,12 +44,9 @@ Plot the empirical cumulative distribution function (ECDF) of `values`. `npoints` controls the resolution of the plot. If `weights` for the values are provided, a weighted ECDF is plotted. - -## Attributes -$(ATTRIBUTES) """ -@recipe(ECDFPlot) do scene - default_theme(scene, Stairs) +@recipe ECDFPlot begin + MakieCore.documented_attributes(Stairs)... end used_attributes(::Type{<:ECDFPlot}, ::AbstractVector) = (:npoints, :weights) diff --git a/src/stats/hexbin.jl b/src/stats/hexbin.jl index c163f29bf0a..a8f5ca9f493 100644 --- a/src/stats/hexbin.jl +++ b/src/stats/hexbin.jl @@ -2,36 +2,19 @@ hexbin(xs, ys; kwargs...) Plots a heatmap with hexagonal bins for the observations `xs` and `ys`. - -## Attributes - -### Specific to `Hexbin` - -- `weights = nothing`: Weights for each observation. Can be `nothing` (each observation carries weight 1) or any `AbstractVector{<: Real}` or `StatsBase.AbstractWeights`. -- `bins = 20`: If an `Int`, sets the number of bins in x and y direction. If a `Tuple{Int, Int}`, sets the number of bins for x and y separately. -- `cellsize = nothing`: If a `Real`, makes equally-sided hexagons with width `cellsize`. If a `Tuple{Real, Real}` specifies hexagon width and height separately. -- `threshold::Int = 1`: The minimal number of observations in the bin to be shown. If 0, all zero-count hexagons fitting into the data limits will be shown. -- `colorscale = identity`: A function to scale the number of observations in a bin, eg. log10. - -### Generic - -- `colormap::Union{Symbol, Vector{<:Colorant}} = :viridis` -- `colorrange::Tuple(<:Real,<:Real} = Makie.automatic` sets the values representing the start and end points of `colormap`. """ -@recipe(Hexbin) do scene - return Attributes(; - colormap=theme(scene, :colormap), - colorscale=identity, - colorrange=Makie.automatic, - lowclip = automatic, - highclip = automatic, - nan_color = :transparent, - bins=20, - weights=nothing, - cellsize=nothing, - threshold=1, - strokewidth=0, - strokecolor=:black) +@recipe Hexbin begin + "If an `Int`, sets the number of bins in x and y direction. If a `Tuple{Int, Int}`, sets the number of bins for x and y separately." + bins=20 + "Weights for each observation. Can be `nothing` (each observation carries weight 1) or any `AbstractVector{<: Real}` or `StatsBase.AbstractWeights`." + weights=nothing + "If a `Real`, makes equally-sided hexagons with width `cellsize`. If a `Tuple{Real, Real}` specifies hexagon width and height separately." + cellsize=nothing + "The minimal number of observations in the bin to be shown. If 0, all zero-count hexagons fitting into the data limits will be shown." + threshold=1 + strokewidth=0 + strokecolor=:black + MakieCore.mixin_colormap_attributes()... end function spacings_offsets_nbins(bins::Tuple{Int,Int}, cellsize::Nothing, xmi, xma, ymi, yma) @@ -63,16 +46,17 @@ end conversion_trait(::Type{<:Hexbin}) = PointBased() function data_limits(hb::Hexbin) - bb = Rect3f(hb.plots[1][1][]) - fn(num::Real) = Float32(num) - fn(tup::Union{Tuple,Vec2}) = Vec2f(tup...) + bb = Rect3d(hb.plots[1][1][]) + fn(num::Real) = Float64(num) + fn(tup::Union{Tuple,Vec2}) = Vec2d(tup...) - ms = 2 .* fn(hb.plots[1].markersize[]) - nw = widths(bb) .+ (ms..., 0.0f0) - no = bb.origin .- ((ms ./ 2.0f0)..., 0.0f0) + ms = 2.0 .* fn(hb.plots[1].markersize[]) + nw = widths(bb) .+ (ms..., 0.0) + no = bb.origin .- ((0.5 .* ms)..., 0.0) - return Rect3f(no, nw) + return Rect3d(no, nw) end +boundingbox(p::Hexbin, space::Symbol = :data) = transform_bbox(p, data_limits(hb)) get_weight(weights, i) = Float64(weights[i]) get_weight(::StatsBase.UnitWeights, i) = 1e0 diff --git a/src/stats/hist.jl b/src/stats/hist.jl index 1b4a5bb2196..5c9c9a0a9f8 100644 --- a/src/stats/hist.jl +++ b/src/stats/hist.jl @@ -23,34 +23,35 @@ function _hist_center_weights(values, edges, normalization, scale_to, wgts) end """ - stephist(values; bins = 15, normalization = :none) + stephist(values) -Plot a step histogram of `values`. `bins` can be an `Int` to create that -number of equal-width bins over the range of `values`. -Alternatively, it can be a sorted iterable of bin edges. The histogram -can be normalized by setting `normalization`. - -Shares most options with `hist` plotting function. - -Statistical weights can be provided via the `weights` keyword argument. - -The following attributes can move the histogram around, -which comes in handy when placing multiple histograms into one plot: -* `scale_to = nothing`: allows to scale all values to a certain height - -## Attributes -$(ATTRIBUTES) +Plot a step histogram of `values`. """ -@recipe(StepHist, values) do scene - Attributes( - bins = 15, # Int or iterable of edges - normalization = :none, - weights = automatic, - cycle = [:color => :patchcolor], - color = theme(scene, :patchcolor), - linestyle = :solid, - scale_to = nothing, - ) +@recipe StepHist values begin + "Can be an `Int` to create that number of equal-width bins over the range of `values`. Alternatively, it can be a sorted iterable of bin edges." + bins = 15 # Int or iterable of edges + """Allows to apply a normalization to the histogram. + Possible values are: + + * `:pdf`: Normalize by sum of weights and bin sizes. Resulting histogram + has norm 1 and represents a PDF. + * `:density`: Normalize by bin sizes only. Resulting histogram represents + count density of input and does not have norm 1. Will not modify the + histogram if it already represents a density (`h.isdensity == 1`). + * `:probability`: Normalize by sum of weights only. Resulting histogram + represents the fraction of probability mass for each bin and does not have + norm 1. + * `:none`: Do not normalize. + """ + normalization = :none + "Allows to provide statistical weights." + weights = automatic + cycle = [:color => :patchcolor] + color = @inherit patchcolor + linewidth = @inherit linewidth + linestyle = :solid + "Allows to scale all values to a certain height." + scale_to = nothing end function Makie.plot!(plot::StepHist) @@ -65,7 +66,7 @@ function Makie.plot!(plot::StepHist) edges = vcat(edges, phantomedge) z = zero(eltype(weights)) heights = vcat(z, weights, z) - return Point2f.(edges, heights) + return Point2.(edges, heights) end color = lift(plot, plot.color) do color if color === :values @@ -85,65 +86,62 @@ function Makie.plot!(plot::StepHist) end """ - hist(values; bins = 15, normalization = :none) - -Plot a histogram of `values`. `bins` can be an `Int` to create that -number of equal-width bins over the range of `values`. -Alternatively, it can be a sorted iterable of bin edges. The histogram -can be normalized by setting `normalization`. Possible values are: - -* `:pdf`: Normalize by sum of weights and bin sizes. Resulting histogram - has norm 1 and represents a PDF. -* `:density`: Normalize by bin sizes only. Resulting histogram represents - count density of input and does not have norm 1. Will not modify the - histogram if it already represents a density (`h.isdensity == 1`). -* `:probability`: Normalize by sum of weights only. Resulting histogram - represents the fraction of probability mass for each bin and does not have - norm 1. -* `:none`: Do not normalize. - -Statistical weights can be provided via the `weights` keyword argument. - -The following attributes can move the histogram around, -which comes in handy when placing multiple histograms into one plot: -* `offset = 0.0`: adds an offset to every value -* `fillto = 0.0`: defines where the bar starts -* `scale_to = nothing`: allows to scale all values to a certain height. This -can also be set to `:flip` to flip the direction of histogram bars without -scaling them to a common height. - -Color can either be: -* a vector of `bins` colors -* a single color -* `:values`, to color the bars with the values from the histogram - -You can also draw a histogram in x-direction rather than y-direction by setting -`direction = :x`. - -## Attributes -$(ATTRIBUTES) + hist(values) + +Plot a histogram of `values`. """ -@recipe(Hist, values) do scene - Attributes( - bins = 15, # Int or iterable of edges - normalization = :none, - weights = automatic, - cycle = [:color => :patchcolor], - color = theme(scene, :patchcolor), - offset = 0.0, - fillto = automatic, - scale_to = nothing, - - bar_labels = nothing, - flip_labels_at = Inf, - label_color = theme(scene, :textcolor), - over_background_color = automatic, - over_bar_color = automatic, - label_offset = 5, - label_font = theme(scene, :font), - label_size = 20, - label_formatter = bar_label_formatter - ) +@recipe Hist values begin + """ + Can be an `Int` to create that number of equal-width bins over the range of `values`. Alternatively, it can be a sorted iterable of bin edges. + """ + bins = 15 + """ + Allows to normalize the histogram. Possible values are: + + * `:pdf`: Normalize by sum of weights and bin sizes. Resulting histogram + has norm 1 and represents a PDF. + * `:density`: Normalize by bin sizes only. Resulting histogram represents + count density of input and does not have norm 1. Will not modify the + histogram if it already represents a density (`h.isdensity == 1`). + * `:probability`: Normalize by sum of weights only. Resulting histogram + represents the fraction of probability mass for each bin and does not have + norm 1. + * `:none`: Do not normalize. + """ + normalization = :none + "Allows to statistically weight the observations." + weights = automatic + cycle = [:color => :patchcolor] + """ + Color can either be: + * a vector of `bins` colors + * a single color + * `:values`, to color the bars with the values from the histogram + """ + color = @inherit patchcolor + strokewidth = @inherit patchstrokewidth + strokecolor = @inherit patchstrokecolor + "Adds an offset to every value." + offset = 0.0 + "Defines where the bars start." + fillto = automatic + """ + Allows to scale all values to a certain height. This can also be set to + `:flip` to flip the direction of histogram bars without scaling them to a + common height. + """ + scale_to = nothing + bar_labels = nothing + flip_labels_at = Inf + label_color = @inherit textcolor + over_background_color = automatic + over_bar_color = automatic + label_offset = 5 + label_font = @inherit font + label_size = 20 + label_formatter = bar_label_formatter + "Set the direction of the bars." + direction = :y end function pick_hist_edges(vals, bins) @@ -171,7 +169,7 @@ function Makie.plot!(plot::Hist) points = lift(plot, edges, plot.normalization, plot.scale_to, plot.weights) do edges, normalization, scale_to, wgts centers, weights = _hist_center_weights(values, edges, normalization, scale_to, wgts) - return Point2f.(centers, weights) + return Point2.(centers, weights) end widths = lift(diff, plot, edges) color = lift(plot, plot.color) do color @@ -185,8 +183,17 @@ function Makie.plot!(plot::Hist) bar_labels = lift(plot, plot.bar_labels) do x x === :values ? :y : x end + + bar_attrs = copy(plot.attributes) + delete!(bar_attrs, :over_background_color) + delete!(bar_attrs, :bins) + delete!(bar_attrs, :scale_to) + delete!(bar_attrs, :weights) + delete!(bar_attrs, :normalization) + delete!(bar_attrs, :over_bar_color) + # plot the values, not the observables, to be in control of updating - bp = barplot!(plot, points[]; width = widths[], gap = 0, plot.attributes..., fillto=plot.fillto, offset=plot.offset, bar_labels=bar_labels, color=color) + bp = barplot!(plot, points[]; width = widths[], gap = 0, bar_attrs..., fillto=plot.fillto, offset=plot.offset, bar_labels=bar_labels, color=color) # update the barplot points without triggering, then trigger with `width` on(plot, widths) do w diff --git a/src/stats/violin.jl b/src/stats/violin.jl index 73bbe24a788..6aa7473fa6f 100644 --- a/src/stats/violin.jl +++ b/src/stats/violin.jl @@ -1,40 +1,41 @@ """ - violin(x, y; kwargs...) + violin(x, y) Draw a violin plot. -# Arguments +## Arguments - `x`: positions of the categories - `y`: variables whose density is computed -# Keywords -- `weights`: vector of statistical weights (length of data). By default, each observation has weight `1`. -- `orientation=:vertical`: orientation of the violins (`:vertical` or `:horizontal`) -- `width=1`: width of the box before shrinking -- `gap=0.2`: shrinking factor, `width -> width * (1 - gap)` -- `show_median=false`: show median as midline -- `side=:both`: specify `:left` or `:right` to only plot the violin on one side -- `scale=:width`: scale density by area (`:area`), count (`:count`), or width (`:width`). -- `datalimits`: specify values to trim the `violin`. Can be a `Tuple` or a `Function` (e.g. `datalimits=extrema`) """ -@recipe(Violin, x, y) do scene - Theme(; - default_theme(scene, Poly)..., - npoints = 200, - boundary = automatic, - bandwidth = automatic, - weights = automatic, - side = :both, - scale = :area, - orientation = :vertical, - width = automatic, - dodge = automatic, - n_dodge = automatic, - gap = 0.2, - dodge_gap = 0.03, - datalimits = (-Inf, Inf), - max_density = automatic, - show_median = false, - mediancolor = theme(scene, :linecolor), - medianlinewidth = theme(scene, :linewidth), - ) +@recipe Violin x y begin + npoints = 200 + boundary = automatic + bandwidth = automatic + "vector of statistical weights (length of data). By default, each observation has weight `1`." + weights = automatic + "Specify `:left` or `:right` to only plot the violin on one side." + side = :both + "Scale density by area (`:area`), count (`:count`), or width (`:width`)." + scale = :area + "Orientation of the violins (`:vertical` or `:horizontal`)" + orientation = :vertical + "Width of the box before shrinking." + width = automatic + dodge = automatic + n_dodge = automatic + "Shrinking factor, `width -> width * (1 - gap)`." + gap = 0.2 + dodge_gap = 0.03 + "Specify values to trim the `violin`. Can be a `Tuple` or a `Function` (e.g. `datalimits=extrema`)." + datalimits = (-Inf, Inf) + max_density = automatic + "Show median as midline." + show_median = false + mediancolor = @inherit linecolor + medianlinewidth = @inherit linewidth + color = @inherit patchcolor + strokecolor = @inherit patchstrokecolor + strokewidth = @inherit patchstrokewidth + MakieCore.mixin_generic_plot_attributes()... + cycle = [:color => :patchcolor] end conversion_trait(::Type{<:Violin}) = SampleBased() diff --git a/src/types.jl b/src/types.jl index a3144183a7b..0616c09c2e3 100644 --- a/src/types.jl +++ b/src/types.jl @@ -241,9 +241,9 @@ struct Camera resolution::Observable{Vec2f} """ - Focal point of the camera, used for e.g. camera synchronized light direction. + Direction in which the camera looks. """ - lookat::Observable{Vec3f} + view_direction::Observable{Vec3f} """ Eye position of the camera, used for e.g. ray tracing. @@ -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) @@ -461,3 +459,14 @@ struct Cycler end Cycler() = Cycler(IdDict{Type,Int}()) + + +# Float32 conversions +struct LinearScaling + scale::Vec{3, Float64} + offset::Vec{3, Float64} +end +struct Float32Convert + scaling::Observable{LinearScaling} + resolution::Float32 +end \ No newline at end of file diff --git a/src/utilities/quaternions.jl b/src/utilities/quaternions.jl index f5ccf832bc3..d6648306dcc 100644 --- a/src/utilities/quaternions.jl +++ b/src/utilities/quaternions.jl @@ -84,12 +84,11 @@ end function Base.:(*)(quat::Quaternion, bb::Rect3{T}) where {T} points = corners(bb) - first = points[1] - bb = Ref(Rect3{T}(quat * first, zero(first))) - for i in 2:length(points) - bb[] = _update_rect(bb[], Point3{T}(quat * points[i])) + bb = Rect3{T}() + for i in eachindex(points) + bb = update_boundingbox(bb, Point3{T}(quat * points[i])) end - return bb[] + return bb end Base.conj(q::Quaternion) = Quaternion(-q[1], -q[2], -q[3], q[4]) diff --git a/src/utilities/texture_atlas.jl b/src/utilities/texture_atlas.jl index e7afaad9de7..4ee9b36efa4 100644 --- a/src/utilities/texture_atlas.jl +++ b/src/utilities/texture_atlas.jl @@ -157,7 +157,7 @@ function get_texture_atlas(resolution::Int = 2048, pix_per_glyph::Int = 64) end end -const CACHE_DOWNLOAD_URL = "https://github.com/MakieOrg/Makie.jl/releases/download/v0.20.0/" +const CACHE_DOWNLOAD_URL = "https://github.com/MakieOrg/Makie.jl/releases/download/v0.21.0/" function cached_load(resolution::Int, pix_per_glyph::Int) path = get_cache_path(resolution, pix_per_glyph) @@ -463,7 +463,7 @@ _bcast(x) = x # Calculates the scaling factor from unpadded size -> padded size # Here we assume the glyph to be representative of pix_per_glyph # regardless of its true size. -function marker_scale_factor(atlas::TextureAtlas, char::Char, font) +function marker_scale_factor(atlas::TextureAtlas, char::Char, font)::Vec2f lbrt = glyph_uv_width!(atlas, char, font) uv_width = Vec(lbrt[3] - lbrt[1], lbrt[4] - lbrt[2]) full_pixel_size_in_atlas = uv_width .* Vec2f(size(atlas)) @@ -483,7 +483,7 @@ function bezierpath_pad_scale_factor(atlas::TextureAtlas, bp) return full_pixel_size_in_atlas ./ maximum(unpadded_pixel_size) end -function marker_scale_factor(atlas::TextureAtlas, path::BezierPath) +function marker_scale_factor(atlas::TextureAtlas, path::BezierPath)::Vec2f # See offset_bezierpath return bezierpath_pad_scale_factor(atlas, path) * maximum(widths(bbox(path))) end @@ -508,7 +508,7 @@ function rescale_marker(atlas::TextureAtlas, char::Char, font, markersize) return markersize .* factor end -function offset_bezierpath(atlas::TextureAtlas, bp::BezierPath, markersize::Vec2, markeroffset::Vec2) +function offset_bezierpath(atlas::TextureAtlas, bp::BezierPath, markersize::Vec2, markeroffset::Vec2)::Vec2f # - wh = widths(bbox(bp)) is the untouched size of the given bezierpath # - full_pixel_size_in_atlas is the size of the signed distance field in the # texture atlas. This includes glyph padding @@ -530,7 +530,6 @@ function offset_bezierpath(atlas::TextureAtlas, bp::BezierPath, markersize::Vec2 bb = bbox(bp) scaled_size = bezierpath_pad_scale_factor(atlas, bp) * maximum(widths(bb)) return markersize * (origin(bb) .+ 0.5f0 * widths(bb) .- 0.5f0 .* scaled_size) - end function offset_bezierpath(atlas::TextureAtlas, bp, scale, offset) diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index 450b2125854..5d3d85bdd9b 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -414,7 +414,7 @@ Allows to supply `f`, which gets applied to every point. """ function matrix_grid(f, x::AbstractArray, y::AbstractArray, z::AbstractMatrix) g = map(CartesianIndices(z)) do i - return f(Point3f(get_dim(x, i, 1, size(z)), get_dim(y, i, 2, size(z)), z[i])) + return f(Point3(get_dim(x, i, 1, size(z)), get_dim(y, i, 2, size(z)), z[i])) end return vec(g) end @@ -470,3 +470,33 @@ function available_plotting_methods() end return meths end + +mindist(x, a, b) = min(abs(a - x), abs(b - x)) +function gappy(x, ps) + n = length(ps) + x <= first(ps) && return first(ps) - x + for j in 1:(n - 1) + p0 = ps[j] + p1 = ps[min(j + 1, n)] + if p0 <= x && p1 >= x + return mindist(x, p0, p1) * (isodd(j) ? 1 : -1) + end + end + return last(ps) - x +end + + +# This is used to map a vector of `points` to a signed distance field. The +# points mark transition between "on" and "off" section of the pattern. + +# The output should be periodic so the signed distance field value +# representing points[1] should be equal to the one representing points[end]. +# => range(..., length = resolution+1)[1:end-1] + +# points[end] should still represent the full length of the pattern though, +# so we need rescaling by ((resolution + 1) / resolution) +function linestyle_to_sdf(linestyle::AbstractVector{<:Real}, resolution::Real=100) + scaled = ((resolution + 1) / resolution) .* linestyle + r = range(first(scaled); stop=last(scaled), length=resolution + 1)[1:(end - 1)] + return Float16[-gappy(x, scaled) for x in r] +end diff --git a/test/boundingboxes.jl b/test/boundingboxes.jl index 5008cf1fe5c..95c874bf5fd 100644 --- a/test/boundingboxes.jl +++ b/test/boundingboxes.jl @@ -1,6 +1,7 @@ function Base.isapprox(r1::Rect{D}, r2::Rect{D}; kwargs...) where D - return isapprox(minimum(r1), minimum(r2); kwargs...) && - isapprox(widths(r1), widths(r2); kwargs...) + left = vcat(minimum(r1), widths(r1)) + right = vcat(minimum(r2), widths(r2)) + return all((isnan.(left) .& isnan.(right)) .| (left .≈ right)) end @testset "data_limits(plot)" begin @@ -23,15 +24,11 @@ end p2 = hlines!(ax, [0.5]) p3 = vspan!(ax, [0.25], [0.75]) p4 = hspan!(ax, [0.25], [0.75]) - Makie.reset_limits!(ax) - lims = ax.finallimits[] - x, y = minimum(lims); w, h = widths(lims) - - @test data_limits(p1) ≈ Rect3f(Point3f(0.5, y, 0), Vec3f(0, h, 0)) - @test data_limits(p2) ≈ Rect3f(Point3f(x, 0.5, 0), Vec3f(w, 0, 0)) - @test data_limits(p3) ≈ Rect3f(Point3f(0.25, y, 0), Vec3f(0.5, h, 0)) - @test data_limits(p4) ≈ Rect3f(Point3f(x, 0.25, 0), Vec3f(w, 0.5, 0)) + @test data_limits(p1) ≈ Rect3f(Point3f(0.5, NaN, 0), Vec3f(0, NaN, 0)) + @test data_limits(p2) ≈ Rect3f(Point3f(NaN, 0.5, 0), Vec3f(NaN, 0, 0)) + @test data_limits(p3) ≈ Rect3f(Point3f(0.25, NaN, 0), Vec3f(0.5, NaN, 0)) + @test data_limits(p4) ≈ Rect3f(Point3f(NaN, 0.25, 0), Vec3f(NaN, 0.5, 0)) end @testset "boundingbox(plot)" begin @@ -59,7 +56,7 @@ end [Point3f(0) for _ in 1:3], marker = Rect3f(Point3f(-0.1, -0.1, -0.1), Vec3f(0.2, 0.2, 1.2)), markersize = Vec3f(1, 1, 2), - rotations = Makie.rotation_between.((Vec3f(0,0,1),), Vec3f[(1,0,0), (0,1,0), (0,0,1)]) + rotation = Makie.rotation_between.((Vec3f(0,0,1),), Vec3f[(1,0,0), (0,1,0), (0,0,1)]) ) bb = boundingbox(p) @test bb.origin ≈ Point3f(-0.2) @@ -99,9 +96,12 @@ end fig = Figure(size = (400, 400)) ax = Axis(fig[1, 1]) p = text!(ax, Point2f(10), text = "test", fontsize = 20) - bb = boundingbox(p) + bb = boundingbox(p, :pixel) @test bb.origin ≈ Point3f(343.0, 345.0, 0) @test bb.widths ≈ Vec3f(32.24, 23.3, 0) + bb = boundingbox(p, :data) + @test bb.origin ≈ Point3f(10, 10, 0) + @test bb.widths ≈ Vec3f(0) end @testset "invalid contour bounding box" begin @@ -112,3 +112,36 @@ end c = [0 1 2; 1 2 3; 4 5 Inf] contour(a, b, c; levels, labels = true) end + +# Testing mostly how it interacts with marker transforms +@testset "scatter boundingbox & data_limits" begin + f, a, p = scatter( + Point2f(0), markersize = 5, markerspace = :data, + marker = Rect, rotation = 0, transform_marker = false + ) + @test data_limits(p) ≈ Rect3f(Point3d(-2.5, -2.5, 0), Vec3d(5, 5, 0)) + @test boundingbox(p) ≈ Rect3f(Point3d(-2.5, -2.5, 0), Vec3d(5, 5, 0)) + + # model should not affect either with transform_marker = false + scale!(p, Vec3d(0.5)) + @test data_limits(p) ≈ Rect3f(Point3d(-2.5, -2.5, 0), Vec3d(5, 5, 0)) + @test boundingbox(p) ≈ Rect3f(Point3d(-2.5, -2.5, 0), Vec3d(5, 5, 0)) + + # rotation should affect both, always + p.rotation = pi/6 + bb1 = Rect3{Float64}([-3.4150635094610964, -3.4150635094610964, 0.0], [6.830127018922193, 6.830127018922193, 0.0]) + @test data_limits(p) ≈ bb1 + @test boundingbox(p) ≈ bb1 + + # with transform_marker = true both should apply to boundingbox, only p.rotation to data_limits + p.transform_marker = true + bb2 = Rect3{Float64}([-1.7075317547305482, -1.7075317547305482, 0.0], [3.4150635094610964, 3.4150635094610964, 0.0]) + @test data_limits(p) ≈ bb1 + @test boundingbox(p) ≈ bb2 + + # further model transformations should (only) affect boundingbox + Makie.rotate!(p, pi/4) + bb3 = Rect3{Float64}([-1.5309311648155406, -1.5309311648155406, 0.0], [3.061862329631081, 3.061862329631081, 0.0]) + @test data_limits(p) ≈ bb1 + @test boundingbox(p) ≈ bb3 +end diff --git a/test/conversions.jl b/test/conversions.jl index be31c61cc7c..4a068c79286 100644 --- a/test/conversions.jl +++ b/test/conversions.jl @@ -1,8 +1,4 @@ using Makie: - NoConversion, - convert_arguments, - conversion_trait, - convert_single_argument, to_vertices, categorical_colors @@ -29,27 +25,26 @@ end @testset "to_vertices" begin X1 = [Point(rand(3)...) for i = 1:10] V1 = to_vertices(X1) - @test Float32(X1[7][1]) == V1[7][1] + @test (X1[7][1]) == V1[7][1] X2 = [tuple(rand(3)...) for i = 1:10] V2 = to_vertices(X2) - @test Float32(X2[7][1]) == V2[7][1] - + @test (X2[7][1]) == V2[7][1] X4 = rand(2,10) V4 = to_vertices(X4) - @test Float32(X4[1,7]) == V4[7][1] + @test (X4[1,7]) == V4[7][1] X5 = rand(3,10) V5 = to_vertices(X5) - @test Float32(X5[1,7]) == V5[7][1] + @test (X5[1,7]) == V5[7][1] X6 = rand(10,2) V6 = to_vertices(X6) - @test Float32(X6[7,1]) == V6[7][1] + @test (X6[7,1]) == V6[7][1] X7 = rand(10,3) V7 = to_vertices(X7) - @test Float32(X7[7,1]) == V7[7][1] + @test (X7[7,1]) == V7[7][1] end @testset "GeometryBasics Lines & Polygons" begin diff --git a/test/convert_arguments.jl b/test/convert_arguments.jl new file mode 100644 index 00000000000..b1decd46b5d --- /dev/null +++ b/test/convert_arguments.jl @@ -0,0 +1,419 @@ +using Makie: + NoConversion, + convert_arguments, + conversion_trait, + convert_single_argument, + ClosedInterval + +using Logging + +@testset "convert_arguments" begin + #= + TODO: + - consider implementing the commented out conversions + - consider normalizing the conversions with branches here + + Skipped/Missing: + - PointBased: SubArray{<: VecTypes, 1} + - Mesh: AbstractVector{<: Union{AbstractMesh, AbstractPolygon}} + - GridBased: OffsetArray + - Axis3D: Rect + - datashader + - rainclouds + - stats plots + =# + + function test_mesh_result(mesh_convert, dim, eltype, assert_normals = false) + @test mesh_convert isa Tuple{<: GeometryBasics.Mesh{dim, eltype}} + if assert_normals || !isnothing(normals(mesh_convert)) + @test normals(mesh_convert[1]) isa Vector{Vec3f} + end + if !isnothing(texturecoordinates(mesh_convert[1])) + @test texturecoordinates(mesh_convert[1]) isa Vector{Vec2f} + end + return + end + + indices = [1, 2, 3, 2, 3, 4, 5, 6, 7, 8, 9, 10] + str = "test" + strings = ["test" for _ in 1:10] + + @testset "input type -> output type" begin + for (T_in, T_out) in [ + Float32 => Float32, Float64 => Float64, + UInt32 => Float64, Int32 => Float64, Int64 => Float64 + ] + + @testset "$T_in => $T_out" begin + + # COV_EXCL_START + xs = rand(T_in, 10) + ys = rand(T_in, 10) + zs = rand(T_in, 10) + v32 = rand(Float32, 10) + vt = rand(T_in, 1, 10) + miss = vcat(v32[1:4], missing, zs[6:end]) + nan = vcat(xs[1:4], NaN, zs[6:end]) + r = T_in(1):T_in(1):T_in(10) + i = T_in(1)..T_in(10) + + ps2 = Point2.(xs, ys) + ps3 = Point3.(xs, ys, zs) + miss2 = vcat(Point2.(xs, ys), missing) + + rect2 = Rect2{T_in}(0, 0, 1, 1) + rect3 = Rect3{T_in}(Point3{T_in}(0), Vec3{T_in}(1)) + geom = Sphere(Point3{T_in}(0), T_in(1)) + _mesh = GeometryBasics.mesh(rect3; pointtype=Point3{T_in}, facetype=GLTriangleFace) + polygon = Polygon(Point2.(xs, ys)) + line = LineString(Point3.(xs, ys, zs)) + bp = BezierPath([ + MoveTo(T_in(0), T_in(0)), + LineTo(T_in(1), T_in(0)), + CurveTo(T_in(1), T_in(1), T_in(0), T_in(1), T_in(3), T_in(0)), + EllipticalArc(Point2(T_in(0)), T_in(1), T_in(1), T_in(0), T_in(0), T_in(1)), + ClosePath() + ]) + + xgridvec = [x for x in T_in(1):T_in(3) for y in T_in(1):T_in(3)] + ygridvec = [y for x in T_in(1):T_in(3) for y in T_in(1):T_in(3)] + + m = rand(T_in, 10, 10) + m2 = rand(T_in, 2, 10) + m3 = rand(T_in, 10, 3) + + img = rand(RGBf, 10, 10) + vol = rand(T_in, 10, 10, 10) + sparse = Makie.SparseArrays.SparseMatrixCSC(m) + + # COV_EXCL_STOP + + ################################################################ + ### primitives + ################################################################ + + # PointBased and Friends + + for CT in (PointBased(), Scatter, MeshScatter, Lines, LineSegments) + @testset "$CT" begin + # TODO: (missing) + # - FaceView + # - SubArra{<: VecTypes} + + @test convert_arguments(CT, xs[1], xs[2]) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, xs[1], xs[2], xs[3]) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, ps2[1]) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, ps3[1]) isa Tuple{Vector{Point3{T_out}}} + + # because indices are Int we end up converting to Float64 no matter what + @test convert_arguments(CT, xs) isa Tuple{Vector{Point2{Float64}}} + + @test convert_arguments(CT, xs, ys) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, xs, v32) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, i, ys) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, xs, i) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, r, ys) isa Tuple{Vector{Point2{T_out}}} + # @test convert_arguments(CT, vt, ys) isa Tuple{Vector{Point2{T_out}}} + # @test convert_arguments(CT, m, m) isa Tuple{Vector{Point2{T_out}}} + + @test convert_arguments(CT, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, vt, ys, vt) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, xs, ys, vt) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, xs, ys, v32) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, r, v32, zs) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, m, m, m) isa Tuple{Vector{Point3{T_out}}} + # TODO: Does this make sense? + @test convert_arguments(CT, i, i, m) isa Tuple{Vector{Point3{T_out}}} + # @test convert_arguments(CT, r, i, zs) isa Tuple{Vector{Point3{T_out}}} + # @test convert_arguments(CT, i, i, zs) isa Tuple{Vector{Point3{T_out}}} + + # TODO: implement as PointBased conversion? + if CT !== PointBased() + @test convert_arguments(CT, xs, identity) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, r, identity) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, i, identity) isa Tuple{Vector{Point2{T_out}}} + + @test convert_arguments(CT, xs, miss) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, miss, ys, zs) isa Tuple{Vector{Point3{T_out}}} + + @test convert_arguments(CT, miss2) isa Tuple{Vector{Point2{T_out}}} + end + + @test convert_arguments(CT, ps2) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, ps3) isa Tuple{Vector{Point3{T_out}}} + # @test convert_arguments(CT, Point.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} + + # TODO: Should this be Point? + @test convert_arguments(CT, Vec.(xs, ys)) isa Tuple{Vector{Vec2{T_out}}} + @test convert_arguments(CT, Vec.(xs, ys, zs)) isa Tuple{Vector{Vec3{T_out}}} + # @test convert_arguments(CT, Vec.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} + + @test convert_arguments(CT, tuple.(xs, ys)) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, tuple.(xs, v32)) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, tuple.(xs, ys, zs)) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, tuple.(v32, ys, zs)) isa Tuple{Vector{Point3{T_out}}} + # @test convert_arguments(CT, tuple.(miss, ys)) isa Tuple{Vector{Point2{T_out}}} + + @test convert_arguments(CT, rect2) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, rect3) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, geom) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, _mesh) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, polygon) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, [polygon, polygon]) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, MultiPolygon([polygon, polygon])) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, line) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, [line, line]) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, MultiLineString([line, line])) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(CT, bp) isa Tuple{Vector{Point2d}} # BezierPath uses Float64 internally + + @test convert_arguments(CT, m2) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(CT, m3) isa Tuple{Vector{Point3{T_out}}} + end + end + + # Special case: LineSegments + + @testset "LineSegments Extras" begin + if T_out == T_in + @test convert_arguments(LineSegments, Pair.(ps2, ps2)) isa Tuple{<: Base.ReinterpretArray} + @test convert_arguments(LineSegments, tuple.(ps2, ps2)) isa Tuple{<: Base.ReinterpretArray} + else + @test convert_arguments(LineSegments, Pair.(ps2, ps2)) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(LineSegments, tuple.(ps2, ps2)) isa Tuple{Vector{Point2{T_out}}} + end + end + + # CellGrid & Heatmap + + for CT in (CellGrid(), Heatmap) + @testset "$CT" begin + @test convert_arguments(CT, m) isa Tuple{Vector{Float32}, Vector{Float32}, Matrix{Float32}} + + @test convert_arguments(CT, xs, ys, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, xs, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, r, ys, +) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, i, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, i, i, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, r, i, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + # TODO OffsetArray + end + end + + # VertexGrid + + for CT in (VertexGrid(), Surface) + @testset "$CT" begin + @test convert_arguments(CT, xs, ys, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, m, m) isa Tuple{Matrix{T_out}, Matrix{T_out}, Matrix{Float32}} + # TODO: Should these be normalized to Vector? + if T_in == T_out + @test convert_arguments(CT, xs, r, m) isa Tuple{Vector{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test convert_arguments(CT, r, ys, +) isa Tuple{AbstractRange{T_out}, Vector{T_out}, Matrix{Float32}} + else + @test convert_arguments(CT, xs, r, m) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + @test convert_arguments(CT, r, ys, +) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + end + @test convert_arguments(CT, m) isa Tuple{AbstractRange{Float32}, AbstractRange{Float32}, Matrix{Float32}} + @test convert_arguments(CT, i, r, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test convert_arguments(CT, i, i, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test convert_arguments(CT, r, i, m) isa Tuple{AbstractRange{T_out}, AbstractRange{T_out}, Matrix{Float32}} + @test convert_arguments(CT, xgridvec, ygridvec, xgridvec) isa Tuple{Vector{T_out}, Vector{T_out}, Matrix{Float32}} + # TODO OffsetArray + end + end + + # ImageLike, Image + + for CT in (ImageLike(), Image) + @testset "$CT" begin + @test convert_arguments(CT, img) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, Matrix{RGBf}} + @test convert_arguments(CT, m) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, Matrix{Float32}} + @test convert_arguments(CT, i, i, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + + # deprecated + Logging.disable_logging(Logging.Warn) # skip warnings + @test convert_arguments(CT, xs, ys, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test convert_arguments(CT, xs, r, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test convert_arguments(CT, i, r, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test convert_arguments(CT, r, i, m) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + @test convert_arguments(CT, r, ys, +) isa Tuple{ClosedInterval{T_out}, ClosedInterval{T_out}, Matrix{Float32}} + Logging.disable_logging(Logging.Debug) + end + end + + # VolumeLike, Volume + + for CT in (VolumeLike(), Volume) + @testset "$CT" begin + # TODO: Should these be normalized more? + @test convert_arguments(CT, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} + @test convert_arguments(CT, i, i, i, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} + @test convert_arguments(CT, xs, ys, zs, vol) isa Tuple{Vector{Float32}, Vector{Float32}, Vector{Float32}, Array{Float32, 3}} + @test convert_arguments(CT, xs, ys, zs, +) isa Tuple{Vector{Float32}, Vector{Float32}, Vector{Float32}, Array{Float32, 3}} + if T_in == Float32 + @test convert_arguments(CT, r, r, r, vol) isa Tuple{AbstractRange{Float32}, AbstractRange{Float32}, AbstractRange{Float32}, Array{Float32, 3}} + @test convert_arguments(CT, xs, r, i, vol) isa Tuple{Vector{Float32}, AbstractRange{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} + else + @test convert_arguments(CT, r, r, r, vol) isa Tuple{Vector{Float32}, Vector{Float32}, Vector{Float32}, Array{Float32, 3}} + @test convert_arguments(CT, xs, r, i, vol) isa Tuple{Vector{Float32}, Vector{Float32}, ClosedInterval{Float32}, Array{Float32, 3}} + end + end + end + + # Mesh + + @testset "Mesh" begin + test_mesh_result(convert_arguments(Makie.Mesh, xs, ys, zs), 3, T_out, true) + test_mesh_result(convert_arguments(Makie.Mesh, ps3), 3, T_out, true) + test_mesh_result(convert_arguments(Makie.Mesh, _mesh), 3, T_out, true) + test_mesh_result(convert_arguments(Makie.Mesh, geom), 3, T_out, true) + test_mesh_result(convert_arguments(Makie.Mesh, xs, ys, zs, indices), 3, T_out, true) + test_mesh_result(convert_arguments(Makie.Mesh, ps3, indices), 3, T_out, true) + + test_mesh_result(convert_arguments(Makie.Mesh, polygon), 2, T_out) + test_mesh_result(convert_arguments(Makie.Mesh, ps2), 2, T_out) + test_mesh_result(convert_arguments(Makie.Mesh, ps2, indices), 2, T_out) + end + + # internally converted + @testset "Text" begin + @test convert_arguments(Makie.Text, tuple.(strings, ps2)) isa Tuple{Vector{Tuple{String, Point2{T_in}}}} + @test convert_arguments(Makie.Text, tuple.(strings, ps3)) isa Tuple{Vector{Tuple{String, Point3{T_in}}}} + end + + + ################################################################ + ### recipes + ################################################################ + + # If a recipe transforms its input arguments it is fine for it + # to keep T_in in convert_arguments. + + @testset "Annotations" begin + @test convert_arguments(Annotations, strings, ps2) isa Tuple{Vector{Tuple{String, Point{2, T_out}}}} + @test convert_arguments(Annotations, strings, ps3) isa Tuple{Vector{Tuple{String, Point{3, T_out}}}} + end + + @testset "Arrows" begin + @test convert_arguments(Arrows, xs, ys, xs, ys) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} + @test convert_arguments(Arrows, xs, ys, m, m) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} + @test convert_arguments(Arrows, xs, ys, zs, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}, Vector{Vec3{T_out}}} + @test convert_arguments(Arrows, xs, ys, identity) isa Tuple{Vector{Point2{T_out}}, Vector{Vec2{T_out}}} + @test convert_arguments(Arrows, xs, ys, zs, identity) isa Tuple{Vector{Point3{T_out}}, Vector{Vec3{T_out}}} + end + + @testset "Band" begin + @test convert_arguments(Band, xs, ys, zs) isa Tuple{Vector{Point2{T_out}}, Vector{Point2{T_out}}} + end + + @testset "Bracket" begin + @test convert_arguments(Bracket, ps2[1], ps2[2]) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} + @test convert_arguments(Bracket, xs[1], ys[1], xs[2], ys[2]) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} + @test convert_arguments(Bracket, xs, ys, xs, ys) isa Tuple{Vector{Tuple{Point2{T_out}, Point2{T_out}}}} + end + + @testset "Errorbars & Rangebars" begin + @test convert_arguments(Errorbars, xs, ys, zs) isa Tuple{Vector{Vec4{T_out}}} + @test convert_arguments(Errorbars, xs, ys, xs, ys) isa Tuple{Vector{Vec4{T_out}}} + @test convert_arguments(Errorbars, xs, ys, ps2) isa Tuple{Vector{Vec4{T_out}}} + @test convert_arguments(Errorbars, ps2, zs) isa Tuple{Vector{Vec4{T_out}}} + @test convert_arguments(Errorbars, ps2, xs, ys) isa Tuple{Vector{Vec4{T_out}}} + @test convert_arguments(Errorbars, ps2, ps2) isa Tuple{Vector{Vec4{T_out}}} + @test convert_arguments(Errorbars, ps3) isa Tuple{Vector{Vec4{T_out}}} + + @test convert_arguments(Rangebars, xs, ys, zs) isa Tuple{Vector{Vec3{T_out}}} + @test convert_arguments(Rangebars, xs, ps2) isa Tuple{Vector{Vec3{T_out}}} + end + + @testset "Poly" begin + # TODO: Are these ok? All of these are just reflection... + @test convert_arguments(Poly, ps2) isa Tuple{Vector{Point2{T_in}}} + @test convert_arguments(Poly, ps3) isa Tuple{Vector{Point3{T_in}}} + @test convert_arguments(Poly, [polygon]) isa Tuple{Vector{typeof(polygon)}} + @test convert_arguments(Poly, [rect2]) isa Tuple{Vector{typeof(rect2)}} + @test convert_arguments(Poly, polygon) isa Tuple{typeof(polygon)} + @test convert_arguments(Poly, rect2) isa Tuple{typeof(rect2)} + + # And these aren't mesh-like + @test convert_arguments(Poly, xs, ys) isa Tuple{Vector{Point2{T_out}}} + # Vector{Vector{...}} ? + @test convert_arguments(Poly, xs, ys, zs) isa Tuple{Vector{Vector{Point3{T_out}}}} + + @test convert_arguments(Poly, ps2, indices) isa Tuple{<: GeometryBasics.Mesh{2, T_out}} + @test convert_arguments(Poly, ps3, indices) isa Tuple{<: GeometryBasics.Mesh{3, T_out}} + end + + @testset "Series" begin + @test convert_arguments(Series, m) isa Tuple{Vector{Vector{Point2{Float64}}}} + @test convert_arguments(Series, xs, m) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test convert_arguments(Series, miss, m) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test convert_arguments(Series, [(xs, ys)]) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test convert_arguments(Series, (xs, ys)) isa Tuple{Vector{Vector{Point2{T_out}}}} + @test convert_arguments(Series, [ps2, ps2]) isa Tuple{Vector{Vector{Point2{T_out}}}} + end + + @testset "Spy" begin + # TODO: assuming internal processing + @test convert_arguments(Spy, sparse) isa Tuple{ClosedInterval{Int}, ClosedInterval{Int}, typeof(sparse)} + @test convert_arguments(Spy, xs, ys, sparse) isa Tuple{typeof(xs), typeof(ys), typeof(sparse)} + end + + @testset "StreamPlot" begin + # TODO: these have a different argument order than other Function plots... + @test convert_arguments(StreamPlot, identity, xs, ys) isa Tuple{typeof(identity), Rect2{T_in}} + @test convert_arguments(StreamPlot, identity, i, r) isa Tuple{typeof(identity), Rect2{T_in}} + @test convert_arguments(StreamPlot, identity, xs, ys, zs) isa Tuple{typeof(identity), Rect3{T_in}} + @test convert_arguments(StreamPlot, identity, r, i, zs) isa Tuple{typeof(identity), Rect3{T_in}} + @test convert_arguments(StreamPlot, identity, rect2) isa Tuple{typeof(identity), Rect2{T_in}} + @test convert_arguments(StreamPlot, identity, rect3) isa Tuple{typeof(identity), Rect3{T_in}} + end + + @testset "Tooltip" begin + @test convert_arguments(Tooltip, xs[1], ys[1], str) isa Tuple{Point2{T_out}, String} + @test convert_arguments(Tooltip, xs[1], ys[1]) isa Tuple{Point2{T_out}} + end + + @testset "Tricontourf" begin + @test convert_arguments(Tricontourf, xs, ys, zs) isa Tuple{<: Makie.DelTri.Triangulation{Matrix{T_out}}, Vector{T_out}} + end + + @testset "Triplot" begin + @test convert_arguments(Triplot, ps2) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(Triplot, xs, ys) isa Tuple{Vector{Point2{T_out}}} + # TODO: DelTri.Triangulation + end + + @testset "Voronoiplot" begin + @test convert_arguments(Voronoiplot, m) isa Tuple{Vector{Point3{Float64}}} + @test convert_arguments(Voronoiplot, xs, ys, zs) isa Tuple{Vector{Point3{T_out}}} + @test convert_arguments(Voronoiplot, xs, ys) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(Voronoiplot, ps2) isa Tuple{Vector{Point2{T_out}}} + @test convert_arguments(Voronoiplot, ps3) isa Tuple{Vector{Point3{T_out}}} + # TODO: VoronoiTessellation + end + + # pure 3D plots don't implement Float64 -> Float32 rescaling yet + @testset "Voxels" begin + @test convert_arguments(Voxels, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} + @test convert_arguments(Voxels, xs, ys, zs, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} + @test convert_arguments(Voxels, i, i, i, vol) isa Tuple{ClosedInterval{Float32}, ClosedInterval{Float32}, ClosedInterval{Float32}, Array{UInt8, 3}} + end + + @testset "Wireframe" begin + @test convert_arguments(Wireframe, xs, ys, zs) isa Tuple{Vector{T_in}, Vector{T_in}, Vector{T_in}} + end + + end + + end + + # These have nothing to do with Numeric types... + @testset "Text" begin + @test convert_arguments(Makie.Text, str) isa Tuple{String} + @test convert_arguments(Makie.Text, strings) isa Tuple{Vector{String}} + # TODO glyphcollection + end + end + +end diff --git a/test/float32convert.jl b/test/float32convert.jl new file mode 100644 index 00000000000..b50ad464641 --- /dev/null +++ b/test/float32convert.jl @@ -0,0 +1,101 @@ +# Testing TODO: +# - test that none of the plot! functions drops precision (i.e. converts to Float32) +# unless they apply float32convert themselves (e.g. convert to pixel space) + +using Makie: Float32Convert, LinearScaling, f32_convert, update_limits!, + f32_convert_matrix, patch_model, transformationmatrix + +@testset "float32convert" begin + f32c = Float32Convert() + + approx(r1, r2) = minimum(r1) ≈ minimum(r2) && widths(r1) ≈ widths(r2) + unit_rect = Rect2f(-1.0, -1.0, 2.0, 2.0) + unit_scaling = LinearScaling(Vec3d(1), Vec3d(0)) + f32max = Float64(floatmax(Float32)) / f32c.resolution + f32min = Float64(floatmin(Float32)) * f32c.resolution + f32eps = Float64(eps(Float32)) * f32c.resolution + + @testset "Intialization" begin + @test f32c.scaling[] == unit_scaling + @test f32c.resolution == 1f4 # this may be subject to change + end + + @testset "Modificaiton" begin + # no update necessary + @test !update_limits!(f32c, Rect(-1, -1, 2, 2)) + @test f32c.scaling[] == unit_scaling + @test !update_limits!(f32c, Rect(-f32max, -f32max, 2 * f32max, 2 * f32max)) + @test f32c.scaling[] == unit_scaling + @test !update_limits!(f32c, Rect(-f32min, -f32min, 2 * f32min, 2 * f32min)) + @test f32c.scaling[] == unit_scaling + @test !update_limits!(f32c, Rect(1e6, 1e6, 2e6 * f32eps, 2e6 * f32eps)) + @test f32c.scaling[] == unit_scaling + + # should trigger updates based on abs(extrema) > floatmax(Float32) + @test update_limits!(f32c, Rect(-2 * f32max, -2 * f32max, 4 * f32max, 4 * f32max)) + @test f32c.scaling[] != unit_scaling + @test approx(f32_convert(f32c, Rect(-2 * f32max, -2 * f32max, 4 * f32max, 4 * f32max)), unit_rect) + prev = f32c.scaling[] + + # back to -1..1 should reset + @test update_limits!(f32c, unit_rect) + @test f32c.scaling[] == unit_scaling + @test approx(f32_convert(f32c, unit_rect), unit_rect) + prev = f32c.scaling[] + + # abs(extrema) < floatmin(Float32) + @test update_limits!(f32c, Rect(-0.5 * f32min, -0.5 * f32min, f32min, f32min)) + @test f32c.scaling[] != prev + @test approx(f32_convert(f32c, Rect(-0.5 * f32min, -0.5 * f32min, f32min, f32min)), unit_rect) + prev = f32c.scaling[] + + # This wouldn't trigger because Float32 numbers have a little more room + # towards large value than small ones (f32min * f32max > 1) + # @test update_limits!(f32c, unit_rect) + f32c.scaling[] = unit_scaling + prev = f32c.scaling[] + + # widths < resolution * eps(extrema) + @test update_limits!(f32c, Rect(2e6, 2e6, 1e6 * f32eps, 1e6 * f32eps)) + @test f32c.scaling[] != prev + @test approx(f32_convert(f32c,Rect(2e6, 2e6, 1e6 * f32eps, 1e6 * f32eps)), unit_rect) + prev = f32c.scaling[] + end + + # some random scaling + f32c.scaling[] = LinearScaling(Vec3f(1e-5, 2.35e3, 1), Vec3f(3.6e20, 9.2e-50, 0)) + + + @testset "f32_convert & matrix" begin + f32m = f32_convert_matrix(f32c, :data) + for input in (rand(Vec2d), rand(Vec3d), rand(Point2d), rand(Point3d)) + output = f32_convert(f32c, input) + @test eltype(output) == Float32 + @test typeof(output)(f32m * to_ndim(Point4d, to_ndim(Point3d, input, 0), 1)) ≈ output + D = length(input) + @test isapprox(output, f32c.scaling[].scale[1:D] .* input + f32c.scaling[].offset[1:D], rtol = 1e-7) + end + + for input in (rand(Vec2d, 10), rand(Vec3d, 10)) + @test f32_convert(f32c, input) == f32_convert.((f32c,), input) + end + + # TODO: test the rest + end + + @testset "model patching" begin + @assert f32c.scaling[] != unit_scaling "model patching tests invalid" + f32m = f32_convert_matrix(f32c, :data) + + translation = transformationmatrix(rand(Vec3d), Vec3d(1)) + @test Mat4f(f32m * translation) ≈ Mat4f(patch_model(f32c.scaling[], translation) * f32m) + + scaling = transformationmatrix(Vec3d(0), rand(Vec3d)) + @test Mat4f(f32m * scaling) ≈ Mat4f(patch_model(f32c.scaling[], scaling) * f32m) + + # This causes the model/rotation matrix to require higher precision + # because it mixes the vastly different scaling from float32convert + # rotation = transformationmatrix(Vec3d(0), Vec3d(1), qrotation(2 * rand(Vec3d) .- 1, 2pi * rand(Float64))) + # @test f32m * rotation ≈ patch_model(f32c.scaling[], rotation) * f32m + end +end \ No newline at end of file diff --git a/test/pipeline.jl b/test/pipeline.jl index 6f18bc98729..fcc4c0c3ec0 100644 --- a/test/pipeline.jl +++ b/test/pipeline.jl @@ -147,3 +147,17 @@ end plots = test_default(rand(4, 4, 4)) @test all(x -> x isa Volume, plots) end + +@testset "validated attributes" begin + InvalidAttributeError = Makie.MakieCore.InvalidAttributeError + @test_throws InvalidAttributeError heatmap(zeros(10, 10); does_not_exist = 123) + @test_throws InvalidAttributeError image(zeros(10, 10); does_not_exist = 123) + @test_throws InvalidAttributeError scatter(1:10; does_not_exist = 123) + @test_throws InvalidAttributeError lines(1:10; does_not_exist = 123) + @test_throws InvalidAttributeError linesegments(1:10; does_not_exist = 123) + @test_throws InvalidAttributeError text(1:10; does_not_exist = 123) + @test_throws InvalidAttributeError volume(zeros(3, 3, 3); does_not_exist = 123) + @test_throws InvalidAttributeError meshscatter(1:10; does_not_exist = 123) + @test_throws InvalidAttributeError poly(Point2f[]; does_not_exist = 123) + @test_throws InvalidAttributeError mesh(rand(Point3f, 3); does_not_exist = 123) +end \ No newline at end of file diff --git a/test/primitives.jl b/test/primitives.jl index ad5a74ad1e6..2a38916cc1e 100644 --- a/test/primitives.jl +++ b/test/primitives.jl @@ -22,6 +22,42 @@ end @test cbar.limits[] ≈ Vec2f(0.5, 0.6) end +@testset "voxels" begin + data = reshape(collect(range(0.3, 1.8, length=6*5*4)), 6, 5, 4) + f, a, p = voxels( + data, + lowclip = RGBf(1, 0, 1), highclip = RGBf(0, 1, 0), + colormap = [RGBf(0, 0, 0), RGBf(1, 1, 1)], gap = 0.1 + ) + + # data conversion pipeline + @test p.args[1][] === data + @test p.converted[1][] ≈ -3.0..3.0 + @test p.converted[2][] ≈ -2.5..2.5 + @test p.converted[3][] ≈ -2.0..2.0 + + @test p.colorrange[] == Makie.automatic # otherwise no auto _limits + @test all(p._limits[] .≈ (0.3, 1.8)) # controls conversion to voxel ids + ids = map(data) do val + trunc(UInt8, clamp(2 + 253 * (val - 0.3) / (1.8 - 0.3), 2, 254)) + end + @test p.converted[4][] == ids + + # colormap + cc = p.calculated_colors[] + @test length(cc.colormap[]) == 255 + @test cc.colormap[][1] == RGBAf(1,0,1,1) + @test cc.colormap[][2] == RGBAf(0,0,0,1) + @test cc.colormap[][2:end-1] == resample_cmap([RGBAf(0, 0, 0, 1), RGBAf(1, 1, 1, 1)], 253) + @test cc.colormap[][end-1] == RGBAf(1,1,1,1) + @test cc.colormap[][end] == RGBAf(0,1,0,1) + + # voxels-as-meshscatter helpers + @test Makie.voxel_size(p) ≈ Vec3f(0.9) + ps = [Point3f(x - 2.5, y - 2.0, z - 1.5) for z in 0:3 for y in 0:4 for x in 0:5] + @test Makie.voxel_positions(p) ≈ ps + @test Makie.voxel_colors(p) == cc.colormap[][p.converted[end][][:]] +end # TODO, test all primitives and argument conversions diff --git a/test/ray_casting.jl b/test/ray_casting.jl index 73b5beaa20a..521a12be7a5 100644 --- a/test/ray_casting.jl +++ b/test/ray_casting.jl @@ -8,7 +8,7 @@ for set_cam! in (cam2d!, cam_relative!, campixel!, cam3d!, orthographic_cam3d!) @testset "$set_cam!" begin set_cam!(scene) - ray = Makie.Ray(scene, xy) + ray = convert(Makie.Ray{Float32}, Makie.Ray(scene, xy)) ref_ray = Makie.ray_from_projectionview(scene, xy) # Direction matches and is normalized @test ref_ray.direction ≈ ray.direction diff --git a/test/runtests.jl b/test/runtests.jl index 25a86f9f68a..f613a81b78b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,4 @@ +# COV_EXCL_START using Test using Makie using Makie.Observables @@ -8,6 +9,7 @@ using Makie.IntervalSets using GeometryBasics: Pyramid using Makie: volume +# COV_EXCL_STOP @testset "Unit tests" begin @testset "#659 Volume errors if data is not a cube" begin @@ -24,7 +26,6 @@ using Makie: volume include("pipeline.jl") include("record.jl") include("scenes.jl") - include("conversions.jl") include("quaternions.jl") include("projection_math.jl") include("observables.jl") @@ -39,4 +40,11 @@ using Makie: volume include("barplot.jl") include("bezier.jl") include("hist.jl") + + # TODO: move some things in here + include("convert_arguments.jl") + # from here + include("conversions.jl") + + include("float32convert.jl") end diff --git a/test/text.jl b/test/text.jl index 4881afa3a12..9416b5d375e 100644 --- a/test/text.jl +++ b/test/text.jl @@ -78,7 +78,7 @@ end positions, char_offsets, quad_offsets, uvs, scales = Makie.text_quads( atlas, to_ndim(Point3f, p.position[], 0), glyph_collection, - Vec2f(0), Makie.transform_func_obs(scene)[], :data + Vec2f(0), Makie.f32_conversion(scene), Makie.transform_func_obs(scene)[], :data ) # Also doesn't work @@ -118,7 +118,7 @@ end text([L"text", L"text"], position = [Point2f(0, 0), Point2f(1, 1)]) text(collect(zip([L"text", L"text"], [Point2f(0, 0), Point2f(1, 1)]))) - err = ArgumentError("The attribute `textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.") + err = ArgumentError("`textsize` has been renamed to `fontsize` in Makie v0.19. Please change all occurrences of `textsize` to `fontsize` or revert back to an earlier version.") @test_throws err Label(Figure()[1, 1], "hi", textsize = 30) - @test_throws err text(1, 2, text = "hi", textsize = 30) + # @test_throws err text(1, 2, text = "hi", textsize = 30) end diff --git a/test/transformations.jl b/test/transformations.jl index ff3366a9737..f7abc6e3d96 100644 --- a/test/transformations.jl +++ b/test/transformations.jl @@ -3,8 +3,8 @@ using LinearAlgebra function xyz_boundingbox(trans, points) bb_ref = Base.RefValue(Rect3f()) - Makie.foreach_transformed(points, Mat4f(I), trans) do point - Makie.update_boundingbox!(bb_ref, point) + foreach(points) do point + Makie.update_boundingbox!(bb_ref, Makie.apply_transform(trans, point)) end return bb_ref[] end @@ -52,10 +52,10 @@ end @test apply_transform(t1, p2) == Point(sqrt(2.0), sqrt(5.0)) @test apply_transform(t1, p3) == Point(sqrt(2.0), sqrt(5.0), sqrt(4.0)) - @test apply_transform(t2, p2) == Point2f(sqrt(2.0), log(5.0)) - @test apply_transform(t2, p3) == Point3f(sqrt(2.0), log(5.0), 4.0) + @test apply_transform(t2, p2) == Point2(sqrt(2.0), log(5.0)) + @test apply_transform(t2, p3) == Point3(sqrt(2.0), log(5.0), 4.0) - @test apply_transform(t3, p3) == Point3f(sqrt(2.0), log(5.0), log10(4.0)) + @test apply_transform(t3, p3) == Point3(sqrt(2.0), log(5.0), log10(4.0)) i2 = (identity, identity) i3 = (identity, identity, identity) @@ -88,6 +88,11 @@ end end @testset "Polar Transform" begin + function periodic_approx(a::VecTypes{2}, b::VecTypes{2}) + return all(((a .≈ b) .| (abs.(a .- b) .≈ 2.0 * pi))) + end + periodic_approx(as, bs) = all(periodic_approx.(as, bs)) + tf = Makie.Polar() @test tf.theta_as_x == true @test tf.clip_r == true @@ -95,42 +100,42 @@ end @test tf.direction == 1 @test tf.r0 == 0.0 - input = Point2f.([0, pi/3, pi/2, pi, 2pi, 3pi], 1:6) - output = [r * Point2f(cos(phi), sin(phi)) for (phi, r) in input] - inv = Point2f.(mod.([0, pi/3, pi/2, pi, 2pi, 3pi], (0..2pi,)), 1:6) + input = Point2.([0, pi/3, pi/2, pi, 2pi, 3pi], 1:6) + output = [r * Point2(cos(phi), sin(phi)) for (phi, r) in input] + inv = Point2.(mod.([0, pi/3, pi/2, pi, 2pi, 3pi], (0..2pi,)), 1:6) @test apply_transform(tf, input) ≈ output - @test apply_transform(Makie.inverse_transform(tf), output) ≈ inv + @test periodic_approx(apply_transform(Makie.inverse_transform(tf), output), inv) tf = Makie.Polar(pi/2, 1, 0, false) - input = Point2f.(1:6, [0, pi/3, pi/2, pi, 2pi, 3pi]) - output = [r * Point2f(cos(phi+pi/2), sin(phi+pi/2)) for (r, phi) in input] - inv = Point2f.(1:6, mod.([0, pi/3, pi/2, pi, 2pi, 3pi], (0..2pi,))) + input = Point2.(1:6, [0, pi/3, pi/2, pi, 2pi, 3pi]) + output = [r * Point2(cos(phi+pi/2), sin(phi+pi/2)) for (r, phi) in input] + inv = Point2.(1:6, mod.([0, pi/3, pi/2, pi, 2pi, 3pi], Ref(0..2pi))) @test apply_transform(tf, input) ≈ output - @test apply_transform(Makie.inverse_transform(tf), output) ≈ inv + @test periodic_approx(apply_transform(Makie.inverse_transform(tf), output), inv) tf = Makie.Polar(pi/2, -1, 0, false) - output = [r * Point2f(cos(-phi-pi/2), sin(-phi-pi/2)) for (r, phi) in input] + output = [r * Point2(cos(-phi-pi/2), sin(-phi-pi/2)) for (r, phi) in input] @test apply_transform(tf, input) ≈ output - @test apply_transform(Makie.inverse_transform(tf), output) ≈ inv + @test periodic_approx(apply_transform(Makie.inverse_transform(tf), output), inv) tf = Makie.Polar(pi/2, -1, 0.5, false) - output = [(r - 0.5) * Point2f(cos(-phi-pi/2), sin(-phi-pi/2)) for (r, phi) in input] + output = [(r - 0.5) * Point2(cos(-phi-pi/2), sin(-phi-pi/2)) for (r, phi) in input] @test apply_transform(tf, input) ≈ output - @test apply_transform(Makie.inverse_transform(tf), output) ≈ inv + @test periodic_approx(apply_transform(Makie.inverse_transform(tf), output), inv) tf = Makie.Polar(0, 1, 0, true) - input = Point2f.([0, pi/3, pi/2, pi, 2pi, 3pi], 1:6) - output = [r * Point2f(cos(phi), sin(phi)) for (phi, r) in input] - inv = Point2f.(mod.([0, pi/3, pi/2, pi, 2pi, 3pi], (0..2pi,)), 1:6) + input = Point2.([0, pi/3, pi/2, pi, 2pi, 3pi], 1:6) + output = [r * Point2(cos(phi), sin(phi)) for (phi, r) in input] + inv = Point2.(mod.([0, pi/3, pi/2, pi, 2pi, 3pi], (0..2pi,)), 1:6) @test apply_transform(tf, input) ≈ output - @test apply_transform(Makie.inverse_transform(tf), output) ≈ inv + @test periodic_approx(apply_transform(Makie.inverse_transform(tf), output), inv) tf = Makie.Polar(0, 1, 0, true, false) - input = Point2f.([0, pi/3, pi/2, pi, 2pi, 3pi], -6:-1) - output = [r * Point2f(cos(phi), sin(phi)) for (phi, r) in input] - inv = Point2f.(mod.([0, pi/3, pi/2, pi, 2pi, 3pi] .+ pi, (0..2pi,)), 6:-1:1) + input = Point2.([0, pi/3, pi/2, pi, 2pi, 3pi], -6:-1) + output = [r * Point2(cos(phi), sin(phi)) for (phi, r) in input] + inv = Point2.(mod.([0, pi/3, pi/2, pi, 2pi, 3pi] .+ pi, (0..2pi,)), 6:-1:1) @test apply_transform(tf, input) ≈ output - @test apply_transform(Makie.inverse_transform(tf), output) ≈ inv + @test periodic_approx(apply_transform(Makie.inverse_transform(tf), output), inv) end @testset "Coordinate Systems" begin @@ -149,19 +154,6 @@ end end end -@testset "Bounding box utilities" begin - - box = Rect2f(0,0,1,1) - - @test Makie.rotatedrect(box, π) == Rect2f(-1, -1, 1, 1) - - @test Makie.rotatedrect(box, π/2) == Rect2f(0, -1, 1, 1) - - @test all(Makie.rotatedrect(box, π/4).origin .≈ Rect2f(0, -1/(√2f0), √2f0, √2f0).origin) - @test all(Makie.rotatedrect(box, π/4).widths .≈ Rect2f(0, -1/(√2f0), √2f0, √2f0).widths) - -end - @testset "Space dependent transforms" begin t1 = sqrt t2 = (sqrt, log) @@ -183,9 +175,9 @@ end @test apply_transform(t1, p2, space) == desired_transform(p2, Point(sqrt(2.0), sqrt(5.0))) @test apply_transform(t1, p3, space) == desired_transform(p3, Point(sqrt(2.0), sqrt(5.0), sqrt(4.0))) - @test apply_transform(t2, p2, space) == desired_transform(p2, Point2f(sqrt(2.0), log(5.0))) - @test apply_transform(t2, p3, space) == desired_transform(p3, Point3f(sqrt(2.0), log(5.0), 4.0)) + @test apply_transform(t2, p2, space) == desired_transform(p2, Point2(sqrt(2.0), log(5.0))) + @test apply_transform(t2, p3, space) == desired_transform(p3, Point3(sqrt(2.0), log(5.0), 4.0)) - @test apply_transform(t3, p3, space) == desired_transform(p3, Point3f(sqrt(2.0), log(5.0), log10(4.0))) + @test apply_transform(t3, p3, space) == desired_transform(p3, Point3(sqrt(2.0), log(5.0), log10(4.0))) end end