diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 82969cd4b78..6e607064e81 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,7 @@ # These are supported funding model platforms -github: [SimonDanisch] -patreon: # Replace with a single Patreon username +github: [MakieOrg, SimonDanisch] +patreon: SimonDanisch # Replace with a single Patreon username open_collective: simon-danisch ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel diff --git a/.github/ISSUE_TEMPLATE/bug-report-.md b/.github/ISSUE_TEMPLATE/bug-report-.md index a1aa2ac01cf..c2c5e27fcd1 100644 --- a/.github/ISSUE_TEMPLATE/bug-report-.md +++ b/.github/ISSUE_TEMPLATE/bug-report-.md @@ -7,6 +7,6 @@ assignees: '' --- -- [ ] are you running newest version (version from docs) ? +- [ ] what version of Makie are you running? (`]st -m Makie`) - [ ] can you reproduce the bug with a fresh environment ? (`]activate --temp; add Makie`) - [ ] What platform + GPU are you on? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 52b35ce36d4..60b0f52994d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,27 @@ + # Description Fixes # (issue) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cadea1e2436..518c92598ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: matrix: version: - '1.6' - - '1' # automatically expands to the latest stable 1.x release of Julia + - '1' os: - ubuntu-20.04 arch: diff --git a/.github/workflows/reference_tests.yml b/.github/workflows/reference_tests.yml index 07b60c28037..69496a3ae67 100644 --- a/.github/workflows/reference_tests.yml +++ b/.github/workflows/reference_tests.yml @@ -25,7 +25,7 @@ jobs: matrix: version: - '1.6' - - '1' # automatically expands to the latest stable 1.x release of Julia + - '1' os: - ubuntu-20.04 arch: @@ -74,7 +74,7 @@ jobs: matrix: version: - '1.6' - - '1' # automatically expands to the latest stable 1.x release of Julia + - '1' os: - ubuntu-20.04 arch: @@ -124,7 +124,7 @@ jobs: matrix: version: - '1.6' - - '1' # automatically expands to the latest stable 1.x release of Julia + - '1' os: - ubuntu-20.04 arch: @@ -198,9 +198,10 @@ jobs: # 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 + # Concatenate scores.tsv, new_files.txt and missing_files.txt cat "${baseDir}/${dir}/scores.tsv" >> "./ReferenceImagesCombined/scores.tsv" cat "${baseDir}/${dir}/new_files.txt" >> "./ReferenceImagesCombined/new_files.txt" + cat "${baseDir}/${dir}/missing_files.txt" >> "./ReferenceImagesCombined/missing_files.txt" # Copy recorded folder mkdir -p "./ReferenceImagesCombined/recorded/${dir}/" diff --git a/.github/workflows/relocatability.yml b/.github/workflows/relocatability.yml index eca78b56bd2..d3e62f5c33f 100644 --- a/.github/workflows/relocatability.yml +++ b/.github/workflows/relocatability.yml @@ -26,7 +26,7 @@ jobs: fail-fast: false matrix: version: - - '1' # automatically expands to the latest stable 1.x release of Julia + - '1.10' os: - ubuntu-20.04 arch: diff --git a/.github/workflows/rprmakie.yaml b/.github/workflows/rprmakie.yaml index d52cc61a470..73d37684e1d 100644 --- a/.github/workflows/rprmakie.yaml +++ b/.github/workflows/rprmakie.yaml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: version: - - '1' # automatically expands to the latest stable 1.x release of Julia + - '1' os: - ubuntu-20.04 arch: diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e313d41ad..c3a51176e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,91 @@ ## [Unreleased] +- Changed image, heatmap and surface picking indices to correctly index the relevant matrix arguments. [#4459](https://github.com/MakieOrg/Makie.jl/pull/4459) +- Improved performance of `record` by avoiding unnecessary copying in common cases [#4475](https://github.com/MakieOrg/Makie.jl/pull/4475). +- Fix usage of `AggMean()` and other aggregations operating on 3d data for `datashader` [#4346](https://github.com/MakieOrg/Makie.jl/pull/4346). +- Changed default for `circular_rotation` in Camera3D to false, so that the camera doesn't change rotation direction anymore [4492](https://github.com/MakieOrg/Makie.jl/pull/4492) +- Fixed `pick(scene, rect2)` in WGLMakie [#4488](https://github.com/MakieOrg/Makie.jl/pull/4488) + +## [0.21.14] - 2024-10-11 + +- Fixed relocatability of GLMakie [#4461](https://github.com/MakieOrg/Makie.jl/pull/4461). +- Fixed relocatability of WGLMakie [#4467](https://github.com/MakieOrg/Makie.jl/pull/4467). +- Fixed `space` keyword for `barplot` [#4435](https://github.com/MakieOrg/Makie.jl/pull/4435). + +## [0.21.13] - 2024-10-07 + +- Optimize SpecApi, re-use Blocks better and add API to access the created block objects [#4354](https://github.com/MakieOrg/Makie.jl/pull/4354). +- Fixed `merge(attr1, attr2)` modifying nested attributes in `attr1` [#4416](https://github.com/MakieOrg/Makie.jl/pull/4416) +- Fixed issue with CairoMakie rendering scene backgrounds at the wrong position [#4425](https://github.com/MakieOrg/Makie.jl/pull/4425) +- Fixed incorrect inverse transformation in `position_on_plot` for lines, causing incorrect tooltip placement in DataInspector [#4402](https://github.com/MakieOrg/Makie.jl/pull/4402) +- Added new `Checkbox` block [#4336](https://github.com/MakieOrg/Makie.jl/pull/4336). +- Added ability to override legend element attributes by pairing labels or plots with override attributes [#4427](https://github.com/MakieOrg/Makie.jl/pull/4427). +- Added threshold before a drag starts which improves false negative rates for clicks. `Button` can now trigger on click and not mouse-down which is the canonical behavior in other GUI systems [#4336](https://github.com/MakieOrg/Makie.jl/pull/4336). +- `PolarAxis` font size now defaults to global figure `fontsize` in the absence of specific `Axis` theming [#4314](https://github.com/MakieOrg/Makie.jl/pull/4314) +- `MultiplesTicks` accepts new option `strip_zero=true`, allowing labels of the form `0x` to be `0` [#4372](https://github.com/MakieOrg/Makie.jl/pull/4372) +- Make near/far of WGLMakie JS 3d camera dynamic, for better depth_shift scaling [#4430](https://github.com/MakieOrg/Makie.jl/pull/4430). + +## [0.21.12] - 2024-09-28 + +- Fix NaN handling in WGLMakie [#4282](https://github.com/MakieOrg/Makie.jl/pull/4282). +- Show DataInspector tooltip on NaN values if `nan_color` has been set to other than `:transparent` [#4310](https://github.com/MakieOrg/Makie.jl/pull/4310) +- Fix `linestyle` not being used in `triplot` [#4332](https://github.com/MakieOrg/Makie.jl/pull/4332) +- Fix voxel clipping not being based on voxel centers [#4397](https://github.com/MakieOrg/Makie.jl/pull/4397) +- Parsing `Q` and `q` commands in svg paths with `BezierPath` is now supported [#4413](https://github.com/MakieOrg/Makie.jl/pull/4413) + +## [0.21.11] - 2024-09-13 + +- Hot fixes for 0.21.10 [#4356](https://github.com/MakieOrg/Makie.jl/pull/4356). +- Set `Voronoiplot`'s preferred axis type to 2D in all cases [#4349](https://github.com/MakieOrg/Makie.jl/pull/4349) + +## [0.21.10] - 2024-09-12 + +- Introduce `heatmap(Resampler(large_matrix))`, allowing to show big images interactively [#4317](https://github.com/MakieOrg/Makie.jl/pull/4317). +- Make sure we wait for the screen session [#4316](https://github.com/MakieOrg/Makie.jl/pull/4316). +- Fix for absrect [#4312](https://github.com/MakieOrg/Makie.jl/pull/4312). +- Fix attribute updates for SpecApi and SpecPlots (e.g. ecdfplot) [#4265](https://github.com/MakieOrg/Makie.jl/pull/4265). +- Bring back `poly` convert arguments for matrix with points as row [#4258](https://github.com/MakieOrg/Makie.jl/pull/4258). +- Fix gl_ClipDistance related segfault on WSL with GLMakie [#4270](https://github.com/MakieOrg/Makie.jl/pull/4270). +- Added option `label_position = :center` to place labels centered over each bar [#4274](https://github.com/MakieOrg/Makie.jl/pull/4274). +- `plotfunc()` and `func2type()` support functions ending with `!` [#4275](https://github.com/MakieOrg/Makie.jl/pull/4275). +- Fixed Boundserror in clipped multicolor lines in CairoMakie [#4313](https://github.com/MakieOrg/Makie.jl/pull/4313) +- Fix float precision based assertions error in GLMakie.volume [#4311](https://github.com/MakieOrg/Makie.jl/pull/4311) +- Support images with reversed axes [#4338](https://github.com/MakieOrg/Makie.jl/pull/4338) + +## [0.21.9] - 2024-08-27 + +- Hotfix for colormap + color updates [#4258](https://github.com/MakieOrg/Makie.jl/pull/4258). + +## [0.21.8] - 2024-08-26 + +- Fix selected list in `WGLMakie.pick_sorted` [#4136](https://github.com/MakieOrg/Makie.jl/pull/4136). +- Apply px per unit in `pick_closest`/`pick_sorted` [#4137](https://github.com/MakieOrg/Makie.jl/pull/4137). +- Support plot(interval, func) for rangebars and band [#4102](https://github.com/MakieOrg/Makie.jl/pull/4102). +- Fixed the broken OpenGL state cleanup for clip_planes which may cause plots to disappear randomly [#4157](https://github.com/MakieOrg/Makie.jl/pull/4157) +- Reduce updates for image/heatmap, improving performance [#4130](https://github.com/MakieOrg/Makie.jl/pull/4130). +- Add an informative error message to `save` when no backend is loaded [#4177](https://github.com/MakieOrg/Makie.jl/pull/4177) +- Fix rendering of `band` with NaN values [#4178](https://github.com/MakieOrg/Makie.jl/pull/4178). +- Fix plotting of lines with OffsetArrays across all backends [#4242](https://github.com/MakieOrg/Makie.jl/pull/4242). + +## [0.21.7] - 2024-08-19 + +- Hot fix for 1D heatmap [#4147](https://github.com/MakieOrg/Makie.jl/pull/4147). + +## [0.21.6] - 2024-08-14 + +- Fix RectangleZoom in WGLMakie [#4127](https://github.com/MakieOrg/Makie.jl/pull/4127) +- Bring back fastpath for regular heatmaps [#4125](https://github.com/MakieOrg/Makie.jl/pull/4125) +- Data inspector fixes (mostly for bar plots) [#4087](https://github.com/MakieOrg/Makie.jl/pull/4087) +- Added "clip_planes" as a new generic plot and scene attribute. Up to 8 world space clip planes can be specified to hide sections of a plot. [#3958](https://github.com/MakieOrg/Makie.jl/pull/3958) +- Updated handling of `model` matrices with active Float32 rescaling. This should fix issues with Float32-unsafe translations or scalings of plots, as well as rotated plots in Float32-unsafe ranges. [#4026](https://github.com/MakieOrg/Makie.jl/pull/4026) +- Added `events.tick` to allow linking actions like animations to the renderloop. [#3948](https://github.com/MakieOrg/Makie.jl/pull/3948) +- Added the `uv_transform` attribute for meshscatter, mesh, surface and image [#1406](https://github.com/MakieOrg/Makie.jl/pull/1406). +- Added the ability to use textures with `meshscatter` in WGLMakie [#1406](https://github.com/MakieOrg/Makie.jl/pull/1406). +- Don't remove underlying VideoStream file when doing save() [#3883](https://github.com/MakieOrg/Makie.jl/pull/3883). +- Fix label/legend for plotlist [#4079](https://github.com/MakieOrg/Makie.jl/pull/4079). +- Fix wrong order for colors in RPRMakie [#4098](https://github.com/MakieOrg/Makie.jl/pull/4098). +- Fixed incorrect distance calculation in `pick_closest` in WGLMakie [#4082](https://github.com/MakieOrg/Makie.jl/pull/4082). - Suppress keyboard shortcuts and context menu in JupyterLab output [#4068](https://github.com/MakieOrg/Makie.jl/pull/4068). - Introduce stroke_depth_shift + forward normal depth_shift for Poly [#4058](https://github.com/MakieOrg/Makie.jl/pull/4058). - Use linestyle for Poly and Density legend elements [#4000](https://github.com/MakieOrg/Makie.jl/pull/4000). @@ -10,7 +95,18 @@ - Fix label_formatter being called twice in barplot [#4046](https://github.com/MakieOrg/Makie.jl/pull/4046). - Fix error with automatic `highclip` or `lowclip` and scalar colors [#4048](https://github.com/MakieOrg/Makie.jl/pull/4048). - Correct a bug in the `project` function when projecting using a `Scene`. [#3909](https://github.com/MakieOrg/Makie.jl/pull/3909). +- Add position for `pie` plot [#4027](https://github.com/MakieOrg/Makie.jl/pull/4027). - Correct a method ambiguity in `insert!` which was causing `PlotList` to fail on CairoMakie. [#4038](https://github.com/MakieOrg/Makie.jl/pull/4038) +- Delaunay triangulations created via `tricontourf`, `triplot`, and `voronoiplot` no longer use any randomisation in the point insertion order so that results are unique. [#4044](https://github.com/MakieOrg/Makie.jl/pull/4044) +- Improve content scaling support for Wayland and fix incorrect mouse scaling on mac [#4062](https://github.com/MakieOrg/Makie.jl/pull/4062) +- Fix: `band` ignored its `alpha` argument in CairoMakie +- Fix `marker=FastPixel()` makersize and markerspace, improve `spy` recipe [#4043](https://github.com/MakieOrg/Makie.jl/pull/4043). +- Fixed `invert_normals` for surface plots in CairoMakie [#4021](https://github.com/MakieOrg/Makie.jl/pull/4021). +- Improve support for embedding GLMakie. [#4073](https://github.com/MakieOrg/Makie.jl/pull/4073) +- Update JS OrbitControls to match Julia OrbitControls [#4084](https://github.com/MakieOrg/Makie.jl/pull/4084). +- Fix `select_point()` [#4101](https://github.com/MakieOrg/Makie.jl/pull/4101). +- Fix `absrect()` and `select_rectangle()` [#4110](https://github.com/MakieOrg/Makie.jl/issues/4110). +- Allow segment-specific radius for `pie` plot [#4028](https://github.com/MakieOrg/Makie.jl/pull/4028). ## [0.21.5] - 2024-07-07 @@ -537,7 +633,16 @@ All other changes are collected [in this PR](https://github.com/MakieOrg/Makie.j - Fixed rendering of `heatmap`s with one or more reversed ranges in CairoMakie, as in `heatmap(1:10, 10:-1:1, rand(10, 10))` [#1100](https://github.com/MakieOrg/Makie.jl/pull/1100). - Fixed volume slice recipe and added docs for it [#1123](https://github.com/MakieOrg/Makie.jl/pull/1123). -[Unreleased]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.5...HEAD +[Unreleased]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.14...HEAD +[0.21.14]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.13...v0.21.14 +[0.21.13]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.12...v0.21.13 +[0.21.12]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.11...v0.21.12 +[0.21.11]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.10...v0.21.11 +[0.21.10]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.9...v0.21.10 +[0.21.9]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.8...v0.21.9 +[0.21.8]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.7...v0.21.8 +[0.21.7]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.6...v0.21.7 +[0.21.6]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.5...v0.21.6 [0.21.5]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.4...v0.21.5 [0.21.4]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.3...v0.21.4 [0.21.3]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.2...v0.21.3 diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index 0f0bbf8c94b..e3dbf0c22ce 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.12.5" +version = "0.12.14" [deps] CRC32c = "8bf52ea8-c179-5cab-976a-9e18b702a9bc" @@ -24,7 +24,7 @@ FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.11" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.5" +Makie = "=0.21.14" PrecompileTools = "1.0" julia = "1.3" diff --git a/CairoMakie/src/cairo-extension.jl b/CairoMakie/src/cairo-extension.jl index 36b7e3f496a..34e620f7e2c 100644 --- a/CairoMakie/src/cairo-extension.jl +++ b/CairoMakie/src/cairo-extension.jl @@ -10,6 +10,16 @@ function get_font_matrix(ctx) return matrix end +function pattern_set_matrix(ctx, matrix) + ccall((:cairo_pattern_set_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) +end + +function pattern_get_matrix(ctx) + matrix = Cairo.CairoMatrix() + ccall((:cairo_pattern_get_matrix, Cairo.libcairo), Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), ctx.ptr, Ref(matrix)) + return matrix +end + function cairo_font_face_destroy(font_face) ccall( (:cairo_font_face_destroy, Cairo.libcairo), diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 1b54a8fd8e5..0f70934682b 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -109,17 +109,25 @@ function prepare_for_scene(screen::Screen, scene::Scene) end function draw_background(screen::Screen, scene::Scene) + w, h = Makie.widths(viewport(Makie.root(scene))[]) + return draw_background(screen, scene, h) +end + +function draw_background(screen::Screen, scene::Scene, root_h) cr = screen.context Cairo.save(cr) if scene.clear[] bg = scene.backgroundcolor[] Cairo.set_source_rgba(cr, red(bg), green(bg), blue(bg), alpha(bg)); r = viewport(scene)[] - Cairo.rectangle(cr, origin(r)..., widths(r)...) # background + # Makie has (0,0) at bottom left, Cairo at top left. Makie extends up, + # Cairo down. Negative height breaks other backgrounds + x, y = origin(r); w, h = widths(r) + Cairo.rectangle(cr, x, root_h - y - h, w, h) # background fill(cr) end Cairo.restore(cr) - foreach(child_scene-> draw_background(screen, child_scene), scene.children) + foreach(child_scene-> draw_background(screen, child_scene, root_h), scene.children) end function draw_plot(scene::Scene, screen::Screen, primitive::Plot) @@ -179,7 +187,3 @@ end function draw_atomic(::Scene, ::Screen, x) @warn "$(typeof(x)) is not supported by cairo right now" end - -function draw_atomic(::Scene, ::Screen, x::Makie.PlotList) - # Doesn't need drawing -end diff --git a/CairoMakie/src/overrides.jl b/CairoMakie/src/overrides.jl index b9463250462..1788064c0f0 100644 --- a/CairoMakie/src/overrides.jl +++ b/CairoMakie/src/overrides.jl @@ -53,7 +53,8 @@ end function draw_poly(scene::Scene, screen::Screen, poly, points::Vector{<:Point2}, color::Union{Colorant, Cairo.CairoPattern}, model, strokecolor, strokestyle, strokewidth) space = to_value(get(poly, :space, :data)) - points = project_position.(Ref(poly), space, points, Ref(model)) + points = clip_poly(poly.clip_planes[], points, space, model) + points = _project_position(scene, space, points, model, true) Cairo.move_to(screen.context, points[1]...) for p in points[2:end] Cairo.line_to(screen.context, p...) @@ -83,10 +84,11 @@ end draw_poly(scene::Scene, screen::Screen, poly, rect::Rect2) = draw_poly(scene, screen, poly, [rect]) draw_poly(scene::Scene, screen::Screen, poly, bezierpath::BezierPath) = draw_poly(scene, screen, poly, [bezierpath]) -function draw_poly(scene::Scene, screen::Screen, poly, shapes::Vector{<:Union{Rect2,BezierPath}}) +function draw_poly(scene::Scene, screen::Screen, poly, shapes::Vector{<:Union{Rect2, BezierPath}}) model = poly.model[] space = to_value(get(poly, :space, :data)) - projected_shapes = project_shape.(Ref(poly), space, shapes, Ref(model)) + clipped_shapes = clip_shape.(Ref(poly.clip_planes[]), shapes, space, Ref(model)) + projected_shapes = project_shape.(Ref(poly), space, clipped_shapes, Ref(model)) color = to_cairo_color(poly.color[], poly) @@ -164,7 +166,9 @@ draw_poly(scene::Scene, screen::Screen, poly, circle::Circle) = draw_poly(scene, function draw_poly(scene::Scene, screen::Screen, poly, polygons::AbstractArray{<:Polygon}) model = poly.model[] space = to_value(get(poly, :space, :data)) - projected_polys = project_polygon.(Ref(poly), space, polygons, Ref(model)) + projected_polys = map(polygons) do polygon + return project_polygon(poly, space, polygon, poly.clip_planes[], model) + end color = to_cairo_color(poly.color[], poly) strokecolor = to_cairo_color(poly.strokecolor[], poly) @@ -184,7 +188,9 @@ end function draw_poly(scene::Scene, screen::Screen, poly, polygons::AbstractArray{<: MultiPolygon}) model = poly.model[] space = to_value(get(poly, :space, :data)) - projected_polys = project_multipolygon.(Ref(poly), space, polygons, Ref(model)) + projected_polys = map(polygons) do polygon + project_multipolygon(poly, space, polygon, poly.clip_planes[], model) + end color = to_cairo_color(poly.color[], poly) strokecolor = to_cairo_color(poly.strokecolor[], poly) @@ -210,24 +216,50 @@ end # gradients as well via `mesh` we have to intercept the poly use # ################################################################################ +function band_segment_ranges(lowerpoints, upperpoints) + ranges = UnitRange{Int}[] + start = nothing + + for i in eachindex(lowerpoints, upperpoints) + if isnan(lowerpoints[i]) || isnan(upperpoints[i]) + if start !== nothing && i - start > 1 # more than one point + push!(ranges, start:i-1) + end + start = nothing + elseif start === nothing + start = i + elseif i == lastindex(lowerpoints) + push!(ranges, start:i) + end + end + return ranges +end + function draw_plot(scene::Scene, screen::Screen, band::Band{<:Tuple{<:AbstractVector{<:Point2},<:AbstractVector{<:Point2}}}) if !(band.color[] isa AbstractArray) - color = to_cairo_color(band.color[], band) - upperpoints = band[1][] - lowerpoints = band[2][] - points = vcat(lowerpoints, reverse(upperpoints)) + basecolor = to_cairo_color(band.color[], band) + color = coloralpha(basecolor, alpha(basecolor) * band.alpha[]) + model = band.model[] space = to_value(get(band, :space, :data)) - points = project_position.(Ref(band), space, points, Ref(model)) - Cairo.move_to(screen.context, points[1]...) - for p in points[2:end] - Cairo.line_to(screen.context, p...) + + upperpoints = band[1][] + lowerpoints = band[2][] + + for rng in band_segment_ranges(lowerpoints, upperpoints) + points = vcat(@view(lowerpoints[rng]), reverse(@view(upperpoints[rng]))) + points = clip_poly(band.clip_planes[], points, space, model) + points = project_position.(Ref(band), space, points, Ref(model)) + Cairo.move_to(screen.context, points[1]...) + for p in points[2:end] + Cairo.line_to(screen.context, p...) + end + Cairo.close_path(screen.context) + set_source(screen.context, color) + Cairo.fill(screen.context) end - Cairo.close_path(screen.context) - set_source(screen.context, color) - Cairo.fill(screen.context) else for p in band.plots draw_plot(scene, screen, p) @@ -258,7 +290,7 @@ function draw_plot(scene::Scene, screen::Screen, tric::Tricontourf) polygons = pol[1][] model = pol.model[] space = to_value(get(pol, :space, :data)) - projected_polys = project_polygon.(Ref(tric), space, polygons, Ref(model)) + projected_polys = project_polygon.(Ref(tric), space, polygons, Ref(tric.clip_planes[]), Ref(model)) function draw_tripolys(polys, colornumbers, colors) for (i, (pol, colnum, col)) in enumerate(zip(polys, colornumbers, colors)) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index c349ffb790e..a87386c5ce9 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -3,9 +3,8 @@ ################################################################################ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Union{Lines, LineSegments})) - @get_attribute(primitive, (color, linewidth, linestyle)) + @get_attribute(primitive, (color, linewidth, linestyle, space, model)) ctx = screen.context - model = primitive[:model][] positions = primitive[1][] isempty(positions) && return @@ -26,88 +25,16 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio end end - space = to_value(get(primitive, :space, :data)) - # 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[]) - # color is now a color or an array of colors # if it's an array of colors, each segment must be stroked separately + color = to_color(primitive.calculated_colors[]) + + # Lines need to be handled more carefully with perspective projections to + # avoid them inverting. + # TODO: If we have neither perspective projection not clip_planes we can + # use the normal projection_position() here + projected_positions, color, linewidth = + project_line_points(scene, primitive, positions, color, linewidth) # The linestyle can be set globally, as we do here. # However, there is a discrepancy between Makie @@ -157,7 +84,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio draw_multi( primitive, ctx, projected_positions, - color, linewidth, indices, + color, linewidth, isnothing(linestyle) ? nothing : diff(Float64.(linestyle)) ) else @@ -201,9 +128,11 @@ end project_command(c::ClosePath, scene, space, model) = c function draw_single(primitive::Lines, ctx, positions) + isempty(positions) && return + n = length(positions) start = positions[begin] - + @inbounds for i in 1:n p = positions[i] # only take action for non-NaNs @@ -248,35 +177,27 @@ function draw_single(primitive::LineSegments, ctx, positions) Cairo.new_path(ctx) end -# if linewidth is not an array -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, indices, dash) - draw_multi(primitive, ctx, positions, [color for l in linewidths], linewidths, indices, dash) -end +# getindex if array, otherwise just return value +using Makie: sv_getindex -function draw_multi(primitive::LineSegments, ctx, positions, colors::AbstractArray, linewidths::AbstractArray, indices, dash) +function draw_multi(primitive::LineSegments, ctx, positions, colors, linewidths, dash) @assert iseven(length(positions)) - @assert length(positions) == length(colors) - @assert length(linewidths) == length(colors) for i in 1:2:length(positions) if isnan(positions[i+1]) || isnan(positions[i]) continue end - if linewidths[i] != linewidths[i+1] - error("Cairo doesn't support two different line widths ($(linewidths[i]) and $(linewidths[i+1])) at the endpoints of a line.") + lw = sv_getindex(linewidths, i) + if lw != sv_getindex(linewidths, i+1) + error("Cairo doesn't support two different line widths ($lw and $(sv_getindex(linewidths, i+1)) at the endpoints of a line.") end Cairo.move_to(ctx, positions[i]...) Cairo.line_to(ctx, positions[i+1]...) - Cairo.set_line_width(ctx, linewidths[i]) + Cairo.set_line_width(ctx, lw) - !isnothing(dash) && Cairo.set_dash(ctx, dash .* linewidths[i]) - c1 = colors[i] - c2 = colors[i+1] + !isnothing(dash) && Cairo.set_dash(ctx, dash .* lw) + c1 = sv_getindex(colors, i) + c2 = sv_getindex(colors, i+1) # we can avoid the more expensive gradient if the colors are the same # this happens if one color was given for each segment if c1 == c2 @@ -293,19 +214,19 @@ function draw_multi(primitive::LineSegments, ctx, positions, colors::AbstractArr end end -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) +function draw_multi(primitive::Lines, ctx, positions, colors, linewidths, dash) + isempty(positions) && return + + @assert !(colors isa AbstractVector) || length(colors) == length(positions) + @assert !(linewidths isa AbstractVector) || length(linewidths) == length(positions) - prev_color = colors[begin] - prev_linewidth = linewidths[begin] + prev_color = sv_getindex(colors, 1) + prev_linewidth = sv_getindex(linewidths, 1) prev_position = positions[begin] prev_nan = isnan(prev_position) prev_continued = false start = positions[begin] - + if !prev_nan # first is not nan, move_to Cairo.move_to(ctx, positions[begin]...) @@ -315,9 +236,9 @@ function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, lin for i in eachindex(positions)[begin+1:end] this_position = positions[i] - this_color = colors[i] + this_color = sv_getindex(colors, i) this_nan = isnan(this_position) - this_linewidth = linewidths[i] + this_linewidth = sv_getindex(linewidths, i) if this_nan # this is nan if prev_continued @@ -394,7 +315,7 @@ function draw_multi(primitive::Lines, ctx, positions, colors::AbstractArray, lin end prev_nan = this_nan prev_color = this_color - prev_linewidth = linewidths[i] + prev_linewidth = this_linewidth prev_position = this_position end end @@ -404,41 +325,47 @@ end ################################################################################ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Scatter)) - @get_attribute(primitive, (markersize, strokecolor, strokewidth, marker, marker_offset, rotation, transform_marker)) + @get_attribute(primitive, ( + markersize, strokecolor, strokewidth, marker, marker_offset, rotation, + transform_marker, model, markerspace, space, clip_planes) + ) + marker = cairo_scatter_marker(primitive.marker[]) # this goes through CairoMakie's conversion system and not Makie's... ctx = screen.context - model = primitive.model[] positions = primitive[1][] isempty(positions) && return size_model = transform_marker ? model : Mat4d(I) font = to_font(to_value(get(primitive, :font, Makie.defaultfont()))) - colors = to_color(primitive.calculated_colors[]) - markerspace = primitive.markerspace[] space = primitive.space[] transfunc = Makie.transform_func(primitive) return draw_atomic_scatter(scene, ctx, transfunc, colors, markersize, strokecolor, strokewidth, marker, marker_offset, rotation, model, positions, size_model, font, markerspace, - space) + space, clip_planes) end -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(rotation)) do point, col, +function draw_atomic_scatter( + scene, ctx, transfunc, colors, markersize, strokecolor, strokewidth, + marker, marker_offset, rotation, model, positions, size_model, font, + markerspace, space, clip_planes + ) + + transformed = apply_transform(transfunc, positions, space) + indices = unclipped_indices(to_model_space(model, clip_planes), transformed, space) + projected_positions = project_position(scene, space, transformed, indices, model) + + Makie.broadcast_foreach_index(projected_positions, indices, colors, markersize, strokecolor, + strokewidth, marker, marker_offset, remove_billboard(rotation)) do pos, col, markersize, strokecolor, strokewidth, m, mo, rotation + isnan(pos) && return + scale = project_scale(scene, markerspace, markersize, size_model) offset = project_scale(scene, markerspace, mo, size_model) - pos = project_position(scene, transfunc, space, point, model) - isnan(pos) && return - Cairo.set_source_rgba(ctx, rgbatuple(col)...) Cairo.save(ctx) @@ -454,6 +381,7 @@ function draw_atomic_scatter(scene, ctx, transfunc, colors, markersize, strokeco end Cairo.restore(ctx) end + return end @@ -531,7 +459,8 @@ function draw_marker(ctx, ::Type{<: Circle}, pos, scale, strokecolor, strokewidt nothing end -function draw_marker(ctx, ::Type{<: Rect}, pos, scale, strokecolor, strokewidth, marker_offset, rotation) +function draw_marker(ctx, ::Union{Makie.FastPixel,<:Type{<:Rect}}, pos, scale, strokecolor, strokewidth, + marker_offset, rotation) s2 = Point2((scale .* (1, -1))...) pos = pos .+ Point2f(marker_offset[1], -marker_offset[2]) Cairo.rotate(ctx, to_2d_rotation(rotation)) @@ -625,7 +554,7 @@ end function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Text{<:Tuple{<:Union{AbstractArray{<:Makie.GlyphCollection}, Makie.GlyphCollection}}})) ctx = screen.context - @get_attribute(primitive, (rotation, model, space, markerspace, offset)) + @get_attribute(primitive, (rotation, model, space, markerspace, offset, clip_planes)) transform_marker = to_value(get(primitive, :transform_marker, true))::Bool position = primitive.position[] # use cached glyph info @@ -633,7 +562,8 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Text draw_glyph_collection( scene, ctx, position, glyph_collection, remove_billboard(rotation), - model, space, markerspace, offset, primitive.transformation, transform_marker + model, space, markerspace, offset, primitive.transformation, transform_marker, + clip_planes ) nothing @@ -641,14 +571,15 @@ end function draw_glyph_collection( scene, ctx, positions, glyph_collections::AbstractArray, rotation, - model::Mat, space, markerspace, offset, transformation, transform_marker + model::Mat, space, markerspace, offset, transformation, transform_marker, + clip_planes ) # TODO: why is the Ref around model necessary? doesn't broadcast_foreach handle staticarrays matrices? broadcast_foreach(positions, glyph_collections, rotation, Ref(model), space, markerspace, offset) do pos, glayout, ro, mo, sp, msp, off - draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation, transform_marker) + draw_glyph_collection(scene, ctx, pos, glayout, ro, mo, sp, msp, off, transformation, transform_marker, clip_planes) end end @@ -657,7 +588,7 @@ _deref(x::Ref) = x[] function draw_glyph_collection( scene, ctx, position, glyph_collection, rotation, _model, space, - markerspace, offsets, transformation, transform_marker) + markerspace, offsets, transformation, transform_marker, clip_planes) glyphs = glyph_collection.glyphs glyphoffsets = glyph_collection.origins @@ -676,7 +607,9 @@ function draw_glyph_collection( # 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) + p = apply_transform(transform_func, position, space) + + Makie.is_data_space(space) && is_clipped(clip_planes, p) && return Makie.clip_to_space(scene.camera, markerspace) * Makie.space_to_clip(scene.camera, space) * @@ -796,15 +729,15 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: ctx = screen.context image = primitive[3][] xs, ys = primitive[1][], primitive[2][] - if !(xs isa AbstractVector) - l, r = extrema(xs) + if xs isa Makie.EndPoints + l, r = xs N = size(image, 1) xs = range(l, r, length = N+1) else xs = regularly_spaced_array_to_range(xs) end - if !(ys isa AbstractVector) - l, r = extrema(ys) + if ys isa Makie.EndPoints + l, r = ys N = size(image, 2) ys = range(l, r, length = N+1) else @@ -822,17 +755,7 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: t = Makie.transform_func(primitive) identity_transform = (t === identity || t isa Tuple && all(x-> x === identity, t)) && (abs(model[1, 2]) < 1e-15) regular_grid = xs isa AbstractRange && ys isa AbstractRange - xy_aligned = let - # Only allow scaling and translation - pv = scene.camera.projectionview[] - M = Mat4f( - pv[1, 1], 0.0, 0.0, 0.0, - 0.0, pv[2, 2], 0.0, 0.0, - 0.0, 0.0, pv[3, 3], 0.0, - pv[1, 4], pv[2, 4], pv[3, 4], 1.0 - ) - pv ≈ M - end + xy_aligned = Makie.is_translation_scale_matrix(scene.camera.projectionview[]) if interpolate if !regular_grid @@ -851,8 +774,23 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: xymax = project_position(primitive, space, Point2(last.(imsize)), model) w, h = xymax .- xy + uv_transform = if primitive isa Image + val = to_value(get(primitive, :uv_transform, I)) + T = Makie.convert_attribute(val, Makie.key"uv_transform"(), Makie.key"image"()) + # Cairo uses pixel units so we need to transform those to a 0..1 range, + # then apply uv_transform, then scale them back to pixel units. + # Cairo also doesn't have the yflip we have in OpenGL, so we need to + # invert y. + T3 = Mat3f(T[1], T[2], 0, T[3], T[4], 0, T[5], T[6], 1) + T3 = Makie.uv_transform(Vec2f(size(image))) * T3 * + Makie.uv_transform(Vec2f(0, 1), 1f0 ./ Vec2f(size(image, 1), -size(image, 2))) + T3[Vec(1, 2), Vec(1,2,3)] + else + Mat{2, 3, Float32}(1,0,0,1,0,0) + end + can_use_fast_path = !(is_vector && !interpolate) && regular_grid && identity_transform && - (interpolate || xy_aligned) + (interpolate || xy_aligned) && isempty(primitive.clip_planes[]) use_fast_path = can_use_fast_path && !disable_fast_path if use_fast_path @@ -860,7 +798,7 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: weird_cairo_limit = (2^15) - 23 if s.width > weird_cairo_limit || s.height > weird_cairo_limit - error("Cairo stops rendering images bigger than $(weird_cairo_limit), which is likely a bug in Cairo. Please resample your image/heatmap with e.g. `ImageTransformations.imresize`") + error("Cairo stops rendering images bigger than $(weird_cairo_limit), which is likely a bug in Cairo. Please resample your image/heatmap with heatmap(Resampler(data)).") end Cairo.rectangle(ctx, xy..., w, h) Cairo.save(ctx) @@ -875,13 +813,33 @@ function draw_atomic(scene::Scene, screen::Screen{RT}, @nospecialize(primitive:: end filt = interpolate ? Cairo.FILTER_BILINEAR : Cairo.FILTER_NEAREST Cairo.pattern_set_filter(p, filt) + pattern_set_matrix(p, Cairo.CairoMatrix(uv_transform...)) Cairo.fill(ctx) Cairo.restore(ctx) + pattern_set_matrix(p, Cairo.CairoMatrix(1, 0, 0, 1, 0, 0)) else # 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, [Point2(x, y) for x in xs, y in ys], model) + xys = let + ps = [Point2(x, y) for x in xs, y in ys] + transformed = apply_transform(transform_func(primitive), ps, space) + T = eltype(transformed) + + planes = if Makie.is_data_space(space) + to_model_space(model, primitive.clip_planes[]) + else + Plane3f[] + end + + for i in eachindex(transformed) + if is_clipped(planes, transformed[i]) + transformed[i] = T(NaN) + end + end + + _project_position(scene, space, transformed, model, true) + end colors = to_color(primitive.calculated_colors[]) # Note: xs and ys should have size ni+1, nj+1 @@ -899,6 +857,9 @@ function _draw_rect_heatmap(ctx, xys, ni, nj, colors) p2 = xys[i+1, j] p3 = xys[i+1, j+1] p4 = xys[i, j+1] + if isnan(p1) || isnan(p2) || isnan(p3) || isnan(p4) + continue + end # Rectangles and polygons that are directly adjacent usually show # white lines between them due to anti aliasing. To avoid this we @@ -941,7 +902,8 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki if !haskey(primitive, :faceculling) primitive[:faceculling] = Observable(-10) end - draw_mesh3D(scene, screen, primitive, mesh) + uv_transform = Makie.convert_attribute(primitive[:uv_transform][], Makie.key"uv_transform"(), Makie.key"mesh"()) + draw_mesh3D(scene, screen, primitive, mesh; uv_transform = uv_transform) end return nothing end @@ -953,6 +915,11 @@ function draw_mesh2D(scene, screen, @nospecialize(plot), @nospecialize(mesh)) 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}} + # Note: This assume the function is only called from mesh plots + uv_transform = Makie.convert_attribute(plot[:uv_transform][], Makie.key"uv_transform"(), Makie.key"mesh"()) + if uv isa Vector{Vec2f} && to_value(uv_transform) !== nothing + uv = map(uv -> uv_transform * to_ndim(Vec3f, uv, 1), uv) + end color = hasproperty(mesh, :color) ? to_color(mesh.color) : plot.calculated_colors[] cols = per_face_colors(color, nothing, fs, nothing, uv) return draw_mesh2D(screen, cols, vs, fs) @@ -1001,8 +968,11 @@ end nan2zero(x) = !isnan(x) * x -function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0, rotation = Mat4f(I)) - @get_attribute(attributes, (shading, diffuse, specular, shininess, faceculling)) +function draw_mesh3D( + scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f0, rotation = Mat4f(I), + uv_transform = Mat{2, 3, Float32}(1,0,0,1,0,0) + ) + @get_attribute(attributes, (shading, diffuse, specular, shininess, faceculling, clip_planes)) matcap = to_value(get(attributes, :matcap, nothing)) meshpoints = decompose(Point3f, mesh)::Vector{Point3f} @@ -1010,9 +980,12 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f meshnormals = decompose_normals(mesh)::Vector{Vec3f} # note: can be made NaN-aware. meshuvs = texturecoordinates(mesh)::Union{Nothing, Vector{Vec2f}} + if meshuvs isa Vector{Vec2f} && to_value(uv_transform) !== nothing + meshuvs = map(uv -> uv_transform * to_ndim(Vec3f, uv, 1), meshuvs) + end + # Priorize colors of the mesh if present color = hasproperty(mesh, :color) ? mesh.color : to_value(attributes.calculated_colors) - per_face_col = per_face_colors(color, matcap, meshfaces, meshnormals, meshuvs) model = attributes.model[]::Mat4d @@ -1027,11 +1000,15 @@ function draw_mesh3D(scene, screen, attributes, mesh; pos = Vec4f(0), scale = 1f shading_bool = shading != NoShading end + if to_value(get(attributes, :invert_normals, false)) + meshnormals .= -meshnormals + end + draw_mesh3D( scene, screen, space, func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, rotation, model, shading_bool::Bool, diffuse::Vec3f, - specular::Vec3f, shininess::Float32, faceculling::Int + specular::Vec3f, shininess::Float32, faceculling::Int, clip_planes ) end @@ -1039,7 +1016,7 @@ function draw_mesh3D( scene, screen, space, transform_func, meshpoints, meshfaces, meshnormals, per_face_col, pos, scale, rotation, model, shading, diffuse, - specular, shininess, faceculling + specular, shininess, faceculling, clip_planes ) ctx = screen.context projectionview = Makie.space_to_clip(scene.camera, space, true) @@ -1059,6 +1036,12 @@ function draw_mesh3D( return to_ndim(Vec4f, model_f32 * (local_model * p4d .+ to_ndim(Vec4f, pos, 0f0)), NaN32) end + valid = if Makie.is_data_space(space) + [is_visible(clip_planes, p) for p in vs] + else + Bool[] + end + ns = map(n -> normalize(normalmatrix * n), meshnormals) # Light math happens in view/camera space @@ -1105,9 +1088,15 @@ function draw_mesh3D( zorder = sortperm(average_zs) # Face culling - zorder = filter(i -> any(last.(ns[meshfaces[i]]) .> faceculling), zorder) + if isempty(clip_planes) || !Makie.is_data_space(space) + zorder = filter(i -> any(last.(ns[meshfaces[i]]) .> faceculling), zorder) + else + zorder = filter(i -> all(valid[meshfaces[i]]), zorder) + end - draw_pattern(ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, lightdirection, light_color, shininess, diffuse, ambient, specular) + draw_pattern( + ctx, zorder, shading, meshfaces, ts, per_face_col, ns, vs, + lightdirection, light_color, shininess, diffuse, ambient, specular) return end @@ -1194,7 +1183,8 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki if !haskey(primitive, :faceculling) primitive[:faceculling] = Observable(-10) end - draw_mesh3D(scene, screen, primitive, mesh) + uv_transform = Makie.convert_attribute(primitive[:uv_transform][], Makie.key"uv_transform"(), Makie.key"surface"()) + draw_mesh3D(scene, screen, primitive, mesh; uv_transform = uv_transform) primitive[:color] = old return nothing end @@ -1221,33 +1211,31 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki color = to_color(primitive.calculated_colors[]) submesh = Attributes( - model=model, + model = model, calculated_colors = color, - shading=primitive.shading, diffuse=primitive.diffuse, - specular=primitive.specular, shininess=primitive.shininess, - faceculling=get(primitive, :faceculling, -10), - transformation=Makie.transformation(primitive) - + shading = primitive.shading, diffuse = primitive.diffuse, + specular = primitive.specular, shininess = primitive.shininess, + faceculling = get(primitive, :faceculling, -10), + transformation = Makie.transformation(primitive), + clip_planes = primitive.clip_planes ) submesh[:model] = model scales = primitive[:markersize][] + uv_transform = Makie.convert_attribute(primitive[:uv_transform][], Makie.key"uv_transform"(), Makie.key"meshscatter"()) for i in zorder p = pos[i] if color isa AbstractVector submesh[:calculated_colors] = color[i] end scale = markersize isa Vector ? markersize[i] : markersize - _rotation = if rotation isa Vector - Makie.rotationmatrix4(to_rotation(rotation[i])) - else - Makie.rotationmatrix4(to_rotation(rotation)) - end + _rotation = Makie.rotationmatrix4(to_rotation(Makie.sv_getindex(rotation, i))) + _uv_transform = Makie.sv_getindex(uv_transform, i) draw_mesh3D( scene, screen, submesh, marker, pos = p, scale = scale isa Real ? Vec3f(scale) : to_ndim(Vec3f, scale, 1f0), - rotation = _rotation + rotation = _rotation, uv_transform = _uv_transform ) end @@ -1265,7 +1253,14 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki pos = Makie.voxel_positions(primitive) scale = Makie.voxel_size(primitive) colors = Makie.voxel_colors(primitive) - marker = normal_mesh(Rect3f(Point3f(-0.5), Vec3f(1))) + marker = GeometryBasics.normal_mesh(Rect3f(Point3f(-0.5), Vec3f(1))) + + # Face culling + if !isempty(primitive.clip_planes[]) && Makie.is_data_space(primitive.space[]) + valid = [is_visible(primitive.clip_planes[], p) for p in pos] + pos = pos[valid] + colors = colors[valid] + end # For correct z-ordering we need to be in view/camera or screen space model = copy(primitive.model[]) @@ -1278,11 +1273,12 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki 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) + model = model, + shading = primitive.shading, diffuse = primitive.diffuse, + specular = primitive.specular, shininess = primitive.shininess, + faceculling = get(primitive, :faceculling, -10), + transformation = Makie.transformation(primitive), + clip_planes = Plane3f[] ) for i in zorder diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index e24b488332a..b2fc618fa0b 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -204,8 +204,7 @@ Base.size(screen::Screen) = round.(Int, (screen.surface.width, screen.surface.he # we render the scene directly, since we have # no screen dependent state like in e.g. opengl Base.insert!(screen::Screen, scene::Scene, plot) = nothing -# to resolve method ambiguity, since this method is defined in Makie for MakieScreen and PlotList: -Base.insert!(screen::Screen, scene::Scene, plot::Plot{plotlist}) = nothing + function Base.delete!(screen::Screen, scene::Scene, plot::AbstractPlot) # Currently, we rerender every time, so nothing needs # to happen here. However, in the event that changes, diff --git a/CairoMakie/src/utils.jl b/CairoMakie/src/utils.jl index 859edc5009a..fcf49b1d2ad 100644 --- a/CairoMakie/src/utils.jl +++ b/CairoMakie/src/utils.jl @@ -2,14 +2,52 @@ # Projection utilities # ################################################################################ -function project_position(scene::Scene, transform_func::T, space, point, model::Mat4, yflip::Bool = true) where T + +using Makie: apply_transform, transform_func, unclipped_indices, to_model_space, + broadcast_foreach_index, is_clipped, is_visible + +function project_position(scene::Scene, transform_func::T, space::Symbol, point, model::Mat4, yflip::Bool = true) where T # use transform func point = Makie.apply_transform(transform_func, point, space) _project_position(scene, space, point, model, yflip) end # much faster than dot-ing `project_position` because it skips all the repeated mat * mat +function project_position( + scene::Scene, space::Symbol, ps::Vector{<: VecTypes{N, T1}}, + indices::Vector{<:Integer}, model::Mat4, yflip::Bool = true + ) 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 = Vector{Point2f}(undef, length(indices)) + + @inbounds for (i_out, i_in) in enumerate(indices) + p4d = to_ndim(Point4d, to_ndim(Point3d, ps[i_in], 0), 1) + px_pos = transform * p4d + output[i_out] = px_pos[Vec(1, 2)] / px_pos[3] + end + + return output +end + function _project_position(scene::Scene, space, ps::AbstractArray{<: VecTypes{N, T1}}, model, yflip::Bool) where {N, T1} + return project_position(scene, space, ps, eachindex(ps), model, yflip) +end + +function project_position( + scene::Scene, space::Symbol, ps::AbstractArray{<: VecTypes{N, T1}}, + indices::Base.OneTo, model::Mat4, yflip::Bool = true + ) where {N, T1} + transform = let f32convert = Makie.f32_convert_matrix(scene.float32convert, space) M = Makie.space_to_clip(scene.camera, space) * model * f32convert @@ -22,7 +60,7 @@ function _project_position(scene::Scene, space, ps::AbstractArray{<: VecTypes{N, output = similar(ps, Point2f) - @inbounds for i in eachindex(ps) + @inbounds for i in indices 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] @@ -78,21 +116,322 @@ function project_shape(@nospecialize(scenelike), space, rect::Rect, model) return Rect(mini, maxi .- mini) end -function project_polygon(@nospecialize(scenelike), space, poly::Polygon{N, T}, model) where {N, T} +function clip_poly(clip_planes::Vector{Plane3f}, ps::Vector{PT}, space::Symbol, model::Mat4) where {PT <: VecTypes{2}} + if isempty(clip_planes) || !Makie.is_data_space(space) + return ps + end + + planes = to_model_space(model, clip_planes) + last_distance = Makie.min_clip_distance(planes, first(ps)) + last_point = first(ps) + output = sizehint!(PT[], length(ps)) + + for p in ps + d = Makie.min_clip_distance(planes, p) + if (last_distance < 0) && (d >= 0) # clipped -> unclipped + # point between last and this on clip plane + clip_point = - last_distance * (p - last_point) / (d - last_distance) + last_point + push!(output, clip_point, p) + elseif (last_distance >= 0) && (d < 0) # unclipped -> clipped + clip_point = - last_distance * (p - last_point) / (d - last_distance) + last_point + push!(output, clip_point) + elseif (last_distance >= 0) && (d >= 0) # unclipped -> unclipped + push!(output, p) + end + last_point = p + last_distance = d + end + + return output +end + +function clip_shape(clip_planes::Vector{Plane3f}, shape::Rect2, space::Symbol, model::Mat4) + if !Makie.is_data_space(space) || isempty(clip_planes) + return shape + end + + xy = origin(shape) + w, h = widths(shape) + ps = [xy, xy + Vec2(w, 0), xy + Vec2f(w, h), xy + Vec2(0, h)] + if any(p -> Makie.is_clipped(clip_planes, p), ps) + push!(ps, xy) + ps = clip_poly(clip_planes, ps, space, model) + return BezierPath([MoveTo(ps[1]), LineTo.(ps[2:end])..., ClosePath()]) + else + return shape + end +end + +function clip_shape(clip_planes::Vector{Plane3f}, shape::BezierPath, space::Symbol, model::Mat4) + return shape +end + +function project_polygon(@nospecialize(scenelike), space, poly::Polygon{N, T}, clip_planes, 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] + + ext_proj = PT[project(p) for p in clip_poly(clip_planes, ext, space, model)] + interiors_proj = Vector{PT}[ + PT[project(p) for p in clip_poly(clip_planes, decompose(PT, points), space, model)] + for points in poly.interiors] + return Polygon(ext_proj, interiors_proj) end -function project_multipolygon(@nospecialize(scenelike), space, multipoly::MP, model) where MP <: MultiPolygon - return MultiPolygon(project_polygon.(Ref(scenelike), Ref(space), multipoly.polygons, Ref(model))) +function project_multipolygon(@nospecialize(scenelike), space, multipoly::MP, clip_planes, model) where MP <: MultiPolygon + return MultiPolygon(project_polygon.(Ref(scenelike), Ref(space), multipoly.polygons, Ref(clip_planes), Ref(model))) end scale_matrix(x, y) = Cairo.CairoMatrix(x, 0.0, 0.0, y, 0.0, 0.0) +function clip2screen(p, res) + s = Vec2f(0.5f0, -0.5f0) .* p[Vec(1, 2)] / p[4].+ 0.5f0 + return res .* s +end + +@generated function project_line_points(scene, plot::T, positions, colors, linewidths) where {T <: Union{Lines, LineSegments}} + # If colors are defined per point they need to be interpolated like positions + # at clip planes + per_point_colors = colors <: AbstractArray + per_point_linewidths = (T <: Lines) && (linewidths <: AbstractArray) + + quote + @get_attribute(plot, (space, model)) + + # Standard transform from input space to clip space + points = Makie.apply_transform(transform_func(plot), positions, space) + f32convert = Makie.f32_convert_matrix(scene.float32convert, space) + transform = Makie.space_to_clip(scene.camera, space) * model * f32convert + clip_points = Vector{Vec4f}(undef, length(points)) + @inbounds for (i, point) in enumerate(points) + clip_points[i] = transform * to_ndim(Vec4d, to_ndim(Vec3d, point, 0), 1) + end + + # yflip and clip -> screen/pixel coords + res = scene.camera.resolution[] + + # clip planes in clip space + clip_planes = if Makie.is_data_space(space) + Makie.to_clip_space(scene.camera.projectionview[], plot.clip_planes[]) + else + Makie.Plane3f[] + end + + # Fix lines with points far outside the clipped region not drawing at all + # TODO this can probably be done more efficiently by checking -1 ≤ x, y ≤ 1 + # directly and calculating intersections directly (1D) + push!(clip_planes, + Plane3f(Vec3f(-1, 0, 0), -1f0), Plane3f(Vec3f(+1, 0, 0), -1f0), + Plane3f(Vec3f(0, -1, 0), -1f0), Plane3f(Vec3f(0, +1, 0), -1f0) + ) + + + # outputs + screen_points = sizehint!(Vec2f[], length(clip_points)) + $(if per_point_colors + quote + color_output = sizehint!(eltype(colors)[], length(clip_points)) + skipped_color = RGBAf(1,0,1,1) # for debug purposes, should not show + end + end) + $(if per_point_linewidths + quote + linewidth_output = sizehint!(eltype(linewidths)[], length(clip_points)) + end + end) + + # Handling one segment per iteration + if plot isa Lines + + last_is_nan = true + for i in 1:length(clip_points)-1 + hidden = false + disconnect1 = false + disconnect2 = false + + $(if per_point_colors + quote + c1 = colors[i] + c2 = colors[i+1] + end + end) + + p1 = clip_points[i] + p2 = clip_points[i+1] + v = p2 - p1 + + # Handle near/far clipping + if p1[4] <= 0.0 + disconnect1 = true + p1 = p1 + (-p1[4] - p1[3]) / (v[3] + v[4]) * v + $(if per_point_colors + :(c1 = c1 + (-p1[4] - p1[3]) / (v[3] + v[4]) * (c2 - c1)) + end) + end + if p2[4] <= 0.0 + disconnect2 = true + p2 = p2 + (-p2[4] - p2[3]) / (v[3] + v[4]) * v + $(if per_point_colors + :(c2 = c2 + (-p2[4] - p2[3]) / (v[3] + v[4]) * (c2 - c1)) + end) + end + + for plane in clip_planes + d1 = dot(plane.normal, Vec3f(p1)) - plane.distance * p1[4] + d2 = dot(plane.normal, Vec3f(p2)) - plane.distance * p2[4] + + if (d1 <= 0.0) && (d2 <= 0.0) + # start and end clipped by one plane -> not visible + hidden = true + break; + elseif (d1 < 0.0) && (d2 > 0.0) + # p1 clipped, move it towards p2 until unclipped + disconnect1 = true + p1 = p1 - d1 * (p2 - p1) / (d2 - d1) + $(if per_point_colors + :(c1 = c1 - d1 * (c2 - c1) / (d2 - d1)) + end) + elseif (d1 > 0.0) && (d2 < 0.0) + # p2 clipped, move it towards p1 until unclipped + disconnect2 = true + p2 = p2 - d2 * (p1 - p2) / (d1 - d2) + $(if per_point_colors + :(c2 = c2 - d2 * (c1 - c2) / (d1 - d2)) + end) + end + end + + if hidden && !last_is_nan + # if segment hidden make sure the line separates + last_is_nan = true + push!(screen_points, Vec2f(NaN)) + $(if per_point_linewidths + :(push!(linewidth_output, linewidths[i])) + end) + $(if per_point_colors + :(push!(color_output, c1)) + end) + elseif !hidden + # if not hidden, always push the first element to 1:end-1 line points + + # if the start of the segment is disconnected (moved), make sure the + # line separates before it + if disconnect1 && !last_is_nan + push!(screen_points, Vec2f(NaN)) + $(if per_point_linewidths + :(push!(linewidth_output, linewidths[i])) + end) + $(if per_point_colors + :(push!(color_output, c1)) + end) + end + + last_is_nan = false + push!(screen_points, clip2screen(p1, res)) + $(if per_point_linewidths + :(push!(linewidth_output, linewidths[i])) + end) + $(if per_point_colors + :(push!(color_output, c1)) + end) + + # if the end of the segment is disconnected (moved), add the adjusted + # point and separate it from from the next segment + if disconnect2 + last_is_nan = true + push!(screen_points, clip2screen(p2, res), Vec2f(NaN)) + $(if per_point_linewidths + :(push!(linewidth_output, linewidths[i+1], linewidths[i+1])) + end) + $(if per_point_colors + :(push!(color_output, c2, c2)) # relevant, irrelevant + end) + end + end + end + + # If last_is_nan == true, the last segment is either hidden or the moved + # end point has been added. If it is false we're missing the last regular + # clip_points + if !last_is_nan + push!(screen_points, clip2screen(clip_points[end], res)) + $(if per_point_linewidths + :(push!(linewidth_output, linewidths[end])) + end) + $(if per_point_colors + :(push!(color_output, colors[end])) + end) + end + + else # LineSegments + + for i in 1:2:length(clip_points)-1 + $(if per_point_colors + quote + c1 = colors[i] + c2 = colors[i+1] + end + end) + + p1 = clip_points[i] + p2 = clip_points[i+1] + v = p2 - p1 + + # Handle near/far clipping + if p1[4] <= 0.0 + p1 = p1 + (-p1[4] - p1[3]) / (v[3] + v[4]) * v + $(if per_point_colors + :(c1 = c1 + (-p1[4] - p1[3]) / (v[3] + v[4]) * (c2 - c1)) + end) + end + if p2[4] <= 0.0 + p2 = p2 + (-p2[4] - p2[3]) / (v[3] + v[4]) * v + $(if per_point_colors + :(c2 = c2 + (-p2[4] - p2[3]) / (v[3] + v[4]) * (c2 - c1)) + end) + end + + for plane in clip_planes + d1 = dot(plane.normal, Vec3f(p1)) - plane.distance * p1[4] + d2 = dot(plane.normal, Vec3f(p2)) - plane.distance * p2[4] + + if (d1 <= 0.0) && (d2 <= 0.0) + # start and end clipped by one plane -> not visible + # to keep index order we just set p1 and p2 to NaN and insert anyway + p1 = Vec4f(NaN) + p2 = Vec4f(NaN) + break; + elseif (d1 < 0.0) && (d2 > 0.0) + # p1 clipped, move it towards p2 until unclipped + p1 = p1 - d1 * (p2 - p1) / (d2 - d1) + $(if per_point_colors + :(c1 = c1 - d1 * (c2 - c1) / (d2 - d1)) + end) + elseif (d1 > 0.0) && (d2 < 0.0) + # p2 clipped, move it towards p1 until unclipped + p2 = p2 - d2 * (p1 - p2) / (d1 - d2) + $(if per_point_colors + :(c2 = c2 - d2 * (c1 - c2) / (d1 - d2)) + end) + end + end + + # no need to disconnected segments, just insert adjusted points + push!(screen_points, clip2screen(p1, res), clip2screen(p2, res)) + $(if per_point_colors + :(push!(color_output, c1, c2)) + end) + end + + end + + return screen_points, $(ifelse(per_point_colors, :color_output, :colors)), + $(ifelse(per_point_linewidths, :linewidth_output, :linewidths)) + end +end + + ######################################## # Rotation handling # ######################################## @@ -256,16 +595,17 @@ function per_face_colors(_color, matcap, faces, normals, uv) elseif color isa Colorant return FaceIterator{:Const}(color, faces) elseif color isa AbstractVector{<: Colorant} - return FaceIterator(color, faces) + return FaceIterator{:PerVert}(color, faces) elseif color isa Makie.AbstractPattern # let next level extend and fill with CairoPattern return color elseif color isa AbstractMatrix{<: Colorant} && !isnothing(uv) - wsize = reverse(size(color)) + wsize = size(color) wh = wsize .- 1 + # nearest cvec = map(uv) do uv x, y = clamp.(round.(Int, Tuple(uv) .* wh) .+ 1, 1, wsize) - return color[end - (y - 1), x] + return color[x, y] end # TODO This is wrong and doesn't actually interpolate # Inside the triangle sampling the color image diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index 49158cdb546..01b2f4283a9 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -123,15 +123,25 @@ end N = 3 points = Observable(Point2f[]) f, ax, pl = scatter(points, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(size=(600, 800),)) + vio = Makie.VideoStream(f; format="mp4", px_per_unit=2.0, backend=CairoMakie) + tmp_path = vio.path + @test vio.screen isa CairoMakie.Screen{CairoMakie.IMAGE} @test size(vio.screen) == size(f.scene) .* 2 @test vio.screen.device_scaling_factor == 2.0 Makie.recordframe!(vio) save("test.mp4", vio) - @test isfile("test.mp4") # Make sure no error etc - rm("test.mp4") + save("test_2.mkv", vio) + save("test_3.mp4", vio) + # make sure all files are correctly saved: + @test all(isfile, ["test.mp4", "test_2.mkv", "test_3.mp4"]) + @test filesize("test.mp4") == filesize("test_3.mp4") > 3000 + @test filesize("test.mp4") != filesize("test_2.mkv") > 3000 + rm.(["test.mp4", "test_2.mkv", "test_3.mp4"]) + finalize(vio); yield() + @test !isfile(tmp_path) end @testset "plotlist no ambiguity (#4038)" begin @@ -140,59 +150,42 @@ end plotlist!([Makie.SpecApi.Scatter(1:10)]) end +@testset "multicolor line clipping (#4313)" begin + fig, ax, p = contour(rand(20,20)) + xlims!(ax, 0, 10) + Makie.colorbuffer(fig; backend=CairoMakie) +end excludes = Set([ - "Colored Mesh", "Line GIF", "Streamplot animation", - "Line changing colour", "Axis + Surface", "Streamplot 3D", "Meshscatter Function", - "Hollow pie chart", "Record Video", - "Image on Geometry (Earth)", - "Image on Geometry (Moon)", + # "mesh textured and loaded", # bad texture resolution on mesh "Comparing contours, image, surfaces and heatmaps", - "Textured Mesh", - "Simple pie chart", "Animated surface and wireframe", - "Open pie chart", - "image scatter", "surface + contour3d", - "Orthographic Camera", - "Legend", - "rotation", + "Orthographic Camera", # This renders blank, why? "3D Contour with 2D contour slices", "Surface with image", - "Test heatmap + image overlap", - "Text Annotation", - "step-2", - "FEM polygon 2D.png", - "Text rotation", - "Image on Surface Sphere", - "FEM mesh 2D", - "Hbox", - "Subscenes", + "FEM poly and mesh", # different color due to bad colormap resolution on mesh + "Image on Surface Sphere", # bad texture resolution "Arrows 3D", - "Layouting", - # sigh this is actually super close, - # but doesn't interpolate the values inside the - # triangles, so looks pretty different - "FEM polygon 2D", "Connected Sphere", # markers too big, close otherwise, needs to be assimilated with glmakie - "Unicode Marker", "Depth Shift", "Order Independent Transparency", - "heatmap transparent colormap", "fast pixel marker", - "scatter with glow", - "scatter with stroke", - "heatmaps & surface", + "scatter with glow", # some are missing + "scatter with stroke", # stroke acts inward in CairoMakie, outwards in W/GLMakie + "heatmaps & surface", # different nan_colors in surface "Textured meshscatter", # not yet implemented "Voxel - texture mapping", # not yet implemented "Miter Joints for line rendering", # CairoMakie does not show overlap here + "Scatter with FastPixel", # almost works, but scatter + markerspace=:data seems broken for 3D + "picking", # Not implemented ]) functions = [:volume, :volume!, :uv_mesh] @@ -201,7 +194,7 @@ functions = [:volume, :volume!, :uv_mesh] CairoMakie.activate!(type = "png", px_per_unit = 1) ReferenceTests.mark_broken_tests(excludes, functions=functions) recorded_files, recording_dir = @include_reference_tests CairoMakie "refimages.jl" - missing_images, scores = ReferenceTests.record_comparison(recording_dir) + missing_images, scores = ReferenceTests.record_comparison(recording_dir, "CairoMakie") ReferenceTests.test_comparison(scores; threshold = 0.05) end @@ -238,3 +231,47 @@ end @test_throws ArgumentError save(filename, Figure(), pdf_version="foo") end + +@testset "Tick Events" begin + f, a, p = scatter(rand(10)); + @test events(f).tick[] == Makie.Tick() + + filename = "$(tempname()).png" + try + save(filename, f) + tick = events(f).tick[] + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == 0 + @test tick.time == 0.0 + @test tick.delta_time == 0.0 + finally + rm(filename) + end + + filename = "$(tempname()).mp4" + try + tick_record = Makie.Tick[] + record(_ -> push!(tick_record, events(f).tick[]), f, filename, 1:10, framerate = 30) + dt = 1.0 / 30.0 + + for (i, tick) in enumerate(tick_record) + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == i-1 + @test tick.time ≈ dt * (i-1) + @test tick.delta_time ≈ dt + end + finally + rm(filename) + end + + # test destruction of tick overwrite + f, a, p = scatter(rand(10)); + let + io = VideoStream(f) + @test events(f).tick[] == Makie.Tick(Makie.OneTimeRenderTick, 0, 0.0, 1.0 / io.options.framerate) + nothing + end + tick = Makie.Tick(Makie.UnknownTickState, 1, 1.0, 1.0) + events(f).tick[] = tick + @test events(f).tick[] == tick +end \ No newline at end of file diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index c0a435364e2..cbd5fe66c22 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -1,6 +1,6 @@ name = "GLMakie" uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" -version = "0.10.5" +version = "0.10.14" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -27,10 +27,10 @@ Colors = "0.11, 0.12" FileIO = "1.6" FixedPointNumbers = "0.7, 0.8" FreeTypeAbstraction = "0.10" -GLFW = "3.3" +GLFW = "3.4.3" GeometryBasics = "0.4.11" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.5" +Makie = "=0.21.14" Markdown = "1.0, 1.6" MeshIO = "0.4" ModernGL = "1" diff --git a/GLMakie/assets/loading.bin b/GLMakie/assets/loading.bin deleted file mode 100644 index 6198c533be5..00000000000 Binary files a/GLMakie/assets/loading.bin and /dev/null differ diff --git a/GLMakie/assets/shader/dots.frag b/GLMakie/assets/shader/dots.frag index 4f08d9a57fe..8274ea212a4 100644 --- a/GLMakie/assets/shader/dots.frag +++ b/GLMakie/assets/shader/dots.frag @@ -5,6 +5,23 @@ flat in uvec2 o_objectid; void write2framebuffer(vec4 color, uvec2 id); +uniform mat4 projection; +uniform vec2 scale; +uniform int marker_shape; + void main(){ - write2framebuffer(o_color, o_objectid); + vec2 p = gl_PointCoord - 0.5; // Normalized coordinates centered at (0.5, 0.5) + float len = length(p * 2.0); // Length from center, scale to range [-1, 1] + float alpha; + vec4 color = o_color; + if (marker_shape == 1) { + alpha = 1.0; + } else if (marker_shape == 2) { + alpha = 1.0 - smoothstep(0.0, 1.0, len);// Smoothstep for smooth transition + } else { + alpha = 1.0 - smoothstep(0.0, 1.0, len);// Ensure alpha is in [0, 1] range + gl_FragDepth -= abs(projection[3][2] * alpha); + alpha = 1; + } + write2framebuffer(vec4(color.rgb, alpha * color.a), o_objectid); } diff --git a/GLMakie/assets/shader/dots.vert b/GLMakie/assets/shader/dots.vert index 0e7f4dd3021..f4e8d984418 100644 --- a/GLMakie/assets/shader/dots.vert +++ b/GLMakie/assets/shader/dots.vert @@ -8,13 +8,15 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d {{color_type}} color; {{color_norm_type}} color_norm; {{color_map_type}} color_map; +{{scale_type}} scale; + +uniform vec2 resolution; uniform uint objectid; uniform float depth_shift; flat out vec4 o_color; flat out uvec2 o_objectid; - float _normalize(float val, float from, float to){return (val-from) / (to - from);} vec4 color_lookup(float intensity, sampler1D color_ramp, vec2 norm){ @@ -32,11 +34,44 @@ void colorize(sampler1D color, float intensity, vec2 color_norm){ vec4 _position(vec3 p){return vec4(p,1);} vec4 _position(vec2 p){return vec4(p,0,1);} -uniform mat4 projectionview, model; +uniform int num_clip_planes; +uniform vec4 clip_planes[8]; +out float gl_ClipDistance[8]; + +void process_clip_planes(vec3 world_pos) +{ + // distance = dot(world_pos - plane.point, plane.normal) + // precalculated: dot(plane.point, plane.normal) -> plane.w + for (int i = 0; i < num_clip_planes; i++) + gl_ClipDistance[i] = dot(world_pos, clip_planes[i].xyz) - clip_planes[i].w; + + // TODO: can be skipped? + for (int i = num_clip_planes; i < 8; i++) + gl_ClipDistance[i] = 1.0; +} + +uniform mat4 projection, projectionview, view, model; +uniform int markerspace; +uniform float px_per_unit; +uniform vec3 upvector; void main(){ - colorize(color_map, color, color_norm); + vec4 world_position = model * _position(vertex); + process_clip_planes(world_position.xyz); + vec4 clip_pos = projectionview * world_position; + gl_Position = vec4(clip_pos.xy, clip_pos.z + (clip_pos.w * depth_shift), clip_pos.w); + if (markerspace == 0) { + // pixelspace + gl_PointSize = px_per_unit * scale.x; + } else { + // dataspace with 3D camera + // to have a billboard, we project the upvector + vec3 scale_vec = upvector * scale.x; + vec4 up_clip = projectionview * vec4(world_position.xyz + scale_vec, 1); + float yup = abs(up_clip.y - clip_pos.y) / clip_pos.w; + gl_PointSize = ceil(0.5 * yup * resolution.y); + } + + colorize(color_map, color, color_norm); o_objectid = uvec2(objectid, gl_VertexID+1); - gl_Position = projectionview * model * _position(vertex); - gl_Position.z += gl_Position.w * depth_shift; } diff --git a/GLMakie/assets/shader/heatmap.frag b/GLMakie/assets/shader/heatmap.frag index 29171539322..67b43caa1b2 100644 --- a/GLMakie/assets/shader/heatmap.frag +++ b/GLMakie/assets/shader/heatmap.frag @@ -52,5 +52,5 @@ vec4 get_color(sampler2D intensity, vec2 uv, vec2 color_norm, sampler1D color_ma void main(){ vec4 color = get_color(intensity, o_uv, color_norm, color_map); - write2framebuffer(color, uvec2(o_objectid.x, 0)); + write2framebuffer(color, uvec2(o_objectid.x, o_objectid.y)); } diff --git a/GLMakie/assets/shader/heatmap.vert b/GLMakie/assets/shader/heatmap.vert index 380bf802328..42390a18133 100644 --- a/GLMakie/assets/shader/heatmap.vert +++ b/GLMakie/assets/shader/heatmap.vert @@ -19,6 +19,22 @@ ivec2 ind2sub(ivec2 dim, int linearindex){ return ivec2(linearindex % dim.x, linearindex / dim.x); } +uniform int num_clip_planes; +uniform vec4 clip_planes[8]; +out float gl_ClipDistance[8]; + +void process_clip_planes(vec3 world_pos) +{ + // distance = dot(world_pos - plane.point, plane.normal) + // precalculated: dot(plane.point, plane.normal) -> plane.w + for (int i = 0; i < num_clip_planes; i++) + gl_ClipDistance[i] = dot(world_pos, clip_planes[i].xyz) - clip_planes[i].w; + + // TODO: can be skipped? + for (int i = num_clip_planes; i < 8; i++) + gl_ClipDistance[i] = 1.0; +} + void main(){ //Outputs for ssao, which we don't use for 2d shaders like heatmap/image o_view_pos = vec3(0); @@ -33,11 +49,13 @@ void main(){ vec2 index01 = vec2(index2D) / (vec2(dims)-1.0); o_uv = vec2(index01.x, 1.0 - index01.y); - o_objectid = uvec2(objectid, index1D+1); + o_objectid = uvec2(objectid, 1 + index); float x = texelFetch(position_x, index2D.x, 0).x; float y = texelFetch(position_y, index2D.y, 0).x; - gl_Position = projection * view * model * vec4(x, y, 0, 1); + vec4 world_pos = model * vec4(x, y, 0, 1); + process_clip_planes(world_pos.xyz); + gl_Position = projection * view * world_pos; gl_Position.z += gl_Position.w * depth_shift; } diff --git a/GLMakie/assets/shader/line_segment.geom b/GLMakie/assets/shader/line_segment.geom index dc5425fbf58..fcc738692d4 100644 --- a/GLMakie/assets/shader/line_segment.geom +++ b/GLMakie/assets/shader/line_segment.geom @@ -21,6 +21,8 @@ out vec3 f_quad_sdf; out vec2 f_truncation; out float f_linestart; out float f_linelength; +out vec3 o_view_pos; +out vec3 o_view_normal; flat out float f_linewidth; flat out vec4 f_pattern_overwrite; @@ -37,6 +39,43 @@ flat out vec4 f_miter_vecs; const float AA_RADIUS = 0.8; const float AA_THICKNESS = 2.0 * AA_RADIUS; +uniform mat4 projectionview; +uniform float depth_shift; +uniform int _num_clip_planes; +uniform vec4 clip_planes[8]; + +bool process_clip_planes(inout vec4 p1, inout vec4 p2) +{ + float d1, d2; + for (int i = 0; i < _num_clip_planes; i++) { + // distance from clip planes with negative clipped + d1 = dot(p1.xyz, clip_planes[i].xyz) - clip_planes[i].w; + d2 = dot(p2.xyz, clip_planes[i].xyz) - clip_planes[i].w; + + // both outside - clip everything + if (d1 < 0.0 && d2 < 0.0) { + p2 = p1; + return true; + } + + // one outside - shorten segment + else if (d1 < 0.0) + { + // solve 0 = m * t + b = (d2 - d1) * t + d1 with t in (0, 1) + p1 = p1 - d1 * (p2 - p1) / (d2 - d1); + f_color1 = f_color1 - d1 * (f_color2 - f_color1) / (d2 - d1); + } + else if (d2 < 0.0) + { + p2 = p2 - d2 * (p1 - p2) / (d1 - d2); + f_color2 = f_color2 - d2 * (f_color1 - f_color2) / (d1 - d2); + } + } + + return false; +} + + vec3 screen_space(vec4 vertex) { return vec3((0.5 * vertex.xy / vertex.w + 0.5) * resolution, vertex.z / vertex.w); } @@ -44,9 +83,6 @@ vec3 screen_space(vec4 vertex) { 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; - void main(void) { o_view_pos = vec3(0); @@ -57,18 +93,40 @@ void main(void) return; } + f_color1 = g_color[0]; + f_color2 = g_color[1]; + // 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; + + // Shorten segments to fit clip planes + // returns true if segments are fully clipped + if (process_clip_planes(_p1, _p2)) + return; + + // remaining world -> clip projection + _p1 = projectionview * _p1; + _p2 = projectionview * _p2; + + _p1.z += _p1.w * depth_shift; + _p2.z += _p2.w * depth_shift; + + // Handle near/far clip planes 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; + if (_p1.w < 0.0) { + _p1 = _p1 + (-_p1.w - _p1.z) / (v1.z + v1.w) * v1; + f_color1 = f_color1 + (-_p1.w - _p1.z) / (v1.z + v1.w) * (f_color2 - f_color1); + } + if (_p2.w < 0.0) { + _p2 = _p2 + (-_p2.w - _p2.z) / (v1.z + v1.w) * v1; + f_color2 = f_color2 + (-_p2.w - _p2.z) / (v1.z + v1.w) * (f_color2 - f_color1); + } + // clip -> pixel/screen projection p1 = screen_space(_p1); p2 = screen_space(_p2); } @@ -87,8 +145,6 @@ void main(void) f_miter_vecs = vec4(-1); // 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 diff --git a/GLMakie/assets/shader/line_segment.vert b/GLMakie/assets/shader/line_segment.vert index 3c4f4d14659..159fd9db16a 100644 --- a/GLMakie/assets/shader/line_segment.vert +++ b/GLMakie/assets/shader/line_segment.vert @@ -27,6 +27,5 @@ void main() 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; + gl_Position = model * to_vec4(vertex); } diff --git a/GLMakie/assets/shader/lines.geom b/GLMakie/assets/shader/lines.geom index 2d49f2eb3c9..52ce2d936e8 100644 --- a/GLMakie/assets/shader/lines.geom +++ b/GLMakie/assets/shader/lines.geom @@ -34,6 +34,7 @@ flat out float f_cumulative_length; flat out ivec2 f_capmode; flat out vec4 f_linepoints; flat out vec4 f_miter_vecs; +out float gl_ClipDistance[8]; out vec3 o_view_pos; out vec3 o_view_normal; @@ -47,6 +48,10 @@ uniform int linecap; uniform int joinstyle; uniform float miter_limit; +uniform mat4 view, projection, projectionview; +uniform int _num_clip_planes; +uniform vec4 clip_planes[8]; + // Constants const float AA_RADIUS = 0.8; const float AA_THICKNESS = 4.0 * AA_RADIUS; @@ -86,6 +91,37 @@ vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); } vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); } float sign_no_zero(float value) { return value >= 0.0 ? 1.0 : -1.0; } +bool process_clip_planes(inout vec4 p1, inout vec4 p2, inout bool[4] isvalid) +{ + float d1, d2; + for(int i = 0; i < _num_clip_planes; i++) + { + // distance from clip planes with negative clipped + d1 = dot(p1.xyz, clip_planes[i].xyz) - clip_planes[i].w * p1.w; + d2 = dot(p2.xyz, clip_planes[i].xyz) - clip_planes[i].w * p2.w; + + // both outside - clip everything + if (d1 < 0.0 && d2 < 0.0) { + p2 = p1; + isvalid[1] = false; + isvalid[2] = false; + return true; + // one outside - shorten segment + } else if (d1 < 0.0) { + // solve 0 = m * t + b = (d2 - d1) * t + d1 with t in (0, 1) + p1 = p1 - d1 * (p2 - p1) / (d2 - d1); + f_color1 = f_color1 - d1 * (f_color2 - f_color1) / (d2 - d1); + isvalid[0] = false; + } else if (d2 < 0.0) { + p2 = p2 - d2 * (p1 - p2) / (d1 - d2); + f_color2 = f_color2 - d2 * (f_color1 - f_color2) / (d1 - d2); + isvalid[3] = false; + } + } + + return false; +} + //////////////////////////////////////////////////////////////////////////////// // Linestyle Support // @@ -225,6 +261,10 @@ void main(void) return; } + // line start/end colors for color sampling + f_color1 = g_color[1]; + f_color2 = g_color[2]; + // 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. @@ -237,7 +277,7 @@ void main(void) // moving them to the edge of the visible area. vec3 p0, p1, p2, p3; { - // All in clip space + // Not in clip 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 @@ -256,13 +296,20 @@ void main(void) // 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; + clip_p1 = clip_p1 + (-clip_p1.w - clip_p1.z) / (v1.z + v1.w) * v1; + f_color1 = f_color1 + (-clip_p1.w - clip_p1.z) / (v1.z + v1.w) * (f_color2 - f_color1); } if (clip_p2.w < 0.0) { isvalid[3] = false; - clip_p2 = clip_p2 + (-clip_p2.w - clip_p2.z) / (v1.z + v1.w) * v1; + clip_p2 = clip_p2 + (-clip_p2.w - clip_p2.z) / (v1.z + v1.w) * v1; + f_color2 = f_color2 + (-clip_p2.w - clip_p2.z) / (v1.z + v1.w) * (f_color2 - f_color1); } + // Shorten segments to fit clip planes + // returns true if segments are fully clipped + if (process_clip_planes(clip_p1, clip_p2, isvalid)) + return; + // 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 @@ -424,10 +471,6 @@ void main(void) // 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); diff --git a/GLMakie/assets/shader/mesh.frag b/GLMakie/assets/shader/mesh.frag index 5480da20008..2e9bbfda2ef 100644 --- a/GLMakie/assets/shader/mesh.frag +++ b/GLMakie/assets/shader/mesh.frag @@ -7,11 +7,15 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d // Sets which shading procedures to use {{shading}} +// Selects what is used to calculate the picked index +{{picking_mode}} + in vec3 o_world_normal; in vec3 o_view_normal; in vec4 o_color; in vec2 o_uv; flat in uvec2 o_id; +flat in int o_InstanceID; {{matcap_type}} matcap; {{image_type}} image; @@ -79,18 +83,28 @@ vec4 get_color(sampler1D color, vec2 uv, vec2 color_norm, sampler1D color_map, s } uniform bool fetch_pixel; -uniform vec2 uv_scale; +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, int i, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, int i, vec2 uv){ return transform * vec3(uv, 1); } +vec2 apply_uv_transform(samplerBuffer transforms, int index, vec2 uv){ + // can't have matrices in a texture so we have 3x vec2 instead + mat3x2 transform; + transform[0] = texelFetch(transforms, 3 * index + 0).xy; + transform[1] = texelFetch(transforms, 3 * index + 1).xy; + transform[2] = texelFetch(transforms, 3 * index + 2).xy; + return transform * vec3(uv, 1); +} vec4 get_pattern_color(sampler1D color) { int size = textureSize(color, 0); - vec2 pos = gl_FragCoord.xy * uv_scale; + vec2 pos = apply_uv_transform(uv_transform, o_InstanceID, gl_FragCoord.xy); int idx = int(mod(pos.x, size)); return texelFetch(color, idx, 0); } vec4 get_pattern_color(sampler2D color){ ivec2 size = textureSize(color, 0); - vec2 pos = gl_FragCoord.xy * uv_scale; + vec2 pos = apply_uv_transform(uv_transform, o_InstanceID, gl_FragCoord.xy); return texelFetch(color, ivec2(mod(pos.x, size.x), mod(pos.y, size.y)), 0); } @@ -114,5 +128,13 @@ void main(){ #ifndef NO_SHADING color.rgb = illuminate(normalize(o_world_normal), color.rgb); #endif + +#ifdef PICKING_INDEX_FROM_UV + ivec2 size = textureSize(image, 0); + ivec2 jl_idx = clamp(ivec2(o_uv * size), ivec2(0), size-1); + uint idx = uint(jl_idx.x + jl_idx.y * size.x); + write2framebuffer(color, uvec2(o_id.x, uint(1) + idx)); +#else write2framebuffer(color, o_id); +#endif } diff --git a/GLMakie/assets/shader/mesh.vert b/GLMakie/assets/shader/mesh.vert index 018248c0c5c..7354754fd22 100644 --- a/GLMakie/assets/shader/mesh.vert +++ b/GLMakie/assets/shader/mesh.vert @@ -16,12 +16,16 @@ in vec3 normals; uniform mat4 projection, view, model; +uniform int num_clip_planes; +uniform vec4 clip_planes[8]; + void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); vec4 get_color_from_cmap(float value, sampler1D color_map, vec2 colorrange); uniform uint objectid; + flat out uvec2 o_id; -uniform vec2 uv_scale; +flat out int o_InstanceID; out vec2 o_uv; out vec4 o_color; @@ -31,6 +35,10 @@ vec3 to_3d(vec3 v){return v;} vec2 to_2d(float v){return vec2(v, 0);} vec2 to_2d(vec2 v){return v;} +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, vec2 uv){ return transform * vec3(uv, 1); } + vec4 to_color(vec3 c, Nothing color_map, Nothing color_norm){ return vec4(c, 1); } @@ -59,8 +67,9 @@ void main() { o_id = uvec2(objectid, gl_VertexID+1); vec2 tex_uv = to_2d(texturecoordinates); - o_uv = vec2(1.0 - tex_uv.y, tex_uv.x) * uv_scale; + o_uv = apply_uv_transform(uv_transform, tex_uv); o_color = to_color(vertex_color, color_map, color_norm); + o_InstanceID = 0; vec3 v = to_3d(vertices); render(model * vec4(v, 1), normals, view, projection); } diff --git a/GLMakie/assets/shader/particles.vert b/GLMakie/assets/shader/particles.vert index 4fc672ac4bb..fe7434408e2 100644 --- a/GLMakie/assets/shader/particles.vert +++ b/GLMakie/assets/shader/particles.vert @@ -33,6 +33,7 @@ uniform uint objectid; uniform int len; flat out uvec2 o_id; +flat out int o_InstanceID; out vec4 o_color; out vec2 o_uv; @@ -92,8 +93,22 @@ vec4 get_particle_color(sampler2D color, Nothing intensity, Nothing color_map, N void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection); -vec2 get_uv(Nothing x){return vec2(0.0);} -vec2 get_uv(vec2 x){return vec2(1.0 - x.y, x.x);} +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, int i, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, int i, vec2 uv){ return transform * vec3(uv, 1); } +vec2 apply_uv_transform(samplerBuffer transforms, int index, vec2 uv){ + // can't have matrices in a texture so we have 3x vec2 instead + mat3x2 transform; + transform[0] = texelFetch(transforms, 3 * index + 0).xy; + transform[1] = texelFetch(transforms, 3 * index + 1).xy; + transform[2] = texelFetch(transforms, 3 * index + 2).xy; + return transform * vec3(uv, 1); +} + +vec2 get_uv(int index, Nothing uv){ return vec2(0.0); } +vec2 get_uv(int index, vec2 uv){ + return apply_uv_transform(uv_transform, index, uv); +} void main(){ int index = gl_InstanceID; @@ -105,7 +120,8 @@ void main(){ {{position_calc}} o_color = get_particle_color(color, intensity, color_map, color_norm, index, len); o_color = o_color * to_color(vertex_color); - o_uv = get_uv(texturecoordinates); + o_uv = get_uv(index, texturecoordinates); + o_InstanceID = index; rotate(rotation, index, V, N); render(model * vec4(pos + V, 1), N, view, projection); } diff --git a/GLMakie/assets/shader/sprites.geom b/GLMakie/assets/shader/sprites.geom index fc771ef63b5..6465ed532c4 100644 --- a/GLMakie/assets/shader/sprites.geom +++ b/GLMakie/assets/shader/sprites.geom @@ -39,13 +39,17 @@ uniform float glow_width; uniform int shape; // for RECTANGLE hack below uniform vec2 resolution; uniform float depth_shift; +uniform mat4 preprojection, projection, view, model; +uniform int num_clip_planes; +uniform vec4 clip_planes[8]; in int g_primitive_index[]; in vec4 g_uv_texture_bbox[]; in vec4 g_color[]; in vec4 g_stroke_color[]; in vec4 g_glow_color[]; -in vec3 g_position[]; +in vec3 g_world_position[]; +in vec3 g_marker_offset[]; in vec4 g_rotation[]; in vec4 g_offset_width[]; in uvec2 g_id[]; @@ -62,7 +66,25 @@ out vec2 f_uv; flat out vec4 f_uv_texture_bbox; flat out vec2 f_sprite_scale; -uniform mat4 projection, view, model; + +bool is_clipped(vec3 world_pos) +{ + // We clip scatter points based on the user position rather than the + // sprite vertex positions. + // distance = dot(world_pos - plane.point, plane.normal) + // precalculated: dot(plane.point, plane.normal) -> plane.w + for (int i = 0; i < num_clip_planes; i++) { + // WSL segfaults with geometry shaders that use gl_ClipDistance so we + // instead just check if we should clip here and emit no primitive if + // that's the case. + float dist = dot(world_pos, clip_planes[i].xyz) - clip_planes[i].w; + if (dist < 0.0) { + return true; + } + } + + return false; +} float get_distancefield_scale(sampler2D distancefield){ // Glyph distance field units are in pixels; convert to dimensionless @@ -92,7 +114,6 @@ void emit_vertex(vec4 vertex, vec2 uv) EmitVertex(); } - mat2 diagm(vec2 v){ return mat2(v.x, 0.0, 0.0, v.y); } @@ -105,6 +126,13 @@ void main(void) o_view_pos = vec3(0); o_view_normal = vec3(0); + // Position of sprite center in marker space + clipping + if (is_clipped(g_world_position[0])) + return; + + vec4 p = preprojection * vec4(g_world_position[0], 1); + vec3 position = p.xyz / p.w + g_marker_offset[0]; + // emit quad as triangle strip // v3. ____ . v4 // |\ | @@ -123,7 +151,7 @@ void main(void) trans = (billboard ? projection : pview) * qmat(g_rotation[0]) * trans; // Compute centre of billboard in clipping coordinates - vec4 vclip = pview*vec4(g_position[0],1) + trans*vec4(sprite_bbox_centre,0,0); + vec4 vclip = pview*vec4(position, 1) + trans*vec4(sprite_bbox_centre,0,0); // Extra buffering is required around sprites which are antialiased so that // the antialias blur doesn't get cut off (see #15). This blur falls to diff --git a/GLMakie/assets/shader/sprites.vert b/GLMakie/assets/shader/sprites.vert index ceee24efe7f..b7621980c22 100644 --- a/GLMakie/assets/shader/sprites.vert +++ b/GLMakie/assets/shader/sprites.vert @@ -72,15 +72,15 @@ vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_ {{stroke_color_type}} stroke_color; {{glow_color_type}} glow_color; -uniform bool scale_primitive; -uniform mat4 preprojection; uniform mat4 model; uniform uint objectid; uniform int len; +uniform bool scale_primitive; out uvec2 g_id; out int g_primitive_index; -out vec3 g_position; +out vec3 g_world_position; +out vec3 g_marker_offset; out vec4 g_offset_width; out vec4 g_uv_texture_bbox; out vec4 g_rotation; @@ -96,11 +96,8 @@ void main(){ g_primitive_index = index; vec3 pos; {{position_calc}} - vec4 p = preprojection * model * vec4(pos, 1); - if (scale_primitive) - g_position = p.xyz / p.w + mat3(model) * marker_offset; - else - g_position = p.xyz / p.w + marker_offset; + g_world_position = vec3(model * vec4(pos, 1)); + g_marker_offset = scale_primitive ? mat3(model) * marker_offset : marker_offset; g_offset_width.xy = quad_offset.xy; g_offset_width.zw = scale.xy; g_color = _color(color, intensity, color_map, color_norm, g_primitive_index, len); diff --git a/GLMakie/assets/shader/surface.vert b/GLMakie/assets/shader/surface.vert index b8b16eb98d6..31fd8478f33 100644 --- a/GLMakie/assets/shader/surface.vert +++ b/GLMakie/assets/shader/surface.vert @@ -30,7 +30,6 @@ uniform vec4 nan_color; vec4 color_lookup(float intensity, sampler1D color, vec2 norm); uniform vec3 scale; - uniform mat4 view, model, projection; // See util.vert for implementations @@ -41,6 +40,9 @@ vec2 linear_index(ivec2 dims, int index); vec2 linear_index(ivec2 dims, int index, vec2 offset); vec4 linear_texture(sampler2D tex, int index, vec2 offset); +{{uv_transform_type}} uv_transform; +vec2 apply_uv_transform(Nothing t1, vec2 uv){ return uv; } +vec2 apply_uv_transform(mat3x2 transform, vec2 uv){ return transform * vec3(uv, 1); } // Normal generation @@ -147,8 +149,8 @@ vec3 getnormal(Nothing pos, sampler1D xs, sampler1D ys, sampler2D zs, ivec2 uv){ } uniform uint objectid; -uniform vec2 uv_scale; flat out uvec2 o_id; +flat out int o_InstanceID; // dummy for compat with meshscatter in mesh.frag out vec4 o_color; out vec2 o_uv; @@ -161,8 +163,10 @@ void main() vec3 pos; {{position_calc}} - o_id = uvec2(objectid, index1D+1); - o_uv = index01 * uv_scale; + o_id = uvec2(objectid, 0); // calculated from uv in mesh.frag + o_InstanceID = 0; + // match up with mesh + o_uv = apply_uv_transform(uv_transform, vec2(index01.x, 1 - index01.y)); vec3 normalvec = {{normal_calc}}; o_color = vec4(0.0); diff --git a/GLMakie/assets/shader/util.vert b/GLMakie/assets/shader/util.vert index afb2c379945..c6cbbf3ebb4 100644 --- a/GLMakie/assets/shader/util.vert +++ b/GLMakie/assets/shader/util.vert @@ -249,6 +249,23 @@ vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm } +uniform int num_clip_planes; +uniform vec4 clip_planes[8]; +out float gl_ClipDistance[8]; + +void process_clip_planes(vec3 world_pos) +{ + // distance = dot(world_pos - plane.point, plane.normal) + // precalculated: dot(plane.point, plane.normal) -> plane.w + for (int i = 0; i < num_clip_planes; i++) + gl_ClipDistance[i] = dot(world_pos, clip_planes[i].xyz) - clip_planes[i].w; + + // TODO: can be skipped? + for (int i = num_clip_planes; i < 8; i++) + gl_ClipDistance[i] = 1.0; +} + + uniform float depth_shift; // TODO maybe ifdef SSAO this stuff? @@ -271,6 +288,8 @@ out vec3 o_camdir; void render(vec4 position_world, vec3 normal, mat4 view, mat4 projection) { + process_clip_planes(position_world.xyz); + // position in view space (as seen from camera) vec4 view_pos = view * position_world; view_pos /= view_pos.w; diff --git a/GLMakie/assets/shader/volume.frag b/GLMakie/assets/shader/volume.frag index 1e4eef28b6d..546a093862a 100644 --- a/GLMakie/assets/shader/volume.frag +++ b/GLMakie/assets/shader/volume.frag @@ -27,6 +27,8 @@ uniform float isovalue; uniform float isorange; uniform mat4 model, projectionview; +uniform int _num_clip_planes; +uniform vec4 clip_planes[8]; const float max_distance = 1.3; @@ -299,6 +301,33 @@ void write2framebuffer(vec4 color, uvec2 id); const float typemax = 100000000000000000000000000000000000000.0; + +bool process_clip_planes(inout vec3 p1, inout vec3 p2) +{ + float d1, d2; + for (int i = 0; i < _num_clip_planes; i++) { + // distance from clip planes with negative clipped + d1 = dot(p1.xyz, clip_planes[i].xyz) - clip_planes[i].w; + d2 = dot(p2.xyz, clip_planes[i].xyz) - clip_planes[i].w; + + // both outside - clip everything + if (d1 < 0.0 && d2 < 0.0) { + p2 = p1; + return true; + } + + // one outside - shorten segment + else if (d1 < 0.0) + // solve 0 = m * t + b = (d2 - d1) * t + d1 with t in (0, 1) + p1 = p1 - d1 * (p2 - p1) / (d2 - d1); + else if (d2 < 0.0) + p2 = p2 - d2 * (p1 - p2) / (d1 - d2); + } + + return false; +} + + bool no_solution(float x){ return x <= 0.0001 || isinf(x) || isnan(x); } @@ -342,6 +371,11 @@ void main() float solution = min_bigger_0(solution_1, solution_0); vec3 start = back_position + solution * dir; + + // if completely clipped discard this ray tracing attempt + if (process_clip_planes(start, back_position)) + discard; + vec3 step_in_dir = (back_position - start) / num_samples; float steps = 0.1; diff --git a/GLMakie/assets/shader/voxel.frag b/GLMakie/assets/shader/voxel.frag index cc5caf52c34..35861af89d9 100644 --- a/GLMakie/assets/shader/voxel.frag +++ b/GLMakie/assets/shader/voxel.frag @@ -26,6 +26,8 @@ flat in int plane_front; uniform lowp usampler3D voxel_id; uniform uint objectid; uniform float gap; +uniform int _num_clip_planes; +uniform vec4 clip_planes[8]; {{uv_map_type}} uv_map; {{color_map_type}} color_map; @@ -81,6 +83,22 @@ vec4 get_color(sampler2D color, Nothing color_map, int id) { return get_color_from_texture(color, id); } +bool is_clipped() +{ + float d; + // Center of voxel + ivec3 size = ivec3(textureSize(voxel_id, 0).xyz); + vec3 xyz = vec3(ivec3(o_uvw * size)) + vec3(0.5); + for (int i = 0; i < _num_clip_planes; i++) { + // distance between clip plane and center + d = dot(xyz, clip_planes[i].xyz) - clip_planes[i].w; + + if (d < 0.0) + return true; + } + + return false; +} void write2framebuffer(vec4 color, uvec2 id); @@ -90,6 +108,9 @@ vec3 illuminate(vec3 normal, vec3 base_color); void main() { + if (is_clipped()) + discard; + 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) @@ -125,7 +146,7 @@ void main() // TODO: index into 3d array ivec3 size = ivec3(textureSize(voxel_id, 0).xyz); - ivec3 idx = ivec3(o_uvw * size); + ivec3 idx = clamp(ivec3(o_uvw * size), ivec3(0), size-1); int lin = 1 + idx.x + size.x * (idx.y + size.y * idx.z); // draw diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index d6eb089f410..146eeb6dd44 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -4,12 +4,25 @@ function render(list::Tuple) end return end + +function setup_clip_planes(robj) + N = to_value(get(robj.uniforms, :num_clip_planes, 0)) + for i in 0:min(7, N-1) + glEnable(GL_CLIP_DISTANCE0 + UInt32(i)) + end + for i in max(0, N):7 + glDisable(GL_CLIP_DISTANCE0 + UInt32(i)) + end +end + + """ When rendering a specialised list of Renderables, we can do some optimizations """ function render(list::Vector{RenderObject{Pre}}) where Pre isempty(list) && return nothing first(list).prerenderfunction() + setup_clip_planes(first(list)) vertexarray = first(list).vertexarray program = vertexarray.program glUseProgram(program.id) @@ -57,6 +70,7 @@ a lot of objects. function render(renderobject::RenderObject, vertexarray=renderobject.vertexarray) if renderobject.visible renderobject.prerenderfunction() + setup_clip_planes(renderobject) program = vertexarray.program glUseProgram(program.id) for (key, value) in program.uniformloc diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index 0c1a6a1ebc0..b9ce99f0167 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -392,7 +392,7 @@ function RenderObject( # Otherwise just let the value pass through # TODO: Is this ok/ever not filtered? else - # @debug "Passed on $k -> $(typeof(v)) without conversion." + @debug "Passed on $k -> $(typeof(v)) without conversion." end end end @@ -403,7 +403,7 @@ function RenderObject( # remove all uniforms not occuring in shader # ssao, instances transparency are special for rendering passes. TODO do this more cleanly - special = Set([:ssao, :transparency, :instances, :fxaa]) + special = Set([:ssao, :transparency, :instances, :fxaa, :num_clip_planes]) for k in setdiff(keys(data), keys(program.nametype)) if !(k in special) delete!(data, k) diff --git a/GLMakie/src/GLAbstraction/GLUniforms.jl b/GLMakie/src/GLAbstraction/GLUniforms.jl index a51af4024bc..3da13dec89c 100644 --- a/GLMakie/src/GLAbstraction/GLUniforms.jl +++ b/GLMakie/src/GLAbstraction/GLUniforms.jl @@ -30,7 +30,7 @@ function uniformfunc(typ::DataType, dims::Tuple{Int}) end function uniformfunc(typ::DataType, dims::Tuple{Int, Int}) M, N = dims - Symbol(string("glUniformMatrix", M == N ? "$M" : "$(M)x$(N)", opengl_postfix(typ))) + Symbol(string("glUniformMatrix", M == N ? "$M" : "$(N)x$(M)", opengl_postfix(typ))) end gluniform(location::Integer, x::Nothing) = nothing @@ -105,7 +105,7 @@ end function glsl_typename(t::Type{T}) where T <: Mat M, N = size(t) - string(opengl_prefix(eltype(t)), "mat", M==N ? M : string(M, "x", N)) + string(opengl_prefix(eltype(t)), "mat", M==N ? M : string(N, "x", M)) end toglsltype_string(t::Observable) = toglsltype_string(to_value(t)) toglsltype_string(x::T) where {T<:Union{Real, Mat, StaticVector, Texture, Colorant, TextureBuffer, Nothing}} = "uniform $(glsl_typename(x))" diff --git a/GLMakie/src/GLMakie.jl b/GLMakie/src/GLMakie.jl index ce0c527070c..40642e1c077 100644 --- a/GLMakie/src/GLMakie.jl +++ b/GLMakie/src/GLMakie.jl @@ -55,34 +55,42 @@ function ShaderSource(path) return ShaderSource(typ, source, name) 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, Tuple{Float64, ShaderSource}}() +const SHADER_DIR = normpath(joinpath(@__DIR__, "..", "assets", "shader")) +const LOADED_SHADERS = Dict{String, ShaderSource}() +const WARN_ON_LOAD = Ref(false) function loadshader(name) - # 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: - # 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 + return get!(LOADED_SHADERS, name) do + if WARN_ON_LOAD[] + @warn("Reloading shader") + end + return ShaderSource(joinpath(SHADER_DIR, name)) end +end - # replace source if invalid/add new source - mtime = Base.Filesystem.mtime(path) - src = ShaderSource(path) - LOADED_SHADERS[name] = (mtime, src) - return src +function load_all_shaders(folder) + for name in readdir(folder) + path = joinpath(folder, name) + if isdir(path) + load_all_shaders(path) + elseif any(x -> endswith(name, x), [".frag", ".vert", ".geom"]) + path = relpath(path, SHADER_DIR) + loadshader(replace(path, "\\" => "/")) + end + end end + gl_texture_atlas() = Makie.get_texture_atlas(2048, 64) # don't put this into try catch, to not mess with normal errors include("gl_backend.jl") +# We load all shaders to compile them into the package Image +# Making them relocatable +load_all_shaders(SHADER_DIR) +WARN_ON_LOAD[] = true + function __init__() activate!() end diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl index c8cb5a85064..0763cf6023b 100644 --- a/GLMakie/src/display.jl +++ b/GLMakie/src/display.jl @@ -10,7 +10,7 @@ function Base.display(screen::Screen, scene::Scene; connect=true) else @assert screen.root_scene === scene "internal error. Scene already displayed by screen but not as root scene" end - pollevents(screen) + pollevents(screen, Makie.BackendTick) return screen end diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 0a9e94303e2..f364b2e3468 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -24,7 +24,7 @@ function handle_lights(attr::Dict, screen::Screen, lights::Vector{Makie.Abstract attr[:light_colors] = Observable(sizehint!(RGBf[], MAX_LIGHTS)) attr[:light_parameters] = Observable(sizehint!(Float32[], MAX_PARAMS)) - on(screen.render_tick, priority = typemin(Int)) do _ + on(screen.render_tick, priority = -1000) do _ # derive number of lights from available lights. Both MAX_LIGHTS and # MAX_PARAMS are considered for this. n_lights = 0 @@ -226,17 +226,19 @@ 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, :dim_conversions, :material]) + :calculated_colors, :space, :markerspace, :model, :dim_conversions, :material]) function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) # poll inside functions to make wait on compile less prominent - pollevents(screen) + pollevents(screen, Makie.BackendTick) robj = get!(screen.cache, objectid(plot)) do filtered = filter(plot.attributes) do (k, v) return !in(k, EXCLUDE_KEYS) end + + # Handle update tracking for render on demand track_updates = screen.config.render_on_demand if track_updates for arg in plot.args @@ -251,19 +253,48 @@ function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) screen.requires_update = true end end + + # Pass along attributes gl_attributes = Dict{Symbol, Any}(map(filtered) do key_value key, value = key_value gl_key = to_glvisualize_key(key) gl_value = lift_convert(key, value, plot, screen) gl_key => gl_value end) - gl_attributes[:model] = map(Makie.patch_model, f32_conversion_obs(plot), plot.model) + + # :f32c should get passed to apply_transform_and_f32_conversion but not + # make it to uniforms + gl_attributes[:f32c], gl_attributes[:model] = Makie.patch_model(plot) + if haskey(plot, :markerspace) gl_attributes[:markerspace] = plot.markerspace end gl_attributes[:space] = plot.space gl_attributes[:px_per_unit] = screen.px_per_unit + # Handle clip planes + # OpenGL supports up to 8 + clip_planes = pop!(gl_attributes, :clip_planes) + gl_attributes[:num_clip_planes] = map(plot, clip_planes, gl_attributes[:space]) do planes, space + return Makie.is_data_space(space) ? min(8, length(planes)) : 0 + end + gl_attributes[:clip_planes] = map(plot, clip_planes, gl_attributes[:space]) do planes, space + Makie.is_data_space(space) || return [Vec4f(0, 0, 0, -1e9) for _ in 1:8] + + if length(planes) > 8 + @warn("Only up to 8 clip planes are supported. The rest are ignored!", maxlog = 1) + end + + output = Vector{Vec4f}(undef, 8) + for i in 1:min(length(planes), 8) + output[i] = Makie.gl_plane_format(planes[i]) + end + for i in min(length(planes), 8)+1:8 + output[i] = Vec4f(0, 0, 0, -1e9) + end + return output + end + handle_intensities!(screen, gl_attributes, plot) connect_camera!(plot, gl_attributes, scene.camera, get_space(plot)) @@ -303,7 +334,7 @@ function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) elseif shading == MultiLightShading handle_lights(gl_attributes, screen, scene.lights) end - robj = robj_func(gl_attributes) # <-- here + robj = robj_func(gl_attributes) get!(gl_attributes, :ssao, Observable(false)) screen.cache2plot[robj.id] = plot @@ -313,18 +344,17 @@ function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) return robj end -Base.insert!(::GLMakie.Screen, ::Scene, ::Makie.PlotList) = nothing - function Base.insert!(screen::Screen, scene::Scene, @nospecialize(x::Plot)) ShaderAbstractions.switch_context!(screen.glscreen) + add_scene!(screen, scene) # poll inside functions to make wait on compile less prominent - pollevents(screen) + pollevents(screen, Makie.BackendTick) if isempty(x.plots) # if no plots inserted, this truly is an atomic draw_atomic(screen, scene, x) else foreach(x.plots) do x # poll inside functions to make wait on compile less prominent - pollevents(screen) + pollevents(screen, Makie.BackendTick) insert!(screen, scene, x) end end @@ -383,12 +413,11 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Sca space = plot.space positions = handle_view(plot[1], gl_attributes) - positions = apply_transform_and_f32_conversion(scene, plot, positions) - # positions = lift(apply_transform, plot, transform_func_obs(plot), positions, space) + positions = apply_transform_and_f32_conversion(plot, pop!(gl_attributes, :f32c), positions) + cam = scene.camera if plot isa Scatter mspace = plot.markerspace - cam = scene.camera gl_attributes[:preprojection] = lift(plot, space, mspace, cam.projectionview, cam.resolution) do space, mspace, _, _ return Mat4f(Makie.clip_to_space(cam, mspace) * Makie.space_to_clip(cam, space)) @@ -423,9 +452,18 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Union{Sca if haskey(gl_attributes, :intensity) gl_attributes[:color] = pop!(gl_attributes, :intensity) end + to_keep = Set([:color_map, :color, :color_norm, :px_per_unit, :scale, :model, + :projectionview, :projection, :view, :visible, :resolution, :transparency]) filter!(gl_attributes) do (k, v,) - k in (:color_map, :color, :color_norm, :scale, :model, :projectionview, :visible) + return (k in to_keep) + end + gl_attributes[:markerspace] = lift(plot.markerspace) do space + space == :pixel && return Int32(0) + space == :data && return Int32(1) + return error("Unsupported markerspace for FastPixel marker: $space") end + gl_attributes[:marker_shape] = lift(x -> x.marker_type, plot.marker) + gl_attributes[:upvector] = lift(x-> Vec3f(normalize(x)), cam.upvector) return draw_pixel_scatter(screen, positions, gl_attributes) else if plot isa MeshScatter @@ -450,27 +488,47 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Lines)) data[:scene_origin] = map(plot, data[:px_per_unit], scene.viewport) do ppu, viewport Vec2f(ppu * origin(viewport)) end - space = plot.space + + # Handled manually without using OpenGL clipping + data[:_num_clip_planes] = pop!(data, :num_clip_planes) + data[:num_clip_planes] = Observable(0) + pop!(data, :clip_planes) + data[:clip_planes] = map(plot, data[:projectionview], plot.clip_planes, space) do pv, planes, space + Makie.is_data_space(space) || return [Vec4f(0, 0, 0, -1e9) for _ in 1:8] + + clip_planes = Makie.to_clip_space(pv, planes) + + output = Vector{Vec4f}(undef, 8) + for i in 1:min(length(planes), 8) + output[i] = Makie.gl_plane_format(clip_planes[i]) + end + for i in min(length(planes), 8)+1:8 + output[i] = Vec4f(0, 0, 0, -1e9) + end + return output + end + if isnothing(to_value(linestyle)) data[:pattern] = nothing data[:fast] = true - # positions = lift(apply_transform, plot, transform_func, positions, space) - positions = apply_transform_and_f32_conversion(scene, plot, positions) + positions = apply_transform_and_f32_conversion(plot, pop!(data, :f32c), positions) else data[:pattern] = linestyle data[:fast] = false - pvm = lift(*, plot, data[:projectionview], data[:model]) + # TODO: Skip patch_model() when this branch is used + pop!(data, :f32c) + pvm = lift(plot, data[:projectionview], plot.model, f32_conversion_obs(scene), space) do pv, model, f32c, space + Makie.Mat4d(pv) * Makie.f32_convert_matrix(f32c, space) * model + end 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) + positions = lift(plot, transform_func, positions, space, pvm) do f, ps, space, pvm + transformed = apply_transform(f, ps, space) output = Vector{Point4f}(undef, length(transformed)) for i in eachindex(transformed) - output[i] = pvm * to_ndim(Point4f, to_ndim(Point3f, transformed[i], 0f0), 1f0) + output[i] = pvm * to_ndim(Point4d, to_ndim(Point3d, transformed[i], 0.0), 1.0) end output end @@ -492,9 +550,13 @@ function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::LineSegme Vec2f(ppu * origin(viewport)) end + # Handled manually without using OpenGL clipping + data[:_num_clip_planes] = pop!(data, :num_clip_planes) + data[:num_clip_planes] = Observable(0) + + 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) + positions = apply_transform_and_f32_conversion(plot, pop!(data, :f32c), positions) if haskey(data, :intensity) data[:color] = pop!(data, :intensity) end @@ -508,18 +570,15 @@ function draw_atomic(screen::Screen, scene::Scene, return cached_robj!(screen, scene, plot) do gl_attributes glyphcollection = plot[1] - transfunc = Makie.transform_func_obs(plot) - pos = gl_attributes[:position] + pos = apply_transform_and_f32_conversion(plot, pop!(gl_attributes, :f32c), gl_attributes[:position]) space = plot.space markerspace = plot.markerspace offset = pop!(gl_attributes, :offset, Vec2f(0)) atlas = gl_texture_atlas() # calculate quad metrics - 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) + glyph_data = lift(plot, pos, glyphcollection, offset) do pos, gc, offset + return Makie.text_quads(atlas, pos, to_value(gc), offset) end # unpack values from the one signal: @@ -584,26 +643,29 @@ end # el32convert doesn't copy for array of Float32 # But we assume that xy_convert copies when we use it xy_convert(x::AbstractArray, n) = copy(x) -xy_convert(x, n) = [LinRange(extrema(x)..., n + 1);] +xy_convert(x::Makie.EndPoints, n) = [LinRange(extrema(x)..., n + 1);] function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) + t = Makie.transform_func_obs(plot) + + if plot.x[] isa Makie.EndPoints && plot.y[] isa Makie.EndPoints && Makie.is_identity_transform(t[]) + # Fast path for regular heatmaps + return draw_image(screen, scene, plot) + end 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, f32_conversion_obs(scene), t, plot[1], plot[2], space) do f32c, t, x, y, space + xypos = lift(plot, pop!(gl_attributes, :f32c), t, plot.model, plot[1], plot[2], space) do f32c, t, model, x, y, space + # TODO: fix heatmaps for transforms that mix dimensions: + # - transform_func's like Polar + # - model matrices with rotation & Float32 precisionissues 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 (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. - 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 + + x1d = Makie.apply_transform_and_f32_conversion(f32c, t, model, x1d, 1, space) + y1d = Makie.apply_transform_and_f32_conversion(f32c, t, model, y1d, 2, space) + + return (x1d, y1d) end xpos = lift(first, plot, xypos) ypos = lift(last, plot, xypos) @@ -626,20 +688,18 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Heatmap) end end -function draw_atomic(screen::Screen, scene::Scene, plot::Image) +function draw_image(screen::Screen, scene::Scene, plot::Union{Heatmap, Image}) return cached_robj!(screen, scene, plot) do gl_attributes position = lift(plot, plot[1], plot[2]) do x, y - xmin, xmax = extrema(x) - ymin, ymax = extrema(y) - rect = Rect2(xmin, ymin, xmax - xmin, ymax - ymin) + xmin, xmax = x + ymin, ymax = y + rect = Rect2(xmin, ymin, xmax - xmin, ymax - ymin) return decompose(Point2d, rect) end - gl_attributes[:vertices] = apply_transform_and_f32_conversion(scene, plot, position) + gl_attributes[:vertices] = apply_transform_and_f32_conversion(plot, pop!(gl_attributes, :f32c), position) rect = Rect2f(0, 0, 1, 1) gl_attributes[:faces] = decompose(GLTriangleFace, rect) - gl_attributes[:texturecoordinates] = map(decompose_uv(rect)) do uv - return 1.0f0 .- Vec2f(uv[2], uv[1]) - end + gl_attributes[:texturecoordinates] = decompose_uv(rect) get!(gl_attributes, :shading, NoShading) _interp = to_value(pop!(gl_attributes, :interpolate, true)) interp = _interp ? :linear : :nearest @@ -648,10 +708,15 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Image) else gl_attributes[:image] = Texture(pop!(gl_attributes, :color); minfilter=interp) end + gl_attributes[:picking_mode] = "#define PICKING_INDEX_FROM_UV" return draw_mesh(screen, gl_attributes) end end +function draw_atomic(screen::Screen, scene::Scene, plot::Image) + return draw_image(screen, scene, plot) +end + function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space=:data) # signals not supported for shading yet shading = to_value(gl_attributes[:shading])::Makie.MakieCore.ShadingAlgorithm @@ -667,6 +732,14 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= img = lift(x -> el32convert(Makie.to_image(x)), plot, color) gl_attributes[:image] = ShaderAbstractions.Sampler(img, x_repeat=:repeat, minfilter=:nearest) get!(gl_attributes, :fetch_pixel, true) + # different default with Patterns (no swapping and flipping of axes) + gl_attributes[:uv_transform] = map(plot, plot.attributes[:uv_transform]) do uv_transform + if uv_transform === Makie.automatic + return Mat{2,3,Float32}(1,0,0,1,0,0) + else + return convert_attribute(uv_transform, key"uv_transform"()) + end + end elseif to_value(color) isa AbstractMatrix{<:Colorant} gl_attributes[:image] = Texture(lift(el32convert, plot, color), minfilter = interp) delete!(gl_attributes, :color_map) @@ -692,7 +765,7 @@ function mesh_inner(screen::Screen, mesh, transfunc, gl_attributes, plot, space= # 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[:vertices] = apply_transform_and_f32_conversion(plot, pop!(gl_attributes, :f32c), positions) gl_attributes[:faces] = lift(x-> decompose(GLTriangleFace, x), mesh) if hasproperty(to_value(mesh), :uv) gl_attributes[:texturecoordinates] = lift(decompose_uv, mesh) @@ -746,11 +819,11 @@ 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, f32_conversion_obs(scene), t, plot[1], plot[2], space) do f32c, t, x, y, space + xypos = lift(plot, pop!(gl_attributes, :f32c), plot.model, t, plot[1], plot[2], space) do f32c, model, t, x, y, space # Only if transform doesn't do anything, we can stay linear in 1/2D if Makie.is_identity_transform(t) && isnothing(f32c) return (x, y) - else + elseif Makie.is_translation_scale_matrix(model) matrix = if x isa AbstractMatrix && y isa AbstractMatrix Makie.f32_convert(f32c, apply_transform.((t,), Point.(x, y), space), space) else @@ -759,6 +832,15 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) [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)) + else + matrix = if x isa AbstractMatrix && y isa AbstractMatrix + Makie.f32_convert(f32c, apply_transform_and_model.((model,), (t,), Point.(x, y), space, Point2d), 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. + [Makie.f32_convert(f32c, apply_transform_and_model(model, t, Point(x, y), space, Point2d), space) for x in x, y in y] + end + return (first.(matrix), last.(matrix)) end end xpos = lift(first, plot, xypos) @@ -798,6 +880,35 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Volume) ) return convert(Mat4f, m) * m2 end + gl_attributes[:modelinv] = const_lift(inv, gl_attributes[:model]) + + # Handled manually without using OpenGL clipping + gl_attributes[:_num_clip_planes] = pop!(gl_attributes, :num_clip_planes) + gl_attributes[:num_clip_planes] = Observable(0) + pop!(gl_attributes, :clip_planes) + gl_attributes[:clip_planes] = map(plot, gl_attributes[:modelinv], plot.clip_planes, plot.space) do modelinv, planes, space + Makie.is_data_space(space) || return [Vec4f(0, 0, 0, -1e9) for _ in 1:8] + + # model/modelinv has no perspective projection so we should be fine + # with just applying it to the plane origin and transpose(inv(modelinv)) + # to plane.normal + @assert (length(planes) == 0) || isapprox(modelinv[4, 4], 1, atol = 1e-6) + + output = Vector{Vec4f}(undef, 8) + for i in 1:min(length(planes), 8) + origin = modelinv * to_ndim(Point4f, planes[i].distance * planes[i].normal, 1) + normal = transpose(gl_attributes[:model][]) * to_ndim(Vec4f, planes[i].normal, 0) + distance = dot(Vec3f(origin[1], origin[2], origin[3]) / origin[4], + Vec3f(normal[1], normal[2], normal[3])) + output[i] = Vec4f(normal[1], normal[2], normal[3], distance) + end + for i in min(length(planes), 8)+1:8 + output[i] = Vec4f(0, 0, 0, -1e9) + end + + return output + end + interp = to_value(pop!(gl_attributes, :interpolate)) interp = interp ? :linear : :nearest Tex(x) = Texture(x; minfilter=interp) @@ -844,6 +955,34 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Voxels) ) end + # Handled manually without using OpenGL clipping + gl_attributes[:_num_clip_planes] = pop!(gl_attributes, :num_clip_planes) + gl_attributes[:num_clip_planes] = Observable(0) + pop!(gl_attributes, :clip_planes) + gl_attributes[:clip_planes] = map(plot, gl_attributes[:model], plot.clip_planes, plot.space) do model, planes, space + Makie.is_data_space(space) || return [Vec4f(0, 0, 0, -1e9) for _ in 1:8] + + # model/modelinv has no perspective projection so we should be fine + # with just applying it to the plane origin and transpose(inv(modelinv)) + # to plane.normal + modelinv = inv(model) + @assert (length(planes) == 0) || isapprox(modelinv[4, 4], 1, atol = 1e-6) + + output = Vector{Vec4f}(undef, 8) + for i in 1:min(length(planes), 8) + origin = modelinv * to_ndim(Point4f, planes[i].distance * planes[i].normal, 1) + normal = transpose(model) * to_ndim(Vec4f, planes[i].normal, 0) + distance = dot(Vec3f(origin[1], origin[2], origin[3]) / origin[4], + Vec3f(normal[1], normal[2], normal[3])) + output[i] = Vec4f(normal[1], normal[2], normal[3], distance) + end + for i in min(length(planes), 8)+1:8 + output[i] = Vec4f(0, 0, 0, -1e9) + end + + return output + end + # color attribute adjustments pop!(gl_attributes, :lowclip, nothing) pop!(gl_attributes, :highclip, nothing) diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 52f631095ce..365c7a702ec 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -47,10 +47,12 @@ function Makie.window_area(scene::Scene, screen::Screen) function windowsizecb(window, width::Cint, height::Cint) area = scene.events.window_area - sf = screen.scalefactor[] + winscale = screen.scalefactor[] ShaderAbstractions.switch_context!(window) - winscale = sf / (@static Sys.isapple() ? scale_factor(window) : 1) + if GLFW.GetPlatform() in (GLFW.PLATFORM_COCOA, GLFW.PLATFORM_WAYLAND) + winscale /= scale_factor(window) + end winw, winh = round.(Int, (width, height) ./ winscale) if Vec(winw, winh) != widths(area[]) area[] = Recti(minimum(area[]), winw, winh) @@ -170,13 +172,12 @@ end function correct_mouse(screen::Screen, w, h) nw = to_native(screen) - sf = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(nw) : 1) _, winh = window_size(nw) - @static if Sys.isapple() - return w, (winh / sf) - h - else - return w / sf, (winh - h) / sf + sf = screen.scalefactor[] + if GLFW.GetPlatform() in (GLFW.PLATFORM_COCOA, GLFW.PLATFORM_WAYLAND) + sf /= scale_factor(nw) end + return w / sf, (winh - h) / sf end struct MousePositionUpdater @@ -185,7 +186,7 @@ struct MousePositionUpdater hasfocus::Observable{Bool} end -function (p::MousePositionUpdater)(::Nothing) +function (p::MousePositionUpdater)(::Makie.TickState) !p.hasfocus[] && return nw = to_native(p.screen) x, y = GLFW.GetCursorPos(nw) @@ -294,3 +295,15 @@ end function Makie.disconnect!(window::GLFW.Window, ::typeof(entered_window)) GLFW.SetCursorEnterCallback(window, nothing) end + +function Makie.frame_tick(scene::Scene, screen::Screen) + # Separating screen ticks from event ticks allows us to sanitize: + # Internal on-tick event updates happen first (mouseposition), + # consuming in event.tick listeners doesn't affect backend ticks, + # more control/consistent order + on(Makie.TickCallback(scene), scene, screen.render_tick, priority = typemin(Int)) +end +function Makie.disconnect!(screen::Screen, ::typeof(Makie.frame_tick)) + connections = filter(x -> x[2] isa Makie.TickCallback, screen.render_tick.listeners) + foreach(x -> off(screen.render_tick, x[2]), connections) +end \ No newline at end of file diff --git a/GLMakie/src/glshaders/image_like.jl b/GLMakie/src/glshaders/image_like.jl index 0fd7ccbaa05..21a1eed4081 100644 --- a/GLMakie/src/glshaders/image_like.jl +++ b/GLMakie/src/glshaders/image_like.jl @@ -75,7 +75,7 @@ function draw_volume(screen, main::VolumeTypes, data::Dict) transparency = false shader = GLVisualizeShader( screen, - "util.vert", "volume.vert", + "volume.vert", "fragment_output.frag", "lighting.frag", "volume.frag", view = Dict( "shading" => light_calc(shading), diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index 902aa8292b0..c4fa81d8cf8 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -8,13 +8,13 @@ function sumlengths(points, resolution) T = eltype(eltype(typeof(points))) result = zeros(T, length(points)) - for i in eachindex(points) - i0 = max(i-1, 1) - p1, p2 = points[i0], points[i] + for (i, idx) in enumerate(eachindex(points)) + idx0 = max(idx-1, 1) + p1, p2 = points[idx0], points[idx] if any(map(isnan, p1)) || any(map(isnan, p2)) || invalid(p1) || invalid(p2) result[i] = 0f0 else - result[i] = result[i0] + 0.5 * norm(resolution .* (f(p1) - f(p2))) + result[i] = result[max(i-1, 1)] + 0.5 * norm(resolution .* (f(p1) - f(p2))) end end result diff --git a/GLMakie/src/glshaders/mesh.jl b/GLMakie/src/glshaders/mesh.jl index 877ddf90dea..49808f299e2 100644 --- a/GLMakie/src/glshaders/mesh.jl +++ b/GLMakie/src/glshaders/mesh.jl @@ -50,7 +50,7 @@ function draw_mesh(screen, data::Dict) color_norm = nothing fetch_pixel = false texturecoordinates = Vec2f(0) => GLBuffer - uv_scale = Vec2f(1) + uv_transform = Mat{2,3,Float32}(1, 0, 0, -1, 0, 1) transparency = false interpolate_in_fragment_shader = true shader = GLVisualizeShader( @@ -60,6 +60,7 @@ function draw_mesh(screen, data::Dict) "lighting.frag", view = Dict( "shading" => light_calc(shading), + "picking_mode" => to_value(get(data, :picking_mode, "")), "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)), diff --git a/GLMakie/src/glshaders/particles.jl b/GLMakie/src/glshaders/particles.jl index 2f5d116587c..c4c3f03e620 100644 --- a/GLMakie/src/glshaders/particles.jl +++ b/GLMakie/src/glshaders/particles.jl @@ -75,6 +75,24 @@ function draw_mesh_particle(screen, p, data) texturecoordinates = nothing end + # TODO: use instance attributes + if to_value(data[:uv_transform]) isa Vector + transforms = pop!(data, :uv_transform) + @gen_defaults! data begin + uv_transform = map(transforms) do transforms + # 3x Vec2 should match the element order of glsl mat3x2 + output = Vector{Vec2f}(undef, 3 * length(transforms)) + for i in eachindex(transforms) + output[3 * (i-1) + 1] = transforms[i][Vec(1, 2)] + output[3 * (i-1) + 2] = transforms[i][Vec(3, 4)] + output[3 * (i-1) + 3] = transforms[i][Vec(5, 6)] + end + return output + end => TextureBuffer + end + else + # handled automatically + end shading = pop!(data, :shading)::Makie.MakieCore.ShadingAlgorithm @gen_defaults! data begin @@ -87,7 +105,6 @@ function draw_mesh_particle(screen, p, data) matcap = nothing => Texture fetch_pixel = false interpolate_in_fragment_shader = false - uv_scale = Vec2f(1) backlight = 0f0 instances = const_lift(length, position) @@ -121,7 +138,7 @@ This is supposed to be the fastest way of displaying particles! function draw_pixel_scatter(screen, position::VectorTypes, data::Dict) @gen_defaults! data begin vertex = position => GLBuffer - color_map = nothing => Texture + color_map = nothing => Texture color = nothing => GLBuffer color_norm = nothing scale = 2f0 @@ -136,7 +153,7 @@ function draw_pixel_scatter(screen, position::VectorTypes, data::Dict) ) gl_primitive = GL_POINTS end - data[:prerender] = PointSizeRender(data[:scale]) + data[:prerender] = ()-> glEnable(GL_VERTEX_PROGRAM_POINT_SIZE) return assemble_shader(data) end diff --git a/GLMakie/src/glshaders/surface.jl b/GLMakie/src/glshaders/surface.jl index ef7810a96f8..a868b48bd39 100644 --- a/GLMakie/src/glshaders/surface.jl +++ b/GLMakie/src/glshaders/surface.jl @@ -142,7 +142,7 @@ function draw_surface(screen, main, data::Dict) highclip = RGBAf(0, 0, 0, 0) lowclip = RGBAf(0, 0, 0, 0) - uv_scale = Vec2f(1) + uv_transform = Mat{2,3,Float32}(1, 0, 0, -1, 0, 1) instances = const_lift(x->(size(x,1)-1) * (size(x,2)-1), main) => "number of planes used to render the surface" transparency = false shader = GLVisualizeShader( @@ -153,6 +153,7 @@ function draw_surface(screen, main, data::Dict) "position_calc" => position_calc(position, position_x, position_y, position_z, Texture), "normal_calc" => normal_calc(normal, to_value(invert_normals)), "shading" => light_calc(shading), + "picking_mode" => "#define PICKING_INDEX_FROM_UV", "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)), diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index ab718ba7bcc..47270f9df8e 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -14,7 +14,7 @@ function pick_native(screen::Screen, rect::Rect2i) rw, rh = widths(rect) w, h = size(screen.root_scene) ppu = screen.px_per_unit[] - if rx > 0 && ry > 0 && rx + rw <= w && ry + rh <= h + if rx >= 0 && ry >= 0 && rx + rw <= w && ry + rh <= h rx, ry, rw, rh = round.(Int, ppu .* (rx, ry, rw, rh)) sid = zeros(SelectionID{UInt32}, rw, rh) glReadPixels(rx, ry, rw, rh, buff.format, buff.pixeltype, sid) @@ -67,17 +67,26 @@ end # Skips one set of allocations function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(scene) + w, h = size(screen.root_scene) # unitless dimensions ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) - x0, y0 = max.(1, floor.(Int, xy .- range)) - x1, y1 = min.((w, h), floor.(Int, xy .+ range)) + fb = screen.framebuffer + ppu = screen.px_per_unit[] + w, h = size(fb) # pixel dimensions + x0, y0 = max.(1, floor.(Int, ppu .* (xy .- range))) + x1, y1 = min.((w, h), ceil.(Int, ppu .* (xy .+ range))) dx = x1 - x0; dy = y1 - y0 - sids = pick_native(screen, Rect2i(x0, y0, dx, dy)) - min_dist = range^2 + ShaderAbstractions.switch_context!(screen.glscreen) + glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) + glReadBuffer(GL_COLOR_ATTACHMENT1) + buff = fb.buffers[:objectid] + sids = zeros(SelectionID{UInt32}, dx, dy) + glReadPixels(x0, y0, dx, dy, buff.format, buff.pixeltype, sids) + + min_dist = floatmax(Float32) id = SelectionID{Int}(0, 0) - x, y = xy .+ 1 .- Vec2f(x0, y0) + x, y = xy .* ppu .+ 1 .- Vec2f(x0, y0) for i in 1:dx, j in 1:dy d = (x-i)^2 + (y-j)^2 sid = sids[i, j] @@ -96,20 +105,29 @@ end # Skips some allocations function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) - isopen(screen) || return (nothing, 0) - w, h = size(scene) + isopen(screen) || return Tuple{AbstractPlot, Int}[] + w, h = size(screen.root_scene) # unitless dimensions if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) return Tuple{AbstractPlot, Int}[] end - x0, y0 = max.(1, floor.(Int, xy .- range)) - x1, y1 = min.([w, h], ceil.(Int, xy .+ range)) + + fb = screen.framebuffer + ppu = screen.px_per_unit[] + w, h = size(fb) # pixel dimensions + x0, y0 = max.(1, floor.(Int, ppu .* (xy .- range))) + x1, y1 = min.((w, h), ceil.(Int, ppu .* (xy .+ range))) dx = x1 - x0; dy = y1 - y0 - picks = pick_native(screen, Rect2i(x0, y0, dx, dy)) + ShaderAbstractions.switch_context!(screen.glscreen) + glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) + glReadBuffer(GL_COLOR_ATTACHMENT1) + buff = fb.buffers[:objectid] + picks = zeros(SelectionID{UInt32}, dx, dy) + glReadPixels(x0, y0, dx, dy, buff.format, buff.pixeltype, picks) selected = filter(x -> x.id > 0 && haskey(screen.cache2plot, x.id), unique(vec(picks))) - distances = Float32[range^2 for _ in selected] - x, y = xy .+ 1 .- Vec2f(x0, y0) + distances = Float32[floatmax(Float32) for _ in selected] + x, y = xy .* ppu .+ 1 .- Vec2f(x0, y0) for i in 1:dx, j in 1:dy if picks[i, j].id > 0 d = (x-i)^2 + (y-j)^2 diff --git a/GLMakie/src/precompiles.jl b/GLMakie/src/precompiles.jl index 311f553ad2c..87b0fda0f87 100644 --- a/GLMakie/src/precompiles.jl +++ b/GLMakie/src/precompiles.jl @@ -47,7 +47,7 @@ let close(screen) empty!(atlas_texture_cache) - closeall() + closeall(; empty_shader=false) @assert isempty(SCREEN_REUSE_POOL) @assert isempty(ALL_SCREENS) @assert isempty(SINGLETON_SCREEN) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 23ffcc18ff2..6188b400bd3 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -145,6 +145,8 @@ function activate!(; inline=LAST_INLINE[], screen_config...) return end +const unimplemented_error = "GLMakie doesn't own screen.glscreen! If you're embedding GLMakie with a custom window type you must specialize this function for your window type." + """ Screen(; screen_config...) @@ -158,6 +160,7 @@ $(Base.doc(MakieScreen)) """ mutable struct Screen{GLWindow} <: MakieScreen glscreen::GLWindow + owns_glscreen::Bool shader_cache::GLAbstraction.ShaderCache framebuffer::GLFramebuffer config::Union{Nothing, ScreenConfig} @@ -173,7 +176,7 @@ mutable struct Screen{GLWindow} <: MakieScreen cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, AbstractPlot} framecache::Matrix{RGB{N0f8}} - render_tick::Observable{Nothing} # listeners must not Consume(true) + render_tick::Observable{Makie.TickState} # listeners must not Consume(true) window_open::Observable{Bool} scalefactor::Observable{Float32} @@ -185,6 +188,7 @@ mutable struct Screen{GLWindow} <: MakieScreen function Screen( glscreen::GLWindow, + owns_glscreen::Bool, shader_cache::GLAbstraction.ShaderCache, framebuffer::GLFramebuffer, config::Union{Nothing, ScreenConfig}, @@ -202,11 +206,11 @@ mutable struct Screen{GLWindow} <: MakieScreen s = size(framebuffer) screen = new{GLWindow}( - glscreen, shader_cache, framebuffer, + glscreen, owns_glscreen, shader_cache, framebuffer, config, stop_renderloop, rendertask, BudgetedTimer(1.0 / 30.0), Observable(0f0), screen2scene, screens, renderlist, postprocessors, cache, cache2plot, - Matrix{RGB{N0f8}}(undef, s), Observable(nothing), + Matrix{RGB{N0f8}}(undef, s), Observable(Makie.UnknownTickState), Observable(true), Observable(0f0), nothing, reuse, true, false ) push!(ALL_SCREENS, screen) # track all created screens @@ -220,54 +224,60 @@ Makie.isvisible(screen::Screen) = screen.config.visible # gets removed in destroy!(screen) const ALL_SCREENS = Set{Screen}() -function empty_screen(debugging::Bool; reuse=true) - windowhints = [ - (GLFW.SAMPLES, 0), - (GLFW.DEPTH_BITS, 0), - - # SETTING THE ALPHA BIT IS REALLY IMPORTANT ON OSX, SINCE IT WILL JUST KEEP SHOWING A BLACK SCREEN - # WITHOUT ANY ERROR -.- - (GLFW.ALPHA_BITS, 8), - (GLFW.RED_BITS, 8), - (GLFW.GREEN_BITS, 8), - (GLFW.BLUE_BITS, 8), - - (GLFW.STENCIL_BITS, 0), - (GLFW.AUX_BUFFERS, 0), +function empty_screen(debugging::Bool; reuse=true, window=nothing) + owns_glscreen = isnothing(window) + initial_resolution = (10, 10) + + if isnothing(window) + windowhints = [ + (GLFW.SAMPLES, 0), + (GLFW.DEPTH_BITS, 0), + + # SETTING THE ALPHA BIT IS REALLY IMPORTANT ON OSX, SINCE IT WILL JUST KEEP SHOWING A BLACK SCREEN + # WITHOUT ANY ERROR -.- + (GLFW.ALPHA_BITS, 8), + (GLFW.RED_BITS, 8), + (GLFW.GREEN_BITS, 8), + (GLFW.BLUE_BITS, 8), + + (GLFW.STENCIL_BITS, 0), + (GLFW.AUX_BUFFERS, 0), + + (GLFW.SCALE_TO_MONITOR, true), # Windows & X11 + (GLFW.SCALE_FRAMEBUFFER, true), # OSX & Wayland + ] + window = try + GLFW.Window( + resolution = initial_resolution, + windowhints = windowhints, + visible = false, + focus = false, + fullscreen = false, + debugging = debugging, + ) + catch e + @warn(""" - (GLFW.SCALE_TO_MONITOR, true), - ] - resolution = (10, 10) - window = try - GLFW.Window( - resolution = resolution, - windowhints = windowhints, - visible = false, - focus = false, - fullscreen = false, - debugging = debugging, - ) - catch e - @warn(""" GLFW couldn't create an OpenGL window. This likely means, you don't have an OpenGL capable Graphic Card, or you don't have an OpenGL 3.3 capable video driver installed. Have a look at the troubleshooting section in the GLMakie readme: https://github.com/MakieOrg/Makie.jl/tree/master/GLMakie#troubleshooting-opengl. """) - rethrow(e) - end + rethrow(e) + end - # GLFW doesn't support setting the icon on OSX - if !Sys.isapple() - GLFW.SetWindowIcon(window, Makie.icon()) + # GLFW doesn't support setting the icon on OSX + if !Sys.isapple() + GLFW.SetWindowIcon(window, Makie.icon()) + end end # tell GLAbstraction that we created a new context. # This is important for resource tracking, and only needed for the first context ShaderAbstractions.switch_context!(window) shader_cache = GLAbstraction.ShaderCache(window) - fb = GLFramebuffer(resolution) + fb = GLFramebuffer(initial_resolution) postprocessors = [ empty_postprocessor(), empty_postprocessor(), @@ -276,7 +286,7 @@ function empty_screen(debugging::Bool; reuse=true) ] screen = Screen( - window, shader_cache, fb, + window, owns_glscreen, shader_cache, fb, nothing, false, nothing, Dict{WeakRef, ScreenID}(), @@ -287,8 +297,11 @@ function empty_screen(debugging::Bool; reuse=true) Dict{UInt32, AbstractPlot}(), reuse, ) - GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) - GLFW.SetWindowContentScaleCallback(window, scalechangecb(screen)) + + if owns_glscreen + GLFW.SetWindowRefreshCallback(window, refreshwindowcb(screen)) + GLFW.SetWindowContentScaleCallback(window, scalechangecb(screen)) + end return screen end @@ -296,6 +309,10 @@ end const SCREEN_REUSE_POOL = Set{Screen}() function reopen!(screen::Screen) + if !screen.owns_glscreen + error(unimplemented_error) + end + @debug("reopening screen") gl = screen.glscreen @assert !was_destroyed(gl) @@ -309,10 +326,10 @@ function reopen!(screen::Screen) return screen end -function screen_from_pool(debugging) +function screen_from_pool(debugging; window=nothing) screen = if isempty(SCREEN_REUSE_POOL) @debug("create empty screen for pool") - empty_screen(debugging) + empty_screen(debugging; window) else @debug("get old screen from pool") pop!(SCREEN_REUSE_POOL) @@ -343,15 +360,20 @@ end function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::Bool=true) @debug("Applying screen config! to existing screen") glw = screen.glscreen - ShaderAbstractions.switch_context!(glw) - GLFW.SetWindowAttrib(glw, GLFW.FOCUS_ON_SHOW, config.focus_on_show) - GLFW.SetWindowAttrib(glw, GLFW.DECORATED, config.decorated) - GLFW.SetWindowAttrib(glw, GLFW.FLOATING, config.float) - GLFW.SetWindowTitle(glw, config.title) - if !isnothing(config.monitor) - GLFW.SetWindowMonitor(glw, config.monitor) + if screen.owns_glscreen + ShaderAbstractions.switch_context!(glw) + GLFW.SetWindowAttrib(glw, GLFW.FOCUS_ON_SHOW, config.focus_on_show) + GLFW.SetWindowAttrib(glw, GLFW.DECORATED, config.decorated) + GLFW.SetWindowTitle(glw, config.title) + if GLFW.GetPlatform() != GLFW.PLATFORM_WAYLAND + GLFW.SetWindowAttrib(glw, GLFW.FLOATING, config.float) + end + if !isnothing(config.monitor) + GLFW.SetWindowMonitor(glw, config.monitor) + end end + screen.scalefactor[] = !isnothing(config.scalefactor) ? config.scalefactor : scale_factor(glw) screen.px_per_unit[] = !isnothing(config.px_per_unit) ? config.px_per_unit : screen.scalefactor[] function replace_processor!(postprocessor, idx) @@ -388,11 +410,12 @@ end function Screen(; resolution::Union{Nothing, Tuple{Int, Int}} = nothing, start_renderloop = true, + window = nothing, screen_config... ) # Screen config is managed by the current active theme, so managed by Makie config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) - screen = screen_from_pool(config.debugging) + screen = screen_from_pool(config.debugging; window) apply_config!(screen, config; start_renderloop=start_renderloop) if !isnothing(resolution) resize!(screen, resolution...) @@ -400,7 +423,14 @@ function Screen(; return screen end -set_screen_visibility!(screen::Screen, visible::Bool) = set_screen_visibility!(screen.glscreen, visible) +function set_screen_visibility!(screen::Screen, visible::Bool) + if !screen.owns_glscreen + error(unimplemented_error) + end + + set_screen_visibility!(screen.glscreen, visible) +end + function set_screen_visibility!(nw::GLFW.Window, visible::Bool) @assert nw.handle !== C_NULL GLFW.set_visibility!(nw, visible) @@ -448,10 +478,10 @@ function Screen(scene::Scene, config::ScreenConfig, ::Makie.ImageStorageFormat; return screen end -function pollevents(screen::Screen) +function pollevents(screen::Screen, frame_state::Makie.TickState) ShaderAbstractions.switch_context!(screen.glscreen) GLFW.PollEvents() - notify(screen.render_tick) + screen.render_tick[] = frame_state return end @@ -463,20 +493,24 @@ Base.show(io::IO, screen::Screen) = print(io, "GLMakie.Screen(...)") Base.isopen(x::Screen) = isopen(x.glscreen) Base.size(x::Screen) = size(x.framebuffer) -function Makie.insertplots!(screen::Screen, scene::Scene) - ShaderAbstractions.switch_context!(screen.glscreen) +function add_scene!(screen::Screen, scene::Scene) get!(screen.screen2scene, WeakRef(scene)) do id = length(screen.screens) + 1 push!(screen.screens, (id, scene)) screen.requires_update = true - onany( - (args...) -> screen.requires_update = true, - scene, - scene.visible, scene.backgroundcolor, scene.clear, - scene.ssao.bias, scene.ssao.blur, scene.ssao.radius, scene.camera.projectionview, scene.camera.resolution - ) + onany((args...) -> screen.requires_update = true, + scene, + scene.visible, scene.backgroundcolor, scene.clear, + scene.ssao.bias, scene.ssao.blur, scene.ssao.radius, scene.camera.projectionview, + scene.camera.resolution) return id end + return +end + +function Makie.insertplots!(screen::Screen, scene::Scene) + ShaderAbstractions.switch_context!(screen.glscreen) + add_scene!(screen, scene) for elem in scene.plots insert!(screen, scene, elem) end @@ -643,7 +677,13 @@ function Base.close(screen::Screen; reuse=true) return end -function closeall() +function closeall(; empty_shader=true) + # Since we call closeall to reload any shader + # We empty the shader source cache here + if empty_shader + empty!(LOADED_SHADERS) + WARN_ON_LOAD[] = false + end while !isempty(SCREEN_REUSE_POOL) screen = pop!(SCREEN_REUSE_POOL) delete!(ALL_SCREENS, screen) @@ -665,18 +705,23 @@ function Base.resize!(screen::Screen, w::Int, h::Int) window = to_native(screen) (w > 0 && h > 0 && isopen(window)) || return nothing - # Resize the window which appears on the user desktop (if necessary). - # - # On OSX with a Retina display, the window size is given in logical dimensions and - # is automatically scaled by the OS. To support arbitrary scale factors, we must account - # for the native scale factor when calculating the effective scaling to apply. - # - # On Linux and Windows, scale from the logical size to the pixel size. - ShaderAbstractions.switch_context!(window) - winscale = screen.scalefactor[] / (@static Sys.isapple() ? scale_factor(window) : 1) - winw, winh = round.(Int, winscale .* (w, h)) - if window_size(window) != (winw, winh) - GLFW.SetWindowSize(window, winw, winh) + if screen.owns_glscreen + # Resize the window which appears on the user desktop (if necessary). + # + # On some platforms(OSX and Wayland), the window size is given in logical dimensions and + # is automatically scaled by the OS. To support arbitrary scale factors, we must account + # for the native scale factor when calculating the effective scaling to apply. + # + # On others (Windows and X11), scale from the logical size to the pixel size. + ShaderAbstractions.switch_context!(window) + winscale = screen.scalefactor[] + if GLFW.GetPlatform() in (GLFW.PLATFORM_COCOA, GLFW.PLATFORM_WAYLAND) + winscale /= scale_factor(window) + end + winw, winh = round.(Int, winscale .* (w, h)) + if window_size(window) != (winw, winh) + GLFW.SetWindowSize(window, winw, winh) + end end # Then resize the underlying rendering framebuffers as well, which can be scaled @@ -731,7 +776,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma ctex = screen.framebuffer.buffers[:color] # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! - pollevents(screen) + pollevents(screen, Makie.BackendTick) # keep current buffer size to allows larger-than-window renders render_frame(screen, resize_buffers=false) # let it render if screen.config.visible @@ -769,52 +814,6 @@ end Makie.to_native(x::Screen) = x.glscreen -""" - get_loading_image(resolution) - -Loads the makie loading icon, embeds it in an image the size of `resolution`, -and returns the image. -""" -function get_loading_image(resolution) - icon = Matrix{N0f8}(undef, 192, 192) - open(joinpath(GL_ASSET_DIR, "loading.bin")) do io - read!(io, icon) - end - img = zeros(RGBA{N0f8}, resolution...) - center = resolution .÷ 2 - center_icon = size(icon) .÷ 2 - start = CartesianIndex(max.(center .- center_icon, 1)) - I1 = CartesianIndex(1, 1) - stop = min(start + CartesianIndex(size(icon)) - I1, CartesianIndex(resolution)) - for idx in start:stop - gray = icon[idx - start + I1] - img[idx] = RGBA{N0f8}(gray, gray, gray, 1.0) - end - return img -end - -function display_loading_image(screen::Screen) - fb = screen.framebuffer - fbsize = size(fb) - image = get_loading_image(fbsize) - if size(image) == fbsize - nw = to_native(screen) - # transfer loading image to gpu framebuffer - fb.buffers[:color][1:size(image, 1), 1:size(image, 2)] = image - ShaderAbstractions.is_context_active(nw) || return - w, h = fbsize - glBindFramebuffer(GL_FRAMEBUFFER, 0) # transfer back to window - glViewport(0, 0, w, h) - glClearColor(0, 0, 0, 0) - glClear(GL_COLOR_BUFFER_BIT) - # GLAbstraction.render(fb.postprocess[end]) # copy postprocess - GLAbstraction.render(screen.postprocessors[end].robjs[1]) - GLFW.SwapBuffers(nw) - else - error("loading_image needs to be Matrix{RGBA{N0f8}} with size(loading_image) == resolution") - end -end - function renderloop_running(screen::Screen) return !screen.stop_renderloop && !isnothing(screen.rendertask) && !istaskdone(screen.rendertask) end @@ -864,7 +863,7 @@ function set_framerate!(screen::Screen, fps=30) end function refreshwindowcb(screen, window) - screen.render_tick[] = nothing + screen.render_tick[] = Makie.BackendTick render_frame(screen) GLFW.SwapBuffers(window) return @@ -890,14 +889,13 @@ end scalechangeobs(screen) = scalefactor -> scalechangeobs(screen, scalefactor) -# TODO add render_tick event to scene events function vsynced_renderloop(screen) while isopen(screen) && !screen.stop_renderloop if screen.config.pause_renderloop - pollevents(screen); sleep(0.1) + pollevents(screen, Makie.PausedRenderTick); sleep(0.1) continue end - pollevents(screen) # GLFW poll + pollevents(screen, Makie.RegularRenderTick) # GLFW poll render_frame(screen) yield() GC.safepoint() @@ -908,14 +906,14 @@ end function fps_renderloop(screen::Screen) reset!(screen.timer, 1.0 / screen.config.framerate) while isopen(screen) && !screen.stop_renderloop - pollevents(screen) - - if !screen.config.pause_renderloop - pollevents(screen) # GLFW poll + if screen.config.pause_renderloop + pollevents(screen, Makie.PausedRenderTick) + else + pollevents(screen, Makie.RegularRenderTick) render_frame(screen) GLFW.SwapBuffers(to_native(screen)) end - + GC.safepoint() sleep(screen.timer) end @@ -934,14 +932,18 @@ end # const time_record = sizehint!(Float64[], 100_000) function on_demand_renderloop(screen::Screen) + tick_state = Makie.UnknownTickState # last_time = time_ns() reset!(screen.timer, 1.0 / screen.config.framerate) while isopen(screen) && !screen.stop_renderloop - pollevents(screen) # GLFW poll + pollevents(screen, tick_state) # GLFW poll if !screen.config.pause_renderloop && requires_update(screen) + tick_state = Makie.RegularRenderTick render_frame(screen) GLFW.SwapBuffers(to_native(screen)) + else + tick_state = ifelse(screen.config.pause_renderloop, Makie.PausedRenderTick, Makie.SkippedRenderTick) end GC.safepoint() diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 98e30bc505c..2aa9d1d5765 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -30,10 +30,98 @@ include("unit_tests.jl") @testset "refimages" begin ReferenceTests.mark_broken_tests() recorded_files, recording_dir = @include_reference_tests GLMakie "refimages.jl" joinpath(@__DIR__, "glmakie_refimages.jl") - missing_images, scores = ReferenceTests.record_comparison(recording_dir) + missing_images, scores = ReferenceTests.record_comparison(recording_dir, "GLMakie") ReferenceTests.test_comparison(scores; threshold = 0.05) end GLMakie.closeall() GC.gc(true) # make sure no finalizers act up! end + +@testset "Tick Events" begin + function check_tick(tick, state, count) + @test tick.state == state + @test tick.count == count + @test tick.time > 1e-9 + @test tick.delta_time > 1e-9 + end + + f, a, p = scatter(rand(10)); + @test events(f).tick[] == Makie.Tick() + + filename = "$(tempname()).png" + try + save(filename, f) + tick = events(f).tick[] + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == 0 + @test tick.time == 0.0 + @test tick.delta_time == 0.0 + finally + rm(filename) + end + + f, a, p = scatter(rand(10)); + filename = "$(tempname()).mp4" + try + tick_record = Makie.Tick[] + on(tick -> push!(tick_record, tick), events(f).tick) + record(_ -> nothing, f, filename, 1:10, framerate = 30) + + start = findfirst(tick -> tick.state == Makie.OneTimeRenderTick, tick_record) + dt = 1.0 / 30.0 + + for (i, tick) in enumerate(tick_record[start:end]) + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == i-1 + @test tick.time ≈ dt * (i-1) + @test tick.delta_time ≈ dt + end + finally + rm(filename) + end + + # test destruction of tick overwrite + f, a, p = scatter(rand(10)); + let + io = VideoStream(f) + @test events(f).tick[] == Makie.Tick(Makie.OneTimeRenderTick, 0, 0.0, 1.0 / io.options.framerate) + nothing + end + tick = Makie.Tick(Makie.UnknownTickState, 1, 1.0, 1.0) + events(f).tick[] = tick + @test events(f).tick[] == tick + + + f, a, p = scatter(rand(10)); + tick_record = Makie.Tick[] + on(t -> push!(tick_record, t), events(f).tick) + screen = GLMakie.Screen(render_on_demand = true, framerate = 30.0, pause_rendering = false, visible = false) + display(screen, f.scene) + sleep(0.15) + GLMakie.pause_renderloop!(screen) + sleep(0.1) + GLMakie.closeall() + + # Why does it start with a skipped tick? + i = 1 + while tick_record[i].state == Makie.SkippedRenderTick + check_tick(tick_record[1], Makie.SkippedRenderTick, i) + i += 1 + end + + check_tick(tick_record[i], Makie.RegularRenderTick, i) + i += 1 + + while tick_record[i].state == Makie.SkippedRenderTick + check_tick(tick_record[i], Makie.SkippedRenderTick, i) + i += 1 + end + + while (i <= length(tick_record)) && (tick_record[i].state == Makie.PausedRenderTick) + check_tick(tick_record[i], Makie.PausedRenderTick, i) + i += 1 + end + + @test i == length(tick_record)+1 +end \ No newline at end of file diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 331c813a996..6e08a85aba3 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -323,7 +323,6 @@ end screen = display(GLMakie.Screen(visible = true, scalefactor = 2), fig) @test screen.scalefactor[] === 2f0 @test screen.px_per_unit[] === 2f0 # inherited from scale factor - winscale = screen.scalefactor[] / (@static Sys.isapple() ? GLMakie.scale_factor(screen.glscreen) : 1) @test size(screen.framebuffer) == (2W, 2H) @test GLMakie.window_size(screen.glscreen) == scaled(screen, (W, H)) @@ -341,6 +340,11 @@ end picks = pick(ax.scene, quadrant) points = Set(Int(p[2]) for p in picks if p[1] isa Scatter) @test points == Set(((N+1)÷2):N) + # - pick sorted + xy_px = project_sp(ax.scene, Point2f(x[1], y[1])) + picks = GLMakie.Makie.pick_sorted(ax.scene, screen, xy_px, 50) + points = [i for (p, i) in picks if p == pl] + @test points == [1, 2, 3] # render at lower resolution screen = display(GLMakie.Screen(visible = false, scalefactor = 2, px_per_unit = 1), fig) diff --git a/MakieCore/Project.toml b/MakieCore/Project.toml index 2d2215f3e1c..5dde5dfb08c 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.8.4" +version = "0.8.9" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" diff --git a/MakieCore/src/attributes.jl b/MakieCore/src/attributes.jl index cf092c6eac6..3195f77ec40 100644 --- a/MakieCore/src/attributes.jl +++ b/MakieCore/src/attributes.jl @@ -77,7 +77,7 @@ function Base.merge!(target::Attributes, args::Attributes...) return target end -Base.merge(target::Attributes, args::Attributes...) = merge!(copy(target), args...) +Base.merge(target::Attributes, args::Attributes...) = merge!(deepcopy(target), args...) function Base.getproperty(x::Union{Attributes, AbstractPlot}, key::Symbol) if hasfield(typeof(x), key) diff --git a/MakieCore/src/basic_plots.jl b/MakieCore/src/basic_plots.jl index 0c45e806237..35a0c5b0aa2 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. +- `clip_planes::Vector{Plane3f} = Plane3f[]`: allows you to specify up to 8 planes behind which plot objects get clipped (i.e. become invisible). By default clip planes are inherited from the parent plot or scene. """ function generic_plot_attributes!(attr) attr[:transformation] = automatic @@ -26,6 +27,7 @@ function generic_plot_attributes!(attr) attr[:inspector_label] = automatic attr[:inspector_clear] = automatic attr[:inspector_hover] = automatic + attr[:clip_planes] = automatic return attr end @@ -42,7 +44,9 @@ function generic_plot_attributes(attr) space = attr[:space], inspector_label = attr[:inspector_label], inspector_clear = attr[:inspector_clear], - inspector_hover = attr[:inspector_hover] + inspector_hover = attr[:inspector_hover], + clip_planes = attr[:clip_planes] + ) end @@ -73,6 +77,12 @@ function mixin_generic_plot_attributes() inspector_clear = automatic "Sets a callback function `(inspector, plot, index) -> ...` which replaces the default `show_data` methods." inspector_hover = automatic + """ + Clip planes offer a way to do clipping in 3D space. You can set a Vector of up to 8 `Plane3f` planes here, + behind which plots will be clipped (i.e. become invisible). By default clip planes are inherited from the + parent plot or scene. You can remove parent `clip_planes` by passing `Plane3f[]`. + """ + clip_planes = automatic end end @@ -203,12 +213,23 @@ calculated_attributes!(plot::T) where T = calculated_attributes!(T, plot) Plots an image on a rectangle bounded by `x` and `y` (defaults to size of image). """ -@recipe Image (x::ClosedInterval{<:FloatType}, y::ClosedInterval{<:FloatType}, image::AbstractMatrix{<:Union{FloatType,Colorant}}) begin +@recipe Image ( + x::EndPoints, + y::EndPoints, + image::AbstractMatrix{<:Union{FloatType,Colorant}}) begin "Sets whether colors should be interpolated between pixels." interpolate = true mixin_generic_plot_attributes()... mixin_colormap_attributes()... fxaa = false + """ + Sets a transform for uv coordinates, which controls how the image is mapped to its rectangular area. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + They can also be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic colormap = [:black, :white] end @@ -241,7 +262,9 @@ If `x` and `y` are omitted with a matrix argument, they default to `x, y = axes( Note that `heatmap` is slower to render than `image` so `image` should be preferred for large, regularly spaced grids. """ -@recipe Heatmap (x::RealVector, y::RealVector, values::AbstractMatrix{<:Union{FloatType,Colorant}}) begin +@recipe Heatmap (x::Union{EndPoints,RealVector, RealMatrix}, + y::Union{EndPoints,RealVector, RealMatrix}, + values::AbstractMatrix{<:Union{FloatType,Colorant}}) begin "Sets whether colors should be interpolated" interpolate = false mixin_generic_plot_attributes()... @@ -262,9 +285,9 @@ Available algorithms are: * `:indexedabsorption` => IndexedAbsorptionRGBA """ @recipe Volume ( - x::ClosedInterval, - y::ClosedInterval, - z::ClosedInterval, + x::EndPoints, + y::EndPoints, + z::EndPoints, volume::AbstractArray{Float32,3} ) begin "Sets the volume algorithm that is used." @@ -300,6 +323,14 @@ Plots a surface, where `(x, y)` define a grid whose heights are the entries in ` invert_normals = false "[(W)GLMakie only] Specifies whether the surface matrix gets sampled with interpolation." interpolate = true + """ + Sets a transform for uv coordinates, which controls how a texture is mapped to a surface. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + They can also be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic mixin_generic_plot_attributes()... mixin_shading_attributes()... mixin_colormap_attributes()... @@ -393,6 +424,14 @@ Plots a 3D or 2D mesh. Supported `mesh_object`s include `Mesh` types from [Geome interpolate = true cycle = [:color => :patchcolor] matcap = nothing + """ + Sets a transform for uv coordinates, which controls how a texture is mapped to a mesh. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + They can also be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic mixin_generic_plot_attributes()... mixin_shading_attributes()... mixin_colormap_attributes()... @@ -472,6 +511,17 @@ Plots a mesh for each element in `(x, y, z)`, `(x, y)`, or `positions` (similar "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] + """ + Sets a transform for uv coordinates, which controls how a texture is mapped to the scattered mesh. + Note that the mesh needs to include uv coordinates for this, which is not the case by default + for geometry primitives. You can use `GeometryBasics.uv_normal_mesh(prim)` with, for example `prim = Rect2f(0, 0, 1, 1)`. + The attribute can be `I`, `scale::VecTypes{2}`, `(translation::VecTypes{2}, scale::VecTypes{2})`, + any of :rotr90, :rotl90, :rot180, :swap_xy/:transpose, :flip_x, :flip_y, :flip_xy, or most + generally a `Makie.Mat{2, 3, Float32}` or `Makie.Mat3f` as returned by `Makie.uv_transform()`. + It can also be set per scattered mesh by passing a `Vector` of any of the above and operations + can be changed by passing a tuple `(op3, op2, op1)`. + """ + uv_transform = automatic mixin_generic_plot_attributes()... mixin_shading_attributes()... mixin_colormap_attributes()... diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index 82ea2c3cfc7..4b1d9aaff97 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -12,11 +12,24 @@ end plotfunc(::Plot{F}) where F = F plotfunc(::Type{<: AbstractPlot{Func}}) where Func = Func plotfunc(::T) where T <: AbstractPlot = plotfunc(T) -plotfunc(f::Function) = f +function plotfunc(f::Function) + if endswith(string(nameof(f)), "!") + name = Symbol(string(nameof(f))[begin:end-1]) + return getproperty(parentmodule(f), name) + else + return f + end +end + +function plotfunc!(x) + F = plotfunc(x)::Function + name = Symbol(nameof(F), :!) + return getproperty(parentmodule(F), name) +end func2type(x::T) where T = func2type(T) func2type(x::Type{<: AbstractPlot}) = x -func2type(f::Function) = Plot{f} +func2type(f::Function) = Plot{plotfunc(f)} plotkey(::Type{<: AbstractPlot{Typ}}) where Typ = Symbol(lowercase(func2string(Typ))) plotkey(::T) where T <: AbstractPlot = plotkey(T) @@ -719,7 +732,7 @@ end function attribute_name_allowlist() return (:xautolimits, :yautolimits, :zautolimits, :label, :rasterize, :model, :transformation, - :dim_conversions, :cycle) + :dim_conversions, :cycle, :clip_planes) end function validate_attribute_keys(plot::P) where {P<:Plot} diff --git a/MakieCore/src/types.jl b/MakieCore/src/types.jl index 4a915ee0d90..ffb6b10139c 100644 --- a/MakieCore/src/types.jl +++ b/MakieCore/src/types.jl @@ -69,6 +69,7 @@ mutable struct Plot{PlotFunc, T} <: ScenePlot{PlotFunc} # Unprocessed arguments directly from the user command e.g. `plot(args...; kw...)`` kw::Dict{Symbol,Any} + kw_obs::Observable{Vector{Pair{Symbol,Any}}} args::Vector{Any} converted::Vector{Observable} @@ -80,10 +81,11 @@ mutable struct Plot{PlotFunc, T} <: ScenePlot{PlotFunc} parent::Union{AbstractScene,Plot} function Plot{Typ,T}( - kw::Dict{Symbol,Any}, args::Vector{Any}, converted::Vector{Observable}, - deregister_callbacks::Vector{Observables.ObserverFunction}=Observables.ObserverFunction[] + kw::Dict{Symbol,Any}, kw_obs::Observable{Vector{Pair{Symbol,Any}}}, + args::Vector{Any}, converted::Vector{Observable}, + deregister_callbacks::Vector{Observables.ObserverFunction}=Observables.ObserverFunction[] ) where {Typ,T} - return new{Typ,T}(nothing, kw, args, converted, Attributes(), Plot[], deregister_callbacks) + return new{Typ,T}(nothing, kw, kw_obs, args, converted, Attributes(), Plot[], deregister_callbacks) end end @@ -149,3 +151,22 @@ const RealArray{T,N} = AbstractArray{T,N} where {T<:Real} const RealVector{T} = RealArray{1} const RealMatrix{T} = RealArray{2} const FloatType = Union{Float32,Float64} + +# This could be simply a tuple or ClosedInterval +# But ClosedInterval doesn't support all operations/constructions we need +# And a plain tuple does not work, since for heatmap we need a final type that spans the corners. +# E.g. (0, 3) becomes (-0.5, 3.5) for a 3x3 heatmap, so if we have a tuple as input we need to do this calculation +# And only if it's an EndPoint type, we can be sure its already in the correct format. +struct EndPoints{T} <: AbstractVector{T} + data::NTuple{2,T} +end +EndPoints(a::Number, b::Number) = EndPoints((a, b)) +EndPoints{T}(a::Number, b::Number) where {T} = EndPoints{T}((T(a), T(b))) +Base.size(::EndPoints) = (2,) +Base.getindex(e::EndPoints, i::Int) = e.data[i] +Base.broadcasted(f, e::EndPoints) = EndPoints(f.(e.data)) +Base.broadcasted(f, a::EndPoints, b) = EndPoints(f.(a.data, b)) +Base.broadcasted(f, a, b::EndPoints) = EndPoints(f.(a, b.data)) +Base.:(==)(a::EndPoints, b::NTuple{2}) = a.data == b +# Something we can convert to an EndPoints type +const EndPointsLike = Union{ClosedInterval,Tuple{Real,Real}} diff --git a/Project.toml b/Project.toml index 56d196c2a46..c4026e2a4a0 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.21.5" +version = "0.21.14" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" @@ -26,9 +26,12 @@ FreeType = "b38be410-82b0-50bf-ab77-7b57e271db43" FreeTypeAbstraction = "663a7486-cb36-511b-a19d-713bb74d65c9" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" GridLayoutBase = "3955a311-db13-416c-9275-1d80ed98e5e9" +ImageBase = "c817782e-172a-44cc-b673-b171935fbb9e" ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" +InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112" Isoband = "f1662d9f-8043-43de-a69a-05efc1cc6ff4" KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" @@ -82,21 +85,24 @@ FreeType = "3.0, 4.0" FreeTypeAbstraction = "0.10.3" GeometryBasics = "0.4.11" GridLayoutBase = "0.11" -ImageIO = "0.2, 0.3, 0.4, 0.5, 0.6" +ImageBase = "0.1.7" +ImageIO = "0.5, 0.6" InteractiveUtils = "1.0, 1.6" +Interpolations = "0.14, 0.15.1" IntervalSets = "0.3, 0.4, 0.5, 0.6, 0.7" +InverseFunctions = "0.1" Isoband = "0.1" KernelDensity = "0.5, 0.6" LaTeXStrings = "1.2" LinearAlgebra = "1.0, 1.6" MacroTools = "0.5" -MakieCore = "=0.8.4" +MakieCore = "=0.8.9" Markdown = "1.0, 1.6" MathTeXEngine = "0.5, 0.6" Observables = "0.5.5" OffsetArrays = "1" Packing = "0.5" -PlotUtils = "1" +PlotUtils = "1.4.2" PolygonOps = "0.1.1" PrecompileTools = "1.0" Printf = "1.0, 1.6" diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index ade2d96555a..550b0291ea8 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.7.5" +version = "0.7.14" [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.11" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.5" +Makie = "=0.21.14" 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 fabfc02328d..d635d58a8f7 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -61,6 +61,7 @@ end function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) # Potentially per instance attributes + !plot.visible[] && return nothing positions = to_value(plot[1]) m_mesh = convert_attribute(plot.marker[], key"marker"(), key"meshscatter"()) marker = RPR.Shape(context, m_mesh) @@ -76,29 +77,14 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) end color = plot.calculated_colors[] - if color isa Makie.ColorMapping - color_from_num = to_color(color) + if color isa AbstractVector{<:Union{Number,Colorant}} || color isa Makie.ColorMapping + c_converted = to_color(color) 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 - elseif color isa AbstractMatrix{<:Number} - color_from_num = to_color(color) - 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, color_from_num; uv=uv) - + uv = object_id * Vec3f(0, 1 / (n_instances-1), 0) + tex = RPR.Texture(matsys, reverse(c_converted)'; uv=uv) material.color = tex - elseif color isa Colorant - material.color = color - elseif color isa AbstractMatrix{<: Colorant} + elseif color isa Union{Colorant, AbstractMatrix{<:Colorant}} material.color = color else error("Unsupported color type for RadeonProRender backend: $(typeof(color))") @@ -121,7 +107,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.MeshScatter) end for (instance, position, scale, rotation) in zip(instances, positions, scales, rotations) - mat = Makie.transformationmatrix(position, scale, rotation) + mat = Makie.transformationmatrix(to_ndim(Point3f, position, 0), scale, rotation) transform!(instance, mat) end @@ -164,6 +150,7 @@ end function to_rpr_object(context, matsys, scene, plot::Makie.Surface) + !plot.visible[] && return nothing x = plot[1] y = plot[2] z = plot[3] diff --git a/RPRMakie/src/scene.jl b/RPRMakie/src/scene.jl index 84314e1d5df..179d3138b64 100644 --- a/RPRMakie/src/scene.jl +++ b/RPRMakie/src/scene.jl @@ -11,6 +11,8 @@ function update_rpr_camera!(oldvals, camera, cam_controls, cam) RPR.rprCameraSetSensorSize(camera, res...) RPR.rprCameraSetFocusDistance(camera, wd) lookat!(camera, p, l, u) + far = wd * 10 + near = wd * 0.001 RPR.rprCameraSetFarPlane(camera, far) RPR.rprCameraSetNearPlane(camera, near) focal_length = res[2] / (2 * tand(fov / 2)) # fov is vertical diff --git a/ReferenceTests/Project.toml b/ReferenceTests/Project.toml index 7ba378ebc8e..79d24a9d6c4 100644 --- a/ReferenceTests/Project.toml +++ b/ReferenceTests/Project.toml @@ -23,6 +23,7 @@ MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Tar = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" diff --git a/ReferenceTests/src/ReferenceTests.jl b/ReferenceTests/src/ReferenceTests.jl index 28d62aff621..11189a69cd0 100644 --- a/ReferenceTests/src/ReferenceTests.jl +++ b/ReferenceTests/src/ReferenceTests.jl @@ -27,6 +27,7 @@ using LaTeXStrings using GeometryBasics using DelimitedFiles using DelaunayTriangulation +using SparseArrays basedir(files...) = normpath(joinpath(@__DIR__, "..", files...)) loadasset(files...) = FileIO.load(assetpath(files...)) diff --git a/ReferenceTests/src/database.jl b/ReferenceTests/src/database.jl index 49c5bafdbf0..6bb2b835fd7 100644 --- a/ReferenceTests/src/database.jl +++ b/ReferenceTests/src/database.jl @@ -17,6 +17,7 @@ const RECORDING_DIR = Base.RefValue{String}() const SKIP_TITLES = Set{String}() const SKIP_FUNCTIONS = Set{Symbol}() const COUNTER = Ref(0) +const SKIPPED_NAMES = Set{String}() # names skipped due to title exclusion or function exclusion """ @reference_test(name, code) @@ -32,6 +33,7 @@ macro reference_test(name, code) @testset $(title) begin if $skip @test_broken false + mark_skipped!($title) else t1 = time() if $title in $REGISTERED_TESTS @@ -84,10 +86,13 @@ end function mark_broken_tests(title_excludes = []; functions=[]) empty!(SKIP_TITLES) empty!(SKIP_FUNCTIONS) + empty!(SKIPPED_NAMES) union!(SKIP_TITLES, title_excludes) union!(SKIP_FUNCTIONS, functions) end +mark_skipped!(name::String) = push!(SKIPPED_NAMES, name) + macro include_reference_tests(backend::Symbol, path, paths...) toplevel_folder = dirname(string(__source__.file)) return esc(quote diff --git a/ReferenceTests/src/runtests.jl b/ReferenceTests/src/runtests.jl index cb0d0672322..52a689289d0 100644 --- a/ReferenceTests/src/runtests.jl +++ b/ReferenceTests/src/runtests.jl @@ -81,7 +81,7 @@ function get_all_relative_filepaths_recursively(dir) end end -function record_comparison(base_folder::String; record_folder_name="recorded", tag=last_major_version()) +function record_comparison(base_folder::String, backend::String; record_folder_name="recorded", tag=last_major_version()) record_folder = joinpath(base_folder, record_folder_name) @info "Downloading reference images" reference_folder = download_refimages(tag) @@ -99,6 +99,19 @@ function record_comparison(base_folder::String; record_folder_name="recorded", t println(file, path) end end + + open(joinpath(base_folder, "missing_files.txt"), "w") do file + backend_ref_dir = joinpath(reference_folder, backend) + recorded_paths = mapreduce(vcat, walkdir(backend_ref_dir)) do (root, dirs, files) + relpath.(joinpath.(root, files), reference_folder) + end + skipped = Set([joinpath(backend, "$name.png") for name in SKIPPED_NAMES]) + missing_recordings = setdiff(Set(recorded_paths), Set(testimage_paths), skipped) + + for path in missing_recordings + println(file, path) + end + end open(joinpath(base_folder, "scores.tsv"), "w") do file paths_scores = sort(collect(pairs(scores)), by = last, rev = true) @@ -120,7 +133,12 @@ function test_comparison(scores; threshold) end end -function compare(relative_test_paths::Vector{String}, reference_dir::String, record_dir; o_refdir=reference_dir, missing_refimages=String[], scores=Dict{String,Float64}()) +function compare( + relative_test_paths::Vector{String}, reference_dir::String, record_dir; + o_refdir = reference_dir, missing_refimages = String[], + scores = Dict{String,Float64}() + ) + for relative_test_path in relative_test_paths ref_path = joinpath(reference_dir, relative_test_path) rec_path = joinpath(record_dir, relative_test_path) diff --git a/ReferenceTests/src/tests/attributes.jl b/ReferenceTests/src/tests/attributes.jl index 5b89c3d86bd..c045cc717f3 100644 --- a/ReferenceTests/src/tests/attributes.jl +++ b/ReferenceTests/src/tests/attributes.jl @@ -1,35 +1,3 @@ -@reference_test "glowcolor, glowwidth" begin - scatter(RNG.randn(10), color=:blue, glowcolor=:orange, glowwidth=10) -end - -@reference_test "isorange, isovalue" begin - r = range(-1, stop=1, length=100) - matr = [(x.^2 + y.^2 + z.^2) for x = r, y = r, z = r] - volume(matr .* (matr .> 1.4), algorithm=:iso, isorange=0.05, isovalue=1.7, colorrange=(0, 1)) -end - -@reference_test "levels" begin - x = LinRange(-1, 1, 20) - y = LinRange(-1, 1, 20) - z = x .* y' - contour(x, y, z, linewidth=3, colormap=:colorwheel, levels=50) -end - - -@reference_test "position" begin - fig, ax, sc = scatter(RNG.rand(10), color=:red) - text!(ax, 5, 1.1, text = "adding text", fontsize=0.6) - fig -end - -@reference_test "rotation" begin - text("Hello World", rotation=1.1) -end - -@reference_test "shading" begin - mesh(Sphere(Point3f(0), 1f0), color=:orange, shading=NoShading) -end - @reference_test "visible" begin fig = Figure() colors = Makie.resample(to_colormap(:deep), 20) diff --git a/ReferenceTests/src/tests/dates.jl b/ReferenceTests/src/tests/dates.jl index 5d68b360f7d..68ee8cae1e9 100644 --- a/ReferenceTests/src/tests/dates.jl +++ b/ReferenceTests/src/tests/dates.jl @@ -7,9 +7,13 @@ time_range = some_time .+ range(Second(0); step=Second(5), length=10) date_range = range(date, step=Day(5), length=10) date_time_range = range(date_time, step=Week(5), length=10) -@reference_test "time_range" scatter(time_range, 1:10) -@reference_test "date_range" scatter(date_range, 1:10) -@reference_test "date_time_range" scatter(date_time_range, 1:10) +@reference_test "Time & Date ranges" begin + f = Figure() + scatter(f[1, 1], time_range, 1:10, axis = (xticklabelrotation = pi/4, )) + scatter(f[1, 2], date_range, 1:10, axis = (xticklabelrotation = pi/4, )) + scatter(f[2, 1], date_time_range, 1:10, axis = (xticklabelrotation = pi/4, )) + f +end @reference_test "Don'some_time allow mixing units incorrectly" begin date_time_range = range(date_time, step=Second(5), length=10) diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 69a3c97eef1..4fa7d400b79 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -1,14 +1,11 @@ -@reference_test "Test heatmap + image overlap" begin - heatmap(RNG.rand(32, 32)) +@reference_test "RGB heatmap, heatmap + image overlap" begin + fig = Figure() + heatmap(fig[1, 1], RNG.rand(32, 32)) image!(map(x -> RGBAf(x, 0.5, 0.5, 0.8), RNG.rand(32, 32))) - current_figure() -end -@reference_test "Test RGB heatmaps" begin - fig = Figure() - heatmap(fig[1, 1], RNG.rand(RGBf, 32, 32)) - heatmap(fig[1, 2], RNG.rand(RGBAf, 32, 32)) + heatmap(fig[2, 1], RNG.rand(RGBf, 32, 32)) + heatmap(fig[2, 2], RNG.rand(RGBAf, 32, 32)) fig end @@ -61,7 +58,7 @@ end fig end -@reference_test "FEM polygon 2D" begin +@reference_test "FEM poly and mesh" begin coordinates = [ 0.0 0.0; 0.5 0.0; @@ -84,50 +81,55 @@ end 5 8 9; ] color = [0.0, 0.0, 0.0, 0.0, -0.375, 0.0, 0.0, 0.0, 0.0] - poly(coordinates, connectivity, color=color, strokecolor=(:black, 0.6), strokewidth=4) -end -@reference_test "FEM mesh 2D" begin - coordinates = [ - 0.0 0.0; - 0.5 0.0; - 1.0 0.0; - 0.0 0.5; - 0.5 0.5; - 1.0 0.5; - 0.0 1.0; - 0.5 1.0; - 1.0 1.0; - ] - connectivity = [ - 1 2 5; - 1 4 5; - 2 3 6; - 2 5 6; - 4 5 8; - 4 7 8; - 5 6 9; - 5 8 9; - ] - color = [0.0, 0.0, 0.0, 0.0, -0.375, 0.0, 0.0, 0.0, 0.0] - fig, ax, meshplot = mesh(coordinates, connectivity, color=color, shading=NoShading) - wireframe!(ax, meshplot[1], color=(:black, 0.6), linewidth=3) - fig + f = Figure() + poly(f[1, 1], coordinates, connectivity, color=color, strokecolor=(:black, 0.6), strokewidth=4) + + a, meshplot = mesh(f[2, 1], coordinates, connectivity, color=color, shading=NoShading) + wireframe!(meshplot[1], color=(:black, 0.6), linewidth=3) + + cat = loadasset("cat.obj") + vertices = decompose(Point3f, cat) + faces = decompose(TriangleFace{Int}, cat) + coordinates = [vertices[i][j] for i = 1:length(vertices), j = 1:3] + connectivity = [faces[i][j] for i = 1:length(faces), j = 1:3] + mesh(f[1:2, 2], + coordinates, connectivity, + color=RNG.rand(length(vertices)) + ) + + f end -@reference_test "colored triangle" begin - mesh( +@reference_test "colored triangle (mesh, poly, 3D) + poly stroke" begin + f = Figure() + mesh(f[1, 1], [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)], color=[:red, :green, :blue], shading=NoShading ) -end -@reference_test "colored triangle with poly" begin - poly( + poly(f[1, 2], [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)], color=[:red, :green, :blue], strokecolor=:black, strokewidth=2 ) + + x = [0, 1, 2, 0] + y = [0, 0, 1, 2] + z = [0, 2, 0, 1] + color = [:red, :green, :blue, :yellow] + i = [0, 0, 0, 1] + j = [1, 2, 3, 2] + k = [2, 3, 1, 3] + # indices interpreted as triangles (every 3 sequential indices) + indices = [1, 2, 3, 1, 3, 4, 1, 4, 2, 2, 3, 4] + mesh(f[2, 1], x, y, z, indices, color=color) + + ax, p = poly(f[2, 2], [Rect2f(0, 0, 1, 1)], color=:green, strokewidth=50, strokecolor=:black) + xlims!(ax, -0.5, 1.5) + ylims!(ax, -0.5, 1.5) + + f end @reference_test "scale_plot" begin @@ -152,35 +154,6 @@ end fig end -@reference_test "Text Annotation" begin - text( - ". This is an annotation!", - position=(300, 200), - align=(:center, :center), - fontsize=60, - font="Blackchancery" - ) -end - -@reference_test "Text rotation" begin - fig = Figure() - ax = fig[1, 1] = Axis(fig) - pos = (500, 500) - posis = Point2f[] - for r in range(0, stop=2pi, length=20) - p = pos .+ (sin(r) * 100.0, cos(r) * 100) - push!(posis, p) - text!(ax, "test", - position=p, - fontsize=50, - rotation=1.5pi - r, - align=(:center, :center) - ) - end - scatter!(ax, posis, markersize=10) - fig -end - @reference_test "Standard deviation band" begin # Sample 100 Brownian motion path and plot the mean trajectory together # with a ±1σ band (visualizing uncertainty as marginal standard deviation). @@ -195,6 +168,40 @@ end current_figure() end +@reference_test "Band with NaN" begin + f = Figure() + ax1 = Axis(f[1, 1]) + + # NaN in the middle + band!(ax1, 1:5, [1, 2, NaN, 4, 5], [1.5, 3, 4, 5, 6.5]) + band!(ax1, 1:5, [3, 4, 5, 6, 7], [3.5, 5, NaN, 7, 8.5]) + band!(ax1, [1, 2, NaN, 4, 5], [5, 6, 7, 8, 9], [5.5, 7, 8, 9, 10.5]) + + ax2 = Axis(f[1, 2]) + + # NaN at the beginning and end + band!(ax2, 1:5, [NaN, 2, 3, 4, NaN], [1.5, 3, 4, 5, 6.5]) + band!(ax2, 1:5, [3, 4, 5, 6, 7], [NaN, 5, 6, 7, NaN]) + band!(ax2, [NaN, 2, 3, 4, NaN], [5, 6, 7, 8, 9], [5.5, 7, 8, 9, 10.5]) + + ax3 = Axis(f[2, 1]) + + # No complete section + band!(ax3, 1:5, [NaN, 2, NaN, 4, NaN], [1.5, 3, 4, 5, 6.5]) + band!(ax3, 1:5, [3, 4, 5, 6, 7], [NaN, 5, NaN, 7, NaN]) + band!(ax3, [NaN, 2, NaN, 4, NaN], [5, 6, 7, 8, 9], [5.5, 7, 8, 9, 10.5]) + + ax4 = Axis(f[2, 2]) + # Two adjacent NaNs + band!(ax4, 1:6, [1, 2, NaN, NaN, 5, 6], [1.5, 3, 4, 5, 6, 7.5]) + band!(ax4, 1:6, [3, 4, 5, 6, 7, 8], [3.5, 5, NaN, NaN, 8, 9.5]) + band!(ax4, [1, 2, NaN, NaN, 5, 6], [5, 6, 7, 8, 9, 10], [5.5, 7, 8, 9, 10, 11.5]) + + linkaxes!(ax1, ax2, ax3, ax4) + + f +end + @reference_test "Streamplot animation" begin v(x::Point2{T}, t) where T = Point2{T}(one(T) * x[2] * t, 4 * x[1]) sf = Observable(Base.Fix2(v, 0.0)) @@ -359,18 +366,68 @@ end end -@reference_test "Simple pie chart" begin - fig = Figure(size=(800, 800)) +@reference_test "Simple pie charts" begin + fig = Figure() pie(fig[1, 1], 1:5, color=collect(1:5), axis=(;aspect=DataAspect())) + pie(fig[1, 2], 1:5, color=collect(1.0:5), radius=2, inner_radius=1, axis=(;aspect=DataAspect())) + pie(fig[2, 1], 0.1:0.1:1.0, normalize=false, axis=(;aspect=DataAspect())) fig end -@reference_test "Hollow pie chart" begin - pie(1:5, color=collect(1.0:5), radius=2, inner_radius=1, axis=(;aspect=DataAspect())) +@reference_test "Pie with Segment-specific Radius" begin + fig = Figure() + ax = Axis(fig[1, 1]; autolimitaspect=1) + + kw = (; offset_radius=0.4, strokecolor=:transparent, strokewidth=0) + pie!(ax, ones(7); radius=sqrt.(2:8) * 3, kw..., color=Makie.wong_colors(0.8)[1:7]) + + vs = [2, 3, 4, 5, 6, 7, 8] + vs_inner = [1, 1, 1, 1, 2, 2, 2] + rs = 8 + rs_inner = sqrt.(vs_inner ./ vs) * rs + + lp = Makie.LinePattern(; direction=Makie.Vec2f(1, -1), width=2, tilesize=(12, 12), linecolor=:darkgrey, background_color=:transparent) + # draw the inner pie twice since `color` can not be vector of `LinePattern` currently + pie!(ax, 20, 0, vs; radius=rs_inner, inner_radius=0, kw..., color=Makie.wong_colors(0.4)[eachindex(vs)]) + pie!(ax, 20, 0, vs; radius=rs_inner, inner_radius=0, kw..., color=lp) + pie!(ax, 20, 0, vs; radius=rs, inner_radius=rs_inner, kw..., color=Makie.wong_colors(0.8)[eachindex(vs)]) + + fig end -@reference_test "Open pie chart" begin - pie(0.1:0.1:1.0, normalize=false, axis=(;aspect=DataAspect())) +@reference_test "Pie Position" begin + fig = Figure() + ax = Axis(fig[1, 1]; autolimitaspect=1) + + vs = 0:6 |> Vector + vs_ = vs ./ sum(vs) .* (3/2*π) + cs = Makie.wong_colors() + Δx = [1, 1, 1, -1, -1, -1, 1] ./ 10 + Δy = [1, 1, 1, 1, 1, -1, -1] ./ 10 + Δr1 = [0, 0, 0.2, 0, 0.2, 0, 0] + Δr2 = [0, 0, 0.2, 0, 0, 0, 0] + + pie!(ax, vs; color=cs) + pie!(ax, 3 .+ Δx, 0, vs; color=cs) + pie!(ax, 0, 3 .+ Δy, vs; color=cs) + pie!(ax, 3 .+ Δx, 3 .+ Δy, vs; color=cs) + + pie!(ax, 7, 0, vs; color=cs, offset_radius=Δr1) + pie!(ax, 7, 3, vs; color=cs, offset_radius=0.2) + pie!(ax, 10 .+ Δx, 3 .+ Δy, vs; color=cs, offset_radius=0.2) + pie!(ax, 10, 0, vs_; color=cs, offset_radius=Δr1, normalize=false, offset=π/2) + + pie!(ax, Point2(0.5, -3), vs_; color=cs, offset_radius=Δr2, normalize=false, offset=π/2) + pie!(ax, Point2.(3.5, -3 .+ Δy), vs_; color=cs, offset_radius=Δr2, normalize=false, offset=π/2) + pie!(ax, Point2.(6.5 .+ Δx, -3), vs_; color=cs, offset_radius=Δr2, normalize=false, offset=π/2) + pie!(ax, Point2.(9.5 .+ Δx, -3 .+ Δy), vs_; color=cs, offset_radius=Δr2, normalize=false, offset=π/2) + + pie!(ax, 0.5, -6, vs_; inner_radius=0.2, color=cs, offset_radius=0.2, normalize=false, offset=π/2) + pie!(ax, 3.5, -6 .+ Δy, vs_; inner_radius=0.2, color=cs, offset_radius=0.2, normalize=false, offset=π/2) + pie!(ax, 6.5 .+ Δx, -6, vs_; inner_radius=0.2, color=cs, offset_radius=0.2, normalize=false, offset=π/2) + pie!(ax, 9.5 .+ Δx, -6 .+ Δy, vs_; inner_radius=0.2, color=cs, offset_radius=0.2, normalize=false, offset=π/2) + + fig end @reference_test "intersecting polygon" begin @@ -378,14 +435,6 @@ end poly(Point2f.(zip(sin.(x), sin.(2x))), color = :white, strokecolor = :blue, strokewidth = 10) end - -@reference_test "Line Function" begin - x = range(0, stop=3pi) - fig, ax, lineplot = lines(x, sin.(x)) - lines!(ax, x, cos.(x), color=:blue) - fig -end - @reference_test "Grouped bar" begin x1 = ["a_right", "a_right", "a_right", "a_right"] y1 = [2, 3, -3, -2] @@ -519,14 +568,12 @@ end @reference_test "Array of Images Scatter" begin img = Makie.logo() scatter(1:2, 1:2, marker = [img, img], markersize=reverse(size(img) ./ 10), axis=(limits=(0.5, 2.5, 0.5, 2.5),)) -end -@reference_test "Image Scatter different sizes" begin - img = Makie.logo() img2 = load(Makie.assetpath("doge.png")) images = [img, img2] markersize = map(img-> Vec2f(reverse(size(img) ./ 10)), images) - scatter(1:2, 1:2, marker = images, markersize=markersize, axis=(limits=(0.5, 2.5, 0.5, 2.5),)) + scatter!(2:-1:1, 1:2, marker = images, markersize=markersize) + current_figure() end @reference_test "2D surface with explicit color" begin @@ -626,11 +673,6 @@ end fig end -@reference_test "multi rect with poly" begin - # use thick strokewidth, so it will make tests fail if something is missing - poly([Rect2f(0, 0, 1, 1)], color=:green, strokewidth=100, strokecolor=:black) -end - @reference_test "minor grid & scales" begin data = LinRange(0.01, 0.99, 200) f = Figure(size = (800, 800)) @@ -965,29 +1007,6 @@ end ) end -@reference_test "Latex labels after the fact" begin - f = Figure(fontsize = 50) - ax = Axis(f[1, 1]) - ax.xticks = ([3, 6, 9], [L"x" , L"y" , L"z"]) - ax.yticks = ([3, 6, 9], [L"x" , L"y" , L"z"]) - f -end - -@reference_test "Rich text" begin - f = Figure(fontsize = 30, size = (800, 600)) - ax = Axis(f[1, 1], - limits = (1, 100, 0.001, 1), - xscale = log10, - yscale = log2, - title = rich("A ", rich("title", color = :red, font = :bold_italic)), - xlabel = rich("X", subscript("label", fontsize = 25)), - ylabel = rich("Y", superscript("label")), - ) - Label(f[1, 2], rich("Hi", rich("Hi", offset = (0.2, 0.2), color = :blue)), tellheight = false) - Label(f[1, 3], rich("X", superscript("super"), subscript("sub")), tellheight = false) - f -end - @reference_test "bracket scalar" begin f, ax, l = lines(0..9, sin; axis = (; xgridvisible = false, ygridvisible = false)) ylims!(ax, -1.5, 1.5) @@ -1068,6 +1087,22 @@ end f end +@reference_test "Barplot label positions" begin + f = Figure(size = (450, 450)) + func(fpos; label_position, direction) = barplot(fpos, [1, 1, 2], [1, 2, 3]; + stack = [1, 1, 2], bar_labels = ["One", "Two", "Three"], label_position, + color = [:tomato, :bisque, :slategray2], direction, label_font = :bold) + func(f[1, 1]; label_position = :end, direction = :y) + ylims!(0, 4) + func(f[1, 2]; label_position = :end, direction = :x) + xlims!(0, 4) + func(f[2, 1]; label_position = :center, direction = :y) + ylims!(0, 4) + func(f[2, 2]; label_position = :center, direction = :x) + xlims!(0, 4) + f +end + @reference_test "Histogram" begin data = sin.(1:1000) @@ -1222,59 +1257,57 @@ end fig end -# TODO: as noted in https://github.com/MakieOrg/Makie.jl/pull/3520#issuecomment-1873382060 -# this test has some issues with random number generation across Julia 1.6 and 1, for now -# it's disabled until someone has time to look into it - -# @reference_test "Triplot of a constrained triangulation with holes and a custom bounding box" begin -# curve_1 = [[ -# (0.0, 0.0), (4.0, 0.0), (8.0, 0.0), (12.0, 0.0), (12.0, 4.0), -# (12.0, 8.0), (14.0, 10.0), (16.0, 12.0), (16.0, 16.0), -# (14.0, 18.0), (12.0, 20.0), (12.0, 24.0), (12.0, 28.0), -# (8.0, 28.0), (4.0, 28.0), (0.0, 28.0), (-2.0, 26.0), (0.0, 22.0), -# (0.0, 18.0), (0.0, 10.0), (0.0, 8.0), (0.0, 4.0), (-4.0, 4.0), -# (-4.0, 0.0), (0.0, 0.0), -# ]] -# curve_2 = [[ -# (4.0, 26.0), (8.0, 26.0), (10.0, 26.0), (10.0, 24.0), -# (10.0, 22.0), (10.0, 20.0), (8.0, 20.0), (6.0, 20.0), -# (4.0, 20.0), (4.0, 22.0), (4.0, 24.0), (4.0, 26.0) -# ]] -# curve_3 = [[(4.0, 16.0), (12.0, 16.0), (12.0, 14.0), (4.0, 14.0), (4.0, 16.0)]] -# curve_4 = [[(4.0, 8.0), (10.0, 8.0), (8.0, 6.0), (6.0, 6.0), (4.0, 8.0)]] -# curves = [curve_1, curve_2, curve_3, curve_4] -# points = [ -# (2.0, 26.0), (2.0, 24.0), (6.0, 24.0), (6.0, 22.0), (8.0, 24.0), (8.0, 22.0), -# (2.0, 22.0), (0.0, 26.0), (10.0, 18.0), (8.0, 18.0), (4.0, 18.0), (2.0, 16.0), -# (2.0, 12.0), (6.0, 12.0), (2.0, 8.0), (2.0, 4.0), (4.0, 2.0), -# (-2.0, 2.0), (4.0, 6.0), (10.0, 2.0), (10.0, 6.0), (8.0, 10.0), (4.0, 10.0), -# (10.0, 12.0), (12.0, 12.0), (14.0, 26.0), (16.0, 24.0), (18.0, 28.0), -# (16.0, 20.0), (18.0, 12.0), (16.0, 8.0), (14.0, 4.0), (14.0, -2.0), -# (6.0, -2.0), (2.0, -4.0), (-4.0, -2.0), (-2.0, 8.0), (-2.0, 16.0), -# (-4.0, 22.0), (-4.0, 26.0), (-2.0, 28.0), (6.0, 15.0), (7.0, 15.0), -# (8.0, 15.0), (9.0, 15.0), (10.0, 15.0), (6.2, 7.8), -# (5.6, 7.8), (5.6, 7.6), (5.6, 7.4), (6.2, 7.4), (6.0, 7.6), -# (7.0, 7.8), (7.0, 7.4)] -# boundary_nodes, points = convert_boundary_points_to_indices(curves; existing_points=points) -# tri = triangulate(points; boundary_nodes=boundary_nodes, rng = RNG.STABLE_RNG) -# refine!(tri, max_area = 1e-3get_total_area(tri), rng = RNG.STABLE_RNG) -# fig, ax, sc = triplot(tri, -# show_points=true, -# show_constrained_edges=true, -# constrained_edge_linewidth=2, -# strokewidth=0.2, -# markersize=15, -# point_color=:blue, -# show_ghost_edges=true, # not as good because the outer boundary is not convex, but just testing -# marker='x', -# bounding_box = (-5,20,-5,35)) # also testing the conversion to Float64 for bbox here -# fig -# end +@reference_test "Triplot of a constrained triangulation with holes and a custom bounding box" begin + curve_1 = [[ + (0.0, 0.0), (4.0, 0.0), (8.0, 0.0), (12.0, 0.0), (12.0, 4.0), + (12.0, 8.0), (14.0, 10.0), (16.0, 12.0), (16.0, 16.0), + (14.0, 18.0), (12.0, 20.0), (12.0, 24.0), (12.0, 28.0), + (8.0, 28.0), (4.0, 28.0), (0.0, 28.0), (-2.0, 26.0), (0.0, 22.0), + (0.0, 18.0), (0.0, 10.0), (0.0, 8.0), (0.0, 4.0), (-4.0, 4.0), + (-4.0, 0.0), (0.0, 0.0), + ]] + curve_2 = [[ + (4.0, 26.0), (8.0, 26.0), (10.0, 26.0), (10.0, 24.0), + (10.0, 22.0), (10.0, 20.0), (8.0, 20.0), (6.0, 20.0), + (4.0, 20.0), (4.0, 22.0), (4.0, 24.0), (4.0, 26.0) + ]] + curve_3 = [[(4.0, 16.0), (12.0, 16.0), (12.0, 14.0), (4.0, 14.0), (4.0, 16.0)]] + curve_4 = [[(4.0, 8.0), (10.0, 8.0), (8.0, 6.0), (6.0, 6.0), (4.0, 8.0)]] + curves = [curve_1, curve_2, curve_3, curve_4] + points = [ + (2.0, 26.0), (2.0, 24.0), (6.0, 24.0), (6.0, 22.0), (8.0, 24.0), (8.0, 22.0), + (2.0, 22.0), (0.0, 26.0), (10.0, 18.0), (8.0, 18.0), (4.0, 18.0), (2.0, 16.0), + (2.0, 12.0), (6.0, 12.0), (2.0, 8.0), (2.0, 4.0), (4.0, 2.0), + (-2.0, 2.0), (4.0, 6.0), (10.0, 2.0), (10.0, 6.0), (8.0, 10.0), (4.0, 10.0), + (10.0, 12.0), (12.0, 12.0), (14.0, 26.0), (16.0, 24.0), (18.0, 28.0), + (16.0, 20.0), (18.0, 12.0), (16.0, 8.0), (14.0, 4.0), (14.0, -2.0), + (6.0, -2.0), (2.0, -4.0), (-4.0, -2.0), (-2.0, 8.0), (-2.0, 16.0), + (-4.0, 22.0), (-4.0, 26.0), (-2.0, 28.0), (6.0, 15.0), (7.0, 15.0), + (8.0, 15.0), (9.0, 15.0), (10.0, 15.0), (6.2, 7.8), + (5.6, 7.8), (5.6, 7.6), (5.6, 7.4), (6.2, 7.4), (6.0, 7.6), + (7.0, 7.8), (7.0, 7.4)] + boundary_nodes, points = convert_boundary_points_to_indices(curves; existing_points=points) + tri = triangulate(points; randomise = false, boundary_nodes=boundary_nodes, rng = RNG.STABLE_RNG) + fig, ax, sc = triplot(tri, + show_points=true, + show_constrained_edges=true, + constrained_edge_linewidth=2, + strokewidth=0.2, + markersize=15, + markercolor=:blue, + show_ghost_edges=true, # not as good because the outer boundary is not convex, but just testing + marker='x', + bounding_box = (-5,20,-5,35)) # also testing the conversion to Float64 for bbox here + fig +end @reference_test "Triplot with nonlinear transformation" begin f = Figure() ax = PolarAxis(f[1, 1]) points = Point2f[(phi, r) for r in 1:10 for phi in range(0, 2pi, length=36)[1:35]] + noise = i -> 1f-4 * (isodd(i) ? 1 : -1) * i/sqrt(50) # should have small discrepancy + points = points .+ [Point2f(noise(i), noise(i)) for i in eachindex(points)] + # The noise forces the triangulation to be unique. Not using RNG to not disrupt the RNG stream later tr = triplot!(ax, points) f end @@ -1282,7 +1315,7 @@ end @reference_test "Triplot after adding points and make sure the representative_point_list is correctly updated" begin points = [(0.0,0.0),(0.95,0.0),(1.0,1.4),(0.0,1.0)] # not 1 so that we have a unique triangulation tri = Observable(triangulate(points; delete_ghosts = false)) - fig, ax, sc = triplot(tri, show_points = true, markersize = 14, show_ghost_edges = true, recompute_centers = true) + fig, ax, sc = triplot(tri, show_points = true, markersize = 14, show_ghost_edges = true, recompute_centers = true, linestyle = :dash) for p in [(0.3, 0.5), (-1.5, 2.3), (0.2, 0.2), (0.2, 0.5)] add_point!(tri[], p) end @@ -1312,9 +1345,8 @@ end end @reference_test "Voronoiplot for a centroidal tessellation with an automatic colormap" begin - points = [(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)] + points = [(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0),(0.2,0.2),(0.25,0.6),(0.5,0.3),(0.1,0.15)] tri = triangulate(points; boundary_nodes = [1,2,3,4,1], rng = RNG.STABLE_RNG) - refine!(tri; max_area=1e-2, min_angle = 29.871, rng = RNG.STABLE_RNG) vorn = voronoi(tri) smooth_vorn = centroidal_smooth(vorn; maxiters = 250, rng = RNG.STABLE_RNG) cmap = cgrad(:matter) @@ -1356,15 +1388,12 @@ end fig end -#= - -After DelaunayTriangulation@1.0.4, this test started to show slightly randomized triangulations. -Until this gets fixed, we're disabling it. - @reference_test "Voronoiplot with a nonlinear transform" begin f = Figure() ax = PolarAxis(f[1, 1], theta_as_x = false) points = Point2d[(r, phi) for r in 1:10 for phi in range(0, 2pi, length=36)[1:35]] + noise = i -> 1f-4 * (isodd(i) ? 1 : -1) * i/sqrt(50) # should have small discrepancy + points = points .+ [Point2f(noise(i), noise(i)) for i in eachindex(points)] # make triangulation unique polygon_color = [r for r in 1:10 for phi in range(0, 2pi, length=36)[1:35]] polygon_color_2 = [phi for r in 1:10 for phi in range(0, 2pi, length=36)[1:35]] tr = voronoiplot!(ax, points, smooth = false, show_generators = false, color = polygon_color) @@ -1374,7 +1403,7 @@ Until this gets fixed, we're disabling it. Makie.rlims!(ax, 12) f end -=# + @reference_test "Voronoiplot with some custom bounding boxes may not contain all data sites" begin points = [(-3.0, 7.0), (1.0, 6.0), (-1.0, 3.0), (-2.0, 4.0), (3.0, -2.0), (5.0, 5.0), (-4.0, -3.0), (3.0, 8.0)] @@ -1458,11 +1487,337 @@ end fig = Figure() xs = vcat([fill(i, i * 1000) for i in 1:4]...) ys = vcat(RNG.randn(6000), RNG.randn(4000) * 2) - for (i, scale) in enumerate([:area, :count, :width]) - ax = Axis(fig[i, 1]) - violin!(ax, xs, ys; scale, show_median=true) - Makie.xlims!(0.2, 4.8) - ax.title = "scale=:$(scale)" + ax, p = violin(fig[1, 1], xs, ys; scale = :area, show_median=true) + Makie.xlims!(0.2, 4.8); ax.title = "scale=:area" + ax, p = violin(fig[2, 1], xs, ys; scale = :count, mediancolor = :red, medianlinewidth = 5) + Makie.xlims!(0.2, 4.8); ax.title = "scale=:count" + ax, p = violin(fig[3, 1], xs, ys; scale = :width, show_median=true, mediancolor = :orange, medianlinewidth = 5) + Makie.xlims!(0.2, 4.8); ax.title = "scale=:width" + fig +end + +@reference_test "Violin" begin + fig = Figure() + + categories = vcat(fill(1, 300), fill(2, 300), fill(3, 300)) + values = vcat(RNG.randn(300), (1.5 .* RNG.rand(300)).^2, -(1.5 .* RNG.rand(300)).^2) + violin(fig[1, 1], categories, values) + + dodge = RNG.rand(1:2, 900) + violin(fig[1, 2], categories, values, dodge = dodge, + color = map(d->d==1 ? :yellow : :orange, dodge), + strokewidth = 2, strokecolor = :black, gap = 0.1, dodge_gap = 0.5 + ) + + violin(fig[2, 1], categories, values, orientation = :horizontal, + color = :gray, side = :left + ) + + violin!(categories, values, orientation = :horizontal, + color = :yellow, side = :right, strokewidth = 2, strokecolor = :black, + weights = abs.(values) + ) + + # TODO: test bandwidth, boundary + + fig +end + +@reference_test "Clip planes - CairoMakie overrides" begin + f = Figure() + a = Axis(f[1, 1]) + a.scene.theme[:clip_planes][] = [Plane3f(Vec3f(1, 0, 0), 0)] + xlims!(a, -3.5, 3.5) + ylims!(a, -3.5, 3.5) + + poly!(a, Rect2f(Point2f(-3.0, 1.8), Vec2f(6, 1)), strokewidth = 2) + poly!(a, Point2f[(-3, 1.5), (3, 1.5), (3, 0.5), (-3, 0.5), (-3, 1.5)], strokewidth = 2) + xs = range(-3.0, 3.0, length=101) + b = band!(a, xs, -0.4 .* sin.(3 .* xs) .- 2.5, 0.4 .* sin.(3 .* xs) .- 1.0) + + x = RNG.randn(50) + y = RNG.randn(50) + z = -sqrt.(x .^ 2 .+ y .^ 2) .+ 0.1 .* RNG.randn() + p = tricontourf!(a, x, y, z) + translate!(p, 0, 0, 1) + + f +end + +@reference_test "Spy" begin + f = Figure() + data = RNG.rand(10, 10) + spy(f[1, 1], (0, 1), (0, 1), data) + # if all colorvalues are 1, colorrange will be (0.5, 1.5), mapping everything to blue + # TODO, maybe not ideal for spy? + sdata = sparse(data .> 0.5) + spy(f[1, 2], sdata; colormap=[:black, :blue, :white]) + spy(f[2, 1], sdata; color=:black, alpha=0.7) + data[1, 1] = NaN + spy(f[2, 2], data; highclip=:red, lowclip=(:grey, 0.5), nan_color=:black, colorrange=(0.3, 0.7)) + f +end + +@reference_test "Datashader AggCount" begin + data = [RNG.randn(Point2f, 10_000); (Ref(Point2f(1, 1)) .+ 0.3f0 .* RNG.randn(Point2f, 10_000))] + f = Figure() + ax = Axis(f[1, 1]) + datashader!(ax, data; async = false) + ax2 = Axis(f[1, 2]) + datashader!(ax2, data; async = false, binsize = 3) + ax3 = Axis(f[2, 1]) + datashader!(ax3, data; async = false, operation = xs -> log10.(xs .+ 1)) + ax4 = Axis(f[2, 2]) + datashader!(ax4, data; async = false, point_transform = -) + f +end + +@reference_test "Datashader AggMean" begin + with_z(p2) = Point3f(p2..., cos(p2[1]) * sin(p2[2])) + data2d = RNG.randn(Point2f, 100_000) + data3d = map(with_z, data2d) + f = Figure() + ax = Axis(f[1, 1]) + datashader!(ax, data3d; agg = Makie.AggMean(), operation = identity, async = false) + ax2 = Axis(f[1, 2]) + datashader!(ax2, data3d; agg = Makie.AggMean(), operation = identity, async = false, binsize = 3) + f +end + +@reference_test "Heatmap Shader" begin + data = Makie.peaks(10_000) + data2 = map(data) do x + Float32(round(x)) end + f = Figure() + ax1, pl1 = heatmap(f[1, 1], Resampler(data)) + ax2, pl2 = heatmap(f[1, 2], Resampler(data)) + limits!(ax2, 2800, 4800, 2800, 5000) + ax3, pl3 = heatmap(f[2, 1], Resampler(data2)) + ax4, pl4 = heatmap(f[2, 2], Resampler(data2)) + limits!(ax4, 3000, 3090, 3460, 3500) + heatmap(f[3, 1], (1000, 2000), (500, 1000), Resampler(data2)) + ax = Axis(f[3, 2]) + limits!(ax, (0, 1), (0, 1)) + heatmap!(ax, (1, 2), (1, 2), Resampler(data2)) + Colorbar(f[:, 3], pl1) + sleep(1) # give the async operations some time + f +end + +@reference_test "boxplot" begin + fig = Figure() + + categories = vcat(fill(1, 300), fill(2, 300), fill(3, 300)) + values = RNG.randn(900) .+ range(-1, 1, length=900) + boxplot(fig[1, 1], categories, values) + + dodge = RNG.rand(1:2, 900) + boxplot(fig[1, 2], categories, values, dodge = dodge, show_notch = true, + color = map(d->d==1 ? :blue : :red, dodge), + outliercolor = RNG.rand([:red, :green, :blue, :black, :orange], 900) + ) + + ax_vert = Axis(fig[2,1]; + xlabel = "categories", + ylabel = "values", + xticks = (1:3, ["one", "two", "three"]) + ) + ax_horiz = Axis(fig[2,2]; + xlabel="values", + ylabel="categories", + yticks=(1:3, ["one", "two", "three"]) + ) + + weights = 1.0 ./ (1.0 .+ abs.(values)) + boxplot!(ax_vert, categories, values, orientation=:vertical, weights = weights, + gap = 0.5, + show_notch = true, notchwidth = 0.75, + markersize = 5, strokewidth = 2.0, strokecolor = :black, + medianlinewidth = 5, mediancolor = :orange, + whiskerwidth = 1.0, whiskerlinewidth = 3, whiskercolor = :green, + outlierstrokewidth = 1.0, outlierstrokecolor = :red, + width = 1.5, + + ) + boxplot!(ax_horiz, categories, values; orientation=:horizontal) + fig end + +@reference_test "crossbar" begin + fig = Figure() + + xs = [1, 1, 2, 2, 3, 3] + ys = RNG.rand(6) + ymins = ys .- 1 + ymaxs = ys .+ 1 + dodge = [1, 2, 1, 2, 1, 2] + + crossbar(fig[1, 1], xs, ys, ymins, ymaxs, dodge = dodge, show_notch = true) + + crossbar(fig[1, 2], xs, ys, ymins, ymaxs, + dodge = dodge, dodge_gap = 0.25, + gap = 0.05, + midlinecolor = :blue, midlinewidth = 5, + show_notch = true, notchwidth = 0.3, + notchmin = ys .- (0.05:0.05:0.3), notchmax = ys .+ (0.3:-0.05:0.05), + strokewidth = 2, strokecolor = :black, + orientation = :horizontal, color = :lightblue + ) + fig +end + +@reference_test "ecdfplot" begin + f = Figure(size = (500, 250)) + + x = RNG.randn(200) + ecdfplot(f[1, 1], x, color = (:blue, 0.3)) + ecdfplot!(x, color = :red, npoints=10, step = :pre, linewidth = 3) + ecdfplot!(x, color = :orange, npoints=10, step = :center, linewidth = 3) + ecdfplot!(x, color = :green, npoints=10, step = :post, linewidth = 3) + + w = @. x^2 * (1 - x)^2 + ecdfplot(f[1, 2], x) + ecdfplot!(x; weights = w, color=:orange) + + f +end + +@reference_test "qqnorm" begin + fig = Figure() + xs = 2 .* RNG.randn(10) .+ 3 + qqnorm(fig[1, 1], xs, qqline = :fitrobust, strokecolor = :cyan, strokewidth = 2) + qqnorm(fig[1, 2], xs, qqline = :none, markersize = 15, marker = Rect, markercolor = :red) + qqnorm(fig[2, 1], xs, qqline = :fit, linestyle = :dash, linewidth = 6) + qqnorm(fig[2, 2], xs, qqline = :identity, color = :orange) + fig +end + +@reference_test "qqplot" begin + fig = Figure() + xs = 2 .* RNG.randn(10) .+ 3; ys = RNG.randn(10) + qqplot(fig[1, 1], xs, ys, qqline = :fitrobust, strokecolor = :cyan, strokewidth = 2) + qqplot(fig[1, 2], xs, ys, qqline = :none, markersize = 15, marker = Rect, markercolor = :red) + qqplot(fig[2, 1], xs, ys, qqline = :fit, linestyle = :dash, linewidth = 6) + qqplot(fig[2, 2], xs, ys, qqline = :identity, color = :orange) + fig +end + +@reference_test "rainclouds" begin + Makie.RAINCLOUD_RNG[] = RNG.STABLE_RNG + data = RNG.randn(1000) + data[1:200] .+= 3 + data[201:500] .-= 3 + data[501:end] .= 3 .* abs.(data[501:end]) .- 3 + labels = vcat(fill("red", 500), fill("green", 500)) + + fig = Figure() + rainclouds(fig[1, 1], labels, data, plot_boxplots = false, cloud_width = 2.0, + markersize = 5.0) + rainclouds(fig[1, 2], labels, data, color = labels, orientation = :horizontal, cloud_width = 2.0) + rainclouds(fig[2, 1], labels, data, clouds = hist, hist_bins = 30, boxplot_nudge = 0.1, + center_boxplot = false, boxplot_width = 0.2, whiskerwidth = 1.0, strokewidth = 3.0) + rainclouds(fig[2, 2], labels, data, color = labels, side = :right, violin_limits = extrema) + fig +end + +@reference_test "series" begin + fig = Figure() + data = cumsum(RNG.randn(4, 21), dims = 2) + + ax, sp = series(fig[1, 1], data, labels=["label $i" for i in 1:4], + linewidth = 4, linestyle = :dot, markersize = 15, solid_color = :black) + axislegend(ax, position = :lt) + + ax, sp = series(fig[2, 1], data, labels=["label $i" for i in 1:4], markersize = 10.0, + marker = Circle, markercolor = :transparent, strokewidth = 2.0, strokecolor = :black) + axislegend(ax, position = :lt) + + fig +end + +@reference_test "stairs" begin + f = Figure() + + xs = LinRange(0, 4pi, 21) + ys = sin.(xs) + + stairs(f[1, 1], xs, ys) + stairs(f[2, 1], xs, ys; step=:post, color=:blue, linestyle=:dash) + stairs(f[3, 1], xs, ys; step=:center, color=:red, linestyle=:dot) + + f +end + +@reference_test "stem" begin + f = Figure() + + xs = LinRange(0, 4pi, 30) + stem(f[1, 1], xs, sin.(xs)) + + stem(f[1, 2], xs, sin, + offset = 0.5, trunkcolor = :blue, marker = :rect, + stemcolor = :red, color = :orange, + markersize = 15, strokecolor = :red, strokewidth = 3, + trunklinestyle = :dash, stemlinestyle = :dashdot) + + stem(f[2, 1], xs, sin.(xs), + offset = LinRange(-0.5, 0.5, 30), + color = LinRange(0, 1, 30), colorrange = (0, 0.5), + trunkcolor = LinRange(0, 1, 30), trunkwidth = 5) + + ax, p = stem(f[2, 2], 0.5xs, 2 .* sin.(xs), 2 .* cos.(xs), + offset = Point3f.(0.5xs, sin.(xs), cos.(xs)), + stemcolor = LinRange(0, 1, 30), stemcolormap = :Spectral, stemcolorrange = (0, 0.5)) + + center!(ax.scene) + zoom!(ax.scene, 0.8) + ax.scene.camera_controls.settings[:center] = false + + f +end + +@reference_test "waterfall" begin + y = [6, 4, 2, -8, 3, 5, 1, -2, -3, 7] + + fig = Figure() + waterfall(fig[1, 1], y) + waterfall(fig[1, 2], y, show_direction = true, marker_pos = :cross, + marker_neg = :hline, direction_color = :yellow) + + colors = Makie.wong_colors() + x = repeat(1:2, inner=5) + group = repeat(1:5, outer=2) + + waterfall(fig[2, 1], x, y, dodge = group, color = colors[group], + show_direction = true, show_final = true, final_color=(colors[6], 1//3), + dodge_gap = 0.1, gap = 0.05) + + x = repeat(1:5, outer=2) + group = repeat(1:2, inner=5) + + waterfall(fig[2, 2], x, y, dodge = group, color = colors[group], + show_direction = true, stack = :x, show_final = true) + + fig +end + +@reference_test "ablines + hvlines + hvspan" begin + f = Figure() + + ax = Axis(f[1, 1]) + hspan!(ax, -1, -0.9, color = :lightblue, alpha = 0.5, strokewidth = 2, strokecolor = :black) + hspan!(ax, 0.9, 1, xmin = 0.2, xmax = 0.8) + vspan!(ax, -1, -0.9) + vspan!(ax, 0.9, 1, ymin = 0.2, ymax = 0.8, strokecolor = RGBf(0,1,0.1), strokewidth = 3) + + ablines!([0.3, 0.7], [-0.2, 0.2], color = :orange, linewidth = 4, linestyle = :dash) + + hlines!(ax, -0.8) + hlines!(ax, 0.8, xmin = 0.2, xmax = 0.8) + vlines!(ax, -0.8, color = :green, linewidth = 3) + vlines!(ax, 0.8, ymin = 0.2, ymax = 0.8, color = :red, linewidth = 3, linestyle = :dot) + + f +end diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 6c3caf5bbae..49b5025051e 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -1,16 +1,23 @@ -@reference_test "Image on Geometry (Moon)" begin +@reference_test "mesh textured and loaded" begin + f = Figure(size = (600, 600)) + moon = loadasset("moon.png") - fig, ax, meshplot = mesh(Sphere(Point3f(0), 1f0), color=moon, shading=NoShading, axis = (;show_axis=false)) + ax, meshplot = mesh(f[1, 1], Sphere(Point3f(0), 1f0), color=moon, + shading=NoShading, axis = (;show_axis=false)) update_cam!(ax.scene, Vec3f(-2, 2, 2), Vec3f(0)) cameracontrols(ax).settings.center[] = false # avoid recenter on display - fig -end - -@reference_test "Image on Geometry (Earth)" begin + earth = loadasset("earth.png") m = uv_mesh(Tesselation(Sphere(Point3f(0), 1f0), 60)) - mesh(m, color=earth, shading=NoShading) + mesh(f[1, 2], m, color=earth, shading=NoShading) + + catmesh = loadasset("cat.obj") + mesh(f[2, 1], catmesh, color=loadasset("diffusemap.png")) + + mesh(f[2, 2], loadasset("cat.obj"); color=:black) + + f end @reference_test "Orthographic Camera" begin @@ -40,13 +47,34 @@ end fig end -@reference_test "Volume Function" begin - volume(RNG.rand(32, 32, 32), algorithm=:mip) -end +@reference_test "simple volumes" begin + f = Figure() + r = range(-1, stop=1, length=100) + matr = [(x.^2 + y.^2 + z.^2) for x = r, y = r, z = r] + volume(f[1, 1], matr .* (matr .> 1.4), algorithm=:iso, isorange=0.05, isovalue=1.7, colorrange=(0, 1)) + + volume(f[1, 2], RNG.rand(32, 32, 32), algorithm=:mip) -@reference_test "Textured Mesh" begin - catmesh = loadasset("cat.obj") - mesh(catmesh, color=loadasset("diffusemap.png")) + r = LinRange(-3, 3, 100); # our value range + ρ(x, y, z) = exp(-(abs(x))) # function (charge density) + ax, pl = volume(f[2, 1], + r, r, r, # coordinates to plot on + ρ, # charge density (functions as colorant) + algorithm=:mip, # maximum-intensity-projection + colorrange=(0, 1), + ) + ax.scene[OldAxis].names.textcolor = :lightgray # let axis labels be seen on dark background + ax.scene[OldAxis].ticks.textcolor = :gray # let axis ticks be seen on dark background + ax.scene.backgroundcolor[] = to_color(:black) + ax.scene.clear[] = true + + r = range(-3pi, stop=3pi, length=100) + volume(f[2, 2], r, r, r, (x, y, z) -> cos(x) + sin(y) + cos(z), + colorrange=(0, 1), algorithm=:iso, isorange=0.1f0, axis = (;show_axis=false)) + volume!(r, r, r, (x, y, z) -> cos(x) + sin(y) + cos(z), algorithm=:mip, + colorrange=(0, 1), transformation=(translation=Vec3f(6pi, 0, 0),)) + + f end @reference_test "Textured meshscatter" begin @@ -60,32 +88,11 @@ end ) end -@reference_test "Load Mesh" begin - mesh(loadasset("cat.obj"); color=:black) -end +@reference_test "Wireframe of mesh, GeoemtryPrimitive and Surface" begin + f = Figure() -@reference_test "Colored Mesh" begin - x = [0, 1, 2, 0] - y = [0, 0, 1, 2] - z = [0, 2, 0, 1] - color = [:red, :green, :blue, :yellow] - i = [0, 0, 0, 1] - j = [1, 2, 3, 2] - k = [2, 3, 1, 3] - # indices interpreted as triangles (every 3 sequential indices) - indices = [1, 2, 3, 1, 3, 4, 1, 4, 2, 2, 3, 4] - mesh(x, y, z, indices, color=color) -end - -@reference_test "Wireframe of a Mesh" begin - wireframe(loadasset("cat.obj")) -end + wireframe(f[1, 1], Sphere(Point3f(0), 1f0)) -@reference_test "Wireframe of Sphere" begin - wireframe(Sphere(Point3f(0), 1f0)) -end - -@reference_test "Wireframe of a Surface" begin function xy_data(x, y) r = sqrt(x^2 + y^2) r == 0.0 ? 1f0 : (sin(r) / r) @@ -94,7 +101,11 @@ end lspace = range(-10, stop=10, length=N) z = Float32[xy_data(x, y) for x in lspace, y in lspace] r = range(0, stop=3, length=N) - wireframe(r, r, z) + wireframe(f[2, 1], r, r, z) + + wireframe(f[1:2, 2], loadasset("cat.obj")) + + f end @reference_test "Surface with image" begin @@ -119,20 +130,18 @@ end meshscatter(positions, color=colS, markersize=sizesS) end -@reference_test "scatter" begin - scatter(RNG.rand(20), RNG.rand(20), markersize=10) -end - -@reference_test "Marker sizes" begin - colors = Makie.resample(to_colormap(:Spectral), 20) - scatter(RNG.rand(20), RNG.rand(20), markersize=RNG.rand(20) .* 20, color=colors) -end +@reference_test "Basic Shading" begin + f = Figure(size = (500, 300)) -@reference_test "Ellipsoid marker sizes" begin # see PR #3722 + # 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]] 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)) + meshscatter(f[1, 1], pts; markersize, rotation, color=:white, diffuse=Vec3f(-2, 0, 4), specular=Vec3f(4, 0, -2)) + + mesh(f[1, 2], Sphere(Point3f(0), 1f0), color=:orange, shading=NoShading) + + f end @reference_test "Record Video" begin @@ -232,21 +241,21 @@ end vx = -1:0.01:1 vy = -1:0.01:1 - f(x, y) = (sin(x * 10) + cos(y * 10)) / 4 + foo(x, y) = (sin(x * 10) + cos(y * 10)) / 4 fig = Figure() ax1 = fig[1, 1] = Axis(fig, title = "surface") ax2 = fig[1, 2] = Axis(fig, title = "contour3d") - surface!(ax1, vx, vy, f) - contour3d!(ax2, vx, vy, (x, y) -> f(x, y), levels=15, linewidth=3) + surface!(ax1, vx, vy, foo) + contour3d!(ax2, vx, vy, (x, y) -> foo(x, y), levels=15, linewidth=3) fig end @reference_test "colorscale (surface)" begin x = y = range(-1, 1; length = 20) - f(x, y) = exp(-(x^2 + y^2)^2) + foo(x, y) = exp(-(x^2 + y^2)^2) fig = Figure() - surface(fig[1, 1], x, y, f; colorscale = identity) - surface(fig[1, 2], x, y, f; colorscale = log10) + surface(fig[1, 1], x, y, foo; colorscale = identity) + surface(fig[1, 2], x, y, foo; colorscale = log10) fig end @@ -265,18 +274,6 @@ end fig end -@reference_test "FEM mesh 3D" begin - cat = loadasset("cat.obj") - vertices = decompose(Point3f, cat) - faces = decompose(TriangleFace{Int}, cat) - coordinates = [vertices[i][j] for i = 1:length(vertices), j = 1:3] - connectivity = [faces[i][j] for i = 1:length(faces), j = 1:3] - mesh( - coordinates, connectivity, - color=RNG.rand(length(vertices)) - ) -end - @reference_test "OldAxis + Surface" begin vx = -1:0.01:1 vy = -1:0.01:1 @@ -394,15 +391,6 @@ end meshscatter(positions, color=RGBAf(0.9, 0.2, 0.4, 1), markersize=0.05) end -@reference_test "Text glow and overdraw" begin - p1 = Point3f(0,0,0) - p2 = Point3f(1,0,0) - f, ax, pl = meshscatter([p1, p2]; markersize=0.3, color=[:purple, :yellow]) - text!(ax, p1; text="A", align=(:center, :center), glowwidth=10.0, glowcolor=:white, color=:black, fontsize=40, overdraw=true) - text!(ax, p2; text="B", align=(:center, :center), glowwidth=20.0, glowcolor=(:black, 0.6), color=:white, fontsize=40, overdraw=true) - f -end - @reference_test "Animated surface and wireframe" begin function xy_data(x, y) r = sqrt(x^2 + y^2) @@ -424,12 +412,13 @@ end @reference_test "Normals of a Cat" begin x = loadasset("cat.obj") - mesh(x, color=:black) + f, a, p = mesh(x, color=:black) pos = map(decompose(Point3f, x), GeometryBasics.normals(x)) do p, n p => p .+ Point(normalize(n) .* 0.05f0) end linesegments!(pos, color=:blue) - current_figure() + Makie.update_state_before_display!(f) + f end @reference_test "Sphere Mesh" begin @@ -524,22 +513,6 @@ let end end -@reference_test "Volume on black background" begin - r = LinRange(-3, 3, 100); # our value range - - ρ(x, y, z) = exp(-(abs(x))) # function (charge density) - - fig, ax, pl = volume( - r, r, r, # coordinates to plot on - ρ, # charge density (functions as colorant) - algorithm=:mip, # maximum-intensity-projection - colorrange=(0, 1), - ) - ax.scene[OldAxis].names.textcolor = :gray # let axis labels be seen on dark background - fig.scene.backgroundcolor[] = to_color(:black) - fig -end - @reference_test "Depth Shift" begin # Up to some artifacts from fxaa the left side should be blue and the right red. fig = Figure(size = (800, 400)) @@ -624,8 +597,126 @@ end heatmap(-2..2, -1..1, RNG.rand(100, 100); axis = (; type = LScene)) end +# Clip Planes +@reference_test "Clip planes - general" begin + # Test + # - inheritance of clip planes from scene and parent plot (wireframe) + # - test clipping of linesegments, mesh, surface, scatter, image, heatmap + f = Figure() + a = LScene(f[1, 1]) + a.scene.theme[:clip_planes][] = Makie.planes(Rect3f(Point3f(-0.75), Vec3f(1.5))) + linesegments!( + a, Rect3f(Point3f(-0.75), Vec3f(1.5)), clip_planes = Plane3f[], + fxaa = true, transparency = false, linewidth = 3) + + p = mesh!(Sphere(Point3f(0,0,1), 1f0), transparency = false, color = :orange, backlight = 1.0) + wireframe!(p[1][], fxaa = true, color = :cyan) + r = range(-pi, pi, length = 101) + surface!(-pi..pi, -pi..pi, [sin(-x - y) for x in r, y in r], transparency = false) + scatter!(-1.4:0.1:2, 2:-0.1:-1.4, color = :red) + p = heatmap!(-2..2, -2..2, [sin(x+y) for x in r, y in r], colormap = [:purple, :pink]) + translate!(p, 0, 0, -0.666) + p = image!(-2..2, -2..2, [cos(x+y) for x in r, y in r], colormap = [:red, :orange]) + translate!(p, 0, 0, -0.333) + text!(-1:0.2:1, 1:-0.2:-1, text = ["█" for i in -1:0.2:1], color = :purple) + f +end + +@reference_test "Clip planes - lines" begin + # red vs green matters here, not light vs dark + plane = Plane3f(normalize(Vec3f(1)), 0) + + f,a,p = mesh( + Makie.to_mesh(plane, scale = 1.5), color = (:black, 0.5), + transparency = true, visible = true + ) + + cam3d!(a.scene, center = false) + + attr = (color = :red, linewidth = 5, fxaa = true) + linesegments!(a, Rect3f(Point3f(-1), Vec3f(2)); attr...) + lines!(a, [Point3f(cos(x), sin(x), 0) for x in range(0, 2pi, length=101)]; attr...) + lines!(a, [Point3f(cos(x), sin(x), 0) for x in 1:4:80]; attr...) + lines!(a, [Point3f(-1), Point3f(1)]; attr...) + + attr = (color = RGBf(0,1,0), overdraw = true, clip_planes = [plane], linewidth = 5, fxaa = true) + linesegments!(a, Rect3f(Point3f(-1), Vec3f(2)), ; attr...) + lines!(a, [Point3f(cos(x), sin(x), 0) for x in range(0, 2pi, length=101)]; attr...) + lines!(a, [Point3f(cos(x), sin(x), 0) for x in 1:4:80]; attr...) + lines!(a, [Point3f(-1), Point3f(1)]; attr...) + + lines!(a, [Point3f(1, -1, 0), Point3f(-1, 1, 0)], color = :black, overdraw = true) + + update_cam!(a.scene, Vec3f(1.5, 4, 2), Vec3f(0)) + f +end + +@reference_test "Clip planes - voxel" begin + f = Figure() + a = LScene(f[1, 1]) + a.scene.theme[:clip_planes][] = [Plane3f(Vec3f(-2, -1, -0.5), 0.1), Plane3f(Vec3f(-0.5, -1, -2), 0.1)] + r = -10:10 + p = voxels!(a, [cos(sin(x+y)+z) for x in r, y in r, z in r]) + f +end + +@reference_test "Clip planes - volume" begin + f = Figure(size = (600, 400)) + r = -10:10 + data = [1 - (1 + cos(x^2) + cos(y^2) + cos(z^2)) for x in r, y in r, z in r] + clip_planes = [Plane3f(Vec3f(-1), 0.0)] + + attr = (clip_planes = clip_planes, axis = (show_axis = false,)) + + volume(f[1, 1], -10..10, -10..10, -10..10, data; attr..., + algorithm = :iso, isovalue = 1.0, isorange = 0.1) + volume(f[2, 1], -10..10, -10..10, -10..10, data; attr..., + algorithm = :absorption) + + volume(f[1, 2], -10..10, -10..10, -10..10, data; attr..., + algorithm = :mip) + volume(f[2, 2], -10..10, -10..10, -10..10, data; attr..., + algorithm = :absorptionrgba) + + volume(f[1, 3], -10..10, -10..10, -10..10, data; attr..., + algorithm = :additive) + volume(f[2, 3], -10..10, -10..10, -10..10, data; attr..., + algorithm = :indexedabsorption) + + f +end + +@reference_test "Clip planes - only data space" begin + f = Figure() + a = LScene(f[1, 1]) + a.scene.theme[:clip_planes][] = [Plane3f(Vec3f(-1, 0, 0), 0), Plane3f(Vec3f(-1, 0, 0), -100)] + + # verify that clipping is working + wireframe!(a, Rect3f(Point3f(-1), Vec3f(2)), color = :green, linewidth = 5) + + # verify that space != :data is excluded + lines!(a, -1..1, sin, space = :clip, color = :gray, linewidth = 5) + linesegments!(a, [100, 200, 300, 400], [100, 100, 100, 100], space = :pixel, color = :gray, linewidth = 5) + scatter!(a, [0.2, 0.8], [0.4, 0.6], space = :relative, color = :gray, markersize = 20) + f +end + @reference_test "Surface interpolate attribute" begin f, ls1, pl = surface(Makie.peaks(20); interpolate=true, axis=(; show_axis=false)) ls2, pl = surface(f[1, 2], Makie.peaks(20); interpolate=false, axis=(; show_axis=false)) f end + +@reference_test "volumeslices" begin + r = range(-1, 1, length = 10) + data = RNG.rand(10,10,10) + + fig = Figure() + volumeslices(fig[1, 1], r, r, r, data) + a, p = volumeslices(fig[1, 2], r, r, r, data, bbox_visible = false, colormap = :RdBu, + colorrange = (0.2, 0.8), lowclip = :black, highclip = :green) + p.update_xz[](3) + p.update_yz[](4) + p.update_xy[](10) + fig +end diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index 889c1c7b973..2e3a28d2ca2 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -166,6 +166,39 @@ end f end +@reference_test "Legend overrides" begin + f = Figure() + ax = Axis(f[1, 1]) + + li = lines!( + 1:10, + label = "Line" => (; linewidth = 4, color = :gray60, linestyle = :dot), + ) + sc = scatter!( + 1:10, + 2:11, + color = [1, 2, 3, 1, 2, 3, 1, 2, 3, 1], + colorrange = (1, 3), + marker = :utriangle, + markersize = 20, + label = [ + label => (; markersize = 30, color = i) for (i, label) in enumerate(["blue", "green", "yellow"]) + ] + ) + Legend(f[1, 2], ax) + Legend( + f[1, 3], + [ + sc => (; markersize = 30), + [li => (; color = :red), sc => (; color = :cyan)], + [li, sc] => Dict(:color => :cyan), + ], + ["Scatter", "Line and Scatter", "Another"], + patchsize = (40, 20) + ) + f +end + @reference_test "LaTeXStrings in Axis3 plots" begin xs = LinRange(-10, 10, 100) ys = LinRange(0, 15, 100) @@ -238,7 +271,7 @@ end lines!(po, range(0, 20pi, length=201), range(0, 10, length=201), color = :white, linewidth = 5) b = Box(f[i, j], color = (:blue, 0.2)) - translate!(b.blockscene, 0, 0, 9001) + translate!(b.blockscene, 0, 0, 9999) end end colgap!(f.layout, 5) @@ -340,3 +373,75 @@ end translate!(vl, 1, 0, 0) f end + +@reference_test "Latex labels after the fact" begin + f = Figure(fontsize = 50) + ax = Axis(f[1, 1]) + ax.xticks = ([3, 6, 9], [L"x" , L"y" , L"z"]) + ax.yticks = ([3, 6, 9], [L"x" , L"y" , L"z"]) + f +end + +@reference_test "Rich text" begin + f = Figure(fontsize = 30, size = (800, 600)) + ax = Axis(f[1, 1], + limits = (1, 100, 0.001, 1), + xscale = log10, + yscale = log2, + title = rich("A ", rich("title", color = :red, font = :bold_italic)), + xlabel = rich("X", subscript("label", fontsize = 25)), + ylabel = rich("Y", superscript("label")), + ) + Label(f[1, 2], rich("Hi", rich("Hi", offset = (0.2, 0.2), color = :blue)), tellheight = false) + Label(f[1, 3], rich("X", superscript("super"), subscript("sub")), tellheight = false) + f +end + +@reference_test "Checkbox" begin + f = Figure(size = (300, 200)) + Makie.Checkbox(f[1, 1]) + Makie.Checkbox(f[1, 2], checked = true) + Makie.Checkbox(f[1, 3], checked = true, checkmark = Circle, roundness = 1, checkmarksize = 0.6) + Makie.Checkbox(f[1, 4], checked = true, checkmark = Circle, roundness = 1, checkmarksize = 0.6, size = 20) + Makie.Checkbox(f[1, 5], checkboxstrokewidth = 3) + Makie.Checkbox(f[2, 1], checkboxstrokecolor_unchecked = :red) + Makie.Checkbox(f[2, 2], checked = true, checkboxstrokecolor_checked = :cyan) + Makie.Checkbox(f[2, 3], checked = true, checkmarkcolor_checked = :black) + Makie.Checkbox(f[2, 4], checked = false, checkboxcolor_unchecked = :yellow) + Makie.Checkbox(f[2, 5], checked = true, checkboxcolor_checked = :orange) + f +end + +@reference_test "Button - Slider - Toggle - Textbox" begin + f = Figure(size = (500, 250)) + Makie.Button(f[1, 1:2]) + Makie.Button(f[2, 1:2], buttoncolor = :orange, cornerradius = 20, + strokecolor = :red, strokewidth = 2, # TODO: allocate space for this + fontsize = 16, labelcolor = :blue) + + IntervalSlider(f[1, 3]) + sl = IntervalSlider(f[2, 3], range = 0:100, linewidth = 20, + color_inactive = :orange, color_active_dimmed = :lightgreen) + Makie.set_close_to!(sl, 30, 70) + + Toggle(f[3, 1]) + Toggle(f[4, 1], framecolor_inactive = :lightblue, rimfraction = 0.6) + Toggle(f[3, 2], active = true) + Toggle(f[4, 2], active = true, framecolor_inactive = :lightblue, + framecolor_active = :yellow, rimfraction = 0.6) + + Makie.Slider(f[3, 3]) + sl = Makie.Slider(f[4, 3], range = 0:100, linewidth = 20, color_inactive = :cyan, + color_active_dimmed = :lightgreen) + Makie.set_close_to!(sl, 30) + + gl = GridLayout(f[5, 1:3]) + Textbox(gl[1, 1]) + Textbox(gl[1, 2], bordercolor = :red, cornerradius = 0, + placeholder = "test string", fontsize = 16, textcolor_placeholder = :blue) + tb = Textbox(gl[1, 3], bordercolor = :black, cornerradius = 20, + fontsize =10, textcolor = :red, boxcolor = :lightblue) + Makie.set!(tb, "some string") + + f +end \ No newline at end of file diff --git a/ReferenceTests/src/tests/float32_conversion.jl b/ReferenceTests/src/tests/float32_conversion.jl index 835edcba58e..f07b853fdda 100644 --- a/ReferenceTests/src/tests/float32_conversion.jl +++ b/ReferenceTests/src/tests/float32_conversion.jl @@ -98,3 +98,62 @@ end ) fig end + +@reference_test "Float64 model" begin + fig = Figure() + ax = Axis(fig[1, 1]) + + p = heatmap!(ax, -0.75 .. -0.25, -0.75 .. -0.25, [1 2; 3 4], colormap = [:lightblue, :yellow]) + translate!(p, 1e9, 1e8, 0) + p = image!(ax, 0..1, 0..1, [1 2; 3 4], colormap = [:lightblue, :yellow]) + translate!(p, 1e9, 1e8, 0) + + + ps = 0.5 .* Makie.Point2d[(-1, -1), (-1, 1), (1, 1), (1, -1)] + p = scatter!(ax, ps, marker = '+', markersize = 30) + translate!(p, 1e9, 1e8, 0) + p = text!(ax, ps, text = string.(1:4), fontsize = 20) + translate!(p, 1e9, 1e8, 0) + + p = lines!(ax, [Point2f(cos(x), sin(x)) for x in range(0, 2pi, length=101)]) + translate!(p, 1e9, 1e8, 0) + p = linesegments!(ax, [0.9 * Point2f(cos(x), sin(x)) for x in range(0, 2pi, length=101)]) + translate!(p, 1e9, 1e8, 0) + p = lines!(ax, [0.8 * Point2f(cos(x), sin(x)) for x in range(0, 2pi, length=101)], linestyle = :dash) + translate!(p, 1e9, 1e8, 0) + + fig +end + +@reference_test "Float64 model with rotation" begin + fig = Figure() + ax = Axis(fig[1, 1]) + + # TODO: broken in GLMakie (bad placement), CairoMakie (not supported) + # p = heatmap!(ax, -0.75 .. -0.25, -0.75 .. -0.25, [1 2; 3 4], colormap = [:lightblue, :yellow]) + # translate!(p, 1e9, 1e8, 0) + # rotate!(p, Vec3f(0,0,1), pi/4) + p = image!(ax, 0..1, 0..1, [1 2; 3 4], colormap = [:lightblue, :yellow]) + translate!(p, 1e9, 1e8, 0) + rotate!(p, Vec3f(0,0,1), pi/4) + + ps = 0.5 .* Makie.Point2d[(-1, -1), (-1, 1), (1, 1), (1, -1)] + p = scatter!(ax, ps, marker = '+', markersize = 30) + translate!(p, 1e9, 1e8, 0) + rotate!(p, Vec3f(0,0,1), pi/4) + p = text!(ax, ps, text = string.(1:4), fontsize = 20) + translate!(p, 1e9, 1e8, 0) + rotate!(p, Vec3f(0,0,1), pi/4) + + p = lines!(ax, [Point2f(cos(x), sin(x)) for x in range(0, 2pi, length=101)]) + translate!(p, 1e9, 1e8, 0) + rotate!(p, Vec3f(0,0,1), pi/4) + p = linesegments!(ax, [0.9 * Point2f(cos(x), sin(x)) for x in range(0, 2pi, length=101)]) + translate!(p, 1e9, 1e8, 0) + rotate!(p, Vec3f(0,0,1), pi/4) + p = lines!(ax, [0.8 * Point2f(cos(x), sin(x)) for x in range(0, 2pi, length=101)], linestyle = :dash) + translate!(p, 1e9, 1e8, 0) + rotate!(p, Vec3f(0,0,1), pi/4) + + fig +end \ No newline at end of file diff --git a/ReferenceTests/src/tests/generic_components.jl b/ReferenceTests/src/tests/generic_components.jl new file mode 100644 index 00000000000..1e55b31af39 --- /dev/null +++ b/ReferenceTests/src/tests/generic_components.jl @@ -0,0 +1,291 @@ +# For things that aren't as plot related + +@reference_test "picking" begin + scene = Scene(size = (230, 370)) + campixel!(scene) + + sc1 = scatter!(scene, [20, NaN, 20], [20, NaN, 50], marker = Rect, markersize = 20) + sc2 = scatter!(scene, [50, 50, 20, 50], [20, 50, 80, 80], marker = Circle, markersize = 20, color = [:red, :red, :transparent, :red]) + ms = meshscatter!(scene, [20, NaN, 50], [110, NaN, 110], markersize = 10) + l1 = lines!(scene, [20, 50, 50, 20, 20], [140, 140, 170, 170, 140], linewidth = 10) + l2 = lines!(scene, [20, 50, NaN, 20, 50], [200, 200, NaN, 230, 230], linewidth = 20, linecap = :round) + ls = linesegments!(scene, [20, 50, NaN, NaN, 20, 50], [260, 260, NaN, NaN, 290, 290], linewidth = 20, linecap = :square) + tp = text!(scene, Point2f[(15, 320), (NaN, NaN), (15, 350)], text = ["█ ●", "hi", "●"], fontsize = 20, align = (:left, :center)) + t = tp.plots[1] + + i = image!(scene, 80..140, 20..50, rand(RGBf, 3, 2), interpolate = false) + s = surface!(scene, 80..140, 80..110, rand(3, 2), interpolate = false) + hm = heatmap!(scene, [80, 110, 140], [140, 170], [1 4; 2 5; 3 6]) + # mesh coloring should match triangle placements + m = mesh!(scene, Point2f.([80, 80, 110, 110], [200, 230, 200, 230]), [1 2 3; 2 3 4], color = [1,1,1,2]) + vx = voxels!(scene, [65, 155], [245, 305], [-1, 1], reshape([1,2,3,4,5,6], (3,2,1)), shading = NoShading) + vol = volume!(scene, 80..110, 320..350, -1..1, rand(2,2,2)) + + # reversed axis + i2 = image!(scene, 210..180, 20..50, rand(RGBf, 2, 2)) + s2 = surface!(scene, 210..180, 80..110, rand(2, 2)) + hm2 = heatmap!(scene, [210, 180], [140, 170], [1 2; 3 4]) + + scene # for easy reviewing of the plot + + # render one frame to generate picking texture + colorbuffer(scene, px_per_unit = 2); + + # verify that heatmap path is used for heatmaps + if Symbol(Makie.current_backend()) == :WGLMakie + @test length(faces(WGLMakie.create_shader(scene, hm).vertexarray)) > 2 + @test length(faces(WGLMakie.create_shader(scene, hm2).vertexarray)) > 2 + elseif Symbol(Makie.current_backend()) == :GLMakie + screen = scene.current_screens[1] + for plt in (hm, hm2) + robj = screen.cache[objectid(plt)] + shaders = robj.vertexarray.program.shader + names = [string(shader.name) for shader in shaders] + @test any(name -> endswith(name, "heatmap.vert"), names) && any(name -> endswith(name, "heatmap.frag"), names) + end + else + error("picking tests are only meant to run on GLMakie & WGLMakie") + end + + # raw picking tests + + @testset "scatter" begin + @test pick(scene, Point2f(20, 20)) == (sc1, 1) + @test pick(scene, Point2f(29, 59)) == (sc1, 3) + @test pick(scene, Point2f(57, 58)) == (nothing, 0) # maybe fragile + @test pick(scene, Point2f(57, 13)) == (sc2, 1) # maybe fragile + @test pick(scene, Point2f(20, 80)) == (nothing, 0) + @test pick(scene, Point2f(50, 80)) == (sc2, 4) + end + + @testset "meshscatter" begin + @test pick(scene, (20, 110)) == (ms, 1) + @test pick(scene, (44, 117)) == (ms, 3) + @test pick(scene, (57, 117)) == (nothing, 0) + end + + @testset "lines" begin + # Bit less precise since joints aren't strictly one segment or the other + @test pick(scene, 22, 140) == (l1, 2) + @test pick(scene, 48, 140) == (l1, 2) + @test pick(scene, 50, 142) == (l1, 3) + @test pick(scene, 50, 168) == (l1, 3) + @test pick(scene, 48, 170) == (l1, 4) + @test pick(scene, 22, 170) == (l1, 4) + @test pick(scene, 20, 168) == (l1, 5) + @test pick(scene, 20, 142) == (l1, 5) + + # more precise checks around borders (these maybe off by a pixel due to AA) + @test pick(scene, 20, 200) == (l2, 2) + @test pick(scene, 30, 209) == (l2, 2) + @test pick(scene, 30, 211) == (nothing, 0) + @test pick(scene, 59, 200) == (l2, 2) + @test pick(scene, 61, 200) == (nothing, 0) + @test pick(scene, 57, 206) == (l2, 2) + @test pick(scene, 57, 208) == (nothing, 0) + @test pick(scene, 40, 230) == (l2, 5) # nan handling + end + + @testset "linesegments" begin + @test pick(scene, 8, 260) == (nothing, 0) # off by a pixel due to AA + @test pick(scene, 10, 260) == (ls, 2) + @test pick(scene, 30, 269) == (ls, 2) + @test pick(scene, 30, 271) == (nothing, 0) + @test pick(scene, 59, 260) == (ls, 2) + @test pick(scene, 61, 260) == (nothing, 0) + + @test pick(scene, 8, 290) == (nothing, 0) # off by a pixel due to AA + @test pick(scene, 10, 290) == (ls, 6) + @test pick(scene, 30, 280) == (ls, 6) + @test pick(scene, 30, 278) == (nothing, 0) # off by a pixel due to AA + @test pick(scene, 59, 290) == (ls, 6) + @test pick(scene, 61, 290) == (nothing, 0) + end + + @testset "text" begin + @test pick(scene, 15, 320) == (t, 1) + @test pick(scene, 13, 320) == (nothing, 0) + # edge checks, further outside due to AA + @test pick(scene, 20, 306) == (nothing, 0) + @test pick(scene, 20, 320) == (t, 1) + @test pick(scene, 20, 333) == (nothing, 0) + # space is counted + @test pick(scene, 43, 320) == (t, 3) + @test pick(scene, 48, 324) == (t, 3) + @test pick(scene, 49, 326) == (nothing, 0) + # characters at nan position are counted + @test pick(scene, 20, 350) == (t, 6) + end + + @testset "image" begin + # outside border + for p in vcat( + [(x, y) for x in (79, 141) for y in (21, 49)], + [(x, y) for x in (81, 139) for y in (19, 51)] + ) + @test pick(scene, p) == (nothing, 0) + end + + # cell centered checks + @test pick(scene, 90, 30) == (i, 1) + @test pick(scene, 110, 30) == (i, 2) + @test pick(scene, 130, 30) == (i, 3) + @test pick(scene, 90, 40) == (i, 4) + @test pick(scene, 110, 40) == (i, 5) + @test pick(scene, 130, 40) == (i, 6) + + # precise check (around cell intersection) + @test pick(scene, 100-1, 35-1) == (i, 1) + @test pick(scene, 100+1, 35-1) == (i, 2) + @test pick(scene, 100-1, 35+1) == (i, 4) + @test pick(scene, 100+1, 35+1) == (i, 5) + + @test pick(scene, 120-1, 35-1) == (i, 2) + @test pick(scene, 120+1, 35-1) == (i, 3) + @test pick(scene, 120-1, 35+1) == (i, 5) + @test pick(scene, 120+1, 35+1) == (i, 6) + + # reversed axis check + @test pick(scene, 200, 30) == (i2, 1) + @test pick(scene, 190, 30) == (i2, 2) + @test pick(scene, 200, 40) == (i2, 3) + @test pick(scene, 190, 40) == (i2, 4) + end + + @testset "surface" begin + # outside border + for p in vcat( + [(x, y) for x in (79, 141) for y in (81, 109)], + [(x, y) for x in (81, 139) for y in (79, 111)] + ) + @test pick(scene, p) == (nothing, 0) + end + + # cell centered checks + @test pick(scene, 90, 90) == (s, 1) + @test pick(scene, 110, 90) == (s, 2) + @test pick(scene, 130, 90) == (s, 3) + @test pick(scene, 90, 100) == (s, 4) + @test pick(scene, 110, 100) == (s, 5) + @test pick(scene, 130, 100) == (s, 6) + + # precise check (around cell intersection) + @test pick(scene, 95-1, 95-1) == (s, 1) + @test pick(scene, 95+1, 95-1) == (s, 2) + @test pick(scene, 95-1, 95+1) == (s, 4) + @test pick(scene, 95+1, 95+1) == (s, 5) + + @test pick(scene, 125-1, 95-1) == (s, 2) + @test pick(scene, 125+1, 95-1) == (s, 3) + @test pick(scene, 125-1, 95+1) == (s, 5) + @test pick(scene, 125+1, 95+1) == (s, 6) + + # reversed axis check + @test pick(scene, 200, 90) == (s2, 1) + @test pick(scene, 190, 90) == (s2, 2) + @test pick(scene, 200, 100) == (s2, 3) + @test pick(scene, 190, 100) == (s2, 4) + end + + @testset "heatmap" begin + # outside border + for p in vcat( + [(x, y) for x in (64, 156) for y in (126, 184)], + [(x, y) for x in (66, 154) for y in (124, 186)] + ) + @test pick(scene, p) == (nothing, 0) + end + + # cell centered checks + @test pick(scene, 80, 140) == (hm, 1) + @test pick(scene, 110, 140) == (hm, 2) + @test pick(scene, 140, 140) == (hm, 3) + @test pick(scene, 80, 170) == (hm, 4) + @test pick(scene, 110, 170) == (hm, 5) + @test pick(scene, 140, 170) == (hm, 6) + + # precise check (around cell intersection) + @test pick(scene, 94, 154) == (hm, 1) + @test pick(scene, 96, 154) == (hm, 2) + @test pick(scene, 94, 156) == (hm, 4) + @test pick(scene, 96, 156) == (hm, 5) + + @test pick(scene, 124, 154) == (hm, 2) + @test pick(scene, 126, 154) == (hm, 3) + @test pick(scene, 124, 156) == (hm, 5) + @test pick(scene, 126, 156) == (hm, 6) + + # reversed axis check + @test pick(scene, 210, 140) == (hm2, 1) + @test pick(scene, 180, 140) == (hm2, 2) + @test pick(scene, 210, 170) == (hm2, 3) + @test pick(scene, 180, 170) == (hm2, 4) + end + + @testset "mesh" begin + @test pick(scene, 80, 200)[1] == m + @test pick(scene, 79, 200) == (nothing, 0) + @test pick(scene, 80, 199) == (nothing, 0) + @test pick(scene, 81, 201) == (m, 3) + @test pick(scene, 81, 225) == (m, 3) + @test pick(scene, 105, 201) == (m, 3) + @test pick(scene, 85, 229) == (m, 4) + @test pick(scene, 109, 205) == (m, 4) + @test pick(scene, 109, 229) == (m, 4) + @test pick(scene, 109, 229)[1] == m + @test pick(scene, 111, 230) == (nothing, 0) + @test pick(scene, 110, 231) == (nothing, 0) + end + + @testset "voxel" begin + # outside border + for p in vcat( + [(x, y) for x in (64, 246) for y in (126, 184)], + [(x, y) for x in (66, 244) for y in (124, 186)] + ) + @test pick(scene, p) == (nothing, 0) + end + + # cell centered checks + @test pick(scene, 80, 260) == (vx, 1) + @test pick(scene, 110, 260) == (vx, 2) + @test pick(scene, 140, 260) == (vx, 3) + @test pick(scene, 80, 290) == (vx, 4) + @test pick(scene, 110, 290) == (vx, 5) + @test pick(scene, 140, 290) == (vx, 6) + + # precise check (around cell intersection) + @test pick(scene, 94, 274) == (vx, 1) + @test pick(scene, 96, 274) == (vx, 2) + @test pick(scene, 94, 276) == (vx, 4) + @test pick(scene, 96, 276) == (vx, 5) + + @test pick(scene, 124, 274) == (vx, 2) + @test pick(scene, 126, 274) == (vx, 3) + @test pick(scene, 124, 276) == (vx, 5) + @test pick(scene, 126, 276) == (vx, 6) + end + + @testset "volume" begin + # volume doesn't produce indices because we can't resolve the depth of + # the pick + @test pick(scene, 80, 320)[1] == vol + @test pick(scene, 79, 320) == (nothing, 0) + @test pick(scene, 80, 319) == (nothing, 0) + @test pick(scene, 81, 321) == (vol, 0) + @test pick(scene, 81, 349) == (vol, 0) + @test pick(scene, 109, 321) == (vol, 0) + @test pick(scene, 109, 349) == (vol, 0) + @test pick(scene, 109, 349)[1] == vol + @test pick(scene, 111, 350) == (nothing, 0) + @test pick(scene, 110, 351) == (nothing, 0) + end + + # grab all indices and generate a plot for them (w/ fixed px_per_unit) + full_screen = last.(pick(scene, scene.viewport[])) + + scene2 = Scene(size = 2.0 .* widths(scene.viewport[])) + campixel!(scene2) + image!(scene2, full_screen, colormap = :viridis) + scene2 +end \ No newline at end of file diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index 13a32751b85..f13448dd51d 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -157,8 +157,27 @@ 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]) +@reference_test "line color interpolation with clipping" begin + # Clipping should not change the color interpolation of a line piece, so + # these boxes should match in color + fig = Figure(); + ax = Axis(fig[1,1]); + ylims!(ax, -0.1, 1.1); + lines!( + ax, Rect2f(0, 0, 1, 10), color = 1:5, linewidth = 5, + clip_planes = [Plane3f(Point3f(0, 1.0, 0), Vec3f(0, -1, 0))] + ); + lines!(ax, Rect2f(0.1, 0.0, 0.8, 10.0), color = 1:5, linewidth = 5); + + ax = Axis(fig[1,2]); + ylims!(ax, -0.1, 1.1); + cs = [1, 2, 2, 3, 3, 4, 4, 5] + linesegments!( + ax, Rect2f(0, 0, 1, 10), color = cs, linewidth = 5, + clip_planes = [Plane3f(Point3f(0, 1.0, 0), Vec3f(0, -1, 0))] + ); + linesegments!(ax, Rect2f(0.1, 0.0, 0.8, 10.0), color = cs, linewidth = 5); + fig end @reference_test "scatters" begin @@ -588,6 +607,21 @@ end f end +@reference_test "Surface invert_normals" begin + fig = Figure(size = (400, 200)) + for (i, invert) in ((1, false), (2, true)) + surface( + fig[1, i], + range(-1, 1, length = 21), + -cos.(range(-pi, pi, length = 21)), + [sin(y) for x in range(-0.5pi, 0.5pi, length = 21), y in range(-0.5pi, 0.5pi, length = 21)], + axis = (show_axis = false, ), + invert_normals = invert + ) + end + fig +end + @reference_test "barplot with TeX-ed labels" begin fig = Figure(size = (800, 800)) lab1 = L"\int f(x) dx" @@ -678,8 +712,8 @@ end end @reference_test "Plot transform overwrite" begin - # Tests that (primitive) plots can have different transform function to their - # parent scene (identity in this case) + # Tests that (primitive) plots can have different transform function + # (identity) from their parent scene (log10, log10) fig = Figure() ax = Axis(fig[1, 1], xscale = log10, yscale = log10, backgroundcolor = :transparent) @@ -703,3 +737,108 @@ end fig end + +@reference_test "uv_transform" begin + fig = Figure(size = (400, 400)) + img = [RGBf(1,0,0) RGBf(0,1,0); RGBf(0,0,1) RGBf(1,1,1)] + + function create_block(f, gl, args...; kwargs...) + ax, p = f(gl[1, 1], args..., uv_transform = I; kwargs...) + hidedecorations!(ax) + ax, p = f(gl[1, 2], args..., uv_transform = :rotr90; kwargs...) + hidedecorations!(ax) + ax, p = f(gl[2, 1], args..., uv_transform = (Vec2f(0.5), Vec2f(0.5)); kwargs...) + hidedecorations!(ax) + ax, p = f(gl[2, 2], args..., uv_transform = Makie.Mat{2,3,Float32}(-1,0,0,-1,1,1); kwargs...) + hidedecorations!(ax) + end + + gl = fig[1, 1] = GridLayout() + create_block(mesh, gl, Rect2f(0, 0, 1, 1), color = img) + + gl = fig[1, 2] = GridLayout() + create_block(surface, gl, 0..1, 0..1, zeros(10, 10), color = img) + + gl = fig[2, 1] = GridLayout() + create_block( + meshscatter, gl, Point2f[(0,0), (0,1), (1,0), (1,1)], color = img, + marker = Makie.uv_normal_mesh(Rect2f(0,0,1,1)), markersize = 1.0) + + gl = fig[2, 2] = GridLayout() + create_block(image, gl, 0..1, 0..1, img) + + fig +end + +@testset "per element uv_transform" begin + cow = loadasset("cow.png") + + N = 8; M = 10 + f = Figure(size = (500, 400)) + a, p = meshscatter( + f[1, 1], + [Point2f(x, y) for x in 1:M for y in 1:N], + color = cow, + uv_transform = [ + Makie.uv_transform(:rotl90) * + Makie.uv_transform(Vec2f(x, y+1/N), Vec2f(1/M, -1/N)) + for x in range(0, 1, length = M+1)[1:M] + for y in range(0, 1, length = N+1)[1:N] + ], + markersize = Vec3f(0.9, 0.9, 1), + marker = uv_normal_mesh(Rect2f(-0.5, -0.5, 1, 1)) + ) + hidedecorations!(a) + xlims!(a, 0.3, M+0.7) + ylims!(a, 0.3, N+0.7) + f +end +@reference_test "Scatter with FastPixel" begin + f = Figure() + row = [(1, :pixel, 20), (2, :data, 0.5)] + points3d = decompose(Point3f, Rect3(Point3f(0), Vec3f(1))) + column = [(1, points3d, Axis3), (2, points3d, LScene), + (3, 1:4, Axis)] + for (i, space, msize) in row + for (j, data, AT) in column + ax = AT(f[i, j]) + if ax isa Union{Axis,Axis3} + ax isa Axis && (ax.aspect = DataAspect()) + ax.title = "$space" + end + scatter!(ax, data; markersize=msize, markerspace=space, marker=Makie.FastPixel()) + scatter!(ax, data; + markersize=msize, markerspace=space, marker=Rect, + strokewidth=2, strokecolor=:red, color=:transparent,) + end + end + f +end + +@reference_test "Reverse image, heatmap and surface axes" begin + img = [2 0 0 3; 0 0 0 0; 1 1 0 0; 1 1 0 4] + + f = Figure(size = (600, 400)) + + for (i, interp) in enumerate((true, false)) + for (j, plot_func) in enumerate(( + (fp, x, y, cs, interp) -> image(fp, x, y, cs, colormap = :viridis, interpolate = interp), + (fp, x, y, cs, interp) -> heatmap(fp, x, y, cs, colormap = :viridis, interpolate = interp), + (fp, x, y, cs, interp) -> surface(fp, x, y, zeros(size(cs)), color = cs, colormap = :viridis, interpolate = interp, shading = NoShading) + )) + + gl = GridLayout(f[i, j]) + + a, p = plot_func(gl[1, 1], 1:4, 1:4, img, interp) + hidedecorations!(a) + a, p = plot_func(gl[2, 1], 1:4, 4..1, img, interp) + hidedecorations!(a) + a, p = plot_func(gl[1, 2], 4:-1:1, 1:4, img, interp) + hidedecorations!(a) + a, p = plot_func(gl[2, 2], 4:-1:1, [4, 3, 2, 1], img, interp) + hidedecorations!(a) + end + end + + f +end \ No newline at end of file diff --git a/ReferenceTests/src/tests/refimages.jl b/ReferenceTests/src/tests/refimages.jl index 54fbbd9a2da..1f425a04f76 100644 --- a/ReferenceTests/src/tests/refimages.jl +++ b/ReferenceTests/src/tests/refimages.jl @@ -11,6 +11,7 @@ using ReferenceTests.DelimitedFiles using ReferenceTests.Test using ReferenceTests.Colors: RGB, N0f8 using ReferenceTests.DelaunayTriangulation +using ReferenceTests.SparseArrays using Makie: Record, volume @testset "categorical" begin @@ -49,3 +50,6 @@ end @testset "updating_plots" begin include("updating.jl") end +@testset "generic_components" begin + include("generic_components.jl") +end diff --git a/ReferenceTests/src/tests/short_tests.jl b/ReferenceTests/src/tests/short_tests.jl index 1ff054325e9..6d111230245 100644 --- a/ReferenceTests/src/tests/short_tests.jl +++ b/ReferenceTests/src/tests/short_tests.jl @@ -1,9 +1,9 @@ @reference_test "thick arc" arc(Point2f(0), 10f0, 0f0, pi, linewidth=20) -@reference_test "stroked rect poly" poly(Recti(0, 0, 200, 200), strokewidth=20, strokecolor=:red, color=(:black, 0.4)) - -@reference_test "array of rects poly" begin - f, ax, pl = poly([Rect(0, 0, 20, 20)]) +@reference_test "poly stroke & array input" begin + f = Figure(size = (500, 300)) + poly(f[1, 1], Recti(0, 0, 200, 200), strokewidth=20, strokecolor=:red, color=(:black, 0.4)) + poly(f[1, 2], [Rect(0, 0, 20, 20)]) scatter!(Rect(0, 0, 20, 20), color=:red, markersize=20) f end @@ -14,10 +14,34 @@ end f end -@reference_test "lines number color" lines(RNG.rand(10), RNG.rand(10), color=RNG.rand(10), linewidth=10) -@reference_test "lines array of colors" lines(RNG.rand(10), RNG.rand(10), color=RNG.rand(RGBAf, 10), linewidth=10) -@reference_test "scatter interval" scatter(0..1, RNG.rand(10), markersize=RNG.rand(10) .* 20) -@reference_test "scatter linrange" scatter(LinRange(0, 1, 10), RNG.rand(10)) +@reference_test "lines per element colors, OffsetArrays, #3704" begin + f = Figure() + lines(f[1, 1], RNG.rand(10), RNG.rand(10), color=RNG.rand(10), linewidth=10) + lines(f[1, 2], RNG.rand(10), RNG.rand(10), color=RNG.rand(RGBAf, 10), linewidth=10) + # lines issue #3704 + lines(f[2, 2], 1:10, sin, color = [fill(0, 9); fill(1, 1)], linewidth = 3, colormap = [:red, :cyan]) + f +end + +@reference_test "lines inputs" begin + f = Figure() + lines(f[1, 1], Circle(Point2f(0), Float32(1))) + lines(f[1, 2], -1..1, x -> x^2) + lines(f[2, 1], Makie.OffsetArrays.Origin(-50)(1:100)) + f +end + +@reference_test "scatter inputs" begin + f = Figure() + scatter(f[1, 1], 0..1, RNG.rand(10), markersize=RNG.rand(10) .* 20) + scatter(f[1, 2], LinRange(0, 1, 10), RNG.rand(10)) + colors = Makie.resample(to_colormap(:Spectral), 20) + scatter!(RNG.rand(20), RNG.rand(20), markersize=RNG.rand(20) .* 20, color=colors) + + scatter(f[2, 1], -1..1, x -> x^2) + scatter(f[2, 2], RNG.randn(10), color=:blue, glowcolor=:orange, glowwidth=10) + f +end @reference_test "scatter rotation" begin angles = range(0, stop=2pi, length=20) @@ -27,42 +51,47 @@ end f end -@reference_test "heatmap transparent colormap" heatmap(RNG.rand(50, 50), colormap=(:RdBu, 0.2)) - -@reference_test "contour small x" contour(RNG.rand(10, 100)) -@reference_test "contour small y" contour(RNG.rand(100, 10)) -@reference_test "contour with levels" contour(RNG.randn(100, 90), levels=3) - -@reference_test "contour with levels array" contour(RNG.randn(100, 90), levels=[0.1, 0.5, 0.8]) -@reference_test "contour with color per level" contour(RNG.randn(33, 30), levels=[0.1, 0.5, 0.9], color=[:black, :green, (:blue, 0.4)], linewidth=2) +@reference_test "heatmap log scale, transparent colormap" begin + f = Figure(size = (500, 250)) + heatmap(f[1, 1], RNG.rand(10, 5), axis = (yscale = log10, xscale=log10)) + heatmap(f[1, 2], RNG.rand(50, 50), colormap=(:RdBu, 0.2)) + f +end -@reference_test "contour with colorrange" contour( - RNG.rand(33, 30) .* 6 .- 3, levels=[-2.5, 0.4, 0.5, 0.6, 2.5], - colormap=[(:black, 0.2), :red, :blue, :green, (:black, 0.2)], - colorrange=(0.2, 0.8) -) +@reference_test "contour small x or y" begin + f = Figure(size = (500, 300)) + contour(f[1, 1], RNG.rand(10, 50)) + contour(f[1, 2], RNG.rand(50, 10)) + f +end -@reference_test "circle line" lines(Circle(Point2f(0), Float32(1))) +@reference_test "contour levels and colors" begin + f = Figure() + contour(f[1, 1], RNG.randn(50, 40), levels=3) + contour(f[1, 2], RNG.randn(50, 40), levels=[0.1, 0.5, 0.8]) + contour(f[2, 1], RNG.randn(33, 30), levels=[0.1, 0.5, 0.9], + color=[:black, :green, (:blue, 0.4)], linewidth=2) + contour( + f[2, 2], RNG.rand(33, 30) .* 6 .- 3, levels = [-2.5, 0.4, 0.5, 0.6, 2.5], + colormap = [(:black, 0.2), :red, :blue, :green, (:black, 0.2)], + colorrange = (0.2, 0.8) + ) + f +end @reference_test "streamplot with func" begin v(x::Point2{T}) where T = Point2{T}(x[2], 4 * x[1]) streamplot(v, -2..2, -2..2) end -@reference_test "lines with func" lines(-1..1, x -> x^2) -@reference_test "scatter with func" scatter(-1..1, x -> x^2) - -@reference_test "volume translated" begin - r = range(-3pi, stop=3pi, length=100) - fig, ax, vplot = Makie.volume(r, r, r, (x, y, z) -> cos(x) + sin(y) + cos(z), colorrange=(0, 1), algorithm=:iso, isorange=0.1f0, axis = (;show_axis=false)) - v2 = volume!(ax, r, r, r, (x, y, z) -> cos(x) + sin(y) + cos(z), algorithm=:mip, colorrange=(0, 1), - transformation=(translation=Vec3f(6pi, 0, 0),)) - fig +@reference_test "meshscatter colors, Axis3" begin + f = Figure() + meshscatter(f[1, 1], RNG.rand(10), RNG.rand(10), RNG.rand(10), color=RNG.rand(10)) + meshscatter(f[1, 2], RNG.rand(10), RNG.rand(10), RNG.rand(10), color=RNG.rand(RGBAf, 10), transparency=true) + meshscatter(f[2, 1], RNG.rand(Point3f, 10), axis=(type=Axis3,)) + f end -@reference_test "meshscatter color numbers" meshscatter(RNG.rand(10), RNG.rand(10), RNG.rand(10), color=RNG.rand(10)) -@reference_test "meshscatter color array" meshscatter(RNG.rand(10), RNG.rand(10), RNG.rand(10), color=RNG.rand(RGBAf, 10), transparency=true) - @reference_test "transparent mesh texture" begin s1 = uv_mesh(Sphere(Point3f(0), 1f0)) f, ax, pl = mesh(uv_mesh(Sphere(Point3f(0), 1f0)), color=RNG.rand(50, 50)) @@ -146,16 +175,6 @@ end fig end -@reference_test "log10 heatmap" begin - heatmap(RNG.rand(10, 5), axis = (yscale = log10, xscale=log10)) -end - -@reference_test "reverse range heatmap" begin - x = [1 0 - 2 3] - heatmap(1:2, 1:-1:0, x) -end - @reference_test "lines linesegments width test" begin res = 200 s = Scene(camera=campixel!, size=(res, res)) @@ -188,10 +207,6 @@ end scatter(RNG.rand(Point2f, 10000), marker=Makie.FastPixel()) end -@reference_test "axsi3" begin - meshscatter(RNG.rand(Point3f, 10), axis=(type=Axis3,)) -end - @reference_test "pattern barplot" begin barplot(1:5, color=Makie.LinePattern(linecolor=:red, background_color=:orange)) end @@ -282,6 +297,15 @@ end f end +@reference_test "Scene backgroundcolor" begin + root = Scene(size = (500, 500)) + Scene(root, viewport = Rect2f(0,0,250,250), backgroundcolor = :red, clear = true) + Scene(root, viewport = Rect2f(250,0,250,250), backgroundcolor = :blue, clear = true) + Scene(root, viewport = Rect2f(50,300,300,50), backgroundcolor = :cyan, clear = true) + Scene(root, viewport = Rect2f(350,400,50,200), backgroundcolor = :orange, clear = true) + root +end + # Needs a way to disable autolimits on show # @reference_test "interactions after close" begin @@ -300,4 +324,4 @@ end # f.scene.events.scroll[] = (0, -10) # # reference test the zoomed out plot # f -# end \ No newline at end of file +# end diff --git a/ReferenceTests/src/tests/text.jl b/ReferenceTests/src/tests/text.jl index a15a5c8e4f8..3c09c2a0435 100644 --- a/ReferenceTests/src/tests/text.jl +++ b/ReferenceTests/src/tests/text.jl @@ -14,9 +14,10 @@ fig end -@reference_test "data space" begin +@reference_test "2D text" begin + f = Figure() pos = [Point2f(0, 0), Point2f(10, 10)] - fig = text( + text(f[1, 1], ["0 is the ORIGIN of this", "10 says hi"], position = pos, axis = (aspect = DataAspect(),), @@ -24,6 +25,34 @@ end align = (:center, :center), fontsize = 2) scatter!(pos) + + text(f[2, 1], + ". This is an annotation!", + position=(300, 200), + align=(:center, :center), + fontsize=60, + font="Blackchancery" + ) + + f +end + +@reference_test "Text rotation" begin + fig = Figure() + ax = fig[1, 1] = Axis(fig) + pos = (500, 500) + posis = Point2f[] + for r in range(0, stop=2pi, length=20) + p = pos .+ (sin(r) * 100.0, cos(r) * 100) + push!(posis, p) + text!(ax, "test", + position=p, + fontsize=50, + rotation=1.5pi - r, + align=(:center, :center) + ) + end + scatter!(ax, posis, markersize=10) fig end @@ -107,12 +136,12 @@ end scene end -@reference_test "multi_boundingboxes" begin - scene = Scene(camera = campixel!, size = (800, 800)) +@reference_test "single and multi boundingboxes" begin + scene = Scene(camera = campixel!, size = (600, 600)) t1 = text!(scene, fill("makie", 4), - position = [(200, 200) .+ 60 * Point2f(cos(a), sin(a)) for a in pi/4:pi/2:7pi/4], + position = [(150, 150) .+ 60 * Point2f(cos(a), sin(a)) for a in pi/4:pi/2:7pi/4], rotation = pi/4:pi/2:7pi/4, align = (:left, :center), fontsize = 30, @@ -123,7 +152,7 @@ end t2 = text!(scene, fill("makie", 4), - position = [(200, 600) .+ 60 * Point2f(cos(a), sin(a)) for a in pi/4:pi/2:7pi/4], + position = [(150, 450) .+ 60 * Point2f(cos(a), sin(a)) for a in pi/4:pi/2:7pi/4], rotation = pi/4:pi/2:7pi/4, align = (:left, :center), fontsize = 30, @@ -132,17 +161,11 @@ end wireframe!(scene, boundingbox(t2, :pixel), color = (:red, 0.3)) - scene -end - -@reference_test "single_boundingboxes" begin - scene = Scene(camera = campixel!, size = (800, 800)) - for a in pi/4:pi/2:7pi/4 t = text!(scene, "makie", - position = (200, 200) .+ 60 * Point2f(cos(a), sin(a)), + position = (450, 150) .+ 60 * Point2f(cos(a), sin(a)), rotation = a, align = (:left, :center), fontsize = 30, @@ -153,7 +176,7 @@ end t2 = text!(scene, "makie", - position = (200, 600) .+ 60 * Point2f(cos(a), sin(a)), + position = (450, 450) .+ 60 * Point2f(cos(a), sin(a)), rotation = a, align = (:left, :center), fontsize = 30, @@ -164,11 +187,14 @@ end wireframe!(scene, boundingbox(t2, :pixel), color = (:red, 0.3)) end + scene end -@reference_test "text_in_3d_axis" begin +@reference_test "3D text" begin + f = Figure(size = (600, 600)) text( + f[1, 1], fill("Makie", 7), rotation = [i / 7 * 1.5pi for i in 1:7], position = [Point3f(0, 0, i/2) for i in 1:7], @@ -178,18 +204,36 @@ end markerspace = :data, axis=(; type=LScene) ) + + positions = RNG.rand(Point3f, 10) + meshscatter(f[1, 2], positions, color=:white) + text!( + fill("Annotation", 10), + position = positions, + align = (:center, :center), + fontsize = 16, + markerspace = :pixel, + overdraw=false) + + p1 = Point3f(0,0,0) + p2 = Point3f(1,0,0) + meshscatter(f[2, 1], [p1, p2]; markersize=0.3, color=[:purple, :yellow]) + text!(p1; text="A", align=(:center, :center), glowwidth=10.0, glowcolor=:white, color=:black, fontsize=40, overdraw=true) + text!(p2; text="B", align=(:center, :center), glowwidth=20.0, glowcolor=(:black, 0.6), color=:white, fontsize=40, overdraw=true) + + f end @reference_test "empty_lines" begin - scene = Scene(camera = campixel!, size = (800, 800)) + scene = Scene(camera = campixel!, size = (200, 200)) t1 = text!(scene, "Line1\nLine 2\n\nLine4", - position = (200, 400), align = (:center, :center), markerspace = :data) + position = (50, 100), align = (:center, :center), markerspace = :data) 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) + position = (150, 100), align = (:center, :center), markerspace = :data) wireframe!(scene, boundingbox(t2, :data), color = (:blue, 0.3)) @@ -197,20 +241,6 @@ end end -@reference_test "3D screenspace annotations" begin - positions = RNG.rand(Point3f, 10) - fig, ax, p = meshscatter(positions, color=:white) - text!( - fill("Annotation", 10), - position = positions, - align = (:center, :center), - fontsize = 20, - markerspace = :pixel, - overdraw=false) - fig -end - - @reference_test "Text offset" begin f = Figure(size = (1000, 1000)) barplot(f[1, 1], 3:5) @@ -261,32 +291,24 @@ end f end -@reference_test "latex hlines in axis" begin - text(1, 1, text = L"\frac{\sqrt{x + y}}{\sqrt{x + y}}", fontsize = 50, rotation = pi/4, - align = (:center, :center)) -end - -@reference_test "latex simple" begin - s = Scene(camera = campixel!) - t = text!(s, - L"\sqrt{2}", - position = (50, 50), - rotation = pi/2, - markerspace = :data) - s -end +# TODO: merge +@reference_test "latex (axis, scene, bbox)" begin + f = Figure(size = (500, 300)) -@reference_test "latex bb" begin - s = Scene(camera = campixel!) - t = text!(s, - L"\int_0^5x^2+2ab", - position = Point2f(50, 50), - rotation = 0.0, + text(f[1, 1], 1, 1, text = L"\frac{\sqrt{x + y}}{\sqrt{x + y}}", fontsize = 50, + rotation = pi/4, align = (:center, :center)) + + s = LScene(f[1, 2], scenekw = (camera = campixel!,), show_axis = false) + text!(s, L"\sqrt{2}", position = (100, 50), rotation = pi/2, fontsize = 20, markerspace = :data) + + t = text!(s, L"\int_0^5x^2+2ab", position = Point2f(50, 150), rotation = 0.0, + fontsize = 20, markerspace = :data) wireframe!(s, boundingbox(t, :data), color=:black) - s + f end + @reference_test "latex updates" begin s = Scene(camera = campixel!) st = Stepper(s) diff --git a/ReferenceTests/src/tests/unitful.jl b/ReferenceTests/src/tests/unitful.jl index d3d4ac7be52..30b3a02688d 100644 --- a/ReferenceTests/src/tests/unitful.jl +++ b/ReferenceTests/src/tests/unitful.jl @@ -7,16 +7,12 @@ using Makie.Dates, Makie.Unitful, Test f end -@reference_test "different units for x + y" begin - scatter(u"ns" .* (1:10), u"d" .* (1:10), markersize=20, color=1:10) -end - -@reference_test "Nanoseconds on y" begin - linesegments(1:10, Nanosecond.(round.(LinRange(0, 4599800000000, 10)))) -end - -@reference_test "Meter & time on x, y" begin - scatter(u"cm" .* (1:10), u"d" .* (1:10)) +@reference_test "Basic units" begin + f = Figure() + scatter(f[1, 1], u"ns" .* (1:10), u"d" .* (1:10), markersize=20, color=1:10) + linesegments(f[1, 2], 1:10, Nanosecond.(round.(LinRange(0, 4599800000000, 10)))) + scatter(f[2, 1], u"cm" .* (1:10), u"d" .* (1:10)) + f end @reference_test "Auto units for observables" begin diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index 3c362fbea7f..bf177009373 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -173,3 +173,22 @@ end ax.title = "identity" Makie.step!(st) end + + +@reference_test "event ticks in record" begin + # Checks whether record calculates and triggers event.tick by drawing a + # Point at y = 1 for each frame where it does. The animation is irrelevant + # here, so we can just check the final image. + # The first point maybe at 0 depending on when the backend sets up it's + # reference time + ps = Observable(Point2f[]) + f, a, p = scatter(ps) + xlims!(a, 0, 61) + ylims!(a, -0.1, 1.1) + Record(f, 1:60, framerate = 30) do i + push!(ps.val, Point2f(i, f.scene.events.tick[].delta_time > 1e-6)) + notify(ps) + f.scene.events.tick[] = Makie.Tick(Makie.UnknownTickState, 0, 0.0, 0.0) + end + f +end \ No newline at end of file diff --git a/ReferenceUpdater/Project.toml b/ReferenceUpdater/Project.toml index a8d2097449d..59313979845 100644 --- a/ReferenceUpdater/Project.toml +++ b/ReferenceUpdater/Project.toml @@ -4,6 +4,7 @@ authors = ["Julius Krumbiegel "] version = "0.1.0" [deps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" diff --git a/ReferenceUpdater/src/ReferenceUpdater.jl b/ReferenceUpdater/src/ReferenceUpdater.jl index cdb9d003cf8..bca0ba7513a 100644 --- a/ReferenceUpdater/src/ReferenceUpdater.jl +++ b/ReferenceUpdater/src/ReferenceUpdater.jl @@ -8,6 +8,7 @@ import JSON3 import ZipFile import REPL import TOML +using Dates function github_token() get(ENV, "GITHUB_TOKEN") do @@ -24,4 +25,9 @@ include("image_download.jl") basedir(files...) = normpath(joinpath(@__DIR__, "..", files...)) -end \ No newline at end of file +function __init__() + # cleanup downloaded files when julia closes + atexit(wipe_cache!) +end + +end diff --git a/ReferenceUpdater/src/image_download.jl b/ReferenceUpdater/src/image_download.jl index 063edf9860b..bc3098a0faa 100644 --- a/ReferenceUpdater/src/image_download.jl +++ b/ReferenceUpdater/src/image_download.jl @@ -18,3 +18,13 @@ function upload_reference_images(path=basedir("recorded"), tag=last_major_versio upload_release("MakieOrg", "Makie.jl", github_token(), tag, tarfile) end end + +function download_refimages(tag=last_major_version()) + url = "https://github.com/MakieOrg/Makie.jl/releases/download/$(tag)/reference_images.tar" + images_tar = Downloads.download(url) + images = tempname() + isdir(images) && rm(images, recursive=true, force=true) + Tar.extract(images_tar, images) + rm(images_tar) + return images +end diff --git a/ReferenceUpdater/src/local_server.jl b/ReferenceUpdater/src/local_server.jl index 854ba474fad..4496911129d 100644 --- a/ReferenceUpdater/src/local_server.jl +++ b/ReferenceUpdater/src/local_server.jl @@ -1,26 +1,38 @@ const URL_CACHE = Dict{String, String}() +function wipe_cache!() + for path in values(URL_CACHE) + rm(path, recursive = true) + end + empty!(URL_CACHE) + return +end + function serve_update_page_from_dir(folder) folder = realpath(folder) @assert isdir(folder) "$folder is not a valid directory." - + group_scores(folder) + group_files(folder, "new_files.txt", "new_files_grouped.txt") + group_files(folder, "missing_files.txt", "missing_files_grouped.txt") + router = HTTP.Router() function receive_update(req) data = JSON3.read(req.body) - images = data["images"] + images_to_update = data["images_to_update"] + images_to_delete = data["images_to_delete"] tag = data["tag"] - tempdir = tempname() recorded_folder = joinpath(folder, "recorded") - reference_folder = joinpath(folder, "reference") - @info "Copying reference folder to \"$tempdir\"" - cp(reference_folder, tempdir) + @info "Downloading latest reference folder for $tag" + tempdir = download_refimages(tag) + + @info "Updating files in $tempdir" - for image in images - @info "Overwriting \"$image\" in new reference folder" + for image in images_to_update + @info "Overwriting or adding $image" copy_filepath = joinpath(tempdir, image) copy_dir = splitdir(copy_filepath)[1] # make the path in case a new refimage is in a not yet existing folder @@ -28,6 +40,16 @@ function serve_update_page_from_dir(folder) cp(joinpath(recorded_folder, image), copy_filepath, force = true) end + for image in images_to_delete + @info "Deleting $image" + copy_filepath = joinpath(tempdir, image) + if isfile(copy_filepath) + rm(copy_filepath, recursive = true) + else + @warn "Cannot delete $image - it has already been deleted." + end + end + @info "Uploading updated reference images under tag \"$tag\"" try upload_reference_images(tempdir, tag) @@ -98,7 +120,7 @@ function serve_update_page(; commit = nothing, pr = nothing) checkruns = filter(checksinfo["check_runs"]) do checkrun name = checkrun["name"] id = checkrun["id"] - + if name == "Merge artifacts" job = JSON3.read(authget("https://api.github.com/repos/MakieOrg/Makie.jl/actions/jobs/$(id)").body) run = JSON3.read(authget(job["run_url"]).body) @@ -112,14 +134,20 @@ function serve_update_page(; commit = nothing, pr = nothing) return false end end + if isempty(checkruns) error("\"Merge artifacts\" run is not available.") end if length(checkruns) > 1 - error("Found multiple checkruns for \"Merge artifacts\", this is unexpected.") - end - + datetimes = map(checkruns) do checkrun + DateTime(checkrun["completed_at"], dateformat"y-m-dTH:M:SZ") + end + datetime, idx = findmax(datetimes) + @warn("Found multiple checkruns for \"Merge artifacts\". Using latest with timestamp: $datetime") + check = checkruns[idx] + else check = only(checkruns) + end job = JSON3.read(authget("https://api.github.com/repos/MakieOrg/Makie.jl/actions/jobs/$(check["id"])").body) run = JSON3.read(authget(job["run_url"]).body) @@ -136,6 +164,7 @@ function serve_update_page(; commit = nothing, pr = nothing) @info "Download successful" tmpdir = mktempdir() unzip(filepath, tmpdir) + rm(filepath) URL_CACHE[download_url] = tmpdir else tmpdir = URL_CACHE[download_url] @@ -173,3 +202,90 @@ function unzip(file, exdir = "") close(zarchive) @info "Extracted zip file" end + + +function group_scores(path) + isfile(joinpath(path, "scores_table.tsv")) && return + + # Load all refimg scores into a Dict + # `filename => (score_glmakie, score_cairomakie, score_wglmakie)` + data = Dict{String, Vector{Float64}}() + open(joinpath(path, "scores.tsv"), "r") do file + for line in eachline(file) + score, filepath = split(line, '\t') + pieces = splitpath(filepath) + backend = pieces[1] + filename = join(pieces[2:end], '/') + + scores = get!(data, filename, [-1.0, -1.0, -1.0]) + if backend == "GLMakie" + scores[1] = parse(Float64, score) + elseif backend == "CairoMakie" + scores[2] = parse(Float64, score) + elseif backend == "WGLMakie" + scores[3] = parse(Float64, score) + else + error("$line -> $backend") + end + end + end + + # sort by max score across all backends so problem come first + data_vec = collect(pairs(data)) + sort!(data_vec, by = x -> maximum(x[2]), rev = true) + + # generate new file with + # GLMakie CairoMakie WGLMakie + # score filename score filename score filename + open(joinpath(path, "scores_table.tsv"), "w") do file + for (filename, scores) in data_vec + skip = scores .== -1.0 + println(file, + ifelse(skip[1], "0.0", scores[1]), '\t', ifelse(skip[1], "", "GLMakie/$filename"), '\t', + ifelse(skip[2], "0.0", scores[2]), '\t', ifelse(skip[2], "", "CairoMakie/$filename"), '\t', + ifelse(skip[3], "0.0", scores[3]), '\t', ifelse(skip[3], "", "WGLMakie/$filename") + ) + end + end + + return +end + +function group_files(path, input_filename, output_filename) + isfile(joinpath(path, output_filename)) && return + + # Group files in new_files/missing_files into a table like layout: + # GLMakie CairoMakie WGLMakie + + # collect refimg names and which backends they exist for + data = Dict{String, Vector{Bool}}() + open(joinpath(path, input_filename), "r") do file + for filepath in eachline(file) + pieces = split(filepath, '/') + backend = pieces[1] + if !(backend in ("GLMakie", "CairoMakie", "WGLMakie")) + error("Failed to parse backend in \"$line\", got \"$backend\"") + end + + filename = join(pieces[2:end], '/') + exists = get!(data, filename, [false, false, false]) + + exists[1] |= backend == "GLMakie" + exists[2] |= backend == "CairoMakie" + exists[3] |= backend == "WGLMakie" + end + end + + # generate new structed file + open(joinpath(path, output_filename), "w") do file + for (filename, valid) in data + println(file, + ifelse(valid[1], "GLMakie/$filename", "INVALID"), '\t', + ifelse(valid[2], "CairoMakie/$filename", "INVALID"), '\t', + ifelse(valid[3], "WGLMakie/$filename", "INVALID") + ) + end + end + + return +end diff --git a/ReferenceUpdater/src/reference_images.html b/ReferenceUpdater/src/reference_images.html index b339f313983..29f4f69e309 100644 --- a/ReferenceUpdater/src/reference_images.html +++ b/ReferenceUpdater/src/reference_images.html @@ -6,22 +6,25 @@ font-family: sans-serif; } .refimage { - display: block; + max-width: 100%; } .image-list { - display: flex; - flex-wrap: wrap; + display: table; } .image-list > div { - margin: 1em; + display: table-row; + } + .image-list > div > td > div{ + display: block; + margin: 0.25em; padding: 0.5em; - border: 2px solid #eee; + border: 2px solid lightblue; + background-color: #eee; border-radius: 1em; - } - - .image-list > div > h3 { + } + .image-list > div > td > div > h3 { margin: 0 0 1em 0; - } + } @@ -31,9 +34,31 @@

Reference images

+

+ +

New images without references

+ The selected CI run produced an image for which no reference image exists. + Selected images will be added as new reference images. +

+ Toggle All
+ +

Old reference images without recordings

+ The selected CI run did not produce an image, but a reference image exists. + This implies that a reference test was deleted or renamed. + Selected images will be deleted from the reference images. +

+ Toggle All
+
+

Images with references

+ This is the normal case where the selected CI run produced an image and the reference image exists. + Each row shows one image per backend from the same reference image test, which can be compared with its reference image. + Rows are sorted based on the maximum row score (bigger = more different). + Red cells fail CI (assuming the thresholds are up to date), yellow cells may but likely don't have signficant visual difference and gray cells are visually equivalent. +

@@ -41,7 +66,7 @@

Images with references

// this string should be replaced by the server script with the correct value default_tag = DEFAULT_TAG - fetch('scores.tsv') + fetch('scores_table.tsv') .then(response => response.text()) .then(data => { di = document.querySelector("#refimage-list") @@ -50,27 +75,48 @@

Images with references

return } parts = line.split('\t') - score = parseFloat(parts[0]) - let path = parts[1] - - div = document.createElement("div") - di.append(div) - div.innerHTML = ` - - ${path} -
Score: ${score.toFixed(4)}
- - ` - if (path.endsWith(".png")){ - div.innerHTML += `` - } else if (path.endsWith(".mp4")){ - div.innerHTML += `