From 981d954f3ed5fb8be55d01943404dcee092e2c4e Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:39:43 +0200 Subject: [PATCH 01/80] Figure size and dpi how-to (#4504) * add typst to doc deps * Rewrite figure size implementations and add simplified how-to * remove unnecessary mkpath * fix links --- docs/Project.toml | 1 + docs/makedocs.jl | 43 ++++--- docs/src/explanations/figure.md | 110 +++++++++++++----- docs/src/explanations/scenes.md | 2 +- .../match-figure-size-font-sizes-and-dpi.md | 68 +++++++++++ 5 files changed, 174 insertions(+), 50 deletions(-) create mode 100644 docs/src/how-to/match-figure-size-font-sizes-and-dpi.md diff --git a/docs/Project.toml b/docs/Project.toml index ce3ad5a3733..54e2bd62113 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -26,4 +26,5 @@ NodeJS = "2bd173c7-0d6d-553b-b6af-13a54713934c" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" RPRMakie = "22d9f318-5e34-4b44-b769-6e3734a732a6" RadeonProRender = "27029320-176d-4a42-b57d-56729d2ad457" +Typst_jll = "eb4b1da6-20f6-5c66-9826-fdb8ad410d0e" WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" diff --git a/docs/makedocs.jl b/docs/makedocs.jl index 9a600603633..5ceb92f3170 100644 --- a/docs/makedocs.jl +++ b/docs/makedocs.jl @@ -187,6 +187,7 @@ pages = [ "explanations/transparency.md", ], "How-Tos" => [ + "how-to/match-figure-size-font-sizes-and-dpi.md", "how-to/draw-boxes-around-subfigures.md", "how-to/save-figure-with-transparency.md", ], @@ -197,25 +198,29 @@ pages = [ ] ] -empty!(MakieDocsHelpers.FIGURES) - -# filter pages here when working on docs interactively -# pages = nested_filter(pages, r"reference/blocks/(axis|axis3|overview)") - -Documenter.makedocs(; - sitename="Makie", - format=DocumenterVitepress.MarkdownVitepress(; - repo = "github.com/MakieOrg/Makie.jl", - devurl = "dev", - devbranch = "master", - deploy_url = "https://docs.makie.org", # for local testing not setting this has broken links with Makie.jl in them - description = "Create impressive data visualizations with Makie, the plotting ecosystem for the Julia language. Build aesthetic plots with beautiful customizable themes, control every last detail of publication quality vector graphics, assemble complex layouts and quickly prototype interactive applications to explore your data live.", - deploy_decision, - ), - pages, - expandfirst = unnest(nested_filter(pages, r"reference/(plots|blocks)/(?!overview)")), - warnonly = get(ENV, "CI", "false") != "true", - pagesonly = true, +function make_docs(; pages) + empty!(MakieDocsHelpers.FIGURES) + + Documenter.makedocs(; + sitename="Makie", + format=DocumenterVitepress.MarkdownVitepress(; + repo = "github.com/MakieOrg/Makie.jl", + devurl = "dev", + devbranch = "master", + deploy_url = "https://docs.makie.org", # for local testing not setting this has broken links with Makie.jl in them + description = "Create impressive data visualizations with Makie, the plotting ecosystem for the Julia language. Build aesthetic plots with beautiful customizable themes, control every last detail of publication quality vector graphics, assemble complex layouts and quickly prototype interactive applications to explore your data live.", + deploy_decision, + ), + pages, + expandfirst = unnest(nested_filter(pages, r"reference/(plots|blocks)/(?!overview)")), + warnonly = get(ENV, "CI", "false") != "true", + pagesonly = true, + ) +end + +make_docs(; + # filter pages here when working on docs interactively + pages # = nested_filter(pages, r"explanations/figure|match-figure"), ) ## diff --git a/docs/src/explanations/figure.md b/docs/src/explanations/figure.md index 757393dabb6..0feb5521d54 100644 --- a/docs/src/explanations/figure.md +++ b/docs/src/explanations/figure.md @@ -152,52 +152,102 @@ contents(f[1, 1]) == [ax] content(f[1, 1]) == ax ``` -## Figure size and units +## Figure size and resolution -In Makie, figure size and attributes like line widths, font sizes, scatter marker extents, or layout column and row gaps are usually given as plain numbers, without an explicit unit attached. -What does it mean to have a `Figure` with `size = (600, 450)`, a line with `linewidth = 10` or a column gap of `30`? +In Makie, the **size** of a `Figure` is unitless. That is because `Figure`s can be rendered as images or vector graphics or displayed in interactive windows. The actual physical size of those outputs depends on multiple factors such as screen sizes which are often outside of Makie's control, so we don't want to promise correct output size under all circumstances for a hypothetical `Figure(size = (4cm, 5cm))`. -The first underlying idea is that, no matter what your final output format is, these numbers are _relative_. -You can expect a `linewidth = 10` to cover 1/60th of the width `600` of the `Figure` and a column gap of `30` to span 1/20th of the Figure. -This holds, no matter if you later export that `Figure` as an image made out of pixels, or as a vector graphic that doesn't have pixels at all. +The `size` of a `Figure` first and foremost tells you how much space there is for `Axis` objects and other content. For example, `fontsize` or `Axis(width = ..., height = ...)` are also unitless, but they can be understood relative to the figure size. If there is not enough space in your `Figure`, you can either increase its `size` or decrease the size of the content (for example with smaller fontsizes). However, we don't _only_ care about the `size` of the `Figure` relative to the sizes of its contents. It also has a meaning when we think about how big or small our `Figure` will look when rendered on all the different possible output devices. -The second idea is that, given some `Figure`, we want to be able to export an image at arbitrary resolution, or a vector graphic at any size from it, as long as the relative sizes of all elements stay intact. -So we need to _translate_ our abstract sizes to real sizes when we render. -In Makie, this is done with two scaling factors: `px_per_unit` for images and `pt_per_unit` for vector graphics. +Now, although Makie uses unitless numbers for figure size, it is set up by default such that these numbers can actually be thought of as CSS pixels. We have chosen this convention to simplify using Makie in web contexts which includes browser-based tools like Pluto, Jupyter notebooks or editors like VSCode. All of these use CSS to control the appearance of objects. -A line with `linewidth = 10` will be 10 pixels wide if rendered to an image file with `px_per_unit = 1`. It will be 5 pixels wide if `px_per_unit = 0.5` and 20 pixels if `px_per_unit = 2`. A `Figure` with `size = (600, 450)` will have 600 x 450 pixels when exported with `px_per_unit = 1`, 300 x 225 with `px_per_unit = 0.5` and 1200 x 900 with `px_per_unit = 2`. +At default settings, a `Figure` of size `(600, 450)` will be displayed at a size of 600 x 450 CSS pixels in web contexts (if Makie renders via the `text/html` or `image/svg+xml` MIME types). This is true irrespective of its **resolution**, i.e., how many pixels the output bitmap has. The image will be annotated with `width = "600px" height = "450px"` so that browsers will know the intended display size. -It works exactly the same for vector graphics, just with a different target unit. A `pt` or point is a typographic unit that is defined as 1/72 of an inch, which comes out to about 0.353 mm. A line with `linewidth = 10` will be 10 points wide if rendered to an svg file with `pt_per_unit = 1`, it will be 5 points wide for `pt_per_unit = 0.5` and 20 points wide if `pt_per_unit = 2`. A `Figure` with `size = (600, 450)` will be 600 x 450 points in size when exported with `pt_per_unit = 1`, 300 x 225 with `pt_per_unit = 0.5` and 1200 x 900 with `pt_per_unit = 2`. +The CSS pixel is a physical unit (1 px == 1/96 inch) but of course browsers display content on many different screens and at many different zoom levels, so you would usually not expect an element of 96px width to be exactly 1 inch wide at any given time. But even if we don't know what physical size our plots will have on our screens, we want them to fit in well next to other content and text, so we want to match the sizes conventionally used on today's systems. For example, a common fontsize is `12 pt`, which is equivalent to `16 px` (1 px == 3/4 pt). -### Defaults of `px_per_unit` and `pt_per_unit` +This also applies to pdf outputs. When preparing plots for publications, we usually want to match font sizes of their plots to the base document, for example 12pt. But today we don't usually print pdfs on paper at their intended physical dimensions. Often, they are read on mobile devices where they are zoomed in and out, so any given text will rarely be at 12pt physically. -What are the default values of `px_per_unit` and `pt_per_unit` in each Makie backend, and why are they set that way? +While vector graphics are always rendered sharply at a given zoom level, for bitmaps, the actual number of pixels decides at what zoom level or viewing distance they look sharp or blurry. This "sharpness" factor is often specified in `dpi` or dots per inch. Again, the "inch" here should not be expected to always match an actual physical inch (like in the printing days) because of the way we zoom in and out on digital screens. But if we conventionally use CSS pixels to describe sizes, we can also use `dpi` and we'll know what sharpness to expect on typical devices and typical zoom levels. -Let us start with `pt_per_unit` because this value is only relevant for one backend, which is CairoMakie. -The default value in CairoMakie is `pt_per_unit = 0.75`. So if you `save("output.svg", figure)` a `Figure` with `size = (600, 450)`, this comes out as a vector graphic that is 450 x 337.5 pt large. +To sum up, we have two factors that affect the rendered output of a Makie `Figure`. Its **size**, which determines the space available for content and the display size when interpreted in units like CSS pixels, and the **resolution** or sharpness in terms of pixel density or `dpi`. For vector graphics we only care about the size factor (unless we're embedding rasterized bitmaps in them). -Why 0.75 and not simply 1? This has to do with web standards and device-independent pixels. Websites mix vector graphics and images, so they need some way to relate the sizes of both types to each other. In principle, a pixel in an image doesn't have a real-world width. But you don't want the images on your site to shrink relative to the other content when device pixels are small, or grow when device pixels are large. So web browsers don't directly map image pixels to device pixels. Instead, they use a concept called device-independent pixels. If you place an image with 600 x 450 pixels in a website, this image is interpreted by default to be 600 x 450 device-independent pixels wide. One device-independent pixel is defined to be 0.75 pt wide, that's where the factor 0.75 comes in. So an image with 600 x 450 device-independent pixels is the same apparent size as a vector graphic with size 450 x 337.5 pt. On high-resolution screens, browsers then simply render one device-independent pixel with multiple device pixels (for example 2x2 on an Apple Retina display) so that content stays at readable sizes and doesn't look tiny. +### The `px_per_unit` factor -For Makie, we decided that we want our abstract units to match device-independent pixels when used in web contexts, because that's very convenient and easy to predict for the end user. If you have a Jupyter or Pluto notebook, it's nice if a `Figure` comes out at the same apparent size, no matter if you're currently in CairoMakie's svg mode, or in the bitmap mode of any backend. Therefore, we annotate images with the original `Figure` size in device-independent pixels, so they are of the same apparent size, no matter what the `px_per_unit` value and therefore the effective pixel size is. And we give svg files the default scaling factor of 0.75 so that svgs always match images in apparent size. +If we display a `Figure(size = (600, 450))` in a web context, by Makie's convention the image will be annotated with `width = "600px" height = "450px"`. But how many pixels does the actual bitmap have, i.e., how sharp is the image? -Now let us look at the default values for `px_per_unit`. In CairoMakie, the default is `px_per_unit = 2`. This means, a `Figure` with `size = (600, 450)` will be rendered as a 1200 x 900 pixel image. The reason it isn't `px_per_unit = 1` is that CairoMakie plots are often embedded in notebooks or websites, or looked at in image viewers or IDEs like VSCode. On websites, you don't know in advance what the pixel density of a reader's display is going to be. And in image viewers and IDEs, people like to zoom in to look at details. To cover these use cases by default, we decided `px_per_unit = 2` is a good compromise between sharp resolution and appropriate file size. Again, the _apparent_ size of output images in notebooks and websites (wherever the `MIME"text/html"` type is used) depends only on the `size`, because the output images are embedded with ` Increasing `size` gives you more space for your content and a larger bitmap. When scaled to the same size in an output context (a pdf document for example), a figure with larger `size` will appear to have smaller content. + +```@raw html + +``` + +> Increasing `px_per_unit` leaves the space for your content the same but gives a larger bitmap due to higher resolution. When scaled to the same size in an output context (a pdf document for example), a figure with larger `px_per_unit` will appear to have the same content, but sharper. + +```@raw html + +``` + +There is also a `pt_per_unit` factor with which you can scale the output for vector graphics up or down. But if you keep with the convention that Makie's unitless numbers are actually CSS pixels, you can leave the default `pt_per_unit` at 0.75 and get size-matched bitmaps and vector graphics automatically. diff --git a/docs/src/explanations/scenes.md b/docs/src/explanations/scenes.md index d637d957efe..9a829196324 100644 --- a/docs/src/explanations/scenes.md +++ b/docs/src/explanations/scenes.md @@ -17,7 +17,7 @@ A Scene's subscenes (also called children) can be accessed through `scene.childr Any `Scene` with an axis also has a `camera` associated with it; this can be accessed through `camera(scene)`, and its controls through `cameracontrols(scene)`. More documentation about these is in the [Cameras](@ref) section. -`Scene`s have a configurable size. You can set the size in device-independent pixels by doing `Scene(size = (500, 500))`. (More about sizes, resolutions and units in [Figure size and units](@ref)) +`Scene`s have a configurable size. You can set the size in device-independent pixels by doing `Scene(size = (500, 500))`. (More about sizes, resolutions and units in [Figure size and resolution](@ref) or [How to match Figure size, font sizes and dpi](@ref)) Any keyword argument given to the `Scene` will be propagated to its plots; therefore, you can set the palette or the colormap in the Scene itself. diff --git a/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md b/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md new file mode 100644 index 00000000000..7f9132c5848 --- /dev/null +++ b/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md @@ -0,0 +1,68 @@ +# How to match Figure size, font sizes and dpi + +We want to create three plots for inclusion in a document. These are the requirements: + +- Figure 1: png @ 4x3 inches and 100 dpi +- Figure 2: png @ 9x7 cm and 300 dpi +- Figure 3: svg @ 4x3 inches + +The fontsize of all three should match the document's 12pt setting. + +We assume the convention that Makie's unitless figure size is actually equivalent to CSS pixels. +For a deeper explanation why, check the section [Figure size and resolution](@ref). + +We're using Typst here but the technique applies similarly for all authoring tools that allow you to set the dimensions of included images. + +```@example +using CairoMakie +CairoMakie.activate!() # hide +using Typst_jll + +# these are relative to 1 CSS px +inch = 96 +pt = 4/3 +cm = inch / 2.54 + +f1 = Figure(size = (4inch, 3inch), fontsize = 12pt) +f2 = Figure(size = (9cm, 7cm), fontsize = 12pt) +f3 = Figure(size = (4inch, 3inch), fontsize = 12pt) + +titles = [ + "Figure 1: png @ 4x3 inches and 100 dpi", + "Figure 2: png @ 9x7 cm and 300 dpi", + "Figure 3: svg @ 4x3 inches", +] + +data = cumsum(randn(100)) + +for (f, title) in zip([f1, f2, f3], titles) + ax = Axis(f[1, 1]; title, xlabel = "time (s)", ylabel = "value (€)") + lines!(ax, data) +end + +save("figure1.png", f1, px_per_unit = 100/inch) +save("figure2.png", f2, px_per_unit = 300/inch) +save("figure3.svg", f3) + +typst_code = """ + #set page(fill: rgb("#f5f2eb")) + #set text(font: "TeX Gyre Heros Makie", size: 12pt, fill: luma(50%)) + + This is some text at 12pt which the figures below should match. + + #image("figure1.png", width: 4in, height: 3in) + #image("figure2.png", width: 9cm, height: 7cm) + #image("figure3.svg") // vector graphics have physical dimensions +""" + +open(io -> println(io, typst_code), "document.typ", "w") + +cp(Makie.assetpath("fonts", "TeXGyreHerosMakie-Regular.otf"), "./texgyre.otf") + +run(`$(Typst_jll.typst()) compile --font-path . document.typ output.svg`) + +nothing # hide +``` + +![](output.svg) + From 814d8d5b19e742b9b2bc68ef41aa9724edcb19ca Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:40:52 +0200 Subject: [PATCH 02/80] Use polys for axis3 (#4463) * use poly for axis3 panels so that svgs aren't rasterized * add rasterization tests for Axis3 and PolarAxis * disable stroke * remove depth shift * Update CHANGELOG.md --------- Co-authored-by: Simon --- CHANGELOG.md | 1 + CairoMakie/test/svg_tests.jl | 2 ++ src/makielayout/blocks/axis3d.jl | 43 +++++++++++++++++++++----------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf94ec24d8..9061fd1100a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 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). +- Use polys for axis3 [#4463](https://github.com/MakieOrg/Makie.jl/pull/4463). - 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) diff --git a/CairoMakie/test/svg_tests.jl b/CairoMakie/test/svg_tests.jl index 5efb72c4baf..da30bba0948 100644 --- a/CairoMakie/test/svg_tests.jl +++ b/CairoMakie/test/svg_tests.jl @@ -13,6 +13,8 @@ end @testset "SVG rasterization" begin @test svg_isnt_rasterized(Scene()) @test svg_isnt_rasterized(begin f = Figure(); Axis(f[1, 1]); f end) + @test svg_isnt_rasterized(begin f = Figure(); Axis3(f[1, 1]); f end) + @test svg_isnt_rasterized(begin f = Figure(); PolarAxis(f[1, 1]); f end) @test svg_isnt_rasterized(scatter(1:3)) @test svg_isnt_rasterized(lines(1:3)) @test svg_isnt_rasterized(heatmap(rand(5, 5))) diff --git a/src/makielayout/blocks/axis3d.jl b/src/makielayout/blocks/axis3d.jl index ab3121fb487..a6b97462925 100644 --- a/src/makielayout/blocks/axis3d.jl +++ b/src/makielayout/blocks/axis3d.jl @@ -664,29 +664,42 @@ function add_panel!(scene, ax, dim1, dim2, dim3, limits, min3) string((:x, :y, :z)[dim2]) * string(sym)) attr(sym) = getproperty(ax, dimsym(sym)) - vertices = lift(limits, min3) do lims, mi3 - + rect = lift(limits) do lims mi = minimum(lims) ma = maximum(lims) + Polygon([ + Point2(mi[dim1], mi[dim2]), + Point2(ma[dim1], mi[dim2]), + Point2(ma[dim1], ma[dim2]), + Point2(mi[dim1], ma[dim2]) + ]) + end - v3 = if mi3 - mi[dim3] + 0.005 * (mi[dim3] - ma[dim3]) - else - ma[dim3] + 0.005 * (ma[dim3] - mi[dim3]) - end + plane_offset = lift(limits, min3) do lims, mi3 + mi = minimum(lims) + ma = maximum(lims) - p1 = dim3point(dim1, dim2, dim3, mi[dim1], mi[dim2], v3) - p2 = dim3point(dim1, dim2, dim3, mi[dim1], ma[dim2], v3) - p3 = dim3point(dim1, dim2, dim3, ma[dim1], ma[dim2], v3) - p4 = dim3point(dim1, dim2, dim3, ma[dim1], mi[dim2], v3) - [p1, p2, p3, p4] + mi3 ? mi[dim3] : ma[dim3] end - faces = [1 2 3; 3 4 1] + plane = Symbol((:x, :y, :z)[dim1], (:x, :y, :z)[dim2]) - panel = mesh!(scene, vertices, faces, shading = NoShading, inspectable = false, + panel = poly!(scene, rect, inspectable = false, xautolimits = false, yautolimits = false, zautolimits = false, - color = attr(:panelcolor), visible = attr(:panelvisible)) + color = attr(:panelcolor), visible = attr(:panelvisible), + strokecolor = :transparent, strokewidth = 0, + transformation = (plane, 0), + ) + + on(plane_offset) do offset + translate!( + panel, + dim3 == 1 ? offset : zero(offset), + dim3 == 2 ? offset : zero(offset), + dim3 == 3 ? offset : zero(offset), + ) + end + return panel end From 6b267799d179909ab59632e0b913b79e9f419086 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Mon, 21 Oct 2024 22:54:39 +0200 Subject: [PATCH 03/80] Benchmark simplification (#4493) * use poly for axis3 panels so that svgs aren't rasterized * add rasterization tests for Axis3 and PolarAxis * disable stroke * remove depth shift * don't use BenchmarkTools, just run 100 trials and store all results * interleave project runs * bump run number to 20 * change timing names * store results as artifact * collect gc times separately * only include previous 5 columns in comment to match old template * use median of timings to avoid outliers * store each json artifact separately * remove superfluous arrays --------- Co-authored-by: Simon --- .github/workflows/compilation-benchmark.yaml | 7 ++++- metrics/ttfp/benchmark-ttfp.jl | 32 ++++++++++++++++---- metrics/ttfp/run-benchmark.jl | 16 +++++++--- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index 6df32a3184f..17cb146fed4 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -37,4 +37,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.BENCHMARK_KEY }} PR_NUMBER: ${{ github.event.number }} run: > - DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=./metrics/ttfp/ ./metrics/ttfp/run-benchmark.jl ${{ matrix.package }} 7 ${{ github.event.pull_request.base.ref }} + DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=./metrics/ttfp/ ./metrics/ttfp/run-benchmark.jl ${{ matrix.package }} 20 ${{ github.event.pull_request.base.ref }} + - name: Upload measurements as artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.package }} + path: ./json diff --git a/metrics/ttfp/benchmark-ttfp.jl b/metrics/ttfp/benchmark-ttfp.jl index 274b1c3e38d..d44f016091d 100644 --- a/metrics/ttfp/benchmark-ttfp.jl +++ b/metrics/ttfp/benchmark-ttfp.jl @@ -20,9 +20,9 @@ set_theme!(size=(800, 600)) create_time = @ctime fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) display_time = @ctime colorbuffer(fig; px_per_unit=1) -using BenchmarkTools -using BenchmarkTools.JSON +using JSON using Pkg +using Statistics: median project_name = basename(dirname(Pkg.project().path)) @@ -31,13 +31,33 @@ old = isfile(result) ? JSON.parse(read(result, String)) : [[], [], [], [], []] @show [t_using, create_time, display_time] push!.(old[1:3], [t_using, create_time, display_time]) -b1 = @benchmark fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -b2 = @benchmark colorbuffer(fig; px_per_unit=1) +macro simple_median_time(expr) + time_expr = quote + local elapsedtime = time_ns() + $expr + elapsedtime = time_ns() - elapsedtime + Float64(elapsedtime) + end + + quote + times = Float64[] + for i in 1:101 + t = Core.eval(Main, $(QuoteNode(time_expr))) + if i > 1 + push!(times, t) + end + end + median(times) + end +end +@time "creating figure" figure_time = @simple_median_time fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) +fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) +@time "colorbuffer" colorbuffer_time = @simple_median_time colorbuffer(fig; px_per_unit=1) using Statistics -push!(old[4], mean(b1.times)) -push!(old[5], mean(b2.times)) +push!(old[4], figure_time) +push!(old[5], colorbuffer_time) open(io-> JSON.print(io, old), result, "w") diff --git a/metrics/ttfp/run-benchmark.jl b/metrics/ttfp/run-benchmark.jl index 2d7d4602eca..84374eae260 100644 --- a/metrics/ttfp/run-benchmark.jl +++ b/metrics/ttfp/run-benchmark.jl @@ -182,9 +182,10 @@ using Random function run_benchmarks(projects; n=n_samples) benchmark_file = joinpath(@__DIR__, "benchmark-ttfp.jl") - for project in shuffle!(repeat(projects; outer=n)) + # go A, B, A, B, A, etc. + for project in repeat(projects, n) + println(basename(project)) run(`$(Base.julia_cmd()) --startup-file=no --project=$(project) $benchmark_file $Package`) - project_name = basename(project) end return end @@ -221,13 +222,13 @@ end pkgs = NamedTuple[(; path="./MakieCore"), (; path="."), (; path="./$Package")] # cd("dev/Makie") Pkg.develop(pkgs) -Pkg.add([(; name="BenchmarkTools")]) +Pkg.add([(; name="JSON")]) @time Pkg.precompile() project2 = make_project_folder(base_branch) Pkg.activate(project2) -pkgs = [(; rev=base_branch, name="MakieCore"), (; rev=base_branch, name="Makie"), (; rev=base_branch, name="$Package"), (;name="BenchmarkTools")] +pkgs = [(; rev=base_branch, name="MakieCore"), (; rev=base_branch, name="Makie"), (; rev=base_branch, name="$Package"), (;name="JSON")] Package == "WGLMakie" && push!(pkgs, (; name="Electron")) Pkg.add(pkgs) @time Pkg.precompile() @@ -249,3 +250,10 @@ else @info("Not commenting, no PR found") println(update_comment(COMMENT_TEMPLATE, Package, benchmark_rows)) end + +mkdir("json") +for p in [project1, project2] + name = basename(p) + file = "$name-benchmark.json" + mv(file, joinpath("json", file)) +end From 49b29b61f8f5e5d3d5a8880e3e455238987997ce Mon Sep 17 00:00:00 2001 From: Eddie Groshev Date: Mon, 21 Oct 2024 17:16:03 -0600 Subject: [PATCH 04/80] Allow `width` to be set per box in `boxplot` (#4447) * allow width per category in BoxPlot * fix boxplot when range is zero * update CHANGELOG * add test for variable width --------- Co-authored-by: Simon Co-authored-by: Frederic Freyer --- CHANGELOG.md | 1 + ReferenceTests/src/tests/examples2d.jl | 13 ++++++------- src/stats/boxplot.jl | 20 +++++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9061fd1100a..35dc4c886a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Allow `width` to be set per box in `boxplot` [#4447](https://github.com/MakieOrg/Makie.jl/pull/4447). - For `Textbox`es in which a fixed width is specified, the text is now scrolled if the width is exceeded [#4293](https://github.com/MakieOrg/Makie.jl/pull/4293) - Changed image, heatmap and surface picking indices to correctly index the relevant matrix arguments. [#4459](https://github.com/MakieOrg/Makie.jl/pull/4459) diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 4fa7d400b79..94414126528 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -1613,8 +1613,8 @@ end 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), + 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) ) @@ -1631,16 +1631,15 @@ end 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, + 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, - + width = 1.5, ) - boxplot!(ax_horiz, categories, values; orientation=:horizontal) + boxplot!(ax_horiz, categories, values; orientation=:horizontal, width = categories ./ 3) fig end diff --git a/src/stats/boxplot.jl b/src/stats/boxplot.jl index 10cc2f9c6aa..e761b96301e 100644 --- a/src/stats/boxplot.jl +++ b/src/stats/boxplot.jl @@ -85,11 +85,10 @@ function Makie.plot!(plot::BoxPlot) plot[:color], args..., ) do x, y, color, weights, width, range, show_outliers, whiskerwidth, show_notch, orientation, gap, dodge, n_dodge, dodge_gap - x̂, boxwidth = compute_x_and_width(x, width, gap, dodge, n_dodge, dodge_gap) + x̂, widths = compute_x_and_width(x, width, gap, dodge, n_dodge, dodge_gap) if !(whiskerwidth === :match || whiskerwidth >= 0) error("whiskerwidth must be :match or a positive number. Found: $whiskerwidth") end - ww = whiskerwidth === :match ? boxwidth : whiskerwidth * boxwidth outlier_points = Point2f[] centers = Float32[] medians = Float32[] @@ -99,8 +98,10 @@ function Makie.plot!(plot::BoxPlot) notchmax = Float32[] t_segments = Point2f[] outlier_indices = Int[] - T = color isa AbstractVector ? eltype(color) : typeof(color) - boxcolor = T[] + CT = color isa AbstractVector ? eltype(color) : typeof(color) + boxcolor = CT[] + WT = widths isa AbstractVector ? eltype(widths) : typeof(widths) + boxwidth = WT[] for (i, (center, idxs)) in enumerate(StructArrays.finduniquesorted(x̂)) values = view(y, idxs) @@ -117,7 +118,7 @@ function Makie.plot!(plot::BoxPlot) end # outliers - if Float64(range) != 0.0 # if the range is 0.0, the whiskers will extend to the data + if !iszero(range) # if the range is 0, the whiskers will extend to the data limit = range * (q4 - q2) inside = Float64[] for (value, idx) in zip(values,idxs) @@ -134,18 +135,19 @@ function Makie.plot!(plot::BoxPlot) # change q1 and q5 to show outliers # using maximum and minimum values inside the limits q1, q5 = extrema_nan(inside) - # register boxcolor - push!(boxcolor, getuniquevalue(color, idxs)) end # whiskers - HW = 0.5 * _cycle(ww, i) # Whisker width - lw, rw = center - HW, center + HW + bw = getuniquevalue(widths, idxs) # Box width + ww = whiskerwidth === :match ? bw : whiskerwidth * bw # Whisker width + lw, rw = center - ww/2, center + ww/2 push!(t_segments, (center, q2), (center, q1), (lw, q1), (rw, q1)) # lower T push!(t_segments, (center, q4), (center, q5), (rw, q5), (lw, q5)) # upper T # box + push!(boxcolor, getuniquevalue(color, idxs)) push!(centers, center) + push!(boxwidth, bw) push!(boxmin, q2) push!(medians, q3) push!(boxmax, q4) From e3c7521d3b5bc51bb1234b9f0eae2645d5b6463f Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Mon, 21 Oct 2024 17:09:31 -0700 Subject: [PATCH 05/80] Improve CairoMakie's 2D mesh performance (#4132) * Remove splats in draw_mesh2d inner loop This also restores parity in the loop body with `draw_mesh3d`. * Squash point type instability in project_position This shaves ~20% runtime from mesh2d * Fix types in function signature * Add changelog entry --------- Co-authored-by: Simon Co-authored-by: Frederic Freyer --- CHANGELOG.md | 1 + CairoMakie/src/primitives.jl | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35dc4c886a6..2045a7854b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Improved CairoMakie's 2D mesh drawing performance by ~30% [#4132](https://github.com/MakieOrg/Makie.jl/pull/4132). - Allow `width` to be set per box in `boxplot` [#4447](https://github.com/MakieOrg/Makie.jl/pull/4447). - For `Textbox`es in which a fixed width is specified, the text is now scrolled if the width is exceeded [#4293](https://github.com/MakieOrg/Makie.jl/pull/4293) diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index a87386c5ce9..795359670ca 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -908,11 +908,11 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Maki return nothing end -function draw_mesh2D(scene, screen, @nospecialize(plot), @nospecialize(mesh)) +function draw_mesh2D(scene, screen, @nospecialize(plot::Makie.Mesh), @nospecialize(mesh::GeometryBasics.Mesh)) space = to_value(get(plot, :space, :data))::Symbol transform_func = Makie.transform_func(plot) model = plot.model[]::Mat4d - vs = project_position(scene, transform_func, space, decompose(Point, mesh), model) + vs = project_position(scene, transform_func, space, GeometryBasics.metafree(GeometryBasics.coordinates(mesh)), model)::Vector{Point2f} 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 @@ -943,9 +943,9 @@ function draw_mesh2D(screen, per_face_cols, vs::Vector{<: Point2}, fs::Vector{GL Cairo.mesh_pattern_begin_patch(pattern) - Cairo.mesh_pattern_move_to(pattern, t1...) - Cairo.mesh_pattern_line_to(pattern, t2...) - Cairo.mesh_pattern_line_to(pattern, t3...) + Cairo.mesh_pattern_move_to(pattern, t1[1], t1[2]) + Cairo.mesh_pattern_line_to(pattern, t2[1], t2[2]) + Cairo.mesh_pattern_line_to(pattern, t3[1], t3[2]) mesh_pattern_set_corner_color(pattern, 0, c1) mesh_pattern_set_corner_color(pattern, 1, c2) From 2c739420e2ea330b217fe3156d135f6ef6cdfb82 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:17:55 +0200 Subject: [PATCH 06/80] CompatHelper: bump compat for ColorTypes to 0.12, (keep existing compat) (#4502) Co-authored-by: CompatHelper Julia --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index c4026e2a4a0..92538749fb0 100644 --- a/Project.toml +++ b/Project.toml @@ -69,7 +69,7 @@ Base64 = "1.0, 1.6" CRC32c = "1.0, 1.6" ColorBrewer = "0.4" ColorSchemes = "3.5" -ColorTypes = "0.8, 0.9, 0.10, 0.11" +ColorTypes = "0.8, 0.9, 0.10, 0.11, 0.12" Colors = "0.9, 0.10, 0.11, 0.12" Contour = "0.5, 0.6" DelaunayTriangulation = "1.0" From 948096846edd26e54f2e7e3e8530ecdd32788dac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:44:29 +0200 Subject: [PATCH 07/80] CompatHelper: bump compat for ColorTypes to 0.12 for package MakieCore, (keep existing compat) (#4507) Co-authored-by: CompatHelper Julia --- MakieCore/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MakieCore/Project.toml b/MakieCore/Project.toml index 5dde5dfb08c..f6ec96acc73 100644 --- a/MakieCore/Project.toml +++ b/MakieCore/Project.toml @@ -10,7 +10,7 @@ IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" [compat] -ColorTypes = "0.8, 0.9, 0.10, 0.11" +ColorTypes = "0.8, 0.9, 0.10, 0.11, 0.12" GeometryBasics = "0.4.11" IntervalSets = "0.6, 0.7" Observables = "0.5.1" From 6dfb420445065c56f5af7944c87ecdfc22eff507 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:51:48 +0200 Subject: [PATCH 08/80] Move favicon and logo to where DocumenterVitepress expects them (#4505) * Move favicon and logo to where DocumenterVitepress expects them * move logo.svg --- docs/src/{assets => public}/favicon.ico | Bin docs/src/public/logo.png | Bin 0 -> 20893 bytes docs/src/{assets => public}/logo.svg | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename docs/src/{assets => public}/favicon.ico (100%) create mode 100644 docs/src/public/logo.png rename docs/src/{assets => public}/logo.svg (100%) diff --git a/docs/src/assets/favicon.ico b/docs/src/public/favicon.ico similarity index 100% rename from docs/src/assets/favicon.ico rename to docs/src/public/favicon.ico diff --git a/docs/src/public/logo.png b/docs/src/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..aa8ce3e73d42fe732de854a4a5066d9b7b6cbdd9 GIT binary patch literal 20893 zcmZU5WmHt(7xp9!J(M&IjevCLP!iHz{zw5qT1sk$kdl^0QjzXPVdxf+lr9NL>E`wS zuJwL;Kb&=A?X~tk&)NI#z3+MMjndXsCcve}1poj9sw#@Q000p9zX8MgcgJ@he*f=) z{SK{BHoCe4a@H01yCGMLB)n`GfDfX|_gQyjKgNKktQ4iQq4zOo-mtw2|Ox z-5dH~N#XEane?*YtLUqwgYe&ep$`p%mU8f?t8$dFu)srWeetdJqx}s3wB%B8o0?eo zHz=}cEzR_ox;h*6{kmVvvC7@;i!J{!5;@>GGen-~?z-6QzHzv+eA~osZTHc@&?Lgg z;5W&?6Ck>Fh_eiZGSA&Ko`R-F=Wt%q;_l2b? z$Ofp6D(xVmG}EqcxLQpU8LA3Cj!`3D1L8i;TU=m_(FKL*1OyOt6Wn&dlse6^wx5rgp5?rq>o*dqb28N`MlI#v3Br$dWy zmD?CHo7d=OnD21DD=T5d9SQ;oGTjl~?J}j`Xun;AK|K*1g>B%!Hs8luxR&oHm*;@1 zvAhRz6P%Q%Yd)(x{?L`tItYlX^JRoHpJC3!hUeaILQVwV_pZl=Z64$VZn|L}A8$BH z+?h$z34sk-v-uXAK@Z%Rn{>G!4Ce=8s44rIx%#rJs6lS8eGm^k=X>#}nv=8K3g=Ww zN2#(fSU(hC5$r;QVMFNw#zI-nhLpX>RNo|*;d~W8$KnpH;eP_~J*^Q2^!o$?*dCcS z+i4a8Q#;S*NSA14a*W&E^A}BeNzwz>9=B3nSeAo~8#CKg&7RL|?GvGr%iq<#z#Kk$ zYWW|ZWgWL#xqE4n6(~$kMR4Gb z))S2JtD=pG(GOR4+u1GU)B|;pmQ)X-xfSp3NtfHd^S^^SR@#4O(hDx-vIs&^?mP3J zf9MhX22(eKhbhj2E<1v~3m5&M5H>KN{XAPH-7vjjCHLn^=jacriZm!5;sq@sFgC`L zTl!!szR{Zi;Ch>-#4&GMbPa=mkPZ-_)!Nmj!@98ou=@o1edYezzH#WKu^2j(_~D3$ z?soP_${tVu_TDzz1cU@O|J1f`!6{9YqQnl~6((9e8L=;Fpk56_F%efJSTOe*BrHUD z_9vxgS*zo_9`6hP`9IV~T*yY8ru8)0uJ)nYu>zWG zF1H+rnarJGSfaiMHD~h0@dTdMtvIYbol9lF;KjAl_$gakf_zZ(C({lgg@^Pqv z^s(Q`-fe^j-*-+31LA?k^s19tS*hfPml>tAgRSUh9V+BIL0Iphil zNR0_TW=HxDx4Bn!q*+>bzoJ}P%aI$-QM~LJ0b!*-2MxC=iob_0@c;dm#+m9dj ziQ@0~v>qJrR`mc|a@n(6Q{~f3%uO2*4l$Naqpv2z2tTIN$Bpm2v;;61SG%DW3unV` zFg)8I#zIo9dxs?4JlUfL{KKz>{YFdbQgICr7xtk&XWsYXrykMc8df?z#LYJSn>B=s z0isP)k=6i_x0(!5q;hl16)eqIj0L_)iwRX%xuzxn>Nz48)@JX>P^0}-QWIQ0UO=>W z*W=2A_yueH%tk+VfCIQo!9LLph**Wi0#euh)uoeVxl|VJxMaFdu&ZnfxPO@VivPh9-=oz~$OM!)h+BQo?yT5Qy-{ zj>n+OlE)3AMr1Rom$~#o#8=20(L>}Fq~$XROCK~e+oH$Lv!tLAj3K< zh0=?+@U!0g%cVujg>ah-d!*=SI-VK}b|wF?g6X?0k=upH9e%7PfP!+BE`^!q6J0fW z^41{Fi9Zg%td9{5>&mI4^)T#P&Rrk|5IMG6mY^_J0F=eUe+QSOn(wHe+DCh=j(>Go zA0}sV9BwM2^B{^2c6kZaNYX^Kovs^M`zrq^1veQyU1{xc zx~^60{=~!#b#`kOmieBwew@b_xJ&qf$=HOD3BThN(vhrl6_cBavBwKr)eY}XDaYzL z*4+`A4G|1ki6f$Hj04ZTyBi|qfe4jQ=X)9A5;JuiWLP8gs~TO5Z;{`QR_^R2Ie^|i(25k7Sp z@ldJB)B*7QeMl}`!t6V37Qui5l>l~^tLVKAgX(z2cIX;?%O@#TX7cP0)b&K)U-D3z zA8xyie79AV%W!i46@a8^UlskgUInr-0iEh-j9$D<-R)H2*dnOr^fqGa^we#7GL<5a z68Zd)YVLUj;c%VStHlFDHcu~*GizK{j-G%G-%-DP%=?SzfADC&ZqGA9TAqXj|1D9@ zZ6RE>svkYYUJuJM%>1x0$b%cckVd`v}$ZG@s#ISL^v+ zjNjDLfvmbN3Vl7EJ3KAvo00a5+p5>B6jOO?%Fzm*w=^F?qRi~(-4MpTS-tXWQ=dOn z6S~{@)$TMMGQ|k>W8jDa%6jf{PBu@h^g866J?FNs7X-ov1EdcwbLXe&G!*XMK)xYU zNmUeKwersdA>tb{wguo4{Z}p~8vW0fJ`6WJdH(h`P*iPG;`3tSN3L-JbumNgupMf< zm(+a0`|K4RE zGYPFE3hh1>`kPg6P%Ajb7c8|FOl0s88zI(1MPKxcqo%BEK*^NNb1><5_&+$dE{{Se zkeUwS8KbkA2~5rAz_CZ?r&%p<+o3a+FC<54*Rp>a=zDcHcI5yy>bM>qC`|L+Lt|#~ zGhU>#tD+i1us)g)YP+c>?|4PO2yjf>6Nv4@nd>Z!Jsjd0TSKXIhH;G#P^fzhdI(!} zb!Y_z#~cjq0Jot&MxvXq90vLlUUEm?HtL6f6WB93X&pTe=282$@#E=pNxvQC!-9|Q zA9O|*cCcPt6@0|Gch8%CAC99JB|__{NPlRdyr{Sn>`xpP1JDHfzFQbRmtDEnX26^x z#nS2{ySU|*o_Rj{`5Zp4WlB0RAGS3Hinp)U{O|E8@id?lt%dj*6wF{%w31^A)PR`U zmBxSj`z@-204$fqhR@tzfVCrc# z_10JYzp~+ZIwJR~TSCD@ck~3}ASrSLVpa)oZ+~~wKCOTQ`k(JsK6&_E6FkR(;8R?1 zw{vqC8i{oY5>~|gF0KIz$1}YMYKM?#W9uEg{u)%@VwC27+m}`aXx0nLHAGaGB|5dN zY*@-!5&+(TpTHJt37R@*^8#l@Y=_qE?IwCjOI*ZDRBmmo;(%)B}pWkLir_Uo%UTF6v| z`~I1*=K&Z`1fSkUOF7Hsm2@BS`5=H;6%onr<@4uJNU4*oy@Tx>?>((|8{e`Q`Ymc8 zMFe&uM05&=X6z4_VXjNnL*~P@Vq^5MbT4YmAHkr&mGBH8I`HqU4*`ViYn6xz*!d+$ zTw71r7^LQ)_EhB<>cqjTEt}J^z9hRuvE_H`Rb_4+-hUFH42IbPgq(2Pu!`jdA!Y7M zKW`y`B;NmPt6A2c9aP+szA zgJ&agEZCV8qfw}ucm-;UGACh^#&1q@T2V{wx5IHb2!8A_|LNWgX{&*Bsi1HJw!+%@ zK9UyacAQ0<1+})Xq7;-d**cDRcCV@qK7(d5p!~pQ0JvwB6j;aU-`5*H58M zpwBUG<^KXP5~POXEAb+}_WF9Uyv65qZ=fB-c%;Igj^dX$aW5Fw&a9>sA@{q_<6T$>Tl@m7S#!^QbahYtBR4Q* z`Ow72_OH#Wc1p`DIkGH``}BLUgSY}czL@s%J;#IxK2F}TtW%(TAedg>LD>L5=nd&2u>tiiW4rSZnbhT@Fax~SV z#8p305@HP{gHy^~`JK^3dEh8Tsmh82aAaS2Q%9SV;yf zbvAIl#~h&PbDKXB`^G#(jfivl5NHrJtB{a~^O3MmzHE&2<4d zx?c)wuz}`dv9%H%;7GX9$TO|+s+t53_Wp_Rd=?Tc8C{Q!nyWpO=Hd12u*T$gaqQ2qhh=9KvG+waZ|CrL`(Sj(}gf(v>8kxhMd8d z_qJlzye+cv*}8HG#lYXEO6Lp$@HyUt+?&-RoUTskU+QqCx>#kw7_PZ;31ZG~TZ58q zG1vhV=hPby8nNaisRlC6S;{eaWIUEba+WJ$!79ysM(X;ExzO}X0+c#d{`q+?e{2kK z98dD=?&EUwgWSFP!feOSv$(}JNS+}eB|YnBlhjFyaY5>CFwe+PtzEh1qgQ!wt|{Y;Oo@3Ykvt}i)|Zs`@MP$*he@IC@4Ct{l5 zhS!y4eg;vw?xD8~>o5MZi6|A2hS?ZxJg^_pGQi(-?0&&pVE?Zb%hIHk8x$!pY9`hl zdi4bUr2rxXlH0-YnK((t15JWjom+{Z*+{MeJ_xAfuaceRRkj5FTm1vVk*C{FdI*#c zgHL{Z_aO+nO?7%A_YIBPFO4Y`6FDzDi1bU?Nqn~RB|fVrTDK!!{RgKZ2J#OzFhGcZ zO3G9)#(WID`W))Gl7r7Mo;V_Y^71_aWzQV|DR#cu-$tXg{P$b0G!T{i*n-`~#wnAz z|6xC{-7PUhyV!MlGp4fKYYIockWs}ulN$@niIs<=KN9LNN`8Ci&%<R3M@a*gG_I|>-&=lD|?T8Mz^+RDZpU=nYF@z1uoK20j(o0tAvEsjS=fAT=M zf8qr3gTbZo4GJ}=ERW5&Lw!zm(Onx4)SS+&C=YAobo;=h{(sx}vhW`~BYf{^C95p& zB?{YXTA27(=d@B{=a2LPyR+`;t|-xEkIe#r)kL>`;N8|>yi9Om!*R!LRowl>7KJO& z7tW#^;%TK<@<=`MqMP|Grwk*6hLTIE9GBAoAOoI=C4C*4B`zckzHRS1os#NUcO-jYEncxBbG>xn4P^ zF;pPe^Xlkmr-b@23o>EL&$SY?1o=y_<9u%-I}(Ugu-`0oZiowLQ{H06TuD)car+6S zrFMvz?a6@rYt!WKb%U=B3KIu$(wLY2%yO{?z6Rr?)+f^Clu}M>M#YOUABpb3c#fuG zEk9RO6TiDW8JR;5#8qH>AYP-i8P(EJ>q9TTok0bWUSg}Mc=?(};puqibh=ZoA z!L-2(f1GBkTw0p@5qQ?I+;u~9ZJ6R|>Se!%OZtU=p@%9XdoCVffG|r+yyUAcfV<`&I$K0E&T1Xg@>#z7o3PS-iws!t?xBMy5S_uocf;|S{t4~{-(k?orG1a69W?*oN=_tGwvGi0n)S9!4xMY|XDjF=IKbn$zH@8A_Kc9fO2!Wc zK;8%;SSzpyX#;vqDVgw)N5k@Bv8g}38^}EfuTgUXe5NnouY4Y>8o(im%Jx&<>?9&@ z+=2vfshTpWnX-m3SWb2Apny5&1*>KReSwzVhRR z={4P-irJ&2;(!#jy%C=~(RXT!8&43#)D8;xSQDWpgv>uQ+E%y2aw8n>bOyU%_>>_q z!6y)@y2XsaL-CBHH5BaasXnq{BP zB5+8sF{B^r=3cw3zsmI~yN*Z<_>kBgwx0%h;hh5|b)ogy-SbrWQ^WA$QbnQi4^8|h zz%b=^dG=~Uk`Vh^Eb?pAgvWtff9*+rbqGw2RRDT^PHD{36@O5t?)DQ@rR|rk0G-(^ zWF|?0(ydGj7{tcy$LKYawcz8-(ujyW)>Q%%wCXd7%Pd_A!gKo>?{FGB!jE5crse86 zF<;yTbVI}DSH2x)Ty|kYu}vBfWxp#@hJ(oDOyP_cm^U!-#{4Fo)Srm}c~CtBM-15y zjMY41-C`->Si(2%SkAb@HTZOdA;dEFaxiV!(VSx~XEoW~F;$53s;Tj3NuXHgNmqD& z3H>pSOkSfhn(j}Y?xDYa$fFxi$Y}@p-B@9s9rD*vnZ^U=NJJW!Qdktxos=5Nn5PRD z3Bd>JE~$spStulNOi2>Hjdma6tR9jkl^2>w;igo73LnwPKDKY@$h{ddK>>6JC7%KY zem^!52aFM*rY?@3g&YT(V(3Hx`3+_2#p8$z5PnUod#lxf`6ZH2yA>cEMFYmhT^YnC zYkZ>mX=Kv`bYt_TnfH1jVe~m|WD!Gw5|c3*PRrp?b!G+tpR<-0{VTpfTMrXn3rKV` zm2$#kuE%9M$er8z3)M4C6cbhL%AFl*ck{q>?`vMYGUl#PJ#WVmsZ;x>+-F@s~0Y|AFotYRX(xzR>`20?0G z-`>c5!9!t{+Fa)6-Mzz*!#gPkw*Un?9JAzc^K_O1mB3I9bh1xqf+p)s4r6 zYKVq}99KFH31wsb3*{A{rQ(`a#+^u1kr%&q=CMuUP3DuJXx;aE3->n7vt)Xknr_-$ zdB#bRo3}&qU{3&xODnOT?WZ>!)&jVDJ3YlvMJr{*g&K;WxcQoM{--K&$bQz3MvQ8+ zFx$tkXZD8hb5;5Slh=%%hX~HrW-AJZE;Zkzr@h zTg4nnhl8g-KzoCIN@Mi%5@L3}BxM3XJC(%ci$fiVHM3JsHvfiVBIi4B0J>~jg~bH_ zj|Fy$Xj5(DA_%F! z%p*!tZmd1eJWW}PCY809yz6}g&%P0K6#fjhJN8kU@NISFp`d%QKxu;^wjYT1y7uK8LNu+z}$1~`l`!kLR2AOwy%r6Qi%ft(p z`2eYBKTVj+IlY!9l$#R-2_1K=8tK}C0sIE}a$(h}9PfQ0zdCek2TTm%g& zEMHzRzl&o{CE0t(K|+U0Y+Oc0kjV)^|MaOsm<&AKE#LmB_PCaI<{wxsw(?Vi^@=O| z%-ZO&aV=Qmw{xuL z&2VPrP0MJ$FgE-s+RohuE}}w66m@73b(qyZ|I$#&T+mwk`ZA7n{UzXqKR~4!ht>z1 zI+Y#6c*=_k|9-Z7vd zRx0}CjkQ?UNR#=hCBN6m?+IsRvx*a_L(w~(E&A=Dhs9G&MBM-4>T5~Ut~F#V}S+xP3P3Gzm`SHre+{>=XX)kNtE?Uda$JAftd&Y zZN~aX6sP|F7B3bv{rPzb&a^nk_UZ6I9yjgJBmEmkqe13Yw^yV_2xgr8@HG_^iWE4R z%FC?T`oG)xItev z88n8Bk)Nj97&cB;tgC&~>M)X-?e};zs(8dI4Mod5uugZz{XxD48r}#Vc*f-(xnW1c zz|X>>InCv<)5hIb>K-(moP*npI)c6E@|+2MNqWh7tSV(>OZoC}&8G{0I+GM0D&*A< z7oMHc(TULJ)uM=7U*M)(Si;hZL+MI8vVPtJu|7|hH@NX9UOHTyG7w9Vco)~&wPgGq z*o-stu_;@T%^PYz1Tn7DI$oBzSjt;HM5?bGM|@FpWNjF2e9O?iQ*&PfNvFQ|RV~{$ zyzRFO#t7GIkdZ9F9yG=XG8xv|J3VE$8D!+rRS2WEX>UYdIOxpH7K>DU&!^f+aMXTu1)9>T=wX13{NY7-_KOGMF6B%azEWOcL#CFdNI>%Lpa3FRq zQ~m0Mcui>+v-dXwt(1I}JGTBJ3}>Q}r8#zt7x^lS$_#&gz!;1Nc*Z|TLOj+FVWiz- zFa~d$;2H>!SieX0^h54+ESh23msdkeS#8(kRgdHS?=Cw|p8&)T6T`}-|GIhS4Td7p z9>*FJql1Gv))vex7}~uo8(LrecG93Bl5@H=l6wzdDts#Q$G7gcq69xJ?<#PD-V6$( zid9!}SBvcYcoE+nYGrbK7%oeA-V!Z?KMHyycjYWNDFLRzpZ9&3Ug;RWFb&xD=QU$l z=K;UOisR>#UDcF$hrkVEN|QS!-Jhlt%cm)J+Z6poPa(L_XbY)tUO`RmEH)p9(;H5D-iH?{v(#Mi##*n z1AGsg$LtjRHZEhP)#%aEhPQG!bfv``oe{^c7oU;(1^J@sE67YXHw7N>n_e}I*}rPK z@sv4K&i;3g+1Q)I^P`(zDQ36PW#oKY`7k=e#+Kp{)nY8_NGk162_2OgmF7g7>TQk8 zry^tD2Nkx2!7J~~diVdkq#u=8pV((O$O?*oZNqjMdiADuDR%ChZ{T}TM}#CBI~}Eo z0G)GLQ|<#%d{C_{IpTQ1LZ*#q{AuoInm$s5*;W?bb@*os+Y9jZ-J=~qtUoX4vXsFk zcG&dhk3!Y1fSxP_?Eg~nXFU1aL`oJk`cMA>`cL_{-O;kW4clZ{uaib7hbeSJjB;lx zL!J=w@r7{+lYaCw1R`6s7$JdM1I6288*q`!U;T40NdnXD$|b&IL-OVRvSQt$!nMTB zF(Lq<{#CdxKuG7Ye$D+^@Kss~5G^~Eilk*ze{~mbYc+ru)&l+8c(*6AoU48o6>zPb zUNZarT|gH@C9kaSJ-?l)fx7bN7OgJoW5^0Bv<|=%^7_0ds_ka3-|J}M=HE`}ypH29 zA+eg|xVLL$Pkknt0U*i9Dcp|n<}O?nFp6vjLTOu$j?O}N0l?i&02a!Gdbna$H`_4M z6x@wLKyj^eC&w0T;FO+ymOCJ!+Xu?d$4mA*505Ot+p@5&clqSABm8>`Y&+Hb)^)-R}z@%)$5fWBd-kHQ0arH+OUzbo%mM1F+=39B^9`?Mtqx z{AUrdBnGK1ets;A1!(pt=|FMJyHlq^(O+koX%qbCl`W`ITdHZo5x+v#!Uj2$O*jU{ zPli(VgtiE|_^$`lb*x2Y8Si95M2koA$VT-zYLzuZgskW|7MVcFs zz)v1+RpJ4IwD7+p-6MSSxE_ClpDk~*9GH2P9FDwBu##Epe?k;>oTXv36$us_``~i< zhv7d=Sov7M?;a9CXFj9TpRD69og#u2-&%ZTCD`1S?%xw}a~hm?ifPVovmapTgT0gA zH@lD;s>+XNJS_C=2s+#!(Fa^s2B%fEe-1A1{w6;%tih6ZG!`TO#wF;?9MY+Z3Ow;h z&q6`al#YRa@6PIiypG>Y&z+O z{;L`|qp|{0@MhiR`3gO>s!4N(h|5oL#CGQ##O8kM^1IJi`3J%8M3;HhYU-!sGXST@Ig#t~3X~w^Y~`#W ztKTj$aHIF@yPj`qm;hXSltOc@FWgTf>}i31^DgI!ILt5LZO2ZB3f@w6wT zdiKmsh#wOSMfVDbahVH0tk$u%8*B=<$e9RPj|I>_7VFN(1IfI%q{kW;4>3`$sLI1WyxbMJ>2(h`$eFgM1vWC^6<&#kUwd!RM2fj%*c++BXng&N*!?A zDXDp$=Rl5o$`(|m|C>6FLzHT+=c3a$lB{C!B+=(63hJ@##b9PT&Y517LBj`YhCRP@ zw(#3-e|TaMs2ZH#{q6axQ&P}%b^q`xUS>JSsAn^R7R)Dhf7{V&FM(YB#~t22`(=mU z2u8hZfj$Ew0V4#DyU-OgN$#%42g%!)9DhP^5G%-+81|P8AH$-2H{Zi}v4n{8>LrEa(cOWOm?4S!?S$;ai zaq#_V?$y72J>BYXGfVqr;l4Z1r6Bs&KhPCvZ_2R83&F1x86{)Uami6y{CQY(W!J3! z7Hyz)@x2~~rZEw+o1>P~PlC|A|IMzhJAh2p3ByA4QU;+<5P;rKj>LCGU{=ocxvd%0NODx{EB)}}Xcfl^=aHlhBYm`wba zQi$^}eWML2wmnY&F*S)3?Adcgmn#rc3s>|| zV;5)Y?AZ)Z(c0({cUXPRZetIn9|SVtAaz>2e=YwgkqL!h@d#8))O7_Pm3(=Sz9;BC z%liqitYz_Owqa)|tClt$hd_%P7Fpl&jgN|W7l#6gA7pRnCg9NG$*h|5oTi*l@Am#o zuZTBvh6z3wJeo_jeqE7M4-0wEvo(XCmCgZ>)VI<$(Ci+ziR|N656Ti$5_$rsbdm+K z2!K5^Wp3Y<7SWeHS#5DdhFF^hPsFqfd#+&~zG^<>o8F z0tTk0Z{9sfj=qjvbG&ab6V(cnbPe2l$@O{M|54g?A$`WF1R2P5221xLkj{R^cV-v= zGfAoJE#fe;=L1?(uCn|##v#+gL+$63F{u1rbAoVWW;E-<^jA-g&}kJ(x1_CE6&b)` z*k-F7rI#zyP=GYUeY;qHRXa-)MmK3o)}=KatBP&zw}>J1`Ey}eFq-k};z-E81eiqgQBRCsm%wQ_AeO$Vb1Sx zt;CHhTph6nT!$ou4*~4YfIXAGAihW!5xIw7aZxZQf;NDeeJEP<|+|j6$;=NqpofDZsN6U5W z<%O%b`%%}F30Sc#-^9hAjl}9J)4qO+>w}T7yXm6^)Hd%{?^nl-bXqB)cj*w#+O7om zZ6MLt0TLC5SFa1Yk3KVJ`4cif7WC!nd`3zMRM_;*+%Au==$ktaa}1(z(wg;}7>o+s zpY|UNVi({%U4Y5G@&jP4t(1y)0-LjLNhHo=ot70^p%MnETH3{$PYWutv;t~HHD=Fm zB9=YTQ9oKPHsbE%xkF1GBeC?Oc93@zF3FY{rX)+C+>xj-B2WAG=$*L>Qs6+;*JTUr zdk4t;87|5f`T{*ew`wrACT^dD{WA9ZY`NKL2hZhWlOxZ&s<&L;#V7yB{0)STT~6A> zSo)(xk%z&vQCiN6cki_{HB@wnD_%IEvbe!&##pL+17^6AnW#<6=UuF&+>{)0Byl#e zM368;hE&>*qK{8Yes5H^8GyL|baXtFVcU~Jn&*$-$74F|z(Ckm6Q3daH(q4D2z%M+ zGi;>04EpD+iTkKH=3_56)W9H+ur=E3#XLNV|m zWKx489S)mt;+?n7a!#+59`fHT0a+`Z!O&@{A9$0OKG6CTNwzM&mAjCn`Le}$b1p@xtx}{jAnRJJ%y%5xdjT}+ZjPuhfubds1BO$n3^|p*nABOk z+w?rUK|uB&o*- ziv6?3?Cp!g>Vd;OVf-&~91Q=LgM2%BnY~GGpMy!Q`kp8>t33TMak0qncrH|j$a`6N z3ZZS-E#5Z+=UjZfYR!^o(%MdrIrKef4?X@^1M`mDr>ruK$;|do|JGJ=OF%YZSjwCK zMi>h+suK0hq{!I=e?K6EaYjDD-pwi+i4sYUlFrt;8w`~{b!xL z$N-RGdcWu8`rCHl=5_C2xZnOuB!R21^Z^q!SeV4Ww>>x)5kk@(CbPJ0pA*MeJ^djZ zxdbJ`hihrn)=@lV-4BxR0KqR7Nhq@<-F~0Qz64a$HN)no!d_6TeFoo#eCVmxdK6~W zuu`QI7=1eiNMl9@DWW*6ei+I9K&1z$KV=%1&0+8={`M zpEW1WHMU~R=amx0obYX2Au^6|7AA+=YuVd@@Dd>K*~ngdLS`i>bx4)v!!#DG`f<_;wrqj&uh<0liV*%EDnFd_!52*^c*u z*_E<)+Rd2#Pz+RXpTzn-4m85_j50Q zH&I`qQl7WKP}>SrmX$Vx!ftDbc;$3T6iNCG;ibv)hB4BzobP^nY+=rJ{CfOR%MOYK zqIcxC+ZI-O-F?Fg0Pu~8z*L&liU^6q{Dmwjo>Dp(jbvspUB5yaWK*&TwbuhasB-VE znHXc)EjD@Mwd$$h^WfXcf7B$RH|FY(-y@3TE9S^zEhf2Lz#l2?V*TVMRBYR2;B3%h z``PhUN%o;XwxkXsR9Y!8dH*z4YyCO>3_`yl-3Wu@W8L=*4P`ANPyW=(E%n4u)#v#7!KM(9btiJExeta7%7yntP{Zv#iBcy;3QevzpT$_ZS%wxx&2gPBMdKGw_G!>mx0Cm0^WbNEq3tn{aX9OG-zmfx?1%aQA z#wMH`R0Jz=y3yZe&i$Jnx3G^avP|LQA+1S2uPWOS$y}JJcWv?l&c1d_XqvI*Iu13z z1_1k9U{SHI+ni@S+?Eap;b-oP%7cr}P&%+><20{1TcmUH(d$qD%!`f*6>~UFbQSvv zpzZqNaMPx5(7Z{V^`XIixE+Og^q!6v$tcUx-EXJqsl2`qM z1qEtf3G`~&(Y_UuF^D|m%ia`C3bw50(r~*+O~k%N*HKsaxv=olUIEpChJhqenF8zd z7gIHsy2&r6vQqcnou(N!0v0itM&h4ntk6#^{OLaAVi`u`<|dSPpBS3RtE&(I!2e3* zJ^l@I&XKfohREdL%+cTr(`c&k^uZ?^_G4h?b!8U_kKeS-gPhMTqf){Y5NN^LgXhfwsrFv0GY#GT;7 z%ky7qT5jVVbirm~^0yu za2|B4Op$*T#jSb$;O6yc6|vCTmT>E=pTMw6j6Swm{)y{$3&fqeUC3$|@VXm$W+Bz$ zHB6yJPoqD5TJJq04%QsdU{2BBNUZ*ow84#>ovg-6k4P_G_?IMCYG3w?po>Ru4lgZQ zd(_KHU8JnS#kgTZc)=$PG5Dt6|GvsS7%QN0yMLK$Zdvqf%W=8UzAn~~AR$^N&O7A& z-qxENXtQ&*L3mMW=x4u9Cc_`ry74&vbw6rj*!D27W2F|7rzw&a!qd-&WPO)@(W4Bk zbB=a)6)VT&t#7pLW)QunFkX`8;B}g&eSf`0FcetVe*QhpC!?eaIcz0_Y^Y-UHlSWc z4X|spTWzh&{Ud1f^_vXA%@|2}mw*RI%Vl0q{>sWu64rSUfG;l31;C2L?d1eQHe*k^ zJn5TH4fI~loul`D{NuSZT85`@D*y%vm<_B~RRHkq$eNQ&i{M%GViPG4WG8*~$tjJj zxF5sMN2lW+j3T3y`m9|cXmkHQEzqS1@BM%N7wu3iXZ}g4m?B2nW;NIq8KUYsHYG0| zo`mO(g@(Ut8ciWwpazv(pHlyo;%~KR`KSHSEbyP%c#t!UG2?M!(a|VGf5_iC3**#d z?7ug=+~Sq6VpD)bR#pYwPU8Vm(_cr);i0!yY{zHL`+v}t{#~yUzulD){B&}|?GcVH zB_U5eeqf(CV8Q|ufN=0oVvSgvjpO^SB2foH+wB*Bo$RB_t}$Nr6gXnfM_xdsU~aE@V%Dw*3|CA*tgry4tRz^v``6C>3EePir| zuJ|<AP&UwEnisgsPi zRR}O|I4`0b&GjC80H=k5RmB(^zTlaUu`M2VDVzvfS+?HyTEk(&p}^S*KDDW$jPXCd zX?mEgoOw`o&Zd`%dWu@31>V=?wz`+0Fx;;nu?-E=OUCm2h&3h0)R%&r54$PYhyi2{ zpIS1F9@4#>f4&f87<`Bs_~z1u_N(Qs`$R|I6GHFg>)T~F?=}pvTag%d>1xCZmF6Gy zcfDO_Lf`g?#i<L^pyL(39#gm4%rjplEVxpA#;m`0XwmioI!>-#*bV5 z|M4e14>}(L<3VW4jL%`l7lLPEfUiObSea!OYI!O5n^hqS^A?26&Ctwv2*m;L#Z!L#sZJc8QCU`T}nK33_?={GRyX_Q{_Mr}{4S zQw1PukgM&&t?+vY06;AIzyAV6SIEOv6ba1zYWR-8GqOsQGf2iTv8R@!JRzhrOQCY< zQ4alI{N!Z~kz!iN%j8SiTq;C*i}me1sQCZ1a_0X~ckds6&&G^x>_%C}PL?DJg|Q3C za+jS+vL<7XEHjo6Zi8-X*~wkm?j($uag%*5vX$M)lBEo?eaHRz7rsB9a~_Y^b|DWOBvg`ubmK5G?izdM~yky$Ax)uh93`V5uQz=Qp{bI^qOU%+&Sv~L{l2~M3Tt*pz&_Z^eY-aN2P8HptWRgZzh#$7#L|{# z*)^2?tY#5ROipD;V0|fV^1Y;WN-W{_kj5n9q>pgU#jAdH`+G0 z11f5TMsm~`>*@d<(V9?c+4O$hrT(=GaE&TWPZ1j*CYox@-AqEDd@{p3@PoR>2`}Nm zPKb}-I7iTY6k3&Pkc5Y3QD~$^IVdB1`5J`<@_GZrsSu;-TyAWw>jERwYmC@%5&I^a zHZJ9zxm3>ONH<#`0a@kUT zY|@J^vyHcf{Jr&}*A&I;lzdk%i>TbElfab~z2%Z_LJnxD`~BW(YSwL4H`d|pdg8;5 z;+7U3ppYQ!;V#p)AWgF;ysQf}t|${0%>B-u36p3adJtmu!ewIs)W00hx?4FQGeGYD zO559$hqQ|2Fi&+abZvkD8grCMafDB#ldYoK))z`?UiMrvE(##dqUL?ZFdc!7fo%NJ!;zT^`^;@{k z7Z8v!6@N2phI}7e6C7mMz(PFc`notrE?(hfSG6&j^>`R5a_(K&+vOPzV81jEY$s^F z11#4w&zz4OEPdU^Z5~kbx#8W>#HK+kSqZ(<_?P)m_r23k52H;-M8u2XnykoP7Wg-e zr|xKU3{=4d>2Q9}6NpVyjfJ9DK55yh5+HWMM{WJxVUKy+pH1H5RO|$mQqk>GpA|(F z$A!n5eHV)NHy5NORDJky(TrOH$s!K;j6%0>=zhW2W;_s!HlE!O#xdS}uFaJ?B>9RM zCU4QZ)$BXH-IL;iSZ|dj_91ZRG*!OIRodQWPAaTqCQdR&`_Rf|%K)I|ep=tA;d)*k z=ltiOr-BJvJjPAHqCTJHxdMkvsa;)j3F5yRA%}@_j&Q+5OqnC2KfD)E1M)3B5l4`fzP{dxBV0B?8%jKY)2uUi&Z`+^{3k&f{d1*1*K)rp{|?6+9Ve-)3`n+e~o7uHXIxCKWyyF zS4Qn;YU>FxMRTyr^b2{e(kx(5Hn%v4>1|0VvdYSIj3QbpJ@Spjyd0+<#PYHa+26G+ zxib_)OkkLZ5!Dy2h?CF-CaE%-LgHUcw)HMd$XBZv|VPt#tT{Y#$(_yUIRWuTi znb8`A5$`Yk#LU|Brt2*-^pXSQmF5P-iR+Bg&fYUL*T0uVI80pgx}M=b zUb4X4V4xI!n?`wPCmJh+f{^4fGy zB*W~%-nR2X`{RCZ$XCBE0Yi=#1#W7&ag&7IE8i(w;SpcFQm5^A?AfH$T(~7vkYS zUsiwUVIo8VPpW})DtfHdy<2rVi7>|aG*MvJensrA-sv#kw`BUaVBL~CQ`vaEck zHUDS_5haibDKzz9m* zq=wpjW(^bw?12w`cXN6;V=|@2PhReQctoe>K;NWJ@8>i3t!&n_cjcKBQjz=eq~i%F zM;GogB3r?i=Bv~ZX@I?;H{QPWp5vF_qsV@p^S)NBypXGU?4nML{E9`((uPbZe~0K= zJIm6NW#+KAXsydCA+zO!;T|QP$<{60s1~l1wy02or?_Xf0|$iziONCiBiFfT`l#6 zMA(yM^Mz2;&}+3h!Hj)jy)S>*SnisS91MGbj=;~G?Qx+B7(KA)@Qnn7Utbg-TJg5l zMg66lB_!naXOz;k!5*Ql>!`2JyUwasR*CKp(K0~ze2VYPXI@IMQcCvgFpN7kKhFp9 zLpk@P-rIr~AVLB0r0iJa#gT#lfckCdD$&ncuis~FQ>9~k3*2m8vx!%By^j?CV?pq%7=@ePBadhRc( zUi2q@;Dh0<57-E|YVkqZ3oy{Iolhc#-I*JnyMc)`_Ok~ti<@HI5b}UY@?HuE{; z$XgBm~lPY9iDQ+-7T+12^W)=_N9K<4&VDojSphJlkjf{34#xvqeub? zQ_)xLLGgS*Fp`B57K{H@?Mn(z13p68rhu?hs*km=z^m8f_+M3_{f-4rF6Flwa`7eXf!2vwGZlnashJGj5L}k#wO#!+S60z zBY(8EWJ#|9tMb-7iL0W+MoL1Rh50{eB}=m)VfAw$B8bp%%>oqDJd--w`H1+ppm2WxMiP)XUHzxDR=L*Jvz@B`%fteM z9sF`z_#n(WhLUEU;oJfp77dHHVB+4K|)UT_>d+`4)QRA&3%FBuMj03D|@8M{Q< z;rpI4X>^V6>%*emIQ3z8=P?WAiUJ0N*RZ3$+v01bb3iDSr@O+fqxa2)lkbatz9~VW zyU5k?bDQ(g2^gI5_GE#v_0uPb-k+8YNz66WH5RY=>iuIndCT{BiLr%5F604Tx-g^J zk3QC8GnjxtBK0R6@3ei$3^(ogorDaJ;vipud;$6`@a){>j6jVuwXrg;^-Q7M_xOOZ zW#)c4J$tWw3f3?^wnS1tO`2SHmJhK65&m|LqMZo8`5dt35!vK`_ZyJ+IC7F0eUB%v zS87F^BKc4@uut3HrJ?Ex6bT38aAtVlNz+b*mL0f;Y3QF^+n-U^k*7WP-aFnt8xzRi zeLfZC|Aqk~pwvO!$L|-P5;cZkei@ybz)TBDnBO zpDgJ6@Hhm}FZir|r*{^S2Xri62X<%_iwk4`VP5`rdUq%CJH3yUF@^^?p487qY*f?L zFNn0#J5vBC^EtSg*(G3wb7p>zk6n7r{~%fg=(U0ZP3`Z`cA;m|M6oNDTgrGp=+)-! zv-M$Y??GcU9~k)+pV{Q4`v_xuxFq@uBtVa#3`w+fY+gj0k7?tz`BFqWYQcDgZTZM3 zI@M1S#1eLnWqD6~-{h6=S(_Wq>s5s}OWD2`nCN@vk!LCt>{f>Ux~I|b?_=52ufexv zw&m$s+yw2jW7Vr_`oA^{URzi!p4+=s_k!h4JbWD{$lg2HD;v;aJFF|Cu|Z4hH`JPzV19DeV98`xKNmp*AAM)5%EZEdn?6 L&Gf2tu+jeqO Date: Wed, 23 Oct 2024 12:04:46 +0200 Subject: [PATCH 09/80] CompatHelper: bump compat for Colors to 0.13, (keep existing compat) (#4509) Co-authored-by: CompatHelper Julia --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 92538749fb0..ac3a5b2fa53 100644 --- a/Project.toml +++ b/Project.toml @@ -70,7 +70,7 @@ CRC32c = "1.0, 1.6" ColorBrewer = "0.4" ColorSchemes = "3.5" ColorTypes = "0.8, 0.9, 0.10, 0.11, 0.12" -Colors = "0.9, 0.10, 0.11, 0.12" +Colors = "0.9, 0.10, 0.11, 0.12, 0.13" Contour = "0.5, 0.6" DelaunayTriangulation = "1.0" Distributions = "0.17, 0.18, 0.19, 0.20, 0.21, 0.22, 0.23, 0.24, 0.25" From a1c513b73f6238f6fcbfcc3d7c9d5ba41493e2a7 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel Date: Wed, 23 Oct 2024 12:17:34 +0200 Subject: [PATCH 10/80] Fix capitalization in section header --- docs/src/explanations/scenes.md | 2 +- docs/src/how-to/match-figure-size-font-sizes-and-dpi.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/explanations/scenes.md b/docs/src/explanations/scenes.md index 9a829196324..fb72cf2e75f 100644 --- a/docs/src/explanations/scenes.md +++ b/docs/src/explanations/scenes.md @@ -17,7 +17,7 @@ A Scene's subscenes (also called children) can be accessed through `scene.childr Any `Scene` with an axis also has a `camera` associated with it; this can be accessed through `camera(scene)`, and its controls through `cameracontrols(scene)`. More documentation about these is in the [Cameras](@ref) section. -`Scene`s have a configurable size. You can set the size in device-independent pixels by doing `Scene(size = (500, 500))`. (More about sizes, resolutions and units in [Figure size and resolution](@ref) or [How to match Figure size, font sizes and dpi](@ref)) +`Scene`s have a configurable size. You can set the size in device-independent pixels by doing `Scene(size = (500, 500))`. (More about sizes, resolutions and units in [Figure size and resolution](@ref) or [How to match figure size, font sizes and dpi](@ref)) Any keyword argument given to the `Scene` will be propagated to its plots; therefore, you can set the palette or the colormap in the Scene itself. diff --git a/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md b/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md index 7f9132c5848..2375a9e3f52 100644 --- a/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md +++ b/docs/src/how-to/match-figure-size-font-sizes-and-dpi.md @@ -1,4 +1,4 @@ -# How to match Figure size, font sizes and dpi +# How to match figure size, font sizes and dpi We want to create three plots for inclusion in a document. These are the requirements: From 06c986d6ddf601698634b4d164cfaeaa386bb760 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:15:17 +0200 Subject: [PATCH 11/80] CompatHelper: bump compat for ColorTypes to 0.12 for package GLMakie, (keep existing compat) (#4518) Co-authored-by: CompatHelper Julia --- GLMakie/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index cbd5fe66c22..93b3fe0470d 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -22,7 +22,7 @@ ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] -ColorTypes = "0.9, 0.10, 0.11" +ColorTypes = "0.9, 0.10, 0.11, 0.12" Colors = "0.11, 0.12" FileIO = "1.6" FixedPointNumbers = "0.7, 0.8" From 445d7aea5035734299f265f22a3e794e68c415c5 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:19:34 +0200 Subject: [PATCH 12/80] Test each backend for relocatability (#4288) * Test each backend for relocatability * different bash syntax * `@info` to see hangs * add Electron for WGLMakie * don't swallow stdout and stderr when executing app * enable color so output is easier to read * fix WGLMakie * Update relocatability.jl * revert using checkout commit and dev instead --------- Co-authored-by: Simon --- .github/workflows/relocatability.yml | 20 ++++++++++++---- relocatability.jl | 35 +++++++++++++++++++++------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/.github/workflows/relocatability.yml b/.github/workflows/relocatability.yml index d3e62f5c33f..7f333b57670 100644 --- a/.github/workflows/relocatability.yml +++ b/.github/workflows/relocatability.yml @@ -17,8 +17,8 @@ concurrency: cancel-in-progress: true jobs: - glmakie: - name: GLMakie relocatability + makie-relocatability: + name: Relocatability ${{ matrix.backend }} env: MODERNGL_DEBUGGING: "true" # turn on errors when running OpenGL tests runs-on: ${{ matrix.os }} @@ -31,6 +31,10 @@ jobs: - ubuntu-20.04 arch: - x64 + backend: + - GLMakie + - WGLMakie + - CairoMakie steps: - name: Checkout uses: actions/checkout@v4 @@ -39,7 +43,13 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev xsettingsd x11-xserver-utils + - name: Install dependencies for GPU backends + if: matrix.backend != 'CairoMakie' + run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev xsettingsd x11-xserver-utils - name: Relocatability test - run: > - DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia ./relocatability.jl + run: | + if [ "${{ matrix.backend }}" != "CairoMakie" ]; then + DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --color=yes ./relocatability.jl ${{ matrix.backend }} + else + julia --color=yes ./relocatability.jl ${{ matrix.backend }} + fi \ No newline at end of file diff --git a/relocatability.jl b/relocatability.jl index b036ef4903f..6977bdd8eff 100644 --- a/relocatability.jl +++ b/relocatability.jl @@ -1,11 +1,22 @@ - +const BACKEND = ARGS[1] +@assert BACKEND in ["CairoMakie", "GLMakie", "WGLMakie"] module_src = """ module MakieApp -using GLMakie +using $BACKEND + +if "$BACKEND" == "WGLMakie" + using Electron + function _display(fig) + disp = WGLMakie.Bonito.use_electron_display() + display(disp, WGLMakie.Bonito.App(fig)) + end +else + _display(fig) = display(fig) +end function julia_main()::Cint - screen = display(scatter(1:4)) + screen = _display(scatter(1:4)) # wait(screen) commented out to test if this blocks anything, but didn't change anything return 0 # if things finished successfully end @@ -21,14 +32,15 @@ Pkg.generate("MakieApp") Pkg.activate("MakieApp") makie_dir = @__DIR__ -commit = cd(makie_dir) do - chomp(read(`git rev-parse --verify HEAD`, String)) -end # Add packages from branch, to make it easier to move the code later (e.g. when running this locally) # Since, package dir is much easier to move then the active project (on windows at least). -paths = ["MakieCore", "Makie", "GLMakie"] -Pkg.add(map(x -> (; name=x, rev=commit), paths)) +paths = ["MakieCore", "", BACKEND] +Pkg.develop(map(x -> (; path=joinpath(makie_dir, x)), paths)) + +if BACKEND == "WGLMakie" + pkg"add Electron@5.1" +end open("MakieApp/src/MakieApp.jl", "w") do io print(io, module_src) @@ -44,14 +56,19 @@ exe = joinpath(pwd(), "executable", "bin", "MakieApp") # `run` allows to see potential informative printouts, `success` swallows those p = run(`$(exe)`) @test p.exitcode == 0 + julia_pkg_dir = joinpath(Base.DEPOT_PATH[1], "packages") @test isdir(julia_pkg_dir) mvd_julia_pkg_dir = julia_pkg_dir * ".old" -mv(julia_pkg_dir, mvd_julia_pkg_dir, force = true) +new_makie_dir = makie_dir * ".old" +mv(julia_pkg_dir, mvd_julia_pkg_dir; force=true) +mv(makie_dir, new_makie_dir; force=true) # Move package dir so that we can test relocatability (hardcoded paths to package dir being invalid now) try + @info "Running executable in relocated mode..." p2 = run(`$(exe)`) @test p2.exitcode == 0 finally mv(mvd_julia_pkg_dir, julia_pkg_dir) + mv(new_makie_dir, makie_dir) end From de0818680bde161022695abad6fad4542582044b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:01:40 +0200 Subject: [PATCH 13/80] CompatHelper: bump compat for Colors to 0.13 for package GLMakie, (keep existing compat) (#4525) Co-authored-by: CompatHelper Julia --- GLMakie/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index 93b3fe0470d..9ade73d46f1 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -23,7 +23,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] ColorTypes = "0.9, 0.10, 0.11, 0.12" -Colors = "0.11, 0.12" +Colors = "0.11, 0.12, 0.13" FileIO = "1.6" FixedPointNumbers = "0.7, 0.8" FreeTypeAbstraction = "0.10" From 9d758c96fd2f684e132b2ef7880a6cdc8bb9c13e Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Fri, 25 Oct 2024 12:01:57 +0200 Subject: [PATCH 14/80] Try temporary fix for WGLMakie CI (#4523) restrict LoggingExtras version --- WGLMakie/Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index 5d469a901af..886599cc21e 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -11,6 +11,7 @@ FreeTypeAbstraction = "663a7486-cb36-511b-a19d-713bb74d65c9" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" Hyperscript = "47d2ed2b-36de-50cf-bf87-49c2cf4b8b91" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" PNGFiles = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" @@ -27,6 +28,7 @@ FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.11" Hyperscript = "0.0.3, 0.0.4, 0.0.5" LinearAlgebra = "1.0, 1.6" +LoggingExtras = "<1.1.0" Makie = "=0.21.14" Observables = "0.5.1" PNGFiles = "0.3, 0.4" From 1f67094e0f90b40f506076cc3276f5e3d6d7b463 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:04:19 +0200 Subject: [PATCH 15/80] Allow creation of `Legend` with entries that have no legend elements (#4526) --- CHANGELOG.md | 1 + src/makielayout/blocks/legend.jl | 3 --- test/makielayout.jl | 5 +++++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2045a7854b5..41db9c09615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Allowed creation of `Legend` with entries that have no legend elements [#4526](https://github.com/MakieOrg/Makie.jl/pull/4526). - Improved CairoMakie's 2D mesh drawing performance by ~30% [#4132](https://github.com/MakieOrg/Makie.jl/pull/4132). - Allow `width` to be set per box in `boxplot` [#4447](https://github.com/MakieOrg/Makie.jl/pull/4447). - For `Textbox`es in which a fixed width is specified, the text is now scrolled diff --git a/src/makielayout/blocks/legend.jl b/src/makielayout/blocks/legend.jl index bf1a07226f3..8344667b28e 100644 --- a/src/makielayout/blocks/legend.jl +++ b/src/makielayout/blocks/legend.jl @@ -406,9 +406,6 @@ function LegendEntry(label, content, legend; kwargs...) else elems = legendelements(content, legend) end - if isempty(elems) - error("`legendelements` returned an empty list for content element of type $(typeof(content)). That could mean that neither this object nor any possible child objects had a method for `legendelements` defined that returned a non-empty result.") - end LegendEntry(elems, attrs) end diff --git a/test/makielayout.jl b/test/makielayout.jl index d62db7bd3e5..d0da12133e5 100644 --- a/test/makielayout.jl +++ b/test/makielayout.jl @@ -476,6 +476,11 @@ end @test_nowarn axislegend() end +@testset "Legend with empty element" begin + f = Figure() + @test_nowarn Legend(f[1, 1], [[]], ["No legend elements"]) +end + @testset "ReversibleScale" begin @test ReversibleScale(identity).inverse === identity @test ReversibleScale(log).inverse === exp From a12c284742215ded6d6691aa75c775caf1c379e7 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 25 Oct 2024 16:05:18 +0200 Subject: [PATCH 16/80] fix pick_sorted for WGLMakie and hover over heatmap(Resampler(data)) (#4521) * fix pick_sorted for WGLMakie and hover over heatmap(Resampler(data)) * test pick_closest & fix range handling * test pick_sorted --------- Co-authored-by: ffreyer --- GLMakie/src/picking.jl | 2 +- .../src/tests/generic_components.jl | 558 +++++++++++------- WGLMakie/src/picking.jl | 8 +- WGLMakie/src/wglmakie.bundled.js | 2 +- WGLMakie/src/wglmakie.js | 2 +- src/interaction/inspector.jl | 4 +- src/interaction/interactive_api.jl | 2 +- 7 files changed, 355 insertions(+), 223 deletions(-) diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index 47270f9df8e..cffc12dd73b 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -84,7 +84,7 @@ function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) sids = zeros(SelectionID{UInt32}, dx, dy) glReadPixels(x0, y0, dx, dy, buff.format, buff.pixeltype, sids) - min_dist = floatmax(Float32) + min_dist = ppu * ppu * range * range id = SelectionID{Int}(0, 0) x, y = xy .* ppu .+ 1 .- Vec2f(x0, y0) for i in 1:dx, j in 1:dy diff --git a/ReferenceTests/src/tests/generic_components.jl b/ReferenceTests/src/tests/generic_components.jl index 1e55b31af39..d7aa43a6f29 100644 --- a/ReferenceTests/src/tests/generic_components.jl +++ b/ReferenceTests/src/tests/generic_components.jl @@ -26,6 +26,9 @@ s2 = surface!(scene, 210..180, 80..110, rand(2, 2)) hm2 = heatmap!(scene, [210, 180], [140, 170], [1 2; 3 4]) + # for ranged picks + m2 = mesh!(scene, Rect2f(190, 330, 10, 10)) + scene # for easy reviewing of the plot # render one frame to generate picking texture @@ -48,239 +51,368 @@ end # raw picking tests + @testset "pick(scene, point)" begin + @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 "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 "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) - @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 + # 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 "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) - @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 + @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 "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) + @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 - - # 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) + @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, 156) for y in (246, 304)], + [(x, y) for x in (66, 154) for y in (244, 306)] + ) + @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 - # 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) + @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 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) + + @testset "ranged pick/pick_sorted" begin + @testset "scatter" begin + @test pick(scene, Point2f(40, 60), 10) == (sc2, 2) + end + @testset "meshscatter" begin + @test pick(scene, (35, 117), 10) == (ms, 3) + end + @testset "lines" begin + @test pick(scene, 10, 160, 10) == (l1, 5) + @test pick(scene, 40, 218, 10) == (l2, 5) + end + @testset "linesegments" begin + @test pick(scene, 5, 280, 10) == (ls, 6) + end + @testset "text" begin + @test pick(scene, 32, 320, 10) == (t, 1) + @test pick(scene, 35, 320, 10) == (t, 3) + end + @testset "image" begin + @test pick(scene, 98, 15, 10) == (i, 1) + @test pick(scene, 102, 15, 10) == (i, 2) + @test pick(scene, 200, 15, 10) == (i2, 1) + @test pick(scene, 190, 15, 10) == (i2, 2) + end + @testset "surface" begin + @test pick(scene, 93, 75, 10) == (s, 1) + @test pick(scene, 97, 75, 10) == (s, 2) + @test pick(scene, 200, 75, 10) == (s2, 1) + @test pick(scene, 190, 75, 10) == (s2, 2) + end + @testset "heatmap" begin + @test pick(scene, 93, 120, 10) == (hm, 1) + @test pick(scene, 97, 120, 10) == (hm, 2) + @test pick(scene, 200, 120, 10) == (hm2, 1) + @test pick(scene, 190, 120, 10) == (hm2, 2) + end + @testset "mesh" begin + @test pick(scene, 115, 230, 10) == (m, 4) + end + @testset "voxel" begin + @test pick(scene, 93, 240, 10) == (vx, 1) + @test pick(scene, 97, 240, 10) == (vx, 2) + end + @testset "volume" begin + @test pick(scene, 75, 320, 10) == (vol, 0) + end + @testset "range" begin + # mesh!(scene, Rect2f(200, 330, 10, 10)) + # verify borders + @test pick(scene, 189, 331) == (nothing, 0) + @test pick(scene, 191, 329) == (nothing, 0) + @test pick(scene, 191, 331) == (m2, 4) + @test pick(scene, 199, 339) == (m2, 4) + @test pick(scene, 201, 339) == (nothing, 0) + @test pick(scene, 199, 341) == (nothing, 0) + + @testset "horizontal" begin + @test pick(scene, 170, 335, 19) == (nothing, 0) + @test pick(scene, 170, 335, 21) == (m2, 3) + @test pick(scene, 220, 335, 19) == (nothing, 0) + @test pick(scene, 220, 335, 21) == (m2, 4) + end + + @testset "vertical" begin + @test pick(scene, 205, 310, 19) == (nothing, 0) + @test pick(scene, 205, 310, 21) == (m2, 4) + @test pick(scene, 205, 360, 19) == (nothing, 0) + @test pick(scene, 205, 360, 22) == (m2, 4) # off by one? + end + @testset "diagonals" begin + # 190, 330 + @test pick(scene, 180, 320, 14) == (nothing, 0) + @test pick(scene, 180, 320, 15) == (m2, 4) + @test pick(scene, 180, 350, 14) == (nothing, 0) + @test pick(scene, 180, 350, 15) == (m2, 3) + @test pick(scene, 210, 320, 14) == (nothing, 0) + @test pick(scene, 210, 320, 15) == (m2, 4) + @test pick(scene, 210, 350, 14) == (nothing, 0) + @test pick(scene, 210, 350, 16) == (m2, 4) # off by one? + end 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) + # pick_sorted + @testset "pick_sorted" begin + list = Makie.pick_sorted(scene, Vec2(100, 100), 50) + @test length(list) == 14 + @test list[1] == (s, 5) + @test list[2] == (s, 2) + @test list[3] == (s, 4) + @test list[4] == (s, 1) + @test list[5] == (s, 6) + @test list[6] == (hm, 2) + @test list[7] == (s, 3) + @test list[8] == (hm, 1) + @test list[9] == (hm, 3) + @test list[10] == (ms, 3) + @test list[11] == (sc2, 4) + @test list[12] == (l1, 3) + @test list[13] == (l1, 2) + @test list[14] == (sc2, 2) 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) + #= + For Verfication + Note that the text only marks the index in the picking list. The position + that is closest (that pick_sorted used) is somewhere else in the marked + element. Check scene2 to see the pickable regions if unsure + + list = Makie.pick_sorted(scene, Vec2(100, 100), 50) + ps = Point2f[] + for (p, idx) in list + if p isa Union{Surface, Heatmap} + data = Point2f.(p.converted[1][], collect(p.converted[2][])') + push!(ps, data[idx]) + else + push!(ps, p.converted[1][][idx]) 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 + scatter!(scene, Vec2f(100, 100), color = :white, strokecolor = :black, strokewidth = 2, overdraw = true) + text!( + scene, ps, text = ["$i" for i in 1:14], + strokecolor = :white, strokewidth = 2, + align = (:center, :center), overdraw = true) + =# + # pick(scene, Rect) # grab all indices and generate a plot for them (w/ fixed px_per_unit) full_screen = last.(pick(scene, scene.viewport[])) diff --git a/WGLMakie/src/picking.jl b/WGLMakie/src/picking.jl index 74c06e227f6..93bf7a56d9d 100644 --- a/WGLMakie/src/picking.jl +++ b/WGLMakie/src/picking.jl @@ -45,12 +45,11 @@ function Makie.pick_closest(scene::Scene, screen::Screen, xy, range::Integer) lookup = plot_lookup(scene) !haskey(lookup, selection[1]) && return (nothing, 0) plt = lookup[selection[1]] - return (plt, selection[2] + !(plt isa Volume)) + return (plt, Int(selection[2]) + !(plt isa Volume)) end # Skips some allocations function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) - xy_vec = Cint[round.(Cint, xy)...] range = round(Int, range) session = get_screen_session(screen) @@ -61,9 +60,10 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) """) isnothing(selection) && return Tuple{Plot,Int}[] lookup = plot_lookup(scene) - return map(filter((id, idx) -> haskey(lookup, id), selection)) do (id, idx) + filter!(((id, idx),) -> haskey(lookup, id), selection) + return map(selection) do (id, idx) plt = lookup[id] - return (plt, index + !(plt isa Volume)) + return (plt, Int(idx) + !(plt isa Volume)) end end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index 34a6cd90b0b..a2e349a9981 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -23280,7 +23280,7 @@ function pick_closest(scene, xy, range) { const dy = y1 - y0; const [plot_data, _] = pick_native(scene, x0, y0, dx, dy, false); const plot_matrix = plot_data.data; - let min_dist = 1e30; + let min_dist = px_per_unit * px_per_unit * range * range; let selection = [ null, 0 diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index 215c8c3b1f2..0cf289a459f 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -607,7 +607,7 @@ export function pick_closest(scene, xy, range) { const dy = y1 - y0; const [plot_data, _] = pick_native(scene, x0, y0, dx, dy, false); const plot_matrix = plot_data.data; - let min_dist = 1e30; + let min_dist = px_per_unit * px_per_unit * range * range; let selection = [null, 0]; const x = xy[0] * px_per_unit + 1 - x0; const y = xy[1] * px_per_unit + 1 - y0; diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index b138d858e19..ba8a6901728 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -655,7 +655,7 @@ function show_imagelike(inspector, plot, name, idx, edge_based) return true end - if plot.interpolate[] + if plot.interpolate[] || isnothing(idx) i, j, z = _interpolated_getindex(xrange, yrange, zrange, pos) x, y = pos else @@ -712,7 +712,7 @@ function show_imagelike(inspector, plot, name, idx, edge_based) notify(p[1]) end else - bbox = _pixelated_image_bbox(xrange, yrange, zrange, i, j, edge_based) + bbox = _pixelated_image_bbox(xrange, yrange, zrange, round(Int, i), round(Int, j), edge_based) if inspector.selection != plot || (length(inspector.temp_plots) != 1) || !(inspector.temp_plots[1] isa Wireframe) clear_temporary_plots!(inspector, plot) diff --git a/src/interaction/interactive_api.jl b/src/interaction/interactive_api.jl index 64753f08321..b3fdff252e4 100644 --- a/src/interaction/interactive_api.jl +++ b/src/interaction/interactive_api.jl @@ -108,7 +108,7 @@ function pick_closest(scene::SceneLike, screen, xy, range) x, y = xy .+ 1 .- Vec2f(x0, y0) for i in 1:dx, j in 1:dy d = (x-i)^2 + (y-j)^2 - if (d < min_dist) && (picks[i, j][1] != nothing) + if (d < min_dist) && (picks[i, j][1] !== nothing) min_dist = d selected = (i, j) end From f0b3a86a6e599862c53fb0a087b6e4579075bc78 Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Fri, 25 Oct 2024 18:45:18 +0200 Subject: [PATCH 17/80] Fix surface update & add test (#4529) * fix surface update & add test * update changelog * fix step syntax --------- Co-authored-by: Simon --- CHANGELOG.md | 1 + GLMakie/src/drawing_primitives.jl | 1 + ReferenceTests/src/tests/updating.jl | 26 +++++++++++++++++++++++++- WGLMakie/src/imagelike.jl | 2 +- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41db9c09615..085af96762e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Use polys for axis3 [#4463](https://github.com/MakieOrg/Makie.jl/pull/4463). - 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) +- Fixed resizing of `surface` data not working correctly. (I.e. drawing out-of-bounds data or only drawing part of the data.) [#4529](https://github.com/MakieOrg/Makie.jl/pull/4529) ## [0.21.14] - 2024-10-11 diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index f364b2e3468..60301cbe3a3 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -814,6 +814,7 @@ function draw_atomic(screen::Screen, scene::Scene, plot::Surface) gl_attributes[:image] = Texture(img; minfilter=interp) @assert to_value(plot[3]) isa AbstractMatrix + gl_attributes[:instances] = map(z -> (size(z,1)-1) * (size(z,2)-1), plot[3]) types = map(v -> typeof(to_value(v)), plot[1:2]) if all(T -> T <: Union{AbstractMatrix, AbstractVector}, types) diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index bf177009373..b80a3b2ef12 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -174,7 +174,6 @@ end 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 @@ -191,4 +190,29 @@ end f.scene.events.tick[] = Makie.Tick(Makie.UnknownTickState, 0, 0.0, 0.0) end f +end + +@reference_test "updating surface size" begin + X = Observable(-5:5) + Y = Observable(-5:5) + Z = Observable([0.01 * x*x * y*y for x in X.val, y in Y.val]) + + f = Figure(size = (800, 400)) + surface(f[1, 1], X, Y, Z) + surface(f[1, 2], map(collect, X), map(collect, Y), Z) + surface(f[1, 3], + map((X, Y) -> [x for x in X, y in Y], X, Y), + map((X, Y) -> [y for x in X, y in Y], X, Y), Z) + st = Stepper(f) + Makie.step!(st) + + X.val = -5:0 + Z.val = Z.val[1:6, :] + notify(Z) + Makie.step!(st) + + X.val = -5:5 + Z.val = [0.01 * x*x * y*y for x in X.val, y in Y.val] + notify(Z) + Makie.step!(st) end \ No newline at end of file diff --git a/WGLMakie/src/imagelike.jl b/WGLMakie/src/imagelike.jl index 0406f3ddb8f..b6c2651ed3c 100644 --- a/WGLMakie/src/imagelike.jl +++ b/WGLMakie/src/imagelike.jl @@ -18,7 +18,7 @@ function create_shader(mscene::Scene, plot::Surface) positions = Buffer(ps) rect = lift(z -> Tesselation(Rect2(0f0, 0f0, 1f0, 1f0), size(z)), plot, pz) fs = lift(r -> decompose(QuadFace{Int}, r), plot, rect) - fs = map((ps, fs) -> filter(f -> !any(i -> isnan(ps[i]), f), fs), plot, ps, fs) + fs = map((ps, fs) -> filter(f -> !any(i -> (i > length(ps)) || isnan(ps[i]), f), fs), plot, ps, fs) faces = Buffer(fs) # This adjusts uvs (compared to decompose_uv) so texture sampling starts at # the center of a texture pixel rather than the edge, fixing From 19c0283ba3c03ff15d3ae19fa6ce48e1867e35b5 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Sat, 26 Oct 2024 09:58:35 +0200 Subject: [PATCH 18/80] Prepare 0.21.15 (#4528) * bump versions * edit changelog --------- Co-authored-by: Frederic Freyer --- CHANGELOG.md | 9 ++++++--- CairoMakie/Project.toml | 4 ++-- GLMakie/Project.toml | 4 ++-- Project.toml | 2 +- RPRMakie/Project.toml | 4 ++-- WGLMakie/Project.toml | 4 ++-- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 085af96762e..dd7bb35e2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.21.15] - 2024-10-25 + - Allowed creation of `Legend` with entries that have no legend elements [#4526](https://github.com/MakieOrg/Makie.jl/pull/4526). - Improved CairoMakie's 2D mesh drawing performance by ~30% [#4132](https://github.com/MakieOrg/Makie.jl/pull/4132). - Allow `width` to be set per box in `boxplot` [#4447](https://github.com/MakieOrg/Makie.jl/pull/4447). @@ -9,8 +11,8 @@ if the width is exceeded [#4293](https://github.com/MakieOrg/Makie.jl/pull/4293) - 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). -- Use polys for axis3 [#4463](https://github.com/MakieOrg/Makie.jl/pull/4463). +- Fixed usage of `AggMean()` and other aggregations operating on 3d data for `datashader` [#4346](https://github.com/MakieOrg/Makie.jl/pull/4346). +- Fixed forced rasterization when rendering figures with `Axis3` to svg [#4463](https://github.com/MakieOrg/Makie.jl/pull/4463). - 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) - Fixed resizing of `surface` data not working correctly. (I.e. drawing out-of-bounds data or only drawing part of the data.) [#4529](https://github.com/MakieOrg/Makie.jl/pull/4529) @@ -640,7 +642,8 @@ 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.14...HEAD +[Unreleased]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.15...HEAD +[0.21.15]: https://github.com/MakieOrg/Makie.jl/compare/v0.21.14...v0.21.15 [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 diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index e3dbf0c22ce..2f368c39073 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.14" +version = "0.12.15" [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.14" +Makie = "=0.21.15" PrecompileTools = "1.0" julia = "1.3" diff --git a/GLMakie/Project.toml b/GLMakie/Project.toml index 9ade73d46f1..2a6fdb5c075 100644 --- a/GLMakie/Project.toml +++ b/GLMakie/Project.toml @@ -1,6 +1,6 @@ name = "GLMakie" uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" -version = "0.10.14" +version = "0.10.15" [deps] ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" @@ -30,7 +30,7 @@ FreeTypeAbstraction = "0.10" GLFW = "3.4.3" GeometryBasics = "0.4.11" LinearAlgebra = "1.0, 1.6" -Makie = "=0.21.14" +Makie = "=0.21.15" Markdown = "1.0, 1.6" MeshIO = "0.4" ModernGL = "1" diff --git a/Project.toml b/Project.toml index ac3a5b2fa53..1f23f1941e9 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.14" +version = "0.21.15" [deps] Animations = "27a7e980-b3e6-11e9-2bcd-0b925532e340" diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 550b0291ea8..7f3b985c80e 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.14" +version = "0.7.15" [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.14" +Makie = "=0.21.15" Printf = "1.0, 1.6" RadeonProRender = "0.3.0" julia = "1.3" diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index 886599cc21e..bfcb818ef32 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -1,7 +1,7 @@ name = "WGLMakie" uuid = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" authors = ["SimonDanisch "] -version = "0.10.14" +version = "0.10.15" [deps] Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" @@ -29,7 +29,7 @@ GeometryBasics = "0.4.11" Hyperscript = "0.0.3, 0.0.4, 0.0.5" LinearAlgebra = "1.0, 1.6" LoggingExtras = "<1.1.0" -Makie = "=0.21.14" +Makie = "=0.21.15" Observables = "0.5.1" PNGFiles = "0.3, 0.4" PrecompileTools = "1.0" From ccf0572ab0b75ae088a8e7f55a8c718080125c3f Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Sat, 26 Oct 2024 18:15:47 +0200 Subject: [PATCH 19/80] Make benchmark plots and post a gist with them (#4515) --- .github/workflows/compilation-benchmark.yaml | 62 +++++- metrics/ttfp/Project.toml | 9 +- metrics/ttfp/run-benchmark.jl | 216 +++---------------- 3 files changed, 90 insertions(+), 197 deletions(-) diff --git a/.github/workflows/compilation-benchmark.yaml b/.github/workflows/compilation-benchmark.yaml index 17cb146fed4..f12dfe01828 100644 --- a/.github/workflows/compilation-benchmark.yaml +++ b/.github/workflows/compilation-benchmark.yaml @@ -10,6 +10,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true + jobs: benchmark: name: ${{ matrix.package }} @@ -38,8 +39,65 @@ jobs: PR_NUMBER: ${{ github.event.number }} run: > DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=./metrics/ttfp/ ./metrics/ttfp/run-benchmark.jl ${{ matrix.package }} 20 ${{ github.event.pull_request.base.ref }} - - name: Upload measurements as artifact + - name: Upload plots as artifact uses: actions/upload-artifact@v4 with: name: ${{ matrix.package }} - path: ./json + path: ./benchmark_results + post-gist: + name: Post Benchmark Gist + needs: benchmark # Wait for all benchmark jobs to complete + runs-on: ubuntu-20.04 + permissions: + statuses: write # Permission to post workflow status + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: ./images + merge-multiple: true + + - name: Create Gist with images + env: + GH_TOKEN: ${{ secrets.BENCHMARK_KEY }} + run: | + # Create a gist with the three images + gist_url=$(gh gist create ./images/CairoMakie.svg ./images/GLMakie.svg ./images/WGLMakie.svg | grep -Eo 'https://gist.github.com[/a-zA-Z0-9]+') + echo "Gist created: $gist_url" + + # Save the gist URL for later steps + echo "GIST_URL=$gist_url" >> $GITHUB_ENV + echo "GIST_URL_USERCONTENT=$(echo $gist_url | sed 's|github|githubusercontent|')" >> $GITHUB_ENV + + - name: Post workflow status with gist link + env: + GH_TOKEN: ${{ github.token }} + run: | + gist_url=$GIST_URL + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }} \ + -f "state=success" \ + -f "context=Benchmark Results" \ + -f "description=Plots are available under Details" \ + -f "target_url=$gist_url" + - name: Post comment + uses: thollander/actions-comment-pull-request@v3 + with: + github-token: ${{ secrets.BENCHMARK_KEY }} + comment-tag: benchmark # this allows to update the same post with new data + message: | + # Benchmark Results + + SHA: [${{ github.event.pull_request.head.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }}) + + > [!WARNING] + > These results are subject to substantial noise because GitHub's CI runs on shared machines that are not ideally suited for benchmarking. + + ![GLMakie](${{ env.GIST_URL_USERCONTENT }}/raw/GLMakie.svg) + ![CairoMakie](${{ env.GIST_URL_USERCONTENT }}/raw/CairoMakie.svg) + ![WGLMakie](${{ env.GIST_URL_USERCONTENT }}/raw/WGLMakie.svg) + + diff --git a/metrics/ttfp/Project.toml b/metrics/ttfp/Project.toml index 22fc2f05a19..ef310db892d 100644 --- a/metrics/ttfp/Project.toml +++ b/metrics/ttfp/Project.toml @@ -1,7 +1,6 @@ [deps] -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -GitHub = "bc5e4493-9b4d-5f90-b8aa-2b2bcaad7a26" -HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5" +AlgebraOfGraphics = "cbdf2221-f076-402e-a563-3d30da359d67" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" diff --git a/metrics/ttfp/run-benchmark.jl b/metrics/ttfp/run-benchmark.jl index 84374eae260..7384031413b 100644 --- a/metrics/ttfp/run-benchmark.jl +++ b/metrics/ttfp/run-benchmark.jl @@ -9,8 +9,8 @@ Pkg.activate(@__DIR__) Pkg.instantiate() pkg"registry up" Pkg.update() -using Statistics, GitHub, Printf, BenchmarkTools, Markdown, HypothesisTests -using BenchmarkTools.JSON + +using JSON, AlgebraOfGraphics, CairoMakie, DataFrames Package = ARGS[1] n_samples = length(ARGS) > 1 ? parse(Int, ARGS[2]) : 7 base_branch = length(ARGS) > 2 ? ARGS[3] : "master" @@ -21,164 +21,6 @@ base_branch = length(ARGS) > 2 ? ARGS[3] : "master" @info("Benchmarking $(Package) against $(base_branch) with $(n_samples)") -COMMENT_TEMPLATE = """ -## Compile Times benchmark - -Note, that these numbers may fluctuate on the CI servers, so take them with a grain of salt. -All benchmark results are based on the mean time and negative percent mean faster than the base branch. -Note, that GLMakie + WGLMakie run on an emulated GPU, so the runtime benchmark is much slower. -Results are from running: - -```julia -using_time = @ctime using Backend -# Compile time -create_time = @ctime fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -display_time = @ctime Makie.colorbuffer(display(fig)) -# Runtime -create_time = @benchmark fig = scatter(1:4; color=1:4, colormap=:turbo, markersize=20, visible=true) -display_time = @benchmark Makie.colorbuffer(fig) -``` - -| | using | create | display | create | display | -|--------------:|:----------|:---------|:---------|:---------|:---------| -| GLMakie | -- | -- | -- | -- | -- | -| $base_branch | -- | -- | -- | -- | -- | -| evaluation | -- | -- | -- | -- | -- | -| CairoMakie | -- | -- | -- | -- | -- | -| $base_branch | -- | -- | -- | -- | -- | -| evaluation | -- | -- | -- | -- | -- | -| WGLMakie | -- | -- | -- | -- | -- | -| $base_branch | -- | -- | -- | -- | -- | -| evaluation | -- | -- | -- | -- | -- | -""" - -function github_context() - owner = "MakieOrg" - return ( - owner = owner, - repo = GitHub.Repo("$(owner)/Makie.jl"), - auth = GitHub.authenticate(ENV["GITHUB_TOKEN"]), - scratch_repo = GitHub.Repo("MakieOrg/scratch") - ) -end - -function best_unit(m) - if m < 1e3 - return 1, "ns" - elseif m < 1e6 - return 1e3, "μs" - elseif m < 1e9 - return 1e6, "ms" - else - return 1e9, "s" - end -end - -function cohen_d(x, y) - nx = length(x); ny = length(y) - ddof = nx + ny - 2 - poolsd = sqrt(((nx - 1) * var(x) + (ny - 1) * var(y)) / ddof) - d = (mean(x) - mean(y)) / poolsd -end - -function analyze(pr, master) - f, unit = best_unit(pr[1]) - pr, master = Float64.(pr) ./ f, Float64.(master) ./ f - tt = UnequalVarianceTTest(pr, master) - d = cohen_d(pr, master) - std_p = (std(pr) + std(master)) / 2 - m_pr = mean(pr) - m_m = mean(master) - mean_diff = m_pr - m_m - speedup = m_m / m_pr - p = pvalue(tt) - mean_diff_str = string(round(mean_diff; digits=2), unit) - percent_change = (speedup - 1) * 100 - result = if p < 0.05 - if abs(d) > 0.2 - indicator = abs(percent_change) < 5 ? ["faster ✓", "slower X"] : ["**faster**✅", "**slower**❌"] - indicator[d < 0 ? 1 : 2] - else - "*invariant*" - end - else - if abs(percent_change) < 5 - "*invariant*" - else - "*noisy*🤷‍♀️" - end - end - - return @sprintf("%.2fx %s, %s (%.2fd, %.2fp, %.2fstd)", speedup, result, mean_diff_str, d, p, - std_p) -end - -function summarize_stats(timings) - f, unit = best_unit(timings[1]) - m = mean(timings) / f - mini = minimum(timings) / f - maxi = maximum(timings) / f - s = std(timings) / f - @sprintf("%.2f%s (%.2f, %.2f) %.2f+-", m, unit, mini, maxi, s) -end - -function get_row_values(results_pr, results_m) - master_row = [] - pr_row = [] - evaluation_row = [] - n = length(results_pr) - for i in 1:n - push!(pr_row, summarize_stats(results_pr[i])) - push!(master_row, summarize_stats(results_m[i])) - push!(evaluation_row, analyze(results_pr[i], results_m[i])) - end - return pr_row, master_row, evaluation_row -end - -function update_comment(old_comment, package_name, (pr_bench, master_bench, evaluation)) - md = Markdown.parse(old_comment) - rows = md.content[end].rows - idx = findfirst(rows) do row - cell = first(row) - isempty(cell) && return false - return first(cell) == package_name - end - if isnothing(idx) - @warn("Could not find $package_name in $(md). Not updating benchmarks") - return old_comment - end - for (i, value) in enumerate(pr_bench) - rows[idx][i + 1] = [value] - end - for (i, value) in enumerate(master_bench) - rows[idx + 1][i + 1] = [value] - end - for (i, value) in enumerate(evaluation) - rows[idx + 2][i + 1] = [value] - end - open("benchmark.md", "w") do io - return show(io, md) - end - return sprint(show, md) -end - -function make_or_edit_comment(ctx, pr, package_name, benchmarks) - prev_comments, _ = GitHub.comments(ctx.repo, pr; auth=ctx.auth) - idx = findfirst(c-> c.user.login == "MakieBot", prev_comments) - if isnothing(idx) - comment = update_comment(COMMENT_TEMPLATE, package_name, benchmarks) - println(comment) - GitHub.create_comment(ctx.repo, pr; auth=ctx.auth, params=Dict("body"=>comment)) - else - old_comment = prev_comments[idx].body - comment = update_comment(old_comment, package_name, benchmarks) - println(comment) - GitHub.edit_comment(ctx.repo, prev_comments[idx], :pr; auth=ctx.auth, params=Dict("body" => comment)) - end -end - - -using Random function run_benchmarks(projects; n=n_samples) benchmark_file = joinpath(@__DIR__, "benchmark-ttfp.jl") @@ -200,18 +42,6 @@ function make_project_folder(name) return project end -function load_results(name) - result = "$name-benchmark.json" - return JSON.parse(read(result, String)) -end - -ctx = try - github_context() -catch e - @warn "Not authorized" exception=e - # bad credentials because PR isn't from a contributor - exit() -end ENV["JULIA_PKG_PRECOMPILE_AUTO"] = 0 project1 = make_project_folder("current-pr") @@ -237,23 +67,29 @@ projects = [project1, project2] run_benchmarks(projects) -results_pr = load_results(basename(project1)) -results_m = load_results(basename(project2)) -benchmark_rows = get_row_values(results_pr, results_m) - -pr_to_comment = get(ENV, "PR_NUMBER", nothing) - -if !isnothing(pr_to_comment) - pr = GitHub.pull_request(ctx.repo, pr_to_comment) - make_or_edit_comment(ctx, pr, Package, benchmark_rows) -else - @info("Not commenting, no PR found") - println(update_comment(COMMENT_TEMPLATE, Package, benchmark_rows)) +json_files = map([project1, project2]) do p + "$(basename(p))-benchmark.json" end -mkdir("json") -for p in [project1, project2] - name = basename(p) - file = "$name-benchmark.json" - mv(file, joinpath("json", file)) -end +colnames = ["using", "first create", "first display", "create", "display"] + +df = reduce(vcat, map(json_files) do filename + name = replace(filename, r"-benchmark.*" => "") + arrs = map(x -> map(identity, x), JSON.parsefile(filename)) + df = DataFrame(colnames .=> arrs) + df.name .= name + df + end) + +plt = AlgebraOfGraphics.data(df) * + mapping(:name, colnames .=> (x -> x / 1e9) .=> "time (s)", color = :name, layout = dims(1) => renamer(colnames)) * + visual(RainClouds, orientation = :horizontal, markersize = 5, show_median = false, plot_boxplots = false) |> + draw( + scales(Color = (; legend = false)), + facet = (; linkxaxes = false), + axis = (; xticklabelrotation = pi/4, width = 200, height = 150), + figure = (; title = "$Package Benchmarks") + ) + +mkpath("benchmark_results") +save(joinpath("benchmark_results", "$Package.svg"), plt) From e5ace3b80659472b6b9ca9111d157c07a771ed87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:35:57 +0100 Subject: [PATCH 20/80] CompatHelper: bump compat for Colors to 0.13 for package WGLMakie, (keep existing compat) (#4533) Co-authored-by: CompatHelper Julia --- WGLMakie/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index bfcb818ef32..3ac4cf07a6b 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -22,7 +22,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] Bonito = "3.2.1" -Colors = "0.11, 0.12" +Colors = "0.11, 0.12, 0.13" FileIO = "1.1" FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.11" From 1e055431e6557f013dffb498cd1c36a95b8f0ba0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:25:04 +0100 Subject: [PATCH 21/80] CompatHelper: bump compat for Colors to 0.13 for package CairoMakie, (keep existing compat) (#4536) Co-authored-by: CompatHelper Julia --- CairoMakie/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CairoMakie/Project.toml b/CairoMakie/Project.toml index 2f368c39073..b0e5b8afd77 100644 --- a/CairoMakie/Project.toml +++ b/CairoMakie/Project.toml @@ -19,7 +19,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" CRC32c = "1.0, 1.6" Cairo = "1.0.4" Cairo_jll = "1.18.0" -Colors = "0.10, 0.11, 0.12" +Colors = "0.10, 0.11, 0.12, 0.13" FileIO = "1.1" FreeType = "3, 4.0" GeometryBasics = "0.4.11" From 042dc468a1054b184d034f55b86bda774e3b9348 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:05:32 +0100 Subject: [PATCH 22/80] Fix the docs build folder artifact (#4541) --- .github/workflows/Docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml index 6323a40ab13..5b404b016ac 100644 --- a/.github/workflows/Docs.yml +++ b/.github/workflows/Docs.yml @@ -50,4 +50,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: Docs build - path: ./docs/__site + path: ./docs/build From 13ac70cb00e408b3549da1715aaabcddbbebbe38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:07:22 +0100 Subject: [PATCH 23/80] CompatHelper: bump compat for Colors to 0.13 for package RPRMakie, (keep existing compat) (#4539) Co-authored-by: CompatHelper Julia --- RPRMakie/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RPRMakie/Project.toml b/RPRMakie/Project.toml index 7f3b985c80e..79976728995 100644 --- a/RPRMakie/Project.toml +++ b/RPRMakie/Project.toml @@ -13,7 +13,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RadeonProRender = "27029320-176d-4a42-b57d-56729d2ad457" [compat] -Colors = "0.9, 0.10, 0.11, 0.12" +Colors = "0.9, 0.10, 0.11, 0.12, 0.13" FileIO = "1.6" GeometryBasics = "0.4.11" LinearAlgebra = "1.0, 1.6" From fd41dd4f5ee2477a264163cedd1e72fcd30ff2f2 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:02:04 +0100 Subject: [PATCH 24/80] Subscript/superscript combinations (#4489) * Add right and left subsup rich text * make text a little smaller again * tweak positioning * add to rich text test * revert to old sizing * add changelog * add example to docs page --- CHANGELOG.md | 2 + .../src/tests/figures_and_makielayout.jl | 8 +- docs/src/reference/plots/text.md | 6 +- src/basic_recipes/text.jl | 140 ++++++++++++++---- 4 files changed, 126 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7bb35e2c9..b2b23539527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489). + ## [0.21.15] - 2024-10-25 - Allowed creation of `Legend` with entries that have no legend elements [#4526](https://github.com/MakieOrg/Makie.jl/pull/4526). diff --git a/ReferenceTests/src/tests/figures_and_makielayout.jl b/ReferenceTests/src/tests/figures_and_makielayout.jl index 42bbad2a222..6660fd7d2db 100644 --- a/ReferenceTests/src/tests/figures_and_makielayout.jl +++ b/ReferenceTests/src/tests/figures_and_makielayout.jl @@ -392,8 +392,12 @@ end 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) + gl = GridLayout(f[1, 2], tellheight = false) + Label(gl[1, 1], rich("Hi", rich("Hi", offset = (0.2, 0.2), color = :blue))) + Label(gl[2, 1], rich("X", superscript("super"), subscript("sub"))) + Label(gl[3, 1], rich(left_subsup("92", "238"), "U")) + Label(gl[4, 1], rich("SO", subsup("4", "2−"))) + Label(gl[5, 1], rich("x", subsup("f", "g"))) f end diff --git a/docs/src/reference/plots/text.md b/docs/src/reference/plots/text.md index 1ee30b5a999..ec7e8145e20 100644 --- a/docs/src/reference/plots/text.md +++ b/docs/src/reference/plots/text.md @@ -195,9 +195,9 @@ f ## Rich text With rich text, you can conveniently plot text whose parts have different colors or fonts, and you can position sections as subscripts and superscripts. -You can create such rich text objects using the functions `rich`, `superscript` and `subscript`, all of which create `RichText` objects. +You can create such rich text objects using the functions `rich`, `superscript`, `subscript`, `subsup` and `left_subsup`, all of which create `RichText` objects. -Each of these functions takes a variable number of arguments, each of which can be a `String` or `RichText`. +Each of these functions takes a variable number of arguments (except `subsup` and `left_subsup` which take exactly two arguments), each of which can be a `String` or `RichText`. Each can also take keyword arguments such as `color` or `font`, to set these attributes for the given part. The top-level settings for font, color, etc. are taken from the `text` attributes as usual. @@ -221,6 +221,8 @@ end Label(f[2, 1], rich(rainbow_chars...), font = :bold) +Label(f[3, 1], rich("Chemists use notations like ", left_subsup("92", "238"), "U or PO", subsup("4", "3−"))) + f ``` diff --git a/src/basic_recipes/text.jl b/src/basic_recipes/text.jl index 7a6b77cc3dd..2c38861e438 100644 --- a/src/basic_recipes/text.jl +++ b/src/basic_recipes/text.jl @@ -307,11 +307,40 @@ function Base.show(io::IO, ::MIME"text/plain", r::RichText) print(io, "RichText: \"$(String(r))\"") end +""" + rich(args...; kwargs...) + +Create a `RichText` object containing all elements in `args`. +""" rich(args...; kwargs...) = RichText(:span, args...; kwargs...) +""" + subscript(args...; kwargs...) + +Create a `RichText` object representing a superscript containing all elements in `args`. +""" subscript(args...; kwargs...) = RichText(:sub, args...; kwargs...) +""" + superscript(args...; kwargs...) + +Create a `RichText` object representing a superscript containing all elements in `args`. +""" superscript(args...; kwargs...) = RichText(:sup, args...; kwargs...) +""" + subsup(subscript, superscript; kwargs...) + +Create a `RichText` object representing a right subscript/superscript combination, +where both scripts are left-aligned against the preceding text. +""" +subsup(args...; kwargs...) = RichText(:subsup, args...; kwargs...) +""" + left_subsup(subscript, superscript; kwargs...) + +Create a `RichText` object representing a left subscript/superscript combination, +where both scripts are right-aligned against the following text. +""" +left_subsup(args...; kwargs...) = RichText(:leftsubsup, args...; kwargs...) -export rich, subscript, superscript +export rich, subscript, superscript, subsup, left_subsup function _get_glyphcollection_and_linesegments(rt::RichText, index, ts, f, fset, al, rot, jus, lh, col, scol, swi, www, offs) gc = layout_text(rt, ts, f, fset, al, rot, jus, lh, col) @@ -381,11 +410,11 @@ function layout_text(rt::RichText, ts, f, fset, al, rot, jus, lh, col) _f = to_font(fset, f) - stack = [GlyphState(0, 0, Vec2f(ts), _f, to_color(col))] - lines = [GlyphInfo[]] - process_rt_node!(stack, lines, rt, fset) + gs = GlyphState(0, 0, Vec2f(ts), _f, to_color(col)) + + process_rt_node!(lines, gs, rt, fset) apply_lineheight!(lines, lh) apply_alignment_and_justification!(lines, jus, al) @@ -463,23 +492,64 @@ function float_justification(ju, al)::Float32 end end -function process_rt_node!(stack, lines, rt::RichText, fonts) - _type(x) = nothing - _type(r::RichText) = r.type +function process_rt_node!(lines, gs::GlyphState, rt::RichText, fonts) + T = Val(rt.type) + + if T === Val(:subsup) || T === Val(:leftsubsup) + if length(rt.children) != 2 + throw(ArgumentError("Found subsup rich text with $(length(rt.children)) which has to have exactly 2 children instead. The children were: $(rt.children)")) + end + sub, sup = rt.children + sub_lines = Vector{GlyphInfo}[[]] + new_gs_sub = new_glyphstate(gs, rt, Val(:subsup_sub), fonts) + new_gs_sub_post = process_rt_node!(sub_lines, new_gs_sub, sub, fonts) + sup_lines = Vector{GlyphInfo}[[]] + new_gs_sup = new_glyphstate(gs, rt, Val(:subsup_sup), fonts) + new_gs_sup_post = process_rt_node!(sup_lines, new_gs_sup, sup, fonts) + if length(sub_lines) != 1 + error("It is not allowed to include linebreaks in a subsup rich text element, the invalid element was: $(repr(sub))") + end + if length(sup_lines) != 1 + error("It is not allowed to include linebreaks in a subsup rich text element, the invalid element was: $(repr(sup))") + end + sub_line = only(sub_lines) + sup_line = only(sup_lines) + if T === Val(:leftsubsup) + right_align!(sub_line, sup_line) + end + append!(lines[end], sub_line) + append!(lines[end], sup_line) + x = max(new_gs_sub_post.x, new_gs_sup_post.x) + else + new_gs = new_glyphstate(gs, rt, T, fonts) + for (i, c) in enumerate(rt.children) + new_gs = process_rt_node!(lines, new_gs, c, fonts) + end + x = new_gs.x + end + + return GlyphState(x, gs.baseline, gs.size, gs.font, gs.color) +end - push!(stack, new_glyphstate(stack[end], rt, Val(rt.type), fonts)) - for (i, c) in enumerate(rt.children) - process_rt_node!(stack, lines, c, fonts) +function right_align!(line1::Vector{GlyphInfo}, line2::Vector{GlyphInfo}) + isempty(line1) || isempty(line2) && return + xmax1, xmax2 = map((line1, line2)) do line + maximum(line; init = 0f0) do ginfo + GlyphInfo + ginfo.origin[1] + ginfo.size[1] * (ginfo.extent.ink_bounding_box.origin[1] + ginfo.extent.ink_bounding_box.widths[1]) + end + end + line_to_shift = xmax1 > xmax2 ? line2 : line1 + for j in eachindex(line_to_shift) + l = line_to_shift[j] + o = l.origin + l = GlyphInfo(l; origin = o .+ Point2f(abs(xmax2 - xmax1), 0)) + line_to_shift[j] = l end - gs = pop!(stack) - gs_top = stack[end] - # x needs to continue even if going a level up - stack[end] = GlyphState(gs.x, gs_top.baseline, gs_top.size, gs_top.font, gs_top.color) return end -function process_rt_node!(stack, lines, s::String, _) - gs = stack[end] +function process_rt_node!(lines, gs::GlyphState, s::String, _) y = gs.baseline x = gs.x for char in s @@ -505,12 +575,7 @@ function process_rt_node!(stack, lines, s::String, _) x = x + gext.hadvance * gs.size[1] end end - stack[end] = GlyphState(x, y, gs.size, gs.font, gs.color) - return -end - -function new_glyphstate(gs::GlyphState, rt::RichText, val::Val, fonts) - gs + return GlyphState(x, y, gs.size, gs.font, gs.color) end _get_color(attributes, default)::RGBAf = haskey(attributes, :color) ? to_color(attributes[:color]) : default @@ -518,7 +583,7 @@ _get_font(attributes, default::NativeFont, fonts)::NativeFont = haskey(attribute _get_fontsize(attributes, default)::Vec2f = haskey(attributes, :fontsize) ? Vec2f(to_fontsize(attributes[:fontsize])) : default _get_offset(attributes, default)::Vec2f = haskey(attributes, :offset) ? Vec2f(attributes[:offset]) : default -function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:sup}, fonts) +function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:sup}, fonts) att = rt.attributes fontsize = _get_fontsize(att, gs.size * 0.66) offset = _get_offset(att, Vec2f(0)) .* fontsize @@ -531,7 +596,7 @@ function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:sup}, fonts) ) end -function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:span}, fonts) +function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:span}, fonts) att = rt.attributes fontsize = _get_fontsize(att, gs.size) offset = _get_offset(att, Vec2f(0)) .* fontsize @@ -544,13 +609,36 @@ function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:span}, fonts) ) end -function new_glyphstate(gs::GlyphState, rt::RichText, val::Val{:sub}, fonts) +function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:sub}, fonts) att = rt.attributes fontsize = _get_fontsize(att, gs.size * 0.66) offset = _get_offset(att, Vec2f(0)) .* fontsize GlyphState( gs.x + offset[1], - gs.baseline - 0.15 * gs.size[2] + offset[2], + gs.baseline - 0.25 * gs.size[2] + offset[2], + fontsize, + _get_font(att, gs.font, fonts), + _get_color(att, gs.color), + ) +end + +function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:subsup_sub}, fonts) + att = rt.attributes + fontsize = _get_fontsize(att, gs.size * 0.66) + GlyphState( + gs.x, + gs.baseline - 0.25 * gs.size[2], + fontsize, + _get_font(att, gs.font, fonts), + _get_color(att, gs.color), + ) +end +function new_glyphstate(gs::GlyphState, rt::RichText, ::Val{:subsup_sup}, fonts) + att = rt.attributes + fontsize = _get_fontsize(att, gs.size * 0.66) + GlyphState( + gs.x, + gs.baseline + 0.4 * gs.size[2], fontsize, _get_font(att, gs.font, fonts), _get_color(att, gs.color), From 4f045d7a804a4325d567951397e711be36c9d4fe Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 29 Oct 2024 20:06:03 +0100 Subject: [PATCH 25/80] Fix CI slowdown by relying on new bonito version with new HTTP.jl (#4542) properly fix CI slowdown by relying on new bonito version with new http version --- WGLMakie/Project.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/WGLMakie/Project.toml b/WGLMakie/Project.toml index 3ac4cf07a6b..bf3a3d33a64 100644 --- a/WGLMakie/Project.toml +++ b/WGLMakie/Project.toml @@ -11,7 +11,6 @@ FreeTypeAbstraction = "663a7486-cb36-511b-a19d-713bb74d65c9" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" Hyperscript = "47d2ed2b-36de-50cf-bf87-49c2cf4b8b91" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" Observables = "510215fc-4207-5dde-b226-833fc4488ee2" PNGFiles = "f57f5aa1-a3ce-4bc8-8ab9-96f992907883" @@ -21,14 +20,13 @@ ShaderAbstractions = "65257c39-d410-5151-9873-9b3e5be5013e" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] -Bonito = "3.2.1" +Bonito = "3.2.4" Colors = "0.11, 0.12, 0.13" FileIO = "1.1" FreeTypeAbstraction = "0.10" GeometryBasics = "0.4.11" Hyperscript = "0.0.3, 0.0.4, 0.0.5" LinearAlgebra = "1.0, 1.6" -LoggingExtras = "<1.1.0" Makie = "=0.21.15" Observables = "0.5.1" PNGFiles = "0.3, 0.4" From 22863fd53f9ffd122ec624735e537ec479dcf2d8 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Wed, 30 Oct 2024 11:17:39 +0100 Subject: [PATCH 26/80] Fix docstrings for 1.11 (#4548) --- MakieCore/src/recipes.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index 4b1d9aaff97..fd44f41fdbe 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -392,6 +392,14 @@ end function types_for_plot_arguments end +function extract_docstring(str) + if VERSION >= v"1.11" && str isa Base.Docs.DocStr + return only(str.text::Core.SimpleVector) + else + return str + end +end + function create_recipe_expr(Tsym, args, attrblock) funcname_sym = to_func_name(Tsym) funcname!_sym = Symbol("$(funcname_sym)!") @@ -421,7 +429,7 @@ function create_recipe_expr(Tsym, args, attrblock) Core.@__doc__ $(esc(docs_placeholder)) = nothing binding = Docs.Binding(@__MODULE__, $(QuoteNode(docs_placeholder))) user_docstring = if haskey(Docs.meta(@__MODULE__), binding) - _docstring = @doc($docs_placeholder) + _docstring = extract_docstring(@doc($docs_placeholder)) delete!(Docs.meta(@__MODULE__), binding) _docstring else @@ -462,7 +470,7 @@ function create_recipe_expr(Tsym, args, attrblock) end $(arg_type_func) - docstring_modified = make_recipe_docstring($PlotType, $(QuoteNode(Tsym)), $(QuoteNode(funcname_sym)),user_docstring) + docstring_modified = make_recipe_docstring($PlotType, $(QuoteNode(Tsym)), $(QuoteNode(funcname_sym)), user_docstring) @doc docstring_modified $funcname_sym @doc "`$($(string(Tsym)))` is the plot type associated with plotting function `$($(string(funcname_sym)))`. Check the docstring for `$($(string(funcname_sym)))` for further information." $Tsym @doc "`$($(string(funcname!_sym)))` is the mutating variant of plotting function `$($(string(funcname_sym)))`. Check the docstring for `$($(string(funcname_sym)))` for further information." $funcname!_sym From 70e5d2ca235f8cae79ec87743c36d372b5cd90dc Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 31 Oct 2024 12:40:14 +0100 Subject: [PATCH 27/80] implement S.Colorbar(plotspec) (#4520) * implement S.Colorbar(plotspec) * improve name * implement lookup_default, to better get colorbar defaults from spec argument * fix tests * Update CHANGELOG.md --- CHANGELOG.md | 1 + MakieCore/src/recipes.jl | 188 ++++++++++++++-------------- ReferenceTests/src/tests/specapi.jl | 40 ++++++ src/specapi.jl | 108 ++++++++++++++-- 4 files changed, 235 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b23539527..7e70668c2f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489). +- Implement S.Colorbar(plotspec) [#4520](https://github.com/MakieOrg/Makie.jl/pull/4520). ## [0.21.15] - 2024-10-25 diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index fd44f41fdbe..6e8da443ba6 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -72,7 +72,7 @@ theme(x::AbstractPlot, key; default=nothing) = deepcopy(get(x.attributes, key, d Attributes(x::AbstractPlot) = x.attributes -default_theme(scene, T) = Attributes() +function default_theme end """ # Plot Recipes in `Makie` @@ -209,17 +209,58 @@ attribute_names(_) = nothing Base.@kwdef struct AttributeMetadata docstring::Union{Nothing,String} + default_value::Any default_expr::String # stringified expression, just needed for docs purposes end update_metadata(am1::AttributeMetadata, am2::AttributeMetadata) = AttributeMetadata( am2.docstring === nothing ? am1.docstring : am2.docstring, + am2.default_value, am2.default_expr # TODO: should it be possible to overwrite only a docstring by not giving a default expr? ) struct DocumentedAttributes d::Dict{Symbol,AttributeMetadata} - closure::Function +end + +struct Inherit + key::Symbol + fallback::Any +end + +function lookup_default(meta::AttributeMetadata, theme) + default = meta.default_value + if default isa Inherit + if haskey(theme, default.key) + to_value(theme[default.key]) # only use value of theme entry + else + if isnothing(default.fallback) + error("Inherited key $(default.key) not found in theme with no fallback given.") + else + return default.fallback + end + end + else + return default + end +end + +function get_default_expr(default) + if default isa Expr && default.head === :macrocall && default.args[1] === Symbol("@inherit") + if length(default.args) ∉ (3, 4) + error("@inherit works with 1 or 2 arguments, expression was $default") + end + if !(default.args[3] isa Symbol) + error("Argument 1 of @inherit must be a Symbol, got $(default.args[3])") + end + key = default.args[3] + _default = get(default.args, 4, :(nothing)) + # first check scene theme + # then default value + return :($(MakieCore.Inherit)($(QuoteNode(key)), $(esc(_default)))) + else + return esc(default) + end end macro DocumentedAttributes(expr::Expr) @@ -256,40 +297,20 @@ macro DocumentedAttributes(expr::Expr) if !(sym isa Symbol) error("$sym should be a symbol") end - - push!(metadata_exprs, quote - am = AttributeMetadata(; docstring = $docs, default_expr = $(_default_expr_string(default))) - if haskey(d, $(QuoteNode(sym))) - d[$(QuoteNode(sym))] = update_metadata(d[$(QuoteNode(sym))], am) + qsym = QuoteNode(sym) + metadata = quote + am = AttributeMetadata(; + docstring = $docs, + default_value = $(get_default_expr(default)), + default_expr = $(default_expr_string(default)) + ) + if haskey(d, $(qsym)) + d[$(qsym)] = update_metadata(d[$(qsym)], am) else - d[$(QuoteNode(sym))] = am - end - end) - - if default isa Expr && default.head === :macrocall && default.args[1] === Symbol("@inherit") - if length(default.args) ∉ (3, 4) - error("@inherit works with 1 or 2 arguments, expression was $d") + d[$(qsym)] = am end - if !(default.args[3] isa Symbol) - error("Argument 1 of @inherit must be a Symbol, got $(default.args[3])") - end - key = default.args[3] - _default = get(default.args, 4, :(error("Inherited key $($(QuoteNode(key))) not found in theme with no fallback given."))) - # first check scene theme - # then default value - d = :( - dict[$(QuoteNode(sym))] = if haskey(thm, $(QuoteNode(key))) - to_value(thm[$(QuoteNode(key))]) # only use value of theme entry - else - $(esc(_default)) - end - ) - push!(closure_exprs, d) - else - push!(closure_exprs, :( - dict[$(QuoteNode(sym))] = $(esc(default)) - )) end + push!(metadata_exprs, metadata) elseif is_mixin_line # this intermediate variable is needed to evaluate each mixin only once # and is inserted at the start of the final code block @@ -302,14 +323,6 @@ macro DocumentedAttributes(expr::Expr) end end) - # the actual runtime values of the mixed in defaults - # are computed using the closure stored in the DocumentedAttributes - closure_exp = quote - # `scene` and `dict` here are defined below where this exp is interpolated into - merge!(dict, $gsym.closure(scene)) - end - push!(closure_exprs, closure_exp) - # docstrings and default expressions of the mixed in # DocumentedAttributes are inserted metadata_exp = quote @@ -330,13 +343,7 @@ macro DocumentedAttributes(expr::Expr) $(mixin_exprs...) d = Dict{Symbol,AttributeMetadata}() $(metadata_exprs...) - closure = function (scene) - thm = theme(scene) - dict = Dict{Symbol,Any}() - $(closure_exprs...) - return dict - end - DocumentedAttributes(d, closure) + DocumentedAttributes(d) end end @@ -392,6 +399,43 @@ end function types_for_plot_arguments end +documented_attributes(_) = nothing + +function attribute_names(T::Type{<:Plot}) + attr = documented_attributes(T) + isnothing(attr) && return nothing + return keys(attr.d) +end + +function lookup_default(::Type{T}, scene, attribute::Symbol) where {T<:Plot} + thm = theme(scene) + metas = documented_attributes(T).d + psym = plotsym(T) + if haskey(thm, psym) + overwrite = thm[psym] + if haskey(overwrite, attribute) + return to_value(overwrite[attribute]) + end + end + if haskey(metas, attribute) + return lookup_default(metas[attribute], thm) + else + return nothing + end +end + +function default_theme(scene, T::Type{<: Plot}) + metas = documented_attributes(T) + attr = Attributes() + isnothing(metas) && return attr + thm = theme(scene) + _attr = attr.attributes + for (k, meta) in metas.d + _attr[k] = lookup_default(meta, thm) + end + return attr +end + function extract_docstring(str) if VERSION >= v"1.11" && str isa Base.Docs.DocStr return only(str.text::Core.SimpleVector) @@ -461,13 +505,6 @@ function create_recipe_expr(Tsym, args, attrblock) _create_plot!($funcname, kwdict, args...) end - function $(MakieCore).attribute_names(T::Type{<:$(PlotType)}) - keys(documented_attributes(T).d) - end - - function $(MakieCore).default_theme(scene, T::Type{<:$(PlotType)}) - Attributes(documented_attributes(T).closure(scene)) - end $(arg_type_func) docstring_modified = make_recipe_docstring($PlotType, $(QuoteNode(Tsym)), $(QuoteNode(funcname_sym)), user_docstring) @@ -490,6 +527,8 @@ function create_recipe_expr(Tsym, args, attrblock) return q end + + function make_recipe_docstring(P::Type{<:Plot}, Tsym, funcname_sym, docstring) io = IOBuffer() @@ -528,8 +567,8 @@ function rmlines(x::Expr) end end -_default_expr_string(x) = string(rmlines(x)) -_default_expr_string(x::String) = repr(x) +default_expr_string(x) = string(rmlines(x)) +default_expr_string(x::String) = repr(x) function extract_attribute_metadata(arg) has_docs = arg isa Expr && arg.head === :macrocall && arg.args[1] isa GlobalRef @@ -561,41 +600,6 @@ function extract_attribute_metadata(arg) (docs = docs, symbol = attr_symbol, type = type, default = default) end -function make_default_theme_expr(attrs, scenesym::Symbol) - - exprs = map(attrs) do a - - d = a.default - if d isa Expr && d.head === :macrocall && d.args[1] == Symbol("@inherit") - if length(d.args) != 4 - error("@inherit works with exactly 2 arguments, expression was $d") - end - if !(d.args[3] isa QuoteNode) - error("Argument 1 of @inherit must be a :symbol, got $(d.args[3])") - end - key, default = d.args[3:4] - # first check scene theme - # then default value - d = quote - if haskey(thm, $key) - to_value(thm[$key]) # only use value of theme entry - else - $default - end - end - end - - :(attr[$(QuoteNode(a.symbol))] = $d) - end - - quote - thm = theme($scenesym) - attr = Attributes() - $(exprs...) - attr - end -end - function expand_mixins(attrblock::Expr) Expr(:block, mapreduce(expand_mixin, vcat, attrblock.args)...) end diff --git a/ReferenceTests/src/tests/specapi.jl b/ReferenceTests/src/tests/specapi.jl index f12a39585ee..ab5847915d9 100644 --- a/ReferenceTests/src/tests/specapi.jl +++ b/ReferenceTests/src/tests/specapi.jl @@ -116,3 +116,43 @@ end sync_step!(st) st end + +function to_plot(plots) + axes = map(permutedims(plots)) do plot + ax = S.Axis(; + plots=[plot], xticksvisible=false, + yticksvisible=false, yticklabelsvisible=false, + xticklabelsvisible=false) + return S.GridLayout([ax S.Colorbar(plot)]) + end + return S.GridLayout(axes) +end + +@reference_test "Colorbar from Plots" begin + data = vcat((1:4)', (4:-1:1)') + plots = [S.Heatmap(data), + S.Image(data), + S.Lines(1:4; linewidth=4, color=1:4), + S.Scatter(1:4; markersize=20, color=1:4)] + obs = Observable(to_plot(plots)) + fig = plot(obs; figure=(; size=(700, 150))) + img1 = copy(colorbuffer(fig)) + plots = [S.Heatmap(data; colormap=:inferno), + S.Image(data; colormap=:inferno), + S.Lines(1:4; linewidth=4, color=1:4, colormap=:inferno), + S.Scatter(1:4; markersize=20, color=1:4, colormap=:inferno)] + obs[] = to_plot(plots) + img2 = copy(colorbuffer(fig)) + + plots = [S.Heatmap(data; colorrange=(2, 3)), + S.Image(data; colorrange=(2, 3)), + S.Lines(1:4; linewidth=4, color=1:4, colorrange=(2, 3)), + S.Scatter(1:4; markersize=20, color=1:4, colorrange=(2, 3))] + obs[] = to_plot(plots) + img3 = copy(colorbuffer(fig)) + + imgs = hcat(rotr90.((img3, img2, img1))...) + s = Scene(; size=size(imgs)) + image!(s, imgs; space=:pixel) + s +end diff --git a/src/specapi.jl b/src/specapi.jl index 6120d0ad08e..2f5e1a6e08d 100644 --- a/src/specapi.jl +++ b/src/specapi.jl @@ -190,6 +190,7 @@ function Base.getproperty(p::BlockSpec, k::Symbol) end Base.propertynames(p::BlockSpec) = Tuple(keys(p.kwargs)) + function BlockSpec(typ::Symbol, args...; plots::Vector{PlotSpec}=PlotSpec[], kw...) attr = Dict{Symbol,Any}(kw) if typ == :Legend @@ -201,8 +202,16 @@ function BlockSpec(typ::Symbol, args...; plots::Vector{PlotSpec}=PlotSpec[], kw. attr[:entrygroups] = entrygroups return BlockSpec(typ, attr, plots) else + if typ == :Colorbar && !isempty(args) + if length(args) == 1 && args[1] isa PlotSpec + attr[:plotspec] = args[1] + args = () + else + error("Only one argument `arg::PlotSpec` is supported for S.Colorbar. Found: $(args)") + end + end if !isempty(args) - error("BlockSpecs, with an exception for Legend, don't support positional arguments yet.") + error("BlockSpecs, with an exception for Legend and Colorbar, don't support positional arguments yet.") end return BlockSpec(typ, attr, plots) end @@ -395,7 +404,6 @@ function Base.getproperty(::_SpecApi, field::Symbol) end end - function update_plot!(obs_to_notify, plot::AbstractPlot, oldspec::PlotSpec, spec::PlotSpec) # Update args in plot `input_args` list for i in eachindex(spec.args) @@ -423,6 +431,22 @@ function update_plot!(obs_to_notify, plot::AbstractPlot, oldspec::PlotSpec, spec push!(obs_to_notify, old_attr) end end + + reset_to_default = setdiff(keys(oldspec.kwargs), keys(spec.kwargs)) + filter!(x -> x != :cycle, reset_to_default) # dont reset cycle + if !isempty(reset_to_default) + for k in reset_to_default + old_attr = plot[k] + new_value = MakieCore.lookup_default(typeof(plot), parent_scene(plot), k) + # In case of e.g. dim_conversions + isnothing(new_value) && continue + # only update if different + if is_different(old_attr[], new_value) + old_attr.val = new_value + push!(obs_to_notify, old_attr) + end + end + end # Cycling needs to be handled separately sadly, # since they're implicitely mutating attributes, e.g. if I re-use a plot # that has been on cycling position 2, and now I re-use it for the first plot in the list @@ -437,7 +461,6 @@ function update_plot!(obs_to_notify, plot::AbstractPlot, oldspec::PlotSpec, spec end end end - if !isempty(uncycled) # remove all attributes that don't need cycling for (attr_vec, _) in cycle.cycle @@ -450,6 +473,7 @@ function update_plot!(obs_to_notify, plot::AbstractPlot, oldspec::PlotSpec, spec return end + """ plotlist!( [ @@ -645,10 +669,64 @@ function add_observer!(block::BlockSpec, obs::AbstractVector{<:ObserverFunction} return end +function get_numeric_colors(plot::PlotSpec) + if plot.type in [:Heatmap, :Image, :Surface] + z = plot.args[end] + if z isa AbstractMatrix{<:Real} + return z + end + else + if haskey(plot.kwargs, :color) && plot.kwargs[:color] isa AbstractArray{<:Real} + return plot.kwargs[:color] + end + end + return nothing +end + +# TODO it's really hard to get from PlotSpec -> Plot object in the +# Colorbar constructor (to_layoutable), +# since the plot may not be created yet and may change when calling +# update_layoutable!. So for now, we manually extract the Colorbar arguments from the spec +# Which is a bit brittle and won't work for Recipes which overload the Colorbar api (extract_colormap) +# We hope to improve the situation after the observable refactor, which may bring us a bit closer to +# Being able to use the Plot object itself instead of a spec. +function extract_colorbar_kw(legend::BlockSpec, scene::Scene) + if haskey(legend.kwargs, :plotspec) + kw = copy(legend.kwargs) + spec = pop!(kw, :plotspec) + pt = plottype(spec) + for k in [:colorrange, :colormap, :lowclip, :highclip] + get!(kw, k) do + haskey(spec.kwargs, k) && return spec.kwargs[k] + if k === :colorrange + color = get_numeric_colors(spec) + if !isnothing(color) + return nan_extrema(color) + end + else + MakieCore.lookup_default(pt, scene, k) + end + end + end + return kw + else + return legend.kwargs + end +end + function to_layoutable(parent, position::GridLayoutPosition, spec::BlockSpec) BType = getfield(Makie, spec.type) - # TODO forward kw - block = BType(get_top_parent(parent); spec.kwargs...) + fig = get_top_parent(parent) + + block = if spec.type === :Colorbar + # We use the root scene to extract any theming + # This means, we dont support a separate theme per scene + # Which I think has been bitrotting anyways. + kw = extract_colorbar_kw(spec, root(get_scene(fig))) + BType(fig; kw...) + else + BType(fig; spec.kwargs...) + end parent[position...] = block for func in spec.then_funcs observers = func(block) @@ -675,8 +753,17 @@ end function update_layoutable!(block::T, plot_obs, old_spec::BlockSpec, spec::BlockSpec) where T <: Block unhide!(block) - old_attr = keys(old_spec.kwargs) - new_attr = keys(spec.kwargs) + if spec.type === :Colorbar + # To get plot defaults for Colorbar(specapi), we need a theme / scene + # So we have to look up the kwargs here instead of the BlockSpec constructor. + old_kw = extract_colorbar_kw(old_spec, root(block.blockscene)) + new_kw = extract_colorbar_kw(spec, root(block.blockscene)) + else + old_kw = old_spec.kwargs + new_kw = spec.kwargs + end + old_attr = keys(old_kw) + new_attr = keys(new_kw) # attributes that have been set previously and need to get unset now reset_to_defaults = setdiff(old_attr, new_attr) if !isempty(reset_to_defaults) @@ -688,7 +775,7 @@ function update_layoutable!(block::T, plot_obs, old_spec::BlockSpec, spec::Block # Attributes needing an update to_update = setdiff(new_attr, reset_to_defaults) for key in to_update - val = spec.kwargs[key] + val = new_kw[key] prev_val = to_value(getproperty(block, key)) if is_different(val, prev_val) setproperty!(block, key, val) @@ -816,7 +903,6 @@ get_layout!(fig::Figure) = fig.layout get_layout!(gp::Union{GridSubposition,GridPosition}) = GridLayoutBase.get_layout_at!(gp; createmissing=true) - delete_layoutable!(block::Block) = delete!(block) function delete_layoutable!(grid::GridLayout) gc = grid.layoutobservables.gridcontent[] @@ -836,7 +922,9 @@ function update_gridlayout!(target_layout::GridLayout, layout_spec::GridLayoutSp update_gridlayout!(target_layout, 1, nothing, layout_spec, unused_layoutables, new_layoutables) foreach(unused_layoutables) do (p, (block, obs)) # disconnect! all unused layoutables, so they dont show up anymore - disconnect!(block) + if block isa Block + disconnect!(block) + end return end layouts_to_update = Set{GridLayout}([target_layout]) From 379bb69bc7d1acd77fbfbfd1ebab2ded40eacd6f Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Thu, 31 Oct 2024 09:18:53 -0700 Subject: [PATCH 28/80] Fix legend for plotlist with multiple plots (#4546) * Fix `legend` for plotlist with multiple plots * Add a test * Update CHANGELOG.md --------- Co-authored-by: SimonDanisch --- CHANGELOG.md | 1 + src/makielayout/blocks/legend.jl | 8 +++++++- test/specapi.jl | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e70668c2f1..05384f15cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489). +- Expand PlotList plots to expose their child plots to the legend interface, allowing `axislegend`show plots within PlotSpecs as individual entries. [#4546](https://github.com/MakieOrg/Makie.jl/pull/4546) - Implement S.Colorbar(plotspec) [#4520](https://github.com/MakieOrg/Makie.jl/pull/4520). ## [0.21.15] - 2024-10-25 diff --git a/src/makielayout/blocks/legend.jl b/src/makielayout/blocks/legend.jl index 8344667b28e..90a3168dc39 100644 --- a/src/makielayout/blocks/legend.jl +++ b/src/makielayout/blocks/legend.jl @@ -661,7 +661,8 @@ end function get_labeled_plots(ax; merge::Bool, unique::Bool) lplots = filter(get_plots(ax)) do plot - haskey(plot.attributes, :label) + haskey(plot.attributes, :label) || + plot isa PlotList && any(x -> haskey(x.attributes, :label), plot.plots) end labels = map(lplots) do l l.label[] @@ -713,6 +714,11 @@ function get_labeled_plots(ax; merge::Bool, unique::Bool) end get_plots(p::AbstractPlot) = [p] +# NOTE: this is important, since we know that `get_plots` is only ever called on the toplevel, +# we can assume that any plotlist on the toplevel should be decomposed into individual plots. +# However, if the user passes a label argument with a legend override, what do we do? +get_plots(p::PlotList) = haskey(p.attributes, :label) && p.attributes[:label] isa Pair ? [p] : p.plots + get_plots(ax::Union{Axis, Axis3}) = get_plots(ax.scene) get_plots(lscene::LScene) = get_plots(lscene.scene) function get_plots(scene::Scene) diff --git a/test/specapi.jl b/test/specapi.jl index d948768d821..cea645640aa 100644 --- a/test/specapi.jl +++ b/test/specapi.jl @@ -170,3 +170,20 @@ end @test isempty(f.content) @test isempty(f.layout.content) end + +@testset "Legend construction" begin + f, ax, pl = plotlist([S.Scatter(1:4, 1:4; marker = :circle, label="A"), S.Scatter(1:6, 1:6; marker = :rect, label="B")]) + leg = axislegend(ax) + # Test that the legend has two scatter plots + @test count(x -> x isa Makie.Scatter, leg.scene.plots) == 2 + + # Test that the scatter plots have the correct markers + # This is too internal and fragile, so we won't actually test this + # @test leg.scene.plots[2].marker[] == :circle + # @test leg.scene.plots[3].marker[] == :rect + + # Test that the legend has the correct labels. + # Again, I consider this too fragile to work with! + # @test contents(contents(leg.grid)[1])[2].text[] == "A" + # @test contents(contents(leg.grid)[2])[4].text[] == "B" +end From 8b914d05ebd42bdfc8cd10f9a00397091aac2de6 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:42:28 +0100 Subject: [PATCH 29/80] Median ratio bootstrap plot for benchmarks (#4553) * add violin plots of bootstrapped median ratios * 1000 samples should be enough * remove manually set ticks * add colored background to visualize good vs bad change in 5% bands --- metrics/ttfp/Project.toml | 1 + metrics/ttfp/run-benchmark.jl | 55 +++++++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/metrics/ttfp/Project.toml b/metrics/ttfp/Project.toml index ef310db892d..a77f1a9977f 100644 --- a/metrics/ttfp/Project.toml +++ b/metrics/ttfp/Project.toml @@ -1,5 +1,6 @@ [deps] AlgebraOfGraphics = "cbdf2221-f076-402e-a563-3d30da359d67" +Bootstrap = "e28b5b4c-05e8-5b66-bc03-6f0c0a0a06e0" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" diff --git a/metrics/ttfp/run-benchmark.jl b/metrics/ttfp/run-benchmark.jl index 7384031413b..e1f916ece07 100644 --- a/metrics/ttfp/run-benchmark.jl +++ b/metrics/ttfp/run-benchmark.jl @@ -10,7 +10,8 @@ Pkg.instantiate() pkg"registry up" Pkg.update() -using JSON, AlgebraOfGraphics, CairoMakie, DataFrames +using JSON, AlgebraOfGraphics, CairoMakie, DataFrames, Bootstrap +using Statistics: median Package = ARGS[1] n_samples = length(ARGS) > 1 ? parse(Int, ARGS[2]) : 7 base_branch = length(ARGS) > 2 ? ARGS[3] : "master" @@ -19,7 +20,7 @@ base_branch = length(ARGS) > 2 ? ARGS[3] : "master" # n_samples = 2 # base_branch = "breaking-release" -@info("Benchmarking $(Package) against $(base_branch) with $(n_samples)") +@info("Benchmarking $(Package) against $(base_branch) with $(n_samples) samples") function run_benchmarks(projects; n=n_samples) @@ -64,24 +65,26 @@ Pkg.add(pkgs) @time Pkg.precompile() projects = [project1, project2] +projnames = map(basename, [project1, project2]) run_benchmarks(projects) -json_files = map([project1, project2]) do p - "$(basename(p))-benchmark.json" +json_files = map(projnames) do pname + "$(pname)-benchmark.json" end colnames = ["using", "first create", "first display", "create", "display"] -df = reduce(vcat, map(json_files) do filename - name = replace(filename, r"-benchmark.*" => "") +df = reduce(vcat, map(json_files, projnames) do filename, pname arrs = map(x -> map(identity, x), JSON.parsefile(filename)) df = DataFrame(colnames .=> arrs) - df.name .= name + df.name .= pname df end) -plt = AlgebraOfGraphics.data(df) * +## + +fgrid = AlgebraOfGraphics.data(df) * mapping(:name, colnames .=> (x -> x / 1e9) .=> "time (s)", color = :name, layout = dims(1) => renamer(colnames)) * visual(RainClouds, orientation = :horizontal, markersize = 5, show_median = false, plot_boxplots = false) |> draw( @@ -91,5 +94,39 @@ plt = AlgebraOfGraphics.data(df) * figure = (; title = "$Package Benchmarks") ) +df_current_pr = df[df.name .== projnames[1], :] +df_base_branch = df[df.name .== projnames[2], :] + +medians_df = map(names(df_current_pr, Not(:name))) do colname + col_base = df_base_branch[!, colname] + col_pr = df_current_pr[!, colname] + medians_base = bootstrap(median, col_base, Bootstrap.BasicSampling(1000)) + medians_pr = bootstrap(median, col_pr, Bootstrap.BasicSampling(1000)) + ratios = Bootstrap.straps(medians_pr)[1] ./ Bootstrap.straps(medians_base)[1] + colname => ratios +end |> DataFrame + +specmedians = AlgebraOfGraphics.data(stack(medians_df)) * + mapping(:variable => presorted => "", :value => "Ratios of medians\n$(projnames[1]) / $(projnames[2])") * visual(Violin, show_median = true) + +background_bands = AlgebraOfGraphics.pregrouped([0.75:0.05:1.20], [0.8:0.05:1.25]) * + AlgebraOfGraphics.visual(HSpan, color = range(-1, 1, length = 10), colormap = [:green, :white, :tomato], alpha = 0.5) + +zeroline = AlgebraOfGraphics.pregrouped([1]) * AlgebraOfGraphics.visual(HLines, color = :gray60) + +spec = background_bands + zeroline + specmedians + +AlgebraOfGraphics.draw!(fgrid.figure[2, 3], spec, axis = (; + yaxisposition = :right, + xticklabelrotation = pi/4, + title = "Bootstrapped median ratios", + yautolimitmargin = (0, 0), + yticks = WilkinsonTicks(7, k_min = 5), +)) + +resize_to_layout!(fgrid.figure) + +## + mkpath("benchmark_results") -save(joinpath("benchmark_results", "$Package.svg"), plt) +save(joinpath("benchmark_results", "$Package.svg"), fgrid) From f5528a663789349d1ea56c75cd7e7153f8638566 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:40:30 +0100 Subject: [PATCH 30/80] Use ABBA pattern for benchmarking (#4555) --- metrics/ttfp/run-benchmark.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/metrics/ttfp/run-benchmark.jl b/metrics/ttfp/run-benchmark.jl index e1f916ece07..c36efce30d1 100644 --- a/metrics/ttfp/run-benchmark.jl +++ b/metrics/ttfp/run-benchmark.jl @@ -25,8 +25,16 @@ base_branch = length(ARGS) > 2 ? ARGS[3] : "master" function run_benchmarks(projects; n=n_samples) benchmark_file = joinpath(@__DIR__, "benchmark-ttfp.jl") - # go A, B, A, B, A, etc. - for project in repeat(projects, n) + # go A, A, B, B, A, A, B, B, etc. because if A or B have some effect on their + # subsequent run, then we distribute those more evenly. If we used A, B, A, B then + # B would always influence A and A always B which might bias the results (something + # that can carry over separate processes like thermal throttling or so) + + A, B = projects + As = Iterators.partition(fill(A, n), 2) + Bs = Iterators.partition(fill(B, n), 2) + + for project in Iterators.flatten(Iterators.flatten(zip(As, Bs))) println(basename(project)) run(`$(Base.julia_cmd()) --startup-file=no --project=$(project) $benchmark_file $Package`) end From 9d5501073def1806af0439a2d18f30b6643310fe Mon Sep 17 00:00:00 2001 From: t-bltg Date: Fri, 1 Nov 2024 13:07:59 +0100 Subject: [PATCH 31/80] correct spelling (#4522) * correct spelling * check if single quote causes timeouts Seems like the only change in the WGLMakie source to me that could possibly have an effect on anything, if interpolation of that string into javascript has some escaping bugs. --------- Co-authored-by: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> --- CHANGELOG.md | 8 ++++---- CairoMakie/src/infrastructure.jl | 2 +- CairoMakie/src/primitives.jl | 4 ++-- CairoMakie/test/svg_tests.jl | 2 +- GLMakie/src/GLAbstraction/AbstractGPUArray.jl | 2 +- GLMakie/src/GLAbstraction/GLAbstraction.jl | 2 +- GLMakie/src/GLAbstraction/GLRender.jl | 2 +- GLMakie/src/GLAbstraction/GLRenderObject.jl | 2 +- GLMakie/src/GLAbstraction/GLTexture.jl | 4 ++-- GLMakie/src/GLAbstraction/GLTypes.jl | 2 +- GLMakie/src/GLAbstraction/GLUtils.jl | 6 +++--- GLMakie/src/glshaders/lines.jl | 8 ++++---- GLMakie/src/screen.jl | 4 ++-- GLMakie/test/glmakie_refimages.jl | 2 +- GLMakie/test/unit_tests.jl | 2 +- MakieCore/src/conversion.jl | 2 +- RPRMakie/src/meshes.jl | 2 +- ReferenceTests/src/tests/categorical.jl | 4 ++-- ReferenceTests/src/tests/primitives.jl | 2 +- ReferenceUpdater/src/local_server.jl | 2 +- ReferenceUpdater/src/reference_images.html | 2 +- WGLMakie/src/display.jl | 8 ++++---- WGLMakie/src/lines.jl | 16 +++++++-------- WGLMakie/src/serialization.jl | 2 +- WGLMakie/src/voxel.jl | 2 +- WGLMakie/test/runtests.jl | 2 +- docs/src/explanations/backends/glmakie.md | 4 ++-- docs/src/explanations/backends/rprmakie.md | 2 +- docs/src/explanations/backends/wglmakie.md | 2 +- docs/src/reference/blocks/colorbar.md | 2 +- docs/src/reference/blocks/intervalslider.md | 2 +- docs/src/reference/blocks/polaraxis.md | 2 +- docs/src/reference/blocks/slider.md | 2 +- docs/src/reference/plots/datashader.md | 2 +- docs/src/reference/plots/heatmap.md | 2 +- docs/src/reference/plots/rainclouds.md | 2 +- docs/src/tutorials/scenes.md | 2 +- src/Makie.jl | 2 +- src/basic_recipes/barplot.jl | 2 +- src/basic_recipes/datashader.jl | 12 +++++------ src/basic_recipes/raincloud.jl | 2 +- src/basic_recipes/text.jl | 2 +- src/basic_recipes/timeseries.jl | 2 +- src/bezier.jl | 2 +- src/configuration.yaml | 2 +- src/conversions.jl | 10 +++++----- src/dim-converts/categorical-integration.jl | 2 +- src/dim-converts/dates-integration.jl | 2 +- src/dim-converts/dim-converts.jl | 2 +- src/display.jl | 6 +++--- src/documentation/documentation.jl | 4 ++-- src/ffmpeg-util.jl | 2 +- src/float32-scaling.jl | 6 +++--- src/interaction/inspector.jl | 4 ++-- src/jl_rasterizer/bmp.jl | 4 ++-- src/makielayout/blocks/colorbar.jl | 20 +++++++++---------- src/makielayout/blocks/polaraxis.jl | 2 +- src/specapi.jl | 4 ++-- src/stats/violin.jl | 2 +- src/theming.jl | 6 +++--- src/types.jl | 2 +- src/utilities/utilities.jl | 4 ++-- test/Plane.jl | 2 +- test/PolarAxis.jl | 2 +- test/convert_arguments.jl | 2 +- test/events.jl | 2 +- test/float32convert.jl | 2 +- test/ray_casting.jl | 2 +- test/scenes.jl | 4 ++-- 69 files changed, 122 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05384f15cd0..a3e4de0309d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -169,7 +169,7 @@ - `boundingbox` overwrites must now include a secondary space argument to work `boundingbox(plot, space::Symbol = :data)` [#3723](https://github.com/MakieOrg/Makie.jl/pull/3723) - `boundingbox` now always consider `transform_func` and `model` - `data_limits(::Scatter)` and `boundingbox(::Scatter)` now consider marker transformations [#3716](https://github.com/MakieOrg/Makie.jl/pull/3716) -- **Breaking** Improved Float64 compatability of Axis [#3681](https://github.com/MakieOrg/Makie.jl/pull/3681) +- **Breaking** Improved Float64 compatibility of Axis [#3681](https://github.com/MakieOrg/Makie.jl/pull/3681) - This added an extra conversion step which only takes effect when Float32 precision becomes relevant. In those cases code using `project()` functions will be wrong as the transformation is not applied. Use `project(plot_or_scene, ...)` or apply the conversion yourself beforehand with `Makie.f32_convert(plot_or_scene, transformed_point)` and use `patched_model = Makie.patch_model(plot_or_scene, model)`. - `Makie.to_world(point, matrix, resolution)` has been deprecated in favor of `Makie.to_world(scene_or_plot, point)` to include float32 conversions. - **Breaking** Reworked line shaders in GLMakie and WGLMakie [#3558](https://github.com/MakieOrg/Makie.jl/pull/3558) @@ -230,7 +230,7 @@ ## [0.20.7] - 2024-02-04 - Equalized alignment point of mirrored ticks to that of normal ticks [#3598](https://github.com/MakieOrg/Makie.jl/pull/3598). -- Fixed stack overflow error on conversion of gridlike data with missings [#3597](https://github.com/MakieOrg/Makie.jl/pull/3597). +- Fixed stack overflow error on conversion of gridlike data with `missing`s [#3597](https://github.com/MakieOrg/Makie.jl/pull/3597). - Fixed mutation of CairoMakie src dir when displaying png files [#3588](https://github.com/MakieOrg/Makie.jl/pull/3588). - Added better error messages for plotting into `FigureAxisPlot` and `AxisPlot` as Plots.jl users are likely to do [#3596](https://github.com/MakieOrg/Makie.jl/pull/3596). - Added compat bounds for IntervalArithmetic.jl due to bug with DelaunayTriangulation.jl [#3595](https://github.com/MakieOrg/Makie.jl/pull/3595). @@ -246,7 +246,7 @@ - Use plot plot instead of scene transform functions in CairoMakie, fixing missplaced h/vspan. [#3552](https://github.com/MakieOrg/Makie.jl/pull/3552) - Fix error printing on shader error [#3530](https://github.com/MakieOrg/Makie.jl/pull/3530). - Update pagefind to 1.0.4 for better headline search [#3534](https://github.com/MakieOrg/Makie.jl/pull/3534). -- Remove unecessary deps, e.g. Setfield [3546](https://github.com/MakieOrg/Makie.jl/pull/3546). +- Remove unnecessary deps, e.g. Setfield [3546](https://github.com/MakieOrg/Makie.jl/pull/3546). - Don't clear args, rely on delete deregister_callbacks [#3543](https://github.com/MakieOrg/Makie.jl/pull/3543). - Add interpolate keyword for Surface [#3541](https://github.com/MakieOrg/Makie.jl/pull/3541). - Fix a DataInspector bug if inspector_label is used with RGB images [#3468](https://github.com/MakieOrg/Makie.jl/pull/3468). @@ -591,7 +591,7 @@ role as `datalimits` in `violin` [#2137](https://github.com/MakieOrg/Makie.jl/pu - **Breaking** Cleaned up `Scene` type [#1192](https://github.com/MakieOrg/Makie.jl/pull/1192), [#1393](https://github.com/MakieOrg/Makie.jl/pull/1393). The `Scene()` constructor doesn't create any axes or limits anymore. All keywords like `raw`, `show_axis` have been removed. A scene now always works like it did when using the deprecated `raw=true`. All the high level functionality like showing an axis and adding a 3d camera has been moved to `LScene`. See the new `Scene` tutorial for more info: https://docs.makie.org/dev/tutorials/scenes/. - **Breaking** Lights got moved to `Scene`, see the [lighting docs](https://docs.makie.org/stable/documentation/lighting) and [RPRMakie examples](https://docs.makie.org/stable/documentation/backends/rprmakie/). - Added ECDF plot [#1310](https://github.com/MakieOrg/Makie.jl/pull/1310). -- Added Order Independent Transparency to GLMakie [#1418](https://github.com/MakieOrg/Makie.jl/pull/1418), [#1506](https://github.com/MakieOrg/Makie.jl/pull/1506). This type of transparency is now used with `transpareny = true`. The old transparency handling is available with `transparency = false`. +- Added Order Independent Transparency to GLMakie [#1418](https://github.com/MakieOrg/Makie.jl/pull/1418), [#1506](https://github.com/MakieOrg/Makie.jl/pull/1506). This type of transparency is now used with `transparency = true`. The old transparency handling is available with `transparency = false`. - Fixed blurry text in GLMakie and WGLMakie [#1494](https://github.com/MakieOrg/Makie.jl/pull/1494). - Introduced a new experimental backend for ray tracing: [RPRMakie](https://docs.makie.org/stable/documentation/backends/rprmakie/). - Added the `Cycled` type, which can be used to select the i-th value from the current cycler for a specific attribute [#1248](https://github.com/MakieOrg/Makie.jl/pull/1248). diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 0f70934682b..4dc5d4deba9 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -154,7 +154,7 @@ end function draw_plot_as_image(scene::Scene, screen::Screen{RT}, primitive::Plot, scale::Number = 1) where RT # you can provide `p.rasterize = scale::Int` or `p.rasterize = true`, both of which are numbers - # Extract scene width in device indepentent units + # Extract scene width in device independent units w, h = size(scene) # Create a new Screen which renders directly to an image surface, # specifically for the plot's parent scene. diff --git a/CairoMakie/src/primitives.jl b/CairoMakie/src/primitives.jl index 795359670ca..4cf37ea8b7c 100644 --- a/CairoMakie/src/primitives.jl +++ b/CairoMakie/src/primitives.jl @@ -928,7 +928,7 @@ end function draw_mesh2D(screen, per_face_cols, vs::Vector{<: Point2}, fs::Vector{GLTriangleFace}) ctx = screen.context - # Priorize colors of the mesh if present + # Prioritize colors of the mesh if present # This is a hack, which needs cleaning up in the Mesh plot type! for (f, (c1, c2, c3)) in zip(fs, per_face_cols) @@ -984,7 +984,7 @@ function draw_mesh3D( meshuvs = map(uv -> uv_transform * to_ndim(Vec3f, uv, 1), meshuvs) end - # Priorize colors of the mesh if present + # Prioritize 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) diff --git a/CairoMakie/test/svg_tests.jl b/CairoMakie/test/svg_tests.jl index da30bba0948..8d7f4239437 100644 --- a/CairoMakie/test/svg_tests.jl +++ b/CairoMakie/test/svg_tests.jl @@ -61,7 +61,7 @@ end @test svg_isnt_rasterized(poly(MultiPolyWrapper([poly1, poly1]); color=[:red, :blue])) end -@testset "reproducable svg ids" begin +@testset "reproducible svg ids" begin # https://github.com/MakieOrg/Makie.jl/issues/2406 f, ax, sc = scatter(1:10) save("test1.svg", f) diff --git a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl index 2241931b7f4..edfba611746 100644 --- a/GLMakie/src/GLAbstraction/AbstractGPUArray.jl +++ b/GLMakie/src/GLAbstraction/AbstractGPUArray.jl @@ -192,7 +192,7 @@ max_dim(t) = error("max_dim not implemented for: $(typeof(t)). This happen function (::Type{GPUArrayType})(data::Observable; kw...) where GPUArrayType <: GPUArray gpu_mem = GPUArrayType(data[]; kw...) - # TODO merge these and handle update tracking during contruction + # TODO merge these and handle update tracking during construction obs2 = on(new_data -> update!(gpu_mem, new_data), data) if GPUArrayType <: TextureBuffer push!(gpu_mem.buffer.observers, obs2) diff --git a/GLMakie/src/GLAbstraction/GLAbstraction.jl b/GLMakie/src/GLAbstraction/GLAbstraction.jl index e8c36de91c7..67c6843ed10 100644 --- a/GLMakie/src/GLAbstraction/GLAbstraction.jl +++ b/GLMakie/src/GLAbstraction/GLAbstraction.jl @@ -53,7 +53,7 @@ export update! # updates a gpu array with a Julia array export gpu_data # gets the data of a gpu array as a Julia Array export RenderObject # An object which holds all GPU handles and datastructes to ready for rendering by calling render(obj) -export prerender! # adds a function to a RenderObject, which gets executed befor setting the OpenGL render state +export prerender! # adds a function to a RenderObject, which gets executed before setting the OpenGL render state export postrender! # adds a function to a RenderObject, which gets executed after setting the OpenGL render states export extract_renderable export set_arg! diff --git a/GLMakie/src/GLAbstraction/GLRender.jl b/GLMakie/src/GLAbstraction/GLRender.jl index 146eeb6dd44..357ce23781b 100644 --- a/GLMakie/src/GLAbstraction/GLRender.jl +++ b/GLMakie/src/GLAbstraction/GLRender.jl @@ -160,7 +160,7 @@ function renderinstanced(vao::GLVertexArray{GLBuffer{T}}, amount::Integer, primi end """ -Renders `amount` instances of an not indexed geoemtry geometry +Renders `amount` instances of an not indexed geometry geometry """ function renderinstanced(vao::GLVertexArray, amount::Integer, primitive=GL_TRIANGLES) glDrawElementsInstanced(primitive, length(vao), GL_UNSIGNED_INT, C_NULL, amount) diff --git a/GLMakie/src/GLAbstraction/GLRenderObject.jl b/GLMakie/src/GLAbstraction/GLRenderObject.jl index fe644f5c586..c47b2479849 100644 --- a/GLMakie/src/GLAbstraction/GLRenderObject.jl +++ b/GLMakie/src/GLAbstraction/GLRenderObject.jl @@ -30,7 +30,7 @@ function (sp::StandardPrerender)() glDepthFunc(GL_LEQUAL) end - # Disable cullface for now, untill all rendering code is corrected! + # Disable cullface for now, until all rendering code is corrected! glDisable(GL_CULL_FACE) # glCullFace(GL_BACK) diff --git a/GLMakie/src/GLAbstraction/GLTexture.jl b/GLMakie/src/GLAbstraction/GLTexture.jl index 39d3cf3bb60..e0334e490f7 100644 --- a/GLMakie/src/GLAbstraction/GLTexture.jl +++ b/GLMakie/src/GLAbstraction/GLTexture.jl @@ -430,7 +430,7 @@ default_colorformat_sym(::Type{T}) where {T <: Colorant} = default_colorformat_s @generated function default_colorformat(::Type{T}) where T sym = default_colorformat_sym(T) if !isdefined(ModernGL, sym) - error("$T doesn't have a propper mapping to an OpenGL format") + error("$T doesn't have a proper mapping to an OpenGL format") end :($sym) end @@ -462,7 +462,7 @@ end @generated function default_internalcolorformat(::Type{T}) where T sym = default_internalcolorformat_sym(T) if !isdefined(ModernGL, sym) - error("$T doesn't have a propper mapping to an OpenGL format") + error("$T doesn't have a proper mapping to an OpenGL format") end :($sym) end diff --git a/GLMakie/src/GLAbstraction/GLTypes.jl b/GLMakie/src/GLAbstraction/GLTypes.jl index b9ce99f0167..b580152fcb9 100644 --- a/GLMakie/src/GLAbstraction/GLTypes.jl +++ b/GLMakie/src/GLAbstraction/GLTypes.jl @@ -401,7 +401,7 @@ function RenderObject( program = gl_convert(to_value(program), data) # "compile" lazyshader vertexarray = GLVertexArray(Dict(buffers), program) - # remove all uniforms not occuring in shader + # remove all uniforms not occurring in shader # ssao, instances transparency are special for rendering passes. TODO do this more cleanly special = Set([:ssao, :transparency, :instances, :fxaa, :num_clip_planes]) for k in setdiff(keys(data), keys(program.nametype)) diff --git a/GLMakie/src/GLAbstraction/GLUtils.jl b/GLMakie/src/GLAbstraction/GLUtils.jl index 4176bdc97b9..5fe35eda5c7 100644 --- a/GLMakie/src/GLAbstraction/GLUtils.jl +++ b/GLMakie/src/GLAbstraction/GLUtils.jl @@ -28,7 +28,7 @@ gen_defaults! dict begin a = 55 b = a * 2 # variables, like a, will get made visible in local scope c::JuliaType = X # `c` needs to be of type JuliaType. `c` will be made available with it's original type and then converted to JuliaType when inserted into `dict` - d = x => GLType # OpenGL convert target. Get's only applied if `x` is convertible to GLType. Will only be converted when passed to RenderObject + d = x => GLType # OpenGL convert target. Gets only applied if `x` is convertible to GLType. Will only be converted when passed to RenderObject d = x => \"doc string\" d = x => (GLType, \"doc string and gl target\") end @@ -39,12 +39,12 @@ macro gen_defaults!(dict, args) a = 55 b = a * 2 # variables, like a, will get made visible in local scope c::JuliaType = X # c needs to be of type JuliaType. c will be made available with it's original type and then converted to JuliaType when inserted into data - d = x => GLType # OpenGL convert target. Get's only applied if x is convertible to GLType. Will only be converted when passed to RenderObject + d = x => GLType # OpenGL convert target. Gets only applied if x is convertible to GLType. Will only be converted when passed to RenderObject end") tuple_list = args.args dictsym = gensym() return_expression = Expr(:block) - push!(return_expression.args, :($dictsym = $dict)) # dict could also be an expression, so we need to asign it to a variable at the beginning + push!(return_expression.args, :($dictsym = $dict)) # dict could also be an expression, so we need to assign it to a variable at the beginning push!(return_expression.args, :(gl_convert_targets = get!($dictsym, :gl_convert_targets, Dict{Symbol, Any}()))) # exceptions for glconvert. push!(return_expression.args, :(doc_strings = get!($dictsym, :doc_string, Dict{Symbol, Any}()))) # exceptions for glconvert. # @gen_defaults can be used multiple times, so we need to reuse gl_convert_targets if already in here diff --git a/GLMakie/src/glshaders/lines.jl b/GLMakie/src/glshaders/lines.jl index c4fa81d8cf8..9f29d47d8b7 100644 --- a/GLMakie/src/glshaders/lines.jl +++ b/GLMakie/src/glshaders/lines.jl @@ -1,5 +1,5 @@ function sumlengths(points, resolution) - # normalize w component if availabke + # normalize w component if available f(p::VecTypes{4}) = p[Vec(1, 2)] / p[4] f(p::VecTypes) = p[Vec(1, 2)] @@ -45,10 +45,10 @@ function generate_indices(positions) # if A != F (no loop): 0 A B C D E F 0 # where 0 is NaN # It marks vertices as invalid (0) if they are NaN, valid (1) if they - # are part of a continous line section, or as ghost edges (2) used to + # are part of a continuous line section, or as ghost edges (2) used to # cleanly close a loop. The shader detects successive vertices with # 1-2-0 and 0-2-1 validity to avoid drawing ghost segments (E-A from - # 0-E-A-B and F-B from E-F-B-0 which would dublicate E-F and A-B) + # 0-E-A-B and F-B from E-F-B-0 which would duplicate E-F and A-B) last_start_pos = eltype(ps)(NaN) last_start_idx = -1 @@ -60,7 +60,7 @@ function generate_indices(positions) if not_nan if last_start_idx == -1 # place nan before section of line vertices - # (or dublicate ps[1]) + # (or duplicate ps[1]) push!(indices, i-1) last_start_idx = length(indices) + 1 last_start_pos = p diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 6188b400bd3..31bd584a089 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -571,7 +571,7 @@ function destroy!(rob::RenderObject) # but we do share the texture atlas, so we check v !== tex, since we can't just free shared resources # TODO, refcounting, or leaving freeing to GC... - # GC is a bit tricky with active contexts, so immediate free is prefered. + # GC is a bit tricky with active contexts, so immediate free is preferred. # I guess as long as we make it hard for users to share buffers directly, this should be fine! GLAbstraction.free(v) end @@ -978,7 +978,7 @@ function renderloop(screen) end if screen.close_after_renderloop try - @debug("Closing screen after quiting renderloop!") + @debug("Closing screen after quitting renderloop!") close(screen) catch e @warn "error closing screen" exception=(e, Base.catch_backtrace()) diff --git a/GLMakie/test/glmakie_refimages.jl b/GLMakie/test/glmakie_refimages.jl index d4e911474db..afc81dd8c17 100644 --- a/GLMakie/test/glmakie_refimages.jl +++ b/GLMakie/test/glmakie_refimages.jl @@ -10,7 +10,7 @@ using ReferenceTests.RNG # Directly access texture parameters: x = Sampler(fill(to_color(:yellow), 100, 100), minfilter=:nearest) scene = image(x) - # indexing will go straight to the GPU, while only transfering the changes + # indexing will go straight to the GPU, while only transferring the changes st = Stepper(scene) x[1:10, 1:50] .= to_color(:red) Makie.step!(st) diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index 6e08a85aba3..d62843af195 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -133,7 +133,7 @@ end end end -@testset "emtpy!(fig)" begin +@testset "empty!(fig)" begin GLMakie.closeall() fig = Figure() ax = Axis(fig[1,1]) diff --git a/MakieCore/src/conversion.jl b/MakieCore/src/conversion.jl index 16c450ffc17..09d91fd9f9b 100644 --- a/MakieCore/src/conversion.jl +++ b/MakieCore/src/conversion.jl @@ -139,7 +139,7 @@ to be overloaded for DimConversions, e.g. for CategoricalConversion: `has_typed_convert(plot_or_trait)` and `should_dim_convert(get_element_type(args))` are true. The former is defined as true by `@convert_target`, i.e. when `convert_arguments_typed` is defined for the given plot type or conversion trait. -The latter marks specific types as convertable. +The latter marks specific types as convertible. If a recipe wants to use dim conversions, it should overload this function: ```julia diff --git a/RPRMakie/src/meshes.jl b/RPRMakie/src/meshes.jl index d635d58a8f7..b77138d10f2 100644 --- a/RPRMakie/src/meshes.jl +++ b/RPRMakie/src/meshes.jl @@ -167,7 +167,7 @@ function to_rpr_object(context, matsys, scene, plot::Makie.Surface) positions = lift(grid, x, y, z, Makie.transform_func_obs(plot)) r = Tesselation(Rect2f((0, 0), (1, 1)), size(z[])) # decomposing a rectangle into uv and triangles is what we need to map the z coordinates on - # since the xyz data assumes the coordinates to have the same neighouring relations + # since the xyz data assumes the coordinates to have the same neighbouring relations # like a grid faces = decompose(GLTriangleFace, r) uv = decompose_uv(r) diff --git a/ReferenceTests/src/tests/categorical.jl b/ReferenceTests/src/tests/categorical.jl index bd2bd4865cb..4740ac2a4a4 100644 --- a/ReferenceTests/src/tests/categorical.jl +++ b/ReferenceTests/src/tests/categorical.jl @@ -11,7 +11,7 @@ using Makie: Categorical end @reference_test "different types without sorting function" begin - # If we set the ticks explicitely, with sortby defaulting to nothing, + # If we set the ticks explicitly, with sortby defaulting to nothing, # we can combine all objects: f = Figure() ax = Axis(f[1, 1]; @@ -42,7 +42,7 @@ end f end -@reference_test "new categories, inbetween old values" begin +@reference_test "new categories, in between old values" begin obs = Observable(Categorical(["a", "c", "e", "g"])) f, ax, p = scatter(1:4, obs, markersize=20, color=1:4, colormap=:viridis) obs[] = Categorical(["b", "d", "f", "h"]) diff --git a/ReferenceTests/src/tests/primitives.jl b/ReferenceTests/src/tests/primitives.jl index f13448dd51d..919e74e903b 100644 --- a/ReferenceTests/src/tests/primitives.jl +++ b/ReferenceTests/src/tests/primitives.jl @@ -573,7 +573,7 @@ end campixel!(scene) # marker is in front, so it should not be smaller than the background rectangle plot_row!(scene, 0, false) - # marker is in the background, so one shouldnt see a single pixel of the marker + # marker is in the background, so one shouldn't see a single pixel of the marker plot_row!(scene, 300, true) center = Point2f(size(scene) ./ 2) diff --git a/ReferenceUpdater/src/local_server.jl b/ReferenceUpdater/src/local_server.jl index 4496911129d..668af3fd6d9 100644 --- a/ReferenceUpdater/src/local_server.jl +++ b/ReferenceUpdater/src/local_server.jl @@ -276,7 +276,7 @@ function group_files(path, input_filename, output_filename) end end - # generate new structed file + # generate new structured file open(joinpath(path, output_filename), "w") do file for (filename, valid) in data println(file, diff --git a/ReferenceUpdater/src/reference_images.html b/ReferenceUpdater/src/reference_images.html index 29f4f69e309..61af9f3e80f 100644 --- a/ReferenceUpdater/src/reference_images.html +++ b/ReferenceUpdater/src/reference_images.html @@ -57,7 +57,7 @@

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. + Red cells fail CI (assuming the thresholds are up to date), yellow cells may but likely don't have significant visual difference and gray cells are visually equivalent.

diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index a6070ebeb4b..16ebcb93588 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -87,13 +87,13 @@ function render_with_init(screen::Screen, session::Session, scene::Scene) if isready(screen.plot_initialized) # plot_initialized contains already an item # This should not happen, but lets check anyways, so it errors and doesn't hang forever - error("Plot inititalized multiple times?") + error("Plot initialized multiple times?") end if initialized == true put!(screen.plot_initialized, true) mark_as_displayed!(screen, scene) else - # Will be an eror from WGLMakie.js + # Will be an error from WGLMakie.js put!(screen.plot_initialized, initialized) end return @@ -230,7 +230,7 @@ function get_screen_session(screen::Screen; timeout=100, success = Bonito.wait_for(() -> isready(screen.plot_initialized); timeout=timeout) # Throw error if error message specified if success !== :success - throw_error("Timed out waiting $(timeout)s for session to get initilize") + throw_error("Timed out waiting $(timeout)s for session to get initialize") return nothing end value = fetch(screen.plot_initialized) @@ -316,7 +316,7 @@ function insert_scene!(session::Session, screen::Screen, scene::Scene) scene_ser = serialize_scene(scene) parent = scene.parent parent_uuid = js_uuid(parent) - err = "Cant find scene js_uuid(scene) == $(parent_uuid)" + err = "Cannot find scene js_uuid(scene) == $(parent_uuid)" evaljs_value(session, js""" $(WGL).then(WGL=> { const parent = WGL.find_scene($(parent_uuid)); diff --git a/WGLMakie/src/lines.jl b/WGLMakie/src/lines.jl index 3a9864657c3..239b3cd2e37 100644 --- a/WGLMakie/src/lines.jl +++ b/WGLMakie/src/lines.jl @@ -41,7 +41,7 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) # This is mostly NaN handling. The shader only draws a segment if each # involved point are not NaN, i.e. p1 -- p2 is only drawn if all of - # (p0, p1, p2, p3) are not NaN. So if p3 is NaN we need to dublicate p2 to + # (p0, p1, p2, p3) are not NaN. So if p3 is NaN we need to duplicate p2 to # make the p1 -- p2 segment draw, which is what indices does. indices = Observable(UInt32[]) points_transformed = lift( @@ -76,12 +76,12 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) # (nan, i-2, j, i+1) the segment (i-2, j) will not # be drawn (which we want as that segment would overlap) - # tweak dublicated vertices to be loop vertices + # tweak duplicated vertices to be loop vertices push!(indices[], indices[][loop_start_idx+1]) indices[][loop_start_idx-1] = i-2 # nan is inserted at bottom (and not necessary for start/end) - else # no loop, dublicate end point + else # no loop, duplicate end point push!(indices[], i-1) end end @@ -90,7 +90,7 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) else if was_nan - # line section start - dublicate point + # line section start - duplicate point push!(indices[], i) # first point in a potential loop loop_start_idx = length(indices[])+1 @@ -102,7 +102,7 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) push!(indices[], i) end - # Finish line (insert dublicate end point or close loop) + # Finish line (insert duplicate end point or close loop) if !was_nan if loop_start_idx != -1 && (loop_start_idx + 2 < length(indices[])) && (transformed_points[indices[][loop_start_idx]] ≈ transformed_points[end]) @@ -145,9 +145,9 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) prev = scale .* Point2f(clip) ./ clip[4] # calculate cumulative pixel scale length - output[1] = 0f0 # dublicated point + output[1] = 0f0 # duplicated point output[2] = 0f0 # start of first line segment - output[end] = 0f0 # dublicated end point + output[end] = 0f0 # duplicated end point i = 3 # end of first line segment, start of second while i < length(ps) if isfinite(ps[i]) @@ -187,7 +187,7 @@ function serialize_three(scene::Scene, plot::Union{Lines, LineSegments}) uniforms[Symbol("$(name)_end")] = attr else # TODO: to js? - # dublicates per vertex attributes to match positional dublication + # duplicates per vertex attributes to match positional duplication # min(idxs, end) avoids update order issues here attributes[name] = lift(plot, indices, attr) do idxs, vals serialize_buffer_attribute(vals[min.(idxs, end)]) diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl index a2942c681b3..64adb5bda0b 100644 --- a/WGLMakie/src/serialization.jl +++ b/WGLMakie/src/serialization.jl @@ -317,7 +317,7 @@ end function serialize_plots(scene::Scene, plots::Vector{Plot}, result=[]) for plot in plots - # if no plots inserted, this truely is an atomic + # if no plots inserted, this truly is an atomic if isempty(plot.plots) plot_data = serialize_three(scene, plot) plot_data[:uuid] = js_uuid(plot) diff --git a/WGLMakie/src/voxel.jl b/WGLMakie/src/voxel.jl index a9d395565db..f8ba21e5242 100644 --- a/WGLMakie/src/voxel.jl +++ b/WGLMakie/src/voxel.jl @@ -89,7 +89,7 @@ function create_shader(scene::Scene, plot::Makie.Voxels) onany(plot, plot.gap, plot.converted[end]) do gap, chunk N = sum(size(chunk)) N_instances = ifelse(gap > 0.01, 2 * N, N + 3) - if N_instances != length(dummy_data[]) # avoid updating unneccesarily + if N_instances != length(dummy_data[]) # avoid updating unnecessarily dummy_data[] = [0f0 for _ in 1:N_instances] end return diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index f625dbe893d..a54d8ebce69 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -10,7 +10,7 @@ import Electron @testset for mime in Makie.WEB_MIMES @test showable(mime(), f) end - # I guess we explicitely don't say we can show those since it's highly Inefficient compared to html + # I guess we explicitly don't say we can show those since it's highly Inefficient compared to html # See: https://github.com/MakieOrg/Makie.jl/blob/master/WGLMakie/src/display.jl#L66-L68= @test !showable("image/png", f) @test !showable("image/jpeg", f) diff --git a/docs/src/explanations/backends/glmakie.md b/docs/src/explanations/backends/glmakie.md index fd43d9bd2c6..358e0af89ff 100644 --- a/docs/src/explanations/backends/glmakie.md +++ b/docs/src/explanations/backends/glmakie.md @@ -62,7 +62,7 @@ GLMakie has experimental support for displaying multiple independent figures (or ## Embedding There's experimental support for embedding GLMakie by creating a custom 'window' -type (analagous to the GLFW OS-level window) and grabbing GLMakie's framebuffers +type (analogous to the GLFW OS-level window) and grabbing GLMakie's framebuffers to display in your own GUI. Here's a high-level overview of what you'd need to do: @@ -91,7 +91,7 @@ do: calling `GLMakie.render_frame(screen)` whenever necessary (you can use `GLMakie.requires_update(screen)`). 1. `display(screen, f)` will only draw the figure to a framebuffer. You can get - the color texture attachement of the framebuffer with + the color texture attachment of the framebuffer with `screen.framebuffer.buffers[:color]`, and display that color texture as an image in your chosen GUI framework. 1. If interactivity is desired, you will need to pass input events from the diff --git a/docs/src/explanations/backends/rprmakie.md b/docs/src/explanations/backends/rprmakie.md index e697f59db95..fa3c7efd600 100644 --- a/docs/src/explanations/backends/rprmakie.md +++ b/docs/src/explanations/backends/rprmakie.md @@ -36,7 +36,7 @@ lights = [ ] # Only LScene is supported right now, -# since the other projections don't map to the pysical acurate Camera in RPR. +# since the other projections don't map to the physical accurate Camera in RPR. ax = LScene(fig[1, 1]; show_axis = false, scenekw=(lights=lights,)) # Note that since RPRMakie doesn't yet support text (this is being worked on!), # you can't show a 3d axis yet. diff --git a/docs/src/explanations/backends/wglmakie.md b/docs/src/explanations/backends/wglmakie.md index e2cf49153a5..27a919ea7f9 100644 --- a/docs/src/explanations/backends/wglmakie.md +++ b/docs/src/explanations/backends/wglmakie.md @@ -105,7 +105,7 @@ Bonito allows to record a statemap for all widgets, that satisfy the following i ```julia # must be true to be found inside the DOM is_widget(x) = true -# Updating the widget isn't dependant on any other state (only thing supported right now) +# Updating the widget isn't dependent on any other state (only thing supported right now) is_independant(x) = true # The values a widget can iterate function value_range end diff --git a/docs/src/reference/blocks/colorbar.md b/docs/src/reference/blocks/colorbar.md index a46f4df7e8f..dec42371b99 100644 --- a/docs/src/reference/blocks/colorbar.md +++ b/docs/src/reference/blocks/colorbar.md @@ -75,7 +75,7 @@ fig ``` -We can't use `cgrad(...; categorical=true)` for this, since it has an ambigious meaning for true categorical values. +We can't use `cgrad(...; categorical=true)` for this, since it has an ambiguous meaning for true categorical values. ## Attributes diff --git a/docs/src/reference/blocks/intervalslider.md b/docs/src/reference/blocks/intervalslider.md index c6bcee7b8cb..aec38a7ae92 100644 --- a/docs/src/reference/blocks/intervalslider.md +++ b/docs/src/reference/blocks/intervalslider.md @@ -16,7 +16,7 @@ If the mouse hovers over the central area of the interval and both buttons are e You can double-click the slider to reset it to the values present in `startvalues`. If `startvalues === Makie.automatic`, the full interval will be selected (this is the default). -If you set the attribute `snap = false`, the slider will move continously while dragging and only jump to the closest available values when releasing the mouse. +If you set the attribute `snap = false`, the slider will move continuously while dragging and only jump to the closest available values when releasing the mouse. ```@example intervalslider using GLMakie diff --git a/docs/src/reference/blocks/polaraxis.md b/docs/src/reference/blocks/polaraxis.md index 35a08499b97..04f67285706 100644 --- a/docs/src/reference/blocks/polaraxis.md +++ b/docs/src/reference/blocks/polaraxis.md @@ -80,7 +80,7 @@ Note that by default translations in adjustments of rmin and thetalimits are blo These can be unblocked by calling `autolimits!(ax[, true])` which also tells the PolarAxis to derive r- and thetalimits freely from data, or by setting `ax.fixrmin[] = false` and `ax.thetazoomlock[] = false`. -## Plot type compatability +## Plot type compatibility Not every plot type is compatible with the polar transform. For example `image` is not as it expects to be drawn on a rectangle. diff --git a/docs/src/reference/blocks/slider.md b/docs/src/reference/blocks/slider.md index a05362e9927..99d4c5df7ec 100644 --- a/docs/src/reference/blocks/slider.md +++ b/docs/src/reference/blocks/slider.md @@ -11,7 +11,7 @@ This is necessary to ensure the value is actually present in the `range` attribu You can double-click the slider to reset it (approximately) to the value present in `startvalue`. -If you set the attribute `snap = false`, the slider will move continously while dragging and only jump to the closest available value when releasing the mouse. +If you set the attribute `snap = false`, the slider will move continuously while dragging and only jump to the closest available value when releasing the mouse. ```@figure backend=GLMakie diff --git a/docs/src/reference/plots/datashader.md b/docs/src/reference/plots/datashader.md index 45f0f3a45dc..f000f369a58 100644 --- a/docs/src/reference/plots/datashader.md +++ b/docs/src/reference/plots/datashader.md @@ -203,7 +203,7 @@ datashader(Dict(:category_a => all_points_a, :category_b => all_points_b)) ``` The type of the category doesn't matter, but will get converted to strings internally, to be displayed nicely in the legend. -Categories are currently aggregated in one Canvas per category, and then overlayed with alpha blending. +Categories are currently aggregated in one Canvas per category, and then overlaid with alpha blending. ```@figure backend=GLMakie normaldist = randn(Point2f, 1_000_000) diff --git a/docs/src/reference/plots/heatmap.md b/docs/src/reference/plots/heatmap.md index e2971df597a..ff21cd163b0 100644 --- a/docs/src/reference/plots/heatmap.md +++ b/docs/src/reference/plots/heatmap.md @@ -126,7 +126,7 @@ setting the colorrange explicitly, so that it is independent of the data shown b that particular heatmap. Since the heatmaps in the example below have the same colorrange and colormap, any of them -can be passed to `Colorbar` to give the colorbar the same attributes. Alternativly, +can be passed to `Colorbar` to give the colorbar the same attributes. Alternatively, the colorbar attributes can be set explicitly. ```@figure diff --git a/docs/src/reference/plots/rainclouds.md b/docs/src/reference/plots/rainclouds.md index de99693c2ed..06d571b2ce0 100644 --- a/docs/src/reference/plots/rainclouds.md +++ b/docs/src/reference/plots/rainclouds.md @@ -177,7 +177,7 @@ rainclouds!( plot_boxplots = false, color = colors[indexin(category_labels, unique(category_labels))]) -# Plots wiht more categories +# Plots with more categories # dist_between_categories (0.6, 1.0) # with and without clouds diff --git a/docs/src/tutorials/scenes.md b/docs/src/tutorials/scenes.md index 91d98e929b4..cc62e243eae 100644 --- a/docs/src/tutorials/scenes.md +++ b/docs/src/tutorials/scenes.md @@ -8,7 +8,7 @@ scene = Scene(; clear = true, # the camera struct of the scene. visible = true, - # ssao and light are explained in more detail in `Documetation/Lighting` + # ssao and light are explained in more detail in `Documentation/Lighting` ssao = Makie.SSAO(), # Creates lights from theme, which right now defaults to ` # set_theme!(lightposition=:eyeposition, ambient=RGBf(0.5, 0.5, 0.5))` diff --git a/src/Makie.jl b/src/Makie.jl index 6d6a6452068..1137e94148d 100644 --- a/src/Makie.jl +++ b/src/Makie.jl @@ -198,7 +198,7 @@ include("layouting/text_layouting.jl") include("layouting/boundingbox.jl") include("layouting/text_boundingbox.jl") -# Declaritive SpecApi +# Declarative SpecApi include("specapi.jl") # more default recipes diff --git a/src/basic_recipes/barplot.jl b/src/basic_recipes/barplot.jl index 4cf1d69f7e5..a93aa92d28a 100644 --- a/src/basic_recipes/barplot.jl +++ b/src/basic_recipes/barplot.jl @@ -21,7 +21,7 @@ function bar_default_fillto(tf, ys, offset, in_y_direction) return ys, offset end -# `fillto` is related to `y-axis` transofrmation only, thus we expect `tf::Tuple` +# `fillto` is related to `y-axis` transformation only, thus we expect `tf::Tuple` function bar_default_fillto(tf::Tuple, ys, offset, in_y_direction) _logT = Union{typeof(log), typeof(log2), typeof(log10), Base.Fix1{typeof(log), <: Real}} if in_y_direction && tf[2] isa _logT || (!in_y_direction && tf[1] isa _logT) diff --git a/src/basic_recipes/datashader.jl b/src/basic_recipes/datashader.jl index cffd2b098d6..321d1d75819 100644 --- a/src/basic_recipes/datashader.jl +++ b/src/basic_recipes/datashader.jl @@ -15,10 +15,10 @@ abstract type AggOp end using Makie canvas = Canvas(-1, 1, -1, 1; op=AggCount(), resolution=(800, 800)) aggregate!(canvas, points; point_transform=reverse, method=AggThreads()) -aggregated_values = get_aggregation(canvas; operation=equalize_histogram, local_operation=identiy) -# Recipes are defined for canvas as well and incorperate the `get_aggregation`, but `aggregate!` must be called manually. -image!(canvas; operation=equalize_histogram, local_operation=identiy, colormap=:viridis, colorrange=(0, 20)) -surface!(canvas; operation=equalize_histogram, local_operation=identiy) +aggregated_values = get_aggregation(canvas; operation=equalize_histogram, local_operation=identity) +# Recipes are defined for canvas as well and incorporate the `get_aggregation`, but `aggregate!` must be called manually. +image!(canvas; operation=equalize_histogram, local_operation=identity, colormap=:viridis, colorrange=(0, 20)) +surface!(canvas; operation=equalize_histogram, local_operation=identity) ``` """ mutable struct Canvas @@ -291,7 +291,7 @@ For best performance, use `method=Makie.AggThreads()` and make sure to start jul @recipe DataShader (points,) begin """ Can be `AggCount()`, `AggAny()` or `AggMean()`. - Be sure, to use the correct element type e.g. `AggCount{Float32}()`, which needs to accomodate the output of `local_operation`. + Be sure, to use the correct element type e.g. `AggCount{Float32}()`, which needs to accommodate the output of `local_operation`. User-extensible by overloading: ```julia struct MyAgg{T} <: Makie.AggOp end @@ -500,7 +500,7 @@ function legendelements(plot::FakePlot, legend) return [PolyElement(; color=plot.attributes.color, strokecolor=legend.polystrokecolor, strokewidth=legend.polystrokewidth)] end -# Sadly we must define the colorbar here and cant use the default fallback, +# Sadly we must define the colorbar here and can't use the default fallback, # Since the Image plot will only see the scaled data, and since its hard to make Colorbar support the equalize_histogram # transform, we just create the colorbar form the raw data. # TODO, should we merge the local/global op with colorscale? diff --git a/src/basic_recipes/raincloud.jl b/src/basic_recipes/raincloud.jl index 767d6a32264..a450ae0c750 100644 --- a/src/basic_recipes/raincloud.jl +++ b/src/basic_recipes/raincloud.jl @@ -190,7 +190,7 @@ end function ungroup_labels(category_labels, data_array) if eltype(data_array) <: AbstractVector - @warn "Using a nested array for raincloud is deprected. Read raincloud's documentation and update your usage accordingly." + @warn "Using a nested array for raincloud is deprecated. Read raincloud's documentation and update your usage accordingly." data_array_ = reduce(vcat, data_array) category_labels_ = similar(category_labels, length(data_array_)) ix = 0 diff --git a/src/basic_recipes/text.jl b/src/basic_recipes/text.jl index 2c38861e438..a033fadeaef 100644 --- a/src/basic_recipes/text.jl +++ b/src/basic_recipes/text.jl @@ -189,7 +189,7 @@ function plot!(plot::Text{<:Tuple{<:AbstractArray{<:Tuple{<:Any, <:Point}}}}) pos_unequal = positions.val != poss strings_unequal && (strings.val = strs) pos_unequal && (positions.val = poss) - # Check for equality very imortant, otherwise we get an infinite loop + # Check for equality very important, otherwise we get an infinite loop strings_unequal && notify(strings) pos_unequal && notify(positions) diff --git a/src/basic_recipes/timeseries.jl b/src/basic_recipes/timeseries.jl index cb7bd8ba3bb..7409047fac5 100644 --- a/src/basic_recipes/timeseries.jl +++ b/src/basic_recipes/timeseries.jl @@ -10,7 +10,7 @@ scene = timeseries(signal) display(scene) # @async is optional, but helps to continue evaluating more code @async while isopen(scene) - # aquire data from e.g. a sensor: + # acquire data from e.g. a sensor: data = rand() # update the signal signal[] = data diff --git a/src/bezier.jl b/src/bezier.jl index 20107e0bdbf..03d74c32e34 100644 --- a/src/bezier.jl +++ b/src/bezier.jl @@ -642,7 +642,7 @@ function render_path(path, bitmap_size_px = 256) # freetype has no ClosePath and EllipticalArc, so those need to be replaced path_replaced = replace_nonfreetype_commands(path) - # Minimal size that becomes integer when mutliplying by 64 (target size for + # Minimal size that becomes integer when multiplying by 64 (target size for # atlas). This adds padding to avoid blurring/scaling factors from rounding # during sdf generation path_size = widths(bbox(path)) / maximum(widths(bbox(path))) diff --git a/src/configuration.yaml b/src/configuration.yaml index 5a7f32fec1c..ef9fb5f065b 100644 --- a/src/configuration.yaml +++ b/src/configuration.yaml @@ -18,7 +18,7 @@ theme: SSAO: enable: false - bias: 0.025 # z threshhold for occlusion + bias: 0.025 # z threshold for occlusion radius: 0.5 # range of sample positions (in world space) blur: 2 # A (2blur+1) by (2blur+1) range is used for blurring N_samples: 64 # number of samples (requires shader reload) diff --git a/src/conversions.jl b/src/conversions.jl index 81fdad94dcc..cfdd000a1ae 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -36,7 +36,7 @@ end # if no specific conversion is defined, we don't convert convert_single_argument(@nospecialize(x)) = x -# replace missings with NaNs +# replace `missing`s with `NaN`s function convert_single_argument(a::AbstractArray{<:Union{Missing, <:Real}}) return float_convert(a) end @@ -195,7 +195,7 @@ end function convert_arguments(::Type{<: Lines}, rect::Rect3{T}) where {T} PT = Point3{float_type(T)} points = unique(decompose(PT, rect)) - push!(points, PT(NaN)) # use to seperate linesegments + push!(points, PT(NaN)) # use to separate linesegments return (points[[1, 2, 3, 4, 1, 5, 6, 2, 9, 6, 8, 3, 9, 5, 7, 4, 9, 7, 8]],) end """ @@ -406,7 +406,7 @@ function convert_arguments(::CellGrid, x::EndPointsLike, y::EndPointsLike, Ty = typeof(ye[1]) # heatmaps are centered on the edges, so we need to adjust the range # This is done in conversions, since it's also how we calculate the boundingbox (heatmapplot.x, heatmap.y) - # We need the endpoint type here, since convert_arguments((0, 1), (0, 1), z), whcih only substracts the step + # We need the endpoint type here, since convert_arguments((0, 1), (0, 1), z), which only subtracts the step # Will end in a stackoverflow, since convert_arguments gets called every time the `args_in != args_out`. # If we return a different type with no conversion overload, it stops that recursion return (EndPoints{Tx}(xe[1] - xstep, xe[2] + xstep), EndPoints{Ty}(ye[1] - ystep, ye[2] + ystep), el32convert(z)) @@ -1136,7 +1136,7 @@ function line_diff_pattern(ls::Symbol, gaps::GapType = :normal) else error( """ - Unkown line style: $ls. Available linestyles are: + Unknown line style: $ls. Available linestyles are: :solid, :dash, :dot, :dashdot, :dashdotdot or a sequence of numbers enumerating the next transparent/opaque region. This sequence of numbers must be cumulative; 1 unit corresponds to 1 line width. @@ -1463,7 +1463,7 @@ function available_gradients() end -to_colormap(cm, categories::Integer) = error("`to_colormap(cm, categories)` is deprecated. Use `Makie.categorical_colors(cm, categories)` for categorical colors, and `resample_cmap(cmap, ncolors)` for continous resampling.") +to_colormap(cm, categories::Integer) = error("`to_colormap(cm, categories)` is deprecated. Use `Makie.categorical_colors(cm, categories)` for categorical colors, and `resample_cmap(cmap, ncolors)` for continuous resampling.") """ categorical_colors(colormaplike, categories::Integer) diff --git a/src/dim-converts/categorical-integration.jl b/src/dim-converts/categorical-integration.jl index 126aa859231..8d9d7936e74 100644 --- a/src/dim-converts/categorical-integration.jl +++ b/src/dim-converts/categorical-integration.jl @@ -14,7 +14,7 @@ scatter(1:4, Categorical(["a", "b", "c", "a"])) ``` ```julia -# Explicitely set them for other types: +# Explicitly set them for other types: struct Named value end diff --git a/src/dim-converts/dates-integration.jl b/src/dim-converts/dates-integration.jl index f7b6ac54189..b8d51646481 100644 --- a/src/dim-converts/dates-integration.jl +++ b/src/dim-converts/dates-integration.jl @@ -36,7 +36,7 @@ date_time_range = range(date_time, step=Week(5), length=10) # Automatically chose xticks as DateTeimeTicks: scatter(date_time_range, 1:10) -# explicitely chose DateTimeConversion and use it to plot unitful values into it and display in the `Time` format: +# explicitly chose DateTimeConversion and use it to plot unitful values into it and display in the `Time` format: using Makie.Unitful conversion = Makie.DateTimeConversion(Time) scatter(1:4, (1:4) .* u"s", axis=(dim2_conversion=conversion,)) diff --git a/src/dim-converts/dim-converts.jl b/src/dim-converts/dim-converts.jl index d4c32829311..bedb297883d 100644 --- a/src/dim-converts/dim-converts.jl +++ b/src/dim-converts/dim-converts.jl @@ -75,7 +75,7 @@ end # Recursively gets the dim convert from the plot -# This needs to be recursive to allow recipes to use dim converst +# This needs to be recursive to allow recipes to use dim convert # TODO, should a recipe always set the dim convert to it's parent? get_conversions(any) = nothing diff --git a/src/display.jl b/src/display.jl index b0d33f21979..c5ce72610de 100644 --- a/src/display.jl +++ b/src/display.jl @@ -22,7 +22,7 @@ end """ push_screen!(scene::Scene, screen::MakieScreen) -Adds a screen to the scene and registeres a clean up event when screen closes. +Adds a screen to the scene and registered a clean up event when screen closes. Also, makes sure that always just one screen is active for on scene. """ function push_screen!(scene::Scene, screen::T) where {T<:MakieScreen} @@ -140,7 +140,7 @@ function Base.display(figlike::FigureLike; backend=current_backend(), """) end - # We show inline if explicitely requested or if automatic and we can actually show something inline! + # We show inline if explicitly requested or if automatic and we can actually show something inline! scene = get_scene(figlike) if (inline === true || inline === automatic) && can_show_inline(backend) # We can't forward the screenconfig to show, but show uses the current screen if there is any @@ -187,7 +187,7 @@ end # Since VSCode doesn't call any display/show method for Figurelike if we return # `showable(mime, fig) == false`, we need to return `showable(mime, figlike) == true` # For some vscode displayable mime, even for `Makie.inline!(false)` when we want to display in our own window. -# Only diagnostic can be used for this, since other mimes expect something to be shown afterall and +# Only diagnostic can be used for this, since other mimes expect something to be shown after all and # therefore will look broken in the plotpane if we dont print anything to the IO. # I tried `throw(MethodError(...))` as well, but with plotpane enabled + showable == true, # VScode doesn't catch that method error. diff --git a/src/documentation/documentation.jl b/src/documentation/documentation.jl index 31001416569..f9879545d95 100644 --- a/src/documentation/documentation.jl +++ b/src/documentation/documentation.jl @@ -172,7 +172,7 @@ end """ to_func(Typ) -Maps the input of a Type name to its cooresponding function. +Maps the input of a Type name to its corresponding function. """ function to_func(Typ::Type{<: AbstractPlot{F}}) where F F @@ -184,7 +184,7 @@ to_func(func::Function) = func """ to_type(func) -Maps the input of a function name to its cooresponding Type. +Maps the input of a function name to its corresponding Type. """ to_type(func::Function) = Plot{func} diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl index 2bb14ffad61..9a9aea2d63f 100644 --- a/src/ffmpeg-util.jl +++ b/src/ffmpeg-util.jl @@ -50,7 +50,7 @@ struct VideoStreamOptions pixel_format, loop, loglevel::String, input::String, rawvideo::Bool=true) if !isa(framerate, Integer) - @warn "The given framefrate is not a subtype of `Integer`, and will be rounded to the nearest integer. To supress this warning, provide an integer as the framerate." + @warn "The given framefrate is not a subtype of `Integer`, and will be rounded to the nearest integer. To suppress this warning, provide an integer as the framerate." framerate = round(Int, framerate) end diff --git a/src/float32-scaling.jl b/src/float32-scaling.jl index deb4e5e0464..0b769fbe9b5 100644 --- a/src/float32-scaling.jl +++ b/src/float32-scaling.jl @@ -107,7 +107,7 @@ function patch_model(@nospecialize(plot), f32c::Float32Convert, model::Observabl onany(plot, f32c.scaling, model, update = true) do f32c, model # Neutral f32c can mean that data and model cancel each other and we - # still have Float32 preicsion issues inbetween. + # still have Float32 preicsion issues in between. # works with rotation component as well, but drops signs on scale trans, scale = decompose_translation_scale_matrix(model) @@ -122,7 +122,7 @@ function patch_model(@nospecialize(plot), f32c::Float32Convert, model::Observabl elseif is_float_safe(scale, trans) && is_rot_free # model can be applied on GPU and we can pull f32c through the # model matrix. This can be merged with the option below, but - # keeping them separate improves compatability with transform_marker + # keeping them separate improves compatibility with transform_marker scale = Vec3d(model[1, 1], model[2, 2], model[3, 3]) # existing scale is missing signs f32c_obs[] = Makie.LinearScaling( f32c.scale, ((f32c.scale .- 1) .* trans .+ f32c.offset) ./ scale @@ -195,7 +195,7 @@ conversion applied to the given limits results in a range not representable with Float32 to high enough precision, the conversion will update. After the update update the converted range will be -1 .. 1. -The function returns true if an update has occured. If `Nothing` is passed, the +The function returns true if an update has occurred. If `Nothing` is passed, the function always returns false. """ function update_limits!(c::Float32Convert, mini::VecTypes{3, Float64}, maxi::VecTypes{3, Float64}) diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index ba8a6901728..f5e166ef393 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -286,11 +286,11 @@ function DataInspector(scene::Scene; priority = 100, kwargs...) on_hover(inspector) end end - listners = onany(e.mouseposition, e.scroll) do _, _ + listeners = onany(e.mouseposition, e.scroll) do _, _ empty_channel!(channel) # remove queued up hover requests put!(channel, nothing) end - append!(inspector.obsfuncs, listners) + append!(inspector.obsfuncs, listeners) on(base_attrib.enable_indicators) do enabled if !enabled yield() diff --git a/src/jl_rasterizer/bmp.jl b/src/jl_rasterizer/bmp.jl index bf35407b4d3..e18a6d27f77 100644 --- a/src/jl_rasterizer/bmp.jl +++ b/src/jl_rasterizer/bmp.jl @@ -24,8 +24,8 @@ function writebmp(io, img) write(io, UInt32(0)) # image bits size write(io, Int32(0)) # horz resoluition in pixel / m write(io, Int32(0)) # vert resolutions (0x03C3 = 96 dpi, 0x0B13 = 72 dpi) - write(io, Int32(0)) #colors in pallete - write(io, Int32(0)) #important colors + write(io, Int32(0)) # colors in palette + write(io, Int32(0)) # important colors for i in h:-1:1 for j in 1:w diff --git a/src/makielayout/blocks/colorbar.jl b/src/makielayout/blocks/colorbar.jl index 02075a67272..9dc1be7ab0c 100644 --- a/src/makielayout/blocks/colorbar.jl +++ b/src/makielayout/blocks/colorbar.jl @@ -82,7 +82,7 @@ function extract_colormap_recursive(@nospecialize(plot::T)) where {T <: Abstract # Prefer ColorMapping if in doubt! cmaps = filter(x-> x isa ColorMapping, colormaps) length(cmaps) == 1 && return cmaps[1] - error("Multiple colormaps found for plot $(plot), please specify which one to use manually. Please overload `Makie.extract_colormap(::$(T))` to allow for the automatical creation of a Colorbar.") + error("Multiple colormaps found for plot $(plot), please specify which one to use manually. Please overload `Makie.extract_colormap(::$(T))` to allow for the automatic creation of a Colorbar.") end end end @@ -93,7 +93,7 @@ function Colorbar(fig_or_scene, plot::AbstractPlot; kwargs...) func = plotfunc(plot) if isnothing(cmap) error("Neither $(func) nor any of its children use a colormap. Cannot create a Colorbar from this plot, please create it manually. - If this is a recipe, one needs to overload `Makie.extract_colormap(::$(Plot{func}))` to allow for the automatical creation of a Colorbar.") + If this is a recipe, one needs to overload `Makie.extract_colormap(::$(Plot{func}))` to allow for the automatic creation of a Colorbar.") end if !(cmap isa ColorMapping) error("extract_colormap(::$(Plot{func})) returned an invalid value: $cmap. Needs to return either a `Makie.ColorMapping`.") @@ -222,9 +222,9 @@ function initialize_block!(cb::Colorbar) update_xyrange(barbox[], cb.vertical[], colors[], cmap.scale[], cmap.color_mapping_type[]) onany(update_xyrange, blockscene, barbox, cb.vertical, colors, cmap.scale, cmap.color_mapping_type) - # for continous colormaps we sample a 1d image + # for continuous colormaps we sample a 1d image # to avoid white lines when rendering vector graphics - continous_pixels = lift(blockscene, cb.vertical, colors, + continuous_pixels = lift(blockscene, cb.vertical, colors, cmap.color_mapping_type) do v, colors, mapping_type if mapping_type !== Makie.categorical colors = (colors[1:end-1] .+ colors[2:end]) ./2 @@ -235,26 +235,26 @@ function initialize_block!(cb::Colorbar) # TODO, implement interpolate = true for irregular grics in CairoMakie # Then, we can just use heatmap! and don't need the image plot! show_cats = Observable(false; ignore_equal_values=true) - show_continous = Observable(false; ignore_equal_values=true) + show_continuous = Observable(false; ignore_equal_values=true) on(blockscene, cmap.color_mapping_type; update=true) do type if type === continuous - show_continous[] = true + show_continuous[] = true show_cats[] = false else - show_continous[] = false + show_continuous[] = false show_cats[] = true end end heatmap!(blockscene, - xrange, yrange, continous_pixels; + xrange, yrange, continuous_pixels; colormap=colormap, visible=show_cats, inspectable=false ) image!(blockscene, - lift(extrema, xrange), lift(extrema, yrange), continous_pixels; + lift(extrema, xrange), lift(extrema, yrange), continuous_pixels; colormap = colormap, - visible = show_continous, + visible = show_continuous, inspectable = false ) diff --git a/src/makielayout/blocks/polaraxis.jl b/src/makielayout/blocks/polaraxis.jl index 13ce889dec0..efc6c2d3dcc 100644 --- a/src/makielayout/blocks/polaraxis.jl +++ b/src/makielayout/blocks/polaraxis.jl @@ -1,5 +1,5 @@ ################################################################################ -### Main Block Intialization +### Main Block Initialization ################################################################################ function initialize_block!(po::PolarAxis; palette=nothing) diff --git a/src/specapi.jl b/src/specapi.jl index 2f5e1a6e08d..8f4bf7dd11e 100644 --- a/src/specapi.jl +++ b/src/specapi.jl @@ -288,7 +288,7 @@ end function distance_score(a::BlockSpec, b::BlockSpec, scores_dict) a === b && return 0.0 - (a.type !== b.type) && return 100.0 # Cant update when types dont match + (a.type !== b.type) && return 100.0 # Can't update when types dont match get!(scores_dict, (a, b)) do scores = Float64[ distance_score(a.kwargs, b.kwargs, scores_dict), @@ -448,7 +448,7 @@ function update_plot!(obs_to_notify, plot::AbstractPlot, oldspec::PlotSpec, spec end end # Cycling needs to be handled separately sadly, - # since they're implicitely mutating attributes, e.g. if I re-use a plot + # since they're implicitly mutating attributes, e.g. if I re-use a plot # that has been on cycling position 2, and now I re-use it for the first plot in the list # it will need to change to the color of cycling position 1 if haskey(plot, :cycle) diff --git a/src/stats/violin.jl b/src/stats/violin.jl index 2e4f418223c..b42a61e7161 100644 --- a/src/stats/violin.jl +++ b/src/stats/violin.jl @@ -59,7 +59,7 @@ function plot!(plot::Violin) dodge, n_dodge, gap, dodge_gap, orientation x̂, violinwidth = compute_x_and_width(x, width, gap, dodge, n_dodge, dodge_gap) - # for horizontal violin just flip all componentes + # for horizontal violin just flip all components point_func = Point2f if orientation === :horizontal point_func = flip_xy ∘ point_func diff --git a/src/theming.jl b/src/theming.jl index 00841657e68..a4aebd95c33 100644 --- a/src/theming.jl +++ b/src/theming.jl @@ -9,7 +9,7 @@ function wong_colors(alpha = 1.0) RGB(0/255, 158/255, 115/255), # green RGB(204/255, 121/255, 167/255), # reddish purple RGB(86/255, 180/255, 233/255), # sky blue - RGB(213/255, 94/255, 0/255), # vermillion + RGB(213/255, 94/255, 0/255), # vermilion RGB(240/255, 228/255, 66/255), # yellow ] return RGBAf.(colors, alpha) @@ -64,7 +64,7 @@ const MAKIE_DEFAULT_THEME = Attributes( limits = automatic, SSAO = Attributes( # enable = false, - bias = 0.025f0, # z threshhold for occlusion + bias = 0.025f0, # z threshold for occlusion radius = 0.5f0, # range of sample positions (in world space) blur = Int32(2), # A (2blur+1) by (2blur+1) range is used for blurring # N_samples = 64, # number of samples (requires shader reload) @@ -72,7 +72,7 @@ const MAKIE_DEFAULT_THEME = Attributes( inspectable = true, clip_planes = Vector{Plane3f}(), - # Vec is equvalent to 36° right/east, 39° up/north from camera position + # Vec is equivalent to 36° right/east, 39° up/north from camera position # The order here is Vec3f(right of, up from, towards) viewer/camera light_direction = Vec3f(-0.45679495, -0.6293204, -0.6287243), camera_relative_light = true, # Only applies to default DirectionalLight diff --git a/src/types.jl b/src/types.jl index b99c5d56e60..7160cdc7b4e 100644 --- a/src/types.jl +++ b/src/types.jl @@ -21,7 +21,7 @@ include("interaction/iodevices.jl") Identifies the source of a tick: - `BackendTick`: A tick used for backend purposes which is not present in `event.tick`. -- `UnknownTickState`: A tick from an uncategorized source (e.g. intialization of Events). +- `UnknownTickState`: A tick from an uncategorized source (e.g. initialization of Events). - `PausedRenderTick`: A tick from a paused renderloop. - `SkippedRenderTick`: A tick from a running renderloop where the previous image was reused. - `RegularRenderTick`: A tick from a running renderloop where a new image was produced. diff --git a/src/utilities/utilities.jl b/src/utilities/utilities.jl index ee44ed34814..52e9cf7870a 100644 --- a/src/utilities/utilities.jl +++ b/src/utilities/utilities.jl @@ -298,7 +298,7 @@ function merged_get!(defaults::Function, key, scene::SceneLike, input::Attribute d = defaults() if haskey(theme(scene), key) # we need to merge theme(scene) with the defaults, because it might be an incomplete theme - # TODO have a mark that says "theme uncomplete" and only then get the defaults + # TODO have a mark that says "theme incomplete" and only then get the defaults d = merge!(to_value(theme(scene, key)), d) end return merge!(input, d) @@ -427,7 +427,7 @@ function nan_aware_normals(vertices::AbstractVector{<:GeometryBasics.PointMeta{D end function surface2mesh(xs, ys, zs::AbstractMatrix, transform_func = identity, space = :data) - # crate a `Matrix{Point3}` + # create a `Matrix{Point3}` # ps = matrix_grid(identity, xs, ys, zs) ps = matrix_grid(p -> apply_transform(transform_func, p, space), xs, ys, zs) # create valid tessellations (triangulations) for the mesh diff --git a/test/Plane.jl b/test/Plane.jl index f3cac789700..3b528b5344f 100644 --- a/test/Plane.jl +++ b/test/Plane.jl @@ -86,7 +86,7 @@ using Makie: Plane, Plane3f, Point3d, Rect3d, Vec3d @test eachindex(ps) == Makie.unclipped_indices([plane], ps, :other) end - @testset "Tranformations" begin + @testset "Transformations" begin # Test apply_transform() plane = Plane(Point3f(1), Vec3f(0,0,1)) v = normalize(2f0 .* rand(Vec3f) .- 1) diff --git a/test/PolarAxis.jl b/test/PolarAxis.jl index c05b24c0289..dcfc45535a3 100644 --- a/test/PolarAxis.jl +++ b/test/PolarAxis.jl @@ -13,7 +13,7 @@ ) rticklabelplot = po.overlay.plots[8].plots[1] - # Mostly for verfication that we got the right plot + # Mostly for verification that we got the right plot @test po.overlay.plots[8][1][] == [("0.0", Point2f(0.0, 0.0)), ("2.5", Point2f(0.25, 0.0)), ("5.0", Point2f(0.5, 0.0)), ("7.5", Point2f(0.75, 0.0)), ("10.0", Point2f(1.0, 0.0))] # automatic diff --git a/test/convert_arguments.jl b/test/convert_arguments.jl index 77e9e525815..a075c27b133 100644 --- a/test/convert_arguments.jl +++ b/test/convert_arguments.jl @@ -68,7 +68,7 @@ end @test_throws ArgumentError heatmap(1im) end -# custom vector type to ensure that the conversion can be overriden for vectors +# custom vector type to ensure that the conversion can be overridden for vectors struct MyConvVector <: AbstractVector{Float64} end Makie.convert_arguments(::PointBased, ::MyConvVector) = ([Point(10, 20)],) diff --git a/test/events.jl b/test/events.jl index 3e5ac1b2392..524d029e69e 100644 --- a/test/events.jl +++ b/test/events.jl @@ -230,7 +230,7 @@ Base.:(==)(l::Or, r::Or) = l.left == r.left && l.right == r.right - # Reset state so this is indepentent from the last checks + # Reset state so this is independent from the last checks scene = Scene(size=(800, 600)); e = events(scene) cam3d!(scene, fixed_axis=true, cad=false, zoom_shift_lookat=false) diff --git a/test/float32convert.jl b/test/float32convert.jl index cd2408c8509..d01ace2079d 100644 --- a/test/float32convert.jl +++ b/test/float32convert.jl @@ -16,7 +16,7 @@ using Makie: Mat4f, Vec2d, Vec3d, Point2d, Point3d, Point4d f32min = Float64(floatmin(Float32)) * f32c.resolution f32eps = Float64(eps(Float32)) * f32c.resolution - @testset "Intialization" begin + @testset "Initialization" begin @test f32c.scaling[] == unit_scaling @test f32c.resolution == 1f4 # this may be subject to change end diff --git a/test/ray_casting.jl b/test/ray_casting.jl index 1d159fc0e75..2d9d39ae11f 100644 --- a/test/ray_casting.jl +++ b/test/ray_casting.jl @@ -276,7 +276,7 @@ end end - # Optional - show selected positon + # Optional - show selected position # This may change the camera, so don't use it for test values # for apply_transform = false, add `transformation = transform` scatter!(scene, pos, color = :red, strokewidth = 1.0, strokecolor = :yellow, depth_shift = -1f-1) diff --git a/test/scenes.jl b/test/scenes.jl index a2c26e96b92..1a7876bcca7 100644 --- a/test/scenes.jl +++ b/test/scenes.jl @@ -4,8 +4,8 @@ @testset "getproperty(scene, :$field)" for field in fieldnames(Scene) @test getproperty(scene, field) !== missing # well, just don't error end - @test theme(nothing, :nonexistant, default=1) == 1 - @test theme(scene, :nonexistant, default=1) == 1 + @test theme(nothing, :nonexistent, default=1) == 1 + @test theme(scene, :nonexistent, default=1) == 1 end @testset "Lighting" begin From 0a67ffc71bfb15ef5d187d23aaaabc999cc0c6c4 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 1 Nov 2024 13:50:28 +0100 Subject: [PATCH 32/80] Improve closeall and renderloop task handling (#4538) * stop_renderloop needs atomic + cleanup * correct order --- GLMakie/src/screen.jl | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 31bd584a089..a6782635144 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -164,7 +164,7 @@ mutable struct Screen{GLWindow} <: MakieScreen shader_cache::GLAbstraction.ShaderCache framebuffer::GLFramebuffer config::Union{Nothing, ScreenConfig} - stop_renderloop::Bool + stop_renderloop::Threads.Atomic{Bool} rendertask::Union{Task, Nothing} timer::BudgetedTimer px_per_unit::Observable{Float32} @@ -207,7 +207,7 @@ mutable struct Screen{GLWindow} <: MakieScreen s = size(framebuffer) screen = new{GLWindow}( glscreen, owns_glscreen, shader_cache, framebuffer, - config, stop_renderloop, rendertask, BudgetedTimer(1.0 / 30.0), + config, Threads.Atomic{Bool}(stop_renderloop), rendertask, BudgetedTimer(1.0 / 30.0), Observable(0f0), screen2scene, screens, renderlist, postprocessors, cache, cache2plot, Matrix{RGB{N0f8}}(undef, s), Observable(Makie.UnknownTickState), @@ -661,11 +661,12 @@ Doesn't destroy the screen and instead frees it to be re-used again, if `reuse=t function Base.close(screen::Screen; reuse=true) @debug("Close screen!") set_screen_visibility!(screen, false) - stop_renderloop!(screen; close_after_renderloop=false) if screen.window_open[] # otherwise we trigger an infinite loop of closing screen.window_open[] = false end empty!(screen) + stop_renderloop!(screen; close_after_renderloop=false) + if reuse && screen.reuse @debug("reusing screen!") push!(SCREEN_REUSE_POOL, screen) @@ -684,20 +685,13 @@ function closeall(; empty_shader=true) empty!(LOADED_SHADERS) WARN_ON_LOAD[] = false end - while !isempty(SCREEN_REUSE_POOL) - screen = pop!(SCREEN_REUSE_POOL) - delete!(ALL_SCREENS, screen) - destroy!(screen) - end - if !isempty(SINGLETON_SCREEN) - screen = pop!(SINGLETON_SCREEN) - delete!(ALL_SCREENS, screen) - destroy!(screen) - end + while !isempty(ALL_SCREENS) screen = pop!(ALL_SCREENS) destroy!(screen) end + empty!(SINGLETON_SCREEN) + empty!(SCREEN_REUSE_POOL) return end @@ -815,7 +809,7 @@ end Makie.to_native(x::Screen) = x.glscreen function renderloop_running(screen::Screen) - return !screen.stop_renderloop && !isnothing(screen.rendertask) && !istaskdone(screen.rendertask) + return !screen.stop_renderloop[] && !isnothing(screen.rendertask) && !istaskdone(screen.rendertask) end function start_renderloop!(screen::Screen) @@ -823,7 +817,7 @@ function start_renderloop!(screen::Screen) screen.config.pause_renderloop = false return else - screen.stop_renderloop = false + screen.stop_renderloop[] = false task = @async screen.config.renderloop(screen) yield() if istaskstarted(task) @@ -844,7 +838,7 @@ function stop_renderloop!(screen::Screen; close_after_renderloop=screen.close_af # don't double close when stopping renderloop c = screen.close_after_renderloop screen.close_after_renderloop = close_after_renderloop - screen.stop_renderloop = true + screen.stop_renderloop[] = true screen.close_after_renderloop = c # stop_renderloop! may be called inside renderloop as part of close @@ -890,7 +884,7 @@ scalechangeobs(screen) = scalefactor -> scalechangeobs(screen, scalefactor) function vsynced_renderloop(screen) - while isopen(screen) && !screen.stop_renderloop + while isopen(screen) && !screen.stop_renderloop[] if screen.config.pause_renderloop pollevents(screen, Makie.PausedRenderTick); sleep(0.1) continue @@ -905,7 +899,7 @@ end function fps_renderloop(screen::Screen) reset!(screen.timer, 1.0 / screen.config.framerate) - while isopen(screen) && !screen.stop_renderloop + while isopen(screen) && !screen.stop_renderloop[] if screen.config.pause_renderloop pollevents(screen, Makie.PausedRenderTick) else @@ -935,7 +929,7 @@ 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 + while isopen(screen) && !screen.stop_renderloop[] pollevents(screen, tick_state) # GLFW poll if !screen.config.pause_renderloop && requires_update(screen) @@ -953,7 +947,7 @@ function on_demand_renderloop(screen::Screen) # push!(time_record, 1e-9 * (t - last_time)) # last_time = t end - cause = screen.stop_renderloop ? "stopped renderloop" : "closing window" + cause = screen.stop_renderloop[] ? "stopped renderloop" : "closing window" @debug("Leaving renderloop, cause: $(cause)") end From 695df644c7b6c653311effae185d55c62f6e6655 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Fri, 1 Nov 2024 13:47:32 -0700 Subject: [PATCH 33/80] Add attributes to the Raincloud definition (#4517) * Add attributes to the Raincloud definition * Update CHANGELOG.md --------- Co-authored-by: Simon --- CHANGELOG.md | 1 + src/basic_recipes/raincloud.jl | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e4de0309d..3b5e2b8f3a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489). +- Added the `jitter_width` and `side_nudge` attributes to the `raincloud` plot definition, so that they can be used as kwargs [#4517]https://github.com/MakieOrg/Makie.jl/pull/4517) - Expand PlotList plots to expose their child plots to the legend interface, allowing `axislegend`show plots within PlotSpecs as individual entries. [#4546](https://github.com/MakieOrg/Makie.jl/pull/4546) - Implement S.Colorbar(plotspec) [#4520](https://github.com/MakieOrg/Makie.jl/pull/4520). diff --git a/src/basic_recipes/raincloud.jl b/src/basic_recipes/raincloud.jl index a450ae0c750..bf5fb74116e 100644 --- a/src/basic_recipes/raincloud.jl +++ b/src/basic_recipes/raincloud.jl @@ -29,12 +29,6 @@ between each. # Keywords -## Violin/Histogram Plot Specific Keywords - -## Scatter Plot Specific Keywords -- `side_nudge`: Default value is 0.02 if `plot_boxplots` is true, otherwise `0.075` default. -- `jitter_width=0.05`: Determines the width of the scatter-plot bar in category x-axis - absolute terms. """ @recipe RainClouds (category_labels, data_array) begin """ @@ -116,6 +110,14 @@ between each. If `clouds=hist`, this passes down the number of bins to the histogram call. """ hist_bins = 30 + """ + Scatter plot specific. Default value is 0.02 if `plot_boxplots` is true, otherwise `0.075` default. + """ + side_nudge = automatic + """ + Determines the width of the scatter-plot bar in category x-axis absolute terms. + """ + jitter_width = 0.05 """ A single color, or a vector of colors, one for each point. @@ -246,12 +248,11 @@ function plot!(plot::RainClouds) # Scatter Plot defaults dependent on if there is a boxplot side_scatter_nudge_default = plot_boxplots ? 0.2 : 0.075 - jitter_width_default = 0.05 # Scatter Plot Settings - side_scatter_nudge = to_value(get(plot, :side_nudge, side_scatter_nudge_default)) + side_scatter_nudge = plot.side_nudge[] isa Makie.Automatic ? side_scatter_nudge_default : plot.side_nudge[] side_scatter_nudge < 0 && ArgumentError("`side_nudge` should be positive. Change `side` to :left, :right if you wish.") - jitter_width = abs(to_value(get(plot, :jitter_width, jitter_width_default))) + jitter_width = plot.jitter_width[] jitter_width < 0 && ArgumentError("`jitter_width` should be positive.") markersize = plot.markersize[] From 28862f9cc61161232e9fa9206503b7dc73f0623e Mon Sep 17 00:00:00 2001 From: damianodegaspari <144324904+damianodegaspari@users.noreply.github.com> Date: Sat, 2 Nov 2024 14:19:08 +0100 Subject: [PATCH 34/80] Spelling in the documentation - Update types.jl (#4559) --- src/makielayout/types.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/makielayout/types.jl b/src/makielayout/types.jl index 4da455bf423..83321578634 100644 --- a/src/makielayout/types.jl +++ b/src/makielayout/types.jl @@ -1829,7 +1829,7 @@ end xzpanelvisible = true "The limits that the axis tries to set given other constraints like aspect. Don't set this directly, use `xlims!`, `ylims!` or `limits!` instead." targetlimits = Rect3f(Vec3f(0, 0, 0), Vec3f(1, 1, 1)) - "The limits that the user has manually set. They are reinstated when calling `reset_limits!` and are set to nothing by `autolimits!`. Can be either a tuple (xlow, xhigh, ylow, high, zlow, zhigh) or a tuple (nothing_or_xlims, nothing_or_ylims, nothing_or_zlims). Are set by `xlims!`, `ylims!`, `zlims!` and `limits!`." + "The limits that the user has manually set. They are reinstated when calling `reset_limits!` and are set to nothing by `autolimits!`. Can be either a tuple (xlow, xhigh, ylow, yhigh, zlow, zhigh) or a tuple (nothing_or_xlims, nothing_or_ylims, nothing_or_zlims). Are set by `xlims!`, `ylims!`, `zlims!` and `limits!`." limits = (nothing, nothing, nothing) "The relative margins added to the autolimits in x direction." xautolimitmargin = (0.05, 0.05) From 2b814f463545ff3540ccda4b3b6cd6137964a200 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 4 Nov 2024 13:38:24 +0100 Subject: [PATCH 35/80] Make all backends support Screen() constructor (#4561) * make all backends support Screen() constructor * add changelog --- CHANGELOG.md | 1 + CairoMakie/src/display.jl | 6 ++++-- CairoMakie/src/screen.jl | 33 ++++++++++++++++++++++++++++++++- WGLMakie/src/display.jl | 6 ++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5e2b8f3a4..c831b8148e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Added empty constructor to all backends for `Screen` allowing `display(Makie.current_backend().Screen(), fig)` [#4561](https://github.com/MakieOrg/Makie.jl/pull/4561). - Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489). - Added the `jitter_width` and `side_nudge` attributes to the `raincloud` plot definition, so that they can be used as kwargs [#4517]https://github.com/MakieOrg/Makie.jl/pull/4517) - Expand PlotList plots to expose their child plots to the legend interface, allowing `axislegend`show plots within PlotSpecs as individual entries. [#4546](https://github.com/MakieOrg/Makie.jl/pull/4546) diff --git a/CairoMakie/src/display.jl b/CairoMakie/src/display.jl index 65a2671b37c..813b53b7b10 100644 --- a/CairoMakie/src/display.jl +++ b/CairoMakie/src/display.jl @@ -37,7 +37,9 @@ function Base.display(screen::Screen, scene::Scene; connect=false) return screen end -function Base.display(screen::Screen{IMAGE}, scene::Scene; connect=false) +function Base.display(screen::Screen{IMAGE}, scene::Scene; connect=false, screen_config...) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol,Any}(screen_config)) + screen = Makie.apply_screen_config!(screen, config, scene) path = joinpath(mktempdir(), "display.png") Makie.push_screen!(scene, screen) cairo_draw(screen, scene) @@ -80,7 +82,7 @@ function Makie.backend_show(screen::Screen{SVG}, io::IO, ::MIME"image/svg+xml", # xlink:href="someid" (but not xlink:href="data:someothercontent" which is how image data is attached) # url(#someid) svg = replace(svg, r"((?:(?:id|xlink:href)=\"(?!data:)[^\"]+)|url\(#[^)]+)" => SubstitutionString("\\1-$salt")) - + print(io, svg) return screen end diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index b2fc618fa0b..b6759fee88d 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -175,6 +175,31 @@ mutable struct Screen{SurfaceRenderType} <: Makie.MakieScreen antialias::Int # cairo_antialias_t visible::Bool config::ScreenConfig + + function Screen() + return new{IMAGE}() + end + function Screen{SurfaceRenderType}( + scene::Scene, + surface::Cairo.CairoSurface, + context::Cairo.CairoContext, + device_scaling_factor::Float64, + antialias::Int, + visible::Bool, + config::ScreenConfig + ) where {SurfaceRenderType} + + return new{SurfaceRenderType}( + scene, + surface, + context, + device_scaling_factor, + antialias, + visible, + config, + ) + end + end function Base.empty!(screen::Screen) @@ -192,6 +217,7 @@ end Base.close(screen::Screen) = empty!(screen) function destroy!(screen::Screen) + isdefined(screen, :surface) || return Cairo.destroy(screen.surface) Cairo.destroy(screen.context) end @@ -200,7 +226,10 @@ function Base.isopen(screen::Screen) return !(screen.surface.ptr == C_NULL || screen.context.ptr == C_NULL) end -Base.size(screen::Screen) = round.(Int, (screen.surface.width, screen.surface.height)) +function Base.size(screen::Screen) + isdefined(screen, :surface) || return (0, 0) + round.(Int, (screen.surface.width, screen.surface.height)) +end # 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 @@ -267,6 +296,7 @@ function Makie.apply_screen_config!( destroy!(old_screen) end apply_config!(screen, config) + screen.scene = scene return screen end @@ -275,6 +305,7 @@ function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig, scene: Makie.apply_screen_config!(screen, config, scene, nothing, MIME"image/png"()) end + function Screen(scene::Scene; screen_config...) config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol, Any}(screen_config)) return Screen(scene, config) diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 16ebcb93588..73717f390f9 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -66,6 +66,12 @@ mutable struct Screen <: Makie.MakieScreen end end +function Screen(; config...) + config = Makie.merge_screen_config(ScreenConfig, Dict{Symbol,Any}(config)) + return Screen(nothing, config) +end + + function scene_already_displayed(screen::Screen, scene=screen.scene) scene === nothing && return false screen.scene === scene || return false From 73750724173e5a8ae297d1229621f188b16dfa68 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:26:45 +0100 Subject: [PATCH 36/80] IOCapture VideoStream hang fix (#4562) * avoid hang when closing over VideoStream with IOCapture.capture * add test * add changelog entry * fix testset position relative to backend activation --- CHANGELOG.md | 1 + src/ffmpeg-util.jl | 4 +++- test/Project.toml | 1 + test/record.jl | 9 +++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c831b8148e6..2af1bc5031e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added the `jitter_width` and `side_nudge` attributes to the `raincloud` plot definition, so that they can be used as kwargs [#4517]https://github.com/MakieOrg/Makie.jl/pull/4517) - Expand PlotList plots to expose their child plots to the legend interface, allowing `axislegend`show plots within PlotSpecs as individual entries. [#4546](https://github.com/MakieOrg/Makie.jl/pull/4546) - Implement S.Colorbar(plotspec) [#4520](https://github.com/MakieOrg/Makie.jl/pull/4520). +- Fixed a hang when `Record` was created inside a closure passed to `IOCapture.capture` [#4562](https://github.com/MakieOrg/Makie.jl/pull/4562). ## [0.21.15] - 2024-10-25 diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl index 9a9aea2d63f..6cd0341069d 100644 --- a/src/ffmpeg-util.jl +++ b/src/ffmpeg-util.jl @@ -266,7 +266,9 @@ function VideoStream(fig::FigureLike; buffer = Matrix{RGB{N0f8}}(undef, xdim, ydim) vso = VideoStreamOptions(format, framerate, compression, profile, pixel_format, loop, loglevel, "pipe:0", true) cmd = to_ffmpeg_cmd(vso, xdim, ydim) - process = open(`$(FFMPEG_jll.ffmpeg()) $cmd $path`, "w") + # a plain `open` without the `pipeline` causes hangs when IOCapture.capture closes over a function that creates + # a `VideoStream` without closing the process explicitly, such as when returning `Record` in a cell in Documenter or quarto + process = open(pipeline(`$(FFMPEG_jll.ffmpeg()) $cmd $path`; stdout = devnull, stderr = devnull), "w") tick_controller = TickController(fig, 1.0 / vso.framerate, filter_ticks) result = VideoStream(process.in, process, screen, tick_controller, buffer, abspath(path), vso) finalizer(result) do x diff --git a/test/Project.toml b/test/Project.toml index 42c77e8b94a..2ccea4ae5ba 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,6 +2,7 @@ CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" KernelDensity = "5ab0869b-81aa-558d-bb23-cbf5423bbe9b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/test/record.jl b/test/record.jl index 9e2310a28ed..9d2a92d298b 100644 --- a/test/record.jl +++ b/test/record.jl @@ -1,4 +1,5 @@ using Logging +using IOCapture: IOCapture module VideoBackend using Makie @@ -77,4 +78,12 @@ mktempdir() do tempdir end end end + +@testset "No hang when closing IOCapture.capture over VideoStream" begin + @test_nowarn IOCapture.capture() do + f = Figure() + Makie.VideoStream(f) + end +end + Makie.set_active_backend!(missing) From 0f4b02d1fc9febf6ce0333d81cf5358046a860e0 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 4 Nov 2024 15:06:58 +0100 Subject: [PATCH 37/80] Allow plots to move between scenes in SpecApi (#4478) * refactor JS Plot object to be movable * allow to move plots between scenes * correctly close channel * re-use plots, that got moved between axes * remove lock * fix specapi * fix makie tests * fix CairoMakie * Update CHANGELOG.md * try showing more infos * implement moveto! correctly for GLMakie and test for all backends * test & fix move_to! * add another test * make move_to optional * revert GLMakie changes * revert more changes * fix WGLMakie move_to * rename test * improve tests * clean up test --- CHANGELOG.md | 1 + MakieCore/src/recipes.jl | 4 +- ReferenceTests/src/tests/examples2d.jl | 42 +- ReferenceTests/src/tests/specapi.jl | 35 +- ReferenceTests/src/tests/updating.jl | 70 ++- ReferenceUpdater/src/local_server.jl | 10 +- WGLMakie/src/Serialization.js | 297 +++++++----- WGLMakie/src/display.jl | 7 +- WGLMakie/src/picking.jl | 6 +- WGLMakie/src/serialization.jl | 26 +- WGLMakie/src/three_plot.jl | 17 + WGLMakie/src/wglmakie.bundled.js | 598 ++++++++++++++----------- WGLMakie/test/runtests.jl | 8 +- src/interaction/inspector.jl | 63 +-- src/layouting/transformation.jl | 2 + src/scenes.jl | 44 +- src/specapi.jl | 102 +++-- test/specapi.jl | 6 +- 18 files changed, 844 insertions(+), 494 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af1bc5031e..74cebd149f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] +- Allow plots to move between scenes in SpecApi [#4132](https://github.com/MakieOrg/Makie.jl/pull/4132). - Added empty constructor to all backends for `Screen` allowing `display(Makie.current_backend().Screen(), fig)` [#4561](https://github.com/MakieOrg/Makie.jl/pull/4561). - Added `subsup` and `left_subsup` functions that offer stacked sub- and superscripts for `rich` text which means this style can be used with arbitrary fonts and is not limited to fonts supported by MathTeXEngine.jl [#4489](https://github.com/MakieOrg/Makie.jl/pull/4489). - Added the `jitter_width` and `side_nudge` attributes to the `raincloud` plot definition, so that they can be used as kwargs [#4517]https://github.com/MakieOrg/Makie.jl/pull/4517) diff --git a/MakieCore/src/recipes.jl b/MakieCore/src/recipes.jl index 6e8da443ba6..6d87c95a565 100644 --- a/MakieCore/src/recipes.jl +++ b/MakieCore/src/recipes.jl @@ -435,7 +435,7 @@ function default_theme(scene, T::Type{<: Plot}) end return attr end - + function extract_docstring(str) if VERSION >= v"1.11" && str isa Base.Docs.DocStr return only(str.text::Core.SimpleVector) @@ -502,7 +502,7 @@ function create_recipe_expr(Tsym, args, attrblock) end function ($funcname!)(args...; kw...) kwdict = Dict{Symbol, Any}(kw) - _create_plot!($funcname, kwdict, args...) + _create_plot!($funcname, kwdict, args...) end $(arg_type_func) diff --git a/ReferenceTests/src/tests/examples2d.jl b/ReferenceTests/src/tests/examples2d.jl index 94414126528..89ef03ce26a 100644 --- a/ReferenceTests/src/tests/examples2d.jl +++ b/ReferenceTests/src/tests/examples2d.jl @@ -1498,14 +1498,14 @@ 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), + 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 ) @@ -1513,7 +1513,7 @@ end color = :gray, side = :left ) - violin!(categories, values, orientation = :horizontal, + violin!(categories, values, orientation = :horizontal, color = :yellow, side = :right, strokewidth = 2, strokecolor = :black, weights = abs.(values) ) @@ -1607,7 +1607,7 @@ 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) @@ -1646,16 +1646,16 @@ 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, + + crossbar(fig[1, 2], xs, ys, ymins, ymaxs, dodge = dodge, dodge_gap = 0.25, gap = 0.05, midlinecolor = :blue, midlinewidth = 5, @@ -1679,7 +1679,7 @@ end w = @. x^2 * (1 - x)^2 ecdfplot(f[1, 2], x) ecdfplot!(x; weights = w, color=:orange) - + f end @@ -1710,18 +1710,18 @@ end 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, + 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 +@reference_test "series" begin fig = Figure() data = cumsum(RNG.randn(4, 21), dims = 2) @@ -1729,7 +1729,7 @@ end 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, + 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) @@ -1741,11 +1741,11 @@ end 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 @@ -1760,7 +1760,7 @@ end 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), @@ -1782,21 +1782,21 @@ end fig = Figure() waterfall(fig[1, 1], y) - waterfall(fig[1, 2], y, show_direction = true, marker_pos = :cross, + 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], + 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], + + waterfall(fig[2, 2], x, y, dodge = group, color = colors[group], show_direction = true, stack = :x, show_final = true) fig diff --git a/ReferenceTests/src/tests/specapi.jl b/ReferenceTests/src/tests/specapi.jl index ab5847915d9..f6bd6fb8651 100644 --- a/ReferenceTests/src/tests/specapi.jl +++ b/ReferenceTests/src/tests/specapi.jl @@ -117,12 +117,39 @@ end st end +AxNoTicks(;kw...) = S.Axis(; xticksvisible=false, + yticksvisible=false, yticklabelsvisible=false, + xticklabelsvisible=false, kw...) + +@reference_test "Moving Plots in SpecApi" begin + pl1 = S.Heatmap((1, 4), (1, 4), Makie.peaks(50)) + pl2 = S.Scatter(1:4; color=1:4, markersize=30, strokewidth=1, strokecolor=:black) + ax1 = AxNoTicks(; plots=[pl1, pl2]) + grid = S.GridLayout(AxNoTicks()) + f, _, pl = plot(S.GridLayout([ax1 grid]; colgaps=Fixed(4)); figure=(; figure_padding=2, size=(500, 100))) + cb1 = copy(colorbuffer(f)) + + pl1 = S.Heatmap((1, 4), (1, 4), Makie.peaks(50); colormap=:inferno) + ax1 = AxNoTicks() + grid = S.GridLayout(AxNoTicks(; plots=[pl1, pl2])) + pl[1] = S.GridLayout([ax1 grid]; colgaps=Fixed(4)) + cb2 = copy(colorbuffer(f)) + + pl1 = S.Heatmap((1, 4), (1, 4), Makie.peaks(50)) + ax1 = AxNoTicks(; plots=[pl1]) + ax2 = S.GridLayout(AxNoTicks(; plots=[pl2])) + pl[1] = S.GridLayout([ax1 ax2]; colgaps=Fixed(4)) + cb3 = copy(colorbuffer(f)) + + imgs = hcat(rotr90.((cb1, cb2, cb3))...) + s = Scene(; size=size(imgs)) + image!(s, imgs; space=:pixel) + s +end + function to_plot(plots) axes = map(permutedims(plots)) do plot - ax = S.Axis(; - plots=[plot], xticksvisible=false, - yticksvisible=false, yticklabelsvisible=false, - xticklabelsvisible=false) + ax = AxNoTicks(; plots=[plot]) return S.GridLayout([ax S.Colorbar(plot)]) end return S.GridLayout(axes) diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index b80a3b2ef12..1482da75cc3 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -175,7 +175,7 @@ end end @reference_test "event ticks in record" begin - # Checks whether record calculates and triggers event.tick by drawing a + # 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 @@ -192,6 +192,68 @@ end f end +@reference_test "Moving plots with move_to" begin + f, ax, pl1 = scatter(5:-1:1; markersize=20, axis=(; title="Axis 1")) + pl2 = poly!(ax, Rect2f(10, 10, 100, 100); color=:green, space=:pixel) + pl3 = scatter!(ax, 1:5; color=Float64[1:5;], markersize=0.5, colorrange=(1, 5), lowclip=:black, + highclip=:red, markerspace=:data) + pl4 = poly!(ax, Rect2f(0, 0, 1, 1); color=(:green, 0.5), strokewidth=2, strokecolor=:black) + f + img1 = copy(colorbuffer(f; px_per_unit=1)) + plots = [pl1, pl2, pl3, pl4] + get_listener_lengths() = map(plots) do x + arg_l = length(x[1].listeners) + attr_l = length(x.color.listeners) + return [arg_l, attr_l] + end + listener_lengths_1 = get_listener_lengths() + + ax2 = Axis(f[1, 2]; title="Axis 2") + ls = LScene(f[2, :]; show_axis=false) + scene = Makie.camrelative(ls.scene) + + Makie.move_to!(pl2, ax2.scene) + Makie.move_to!(pl3, ax2.scene) + Makie.move_to!(pl4, scene) + # Make sure updating still works for color + pl3.color = [-1, 2, 3, 4, 7] + pl3.colormap = :inferno + pl3.markersize = 1 + + @test listener_lengths_1 == get_listener_lengths() + + img2 = copy(colorbuffer(f; px_per_unit=1)) + @test length(ax.scene.plots) == 1 + @test ax.scene.plots[1] === pl1 + @test length(ax2.scene.plots) == 2 + @test pl2 in ax2.scene.plots + @test pl3 in ax2.scene.plots + @test pl4 in scene.plots + @test length(scene) == 1 + + # Move everything back + pl3.color = Float64[1:5;] + pl3.colormap = :viridis + pl3.markersize = 0.5 + Makie.move_to!(pl1, ax.scene) + Makie.move_to!(pl2, ax.scene) + Makie.move_to!(pl3, ax.scene) + Makie.move_to!(pl4, ax.scene) + # Make it easier to see similarity to first plot, by removing new scenes + delete!(ls) + delete!(ax2) + trim!(f.layout) + + img3 = copy(colorbuffer(f; px_per_unit=1)) + + @test listener_lengths_1 == get_listener_lengths() + + imgs = hcat(rotr90.((img1, img2, img3))...) + s = Scene(; size=size(imgs)) + image!(s, imgs; space=:pixel) + s +end + @reference_test "updating surface size" begin X = Observable(-5:5) Y = Observable(-5:5) @@ -200,8 +262,8 @@ end f = Figure(size = (800, 400)) surface(f[1, 1], X, Y, Z) surface(f[1, 2], map(collect, X), map(collect, Y), Z) - surface(f[1, 3], - map((X, Y) -> [x for x in X, y in Y], X, Y), + surface(f[1, 3], + map((X, Y) -> [x for x in X, y in Y], X, Y), map((X, Y) -> [y for x in X, y in Y], X, Y), Z) st = Stepper(f) Makie.step!(st) @@ -215,4 +277,4 @@ end Z.val = [0.01 * x*x * y*y for x in X.val, y in Y.val] notify(Z) Makie.step!(st) -end \ No newline at end of file +end diff --git a/ReferenceUpdater/src/local_server.jl b/ReferenceUpdater/src/local_server.jl index 668af3fd6d9..192ce6a9d65 100644 --- a/ReferenceUpdater/src/local_server.jl +++ b/ReferenceUpdater/src/local_server.jl @@ -28,7 +28,7 @@ function serve_update_page_from_dir(folder) @info "Downloading latest reference folder for $tag" tempdir = download_refimages(tag) - + @info "Updating files in $tempdir" for image in images_to_update @@ -253,7 +253,7 @@ 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 @@ -261,12 +261,12 @@ function group_files(path, input_filename, output_filename) data = Dict{String, Vector{Bool}}() open(joinpath(path, input_filename), "r") do file for filepath in eachline(file) - pieces = split(filepath, '/') + pieces = splitpath(filepath) backend = pieces[1] if !(backend in ("GLMakie", "CairoMakie", "WGLMakie")) - error("Failed to parse backend in \"$line\", got \"$backend\"") + error("Failed to parse backend in \"$pieces\", got \"$backend\"") end - + filename = join(pieces[2:end], '/') exists = get!(data, filename, [false, false, false]) diff --git a/WGLMakie/src/Serialization.js b/WGLMakie/src/Serialization.js index 09de2ff7e8f..3c3a876fdc0 100644 --- a/WGLMakie/src/Serialization.js +++ b/WGLMakie/src/Serialization.js @@ -2,6 +2,144 @@ import * as THREE from "./THREE.js"; import * as Camera from "./Camera.js"; import { create_line, create_linesegments } from "./Lines.js"; + +/** + * Updates the value of a given uniform with a new value. + * + * @param {THREE.Uniform} uniform - The uniform to update. + * @param {Object|Array} new_value - The new value to set for the uniform. If the uniform is a texture, this should be an array containing the size and texture data. + */ +function update_uniform(uniform, new_value) { + if (uniform.value.isTexture) { + const im_data = uniform.value.image; + const [size, tex_data] = new_value; + if (tex_data.length == im_data.data.length) { + im_data.data.set(tex_data); + } else { + const old_texture = uniform.value; + uniform.value = re_create_texture(old_texture, tex_data, size); + old_texture.dispose(); + } + uniform.value.needsUpdate = true; + } else { + if (is_three_fixed_array(uniform.value)) { + uniform.value.fromArray(new_value); + } else { + uniform.value = new_value; + } + } +} + + +class Plot { + mesh = undefined; + parent = undefined; + uuid = ""; + name = ""; + is_instanced = false; + geometry_needs_recreation = false; + plot_data = {}; + + constructor(scene, data) { + + this.plot_data = data; + + connect_plot(scene, this); + + if (data.plot_type === "lines") { + this.mesh = create_line(scene, this.plot_data); + } else if (data.plot_type === "linesegments") { + this.mesh = create_linesegments(scene, this.plot_data); + } else if ("instance_attributes" in data) { + this.is_instanced = true + this.mesh = create_instanced_mesh(scene, this.plot_data); + } else { + this.mesh = create_mesh(scene, this.plot_data); + } + + this.name = data.name; + this.uuid = data.uuid; + this.mesh.plot_uuid = data.uuid; + + this.mesh.frustumCulled = false; + this.mesh.matrixAutoUpdate = false; + this.mesh.renderOrder = data.zvalue; + + + data.uniform_updater.on(([name, data]) => { + this.update_uniform(name, data); + }); + + if ( + !(data.plot_type === "lines" || data.plot_type === "linesegments") + ) { + connect_attributes(this.mesh, data.attribute_updater); + } + this.parent = scene; + // Give mesh a reference to the plot object. + this.mesh.plot_object = this; + this.mesh.visible = data.visible.value; + data.visible.on(v=> { + this.mesh.visible = v; + }); + + } + + move_to(scene) { + if (scene === this.parent) { + return + } + this.parent.remove(this.mesh) + connect_plot(scene, this) + scene.add(this.mesh) + this.parent = scene + return + } + + update(attributes) { + attributes.keys().forEach(key=> { + const value = attributes[key] + if (value.type == "uniform") { + this.update_uniform(key, value.data); + } else if (value.type === "geometry") { + this.update_geometries(value.data) + } else if (value.type === "faces") { + this.update_faces(value.data) + } + }) + // For e.g. when we need to re-create the geometry + this.apply_updates() + } + + update_uniform(name, new_data) { + const uniform = this.mesh.material.uniforms[name]; + if (!uniform) { + throw new Error(`Uniform ${name} doesn't exist in Plot: ${this.name}`) + } + update_uniform(uniform, new_data); + } + + update_geometry(name, new_data) { + buffer = this.mesh.geometry.attributes[name]; + if (!buffer) { + throw new Error(`Buffer ${name} doesn't exist in Plot: ${this.name}`) + } + const old_length = buffer.count + if (new_data.length <= old_length) { + buffer.set(new_data.data); + buffer.needsUpdate = true; + } else { + // if we have a larger size we need resizing + recreation of the buffer geometry + buffer.to_update = new_data.data; + this.geometry_needs_recreation = true; + } + } + + update_faces(face_data) { + this.mesh.geometry.setIndex(new THREE.BufferAttribute(face_data, 1)); + } +} + // global scene cache to look them up for dynamic operations in Makie // e.g. insert!(scene, plot) / delete!(scene, plot) const scene_cache = {}; @@ -139,130 +277,71 @@ export function deserialize_uniforms(scene, data) { return result; } -export function deserialize_plot(scene, data) { - let mesh; - const update_visible = (v) => { - mesh.visible = v; - // don't return anything, since that will disable on_update callback - return; - }; - if (data.plot_type === "lines") { - mesh = create_line(scene, data); - } else if (data.plot_type === "linesegments") { - mesh = create_linesegments(scene, data); - } else if ("instance_attributes" in data) { - mesh = create_instanced_mesh(scene, data); - } else { - mesh = create_mesh(scene, data); - } - mesh.name = data.name; - mesh.frustumCulled = false; - mesh.matrixAutoUpdate = false; - mesh.plot_uuid = data.uuid; - mesh.renderOrder = data.zvalue; - update_visible(data.visible.value); - data.visible.on(update_visible); - connect_uniforms(mesh, data.uniform_updater); - if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { - connect_attributes(mesh, data.attribute_updater); - } - return mesh; -} - const ON_NEXT_INSERT = new Set(); export function on_next_insert(f) { ON_NEXT_INSERT.add(f); } -export function add_plot(scene, plot_data) { +/** + * Connects a plot to a scene by setting up the necessary camera uniforms. + * + * @param {THREE.Scene} scene - The scene object containing the camera and screen information. + * @param {Plot} plot - The plot object to be connected to the scene. + */ +function connect_plot(scene, plot) { // fill in the camera uniforms, that we don't sent in serialization per plot const cam = scene.wgl_camera; const identity = new THREE.Uniform(new THREE.Matrix4()); - if (plot_data.cam_space == "data") { - plot_data.uniforms.view = cam.view; - plot_data.uniforms.projection = cam.projection; - plot_data.uniforms.projectionview = cam.projectionview; - plot_data.uniforms.eyeposition = cam.eyeposition; - } else if (plot_data.cam_space == "pixel") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.pixel_space; - plot_data.uniforms.projectionview = cam.pixel_space; - } else if (plot_data.cam_space == "relative") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.relative_space; - plot_data.uniforms.projectionview = cam.relative_space; - } else { + const uniforms = plot.mesh ? plot.mesh.material.uniforms : plot.plot_data.uniforms; + const space = plot.plot_data.cam_space; + if (space == "data") { + uniforms.view = cam.view; + uniforms.projection = cam.projection; + uniforms.projectionview = cam.projectionview; + uniforms.eyeposition = cam.eyeposition; + } else if (space == "pixel") { + uniforms.view = identity; + uniforms.projection = cam.pixel_space; + uniforms.projectionview = cam.pixel_space; + } else if (space == "relative") { + uniforms.view = identity; + uniforms.projection = cam.relative_space; + uniforms.projectionview = cam.relative_space; + } else if (space == "clip") { // clip space - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = identity; - plot_data.uniforms.projectionview = identity; + uniforms.view = identity; + uniforms.projection = identity; + uniforms.projectionview = identity; + } else { + throw new Error(`Space ${space} not supported!`) } const { px_per_unit } = scene.screen; - plot_data.uniforms.resolution = cam.resolution; - plot_data.uniforms.px_per_unit = new THREE.Uniform(px_per_unit); + uniforms.resolution = cam.resolution; + uniforms.px_per_unit = new THREE.Uniform(px_per_unit); - if (plot_data.uniforms.preprojection) { - const { space, markerspace } = plot_data; - plot_data.uniforms.preprojection = cam.preprojection_matrix( + if (plot.plot_data.uniforms.preprojection) { + const { space, markerspace } = plot.plot_data; + uniforms.preprojection = cam.preprojection_matrix( space.value, markerspace.value ); } - if (scene.camera_relative_light) { - plot_data.uniforms.light_direction = cam.light_direction; - scene.light_direction.on((value) => { - cam.update_light_dir(value); - }); - } else { - // TODO how update? - const light_dir = new THREE.Vector3().fromArray( - scene.light_direction.value - ); - plot_data.uniforms.light_direction = new THREE.Uniform(light_dir); - scene.light_direction.on((value) => { - plot_data.uniforms.light_direction.value.fromArray(value); - }); - } + uniforms.light_direction = scene.light_direction; +} + - const p = deserialize_plot(scene, plot_data); - plot_cache[p.plot_uuid] = p; - scene.add(p); +export function add_plot(scene, plot_data) { + // fill in the camera uniforms, that we don't sent in serialization per plot + const p = new Plot(scene, plot_data); + plot_cache[p.uuid] = p.mesh; + scene.add(p.mesh); // execute all next insert callbacks const next_insert = new Set(ON_NEXT_INSERT); // copy next_insert.forEach((f) => f()); } -function connect_uniforms(mesh, updater) { - updater.on(([name, data]) => { - // this is the initial value, which shouldn't end up getting updated - - // TODO, figure out why this gets pushed!! - if (name === "none") { - return; - } - const uniform = mesh.material.uniforms[name]; - if (uniform.value.isTexture) { - const im_data = uniform.value.image; - const [size, tex_data] = data; - if (tex_data.length == im_data.data.length) { - im_data.data.set(tex_data); - } else { - const old_texture = uniform.value; - uniform.value = re_create_texture(old_texture, tex_data, size); - old_texture.dispose(); - } - uniform.value.needsUpdate = true; - } else { - if (is_three_fixed_array(uniform.value)) { - uniform.value.fromArray(data); - } else { - uniform.value = data; - } - } - }); -} - function convert_RGB_to_RGBA(rgbArray) { const length = rgbArray.length; const rgbaArray = new rgbArray.constructor((length / 3) * 4); @@ -365,6 +444,8 @@ function re_create_texture(old_texture, buffer, size) { } return tex; } + + function BufferAttribute(buffer) { const jsbuff = new THREE.BufferAttribute(buffer.flat, buffer.type_length); jsbuff.setUsage(THREE.DynamicDrawUsage); @@ -577,8 +658,6 @@ export function deserialize_scene(data, screen) { scene.backgroundcolor_alpha = data.backgroundcolor_alpha; scene.clearscene = data.clearscene; scene.visible = data.visible; - scene.camera_relative_light = data.camera_relative_light; - scene.light_direction = data.light_direction; const camera = new Camera.MakieCamera(); @@ -607,9 +686,23 @@ export function deserialize_scene(data, screen) { } update_cam(data.camera.value, true); // force update on first call + camera.update_light_dir(data.light_direction.value); data.camera.on(update_cam); + if (data.camera_relative_light) { + scene.light_direction = camera.light_direction; + } else { + const light_dir = new THREE.Vector3().fromArray( + data.light_direction.value + ); + scene.light_direction = new THREE.Uniform(light_dir); + data.light_direction.on((value) => { + plot_data.uniforms.light_direction.value.fromArray(value); + }); + } + + data.plots.forEach((plot_data) => { add_plot(scene, plot_data); }); diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 73717f390f9..cc9be787c70 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -339,18 +339,23 @@ function insert_scene!(session::Session, screen::Screen, scene::Scene) end function insert_plot!(session::Session, scene::Scene, @nospecialize(plot::Plot)) + @assert !haskey(plot, :__wgl_session) plot_data = serialize_plots(scene, Plot[plot]) plot_sub = Session(session) Bonito.init_session(plot_sub) - plot.__wgl_session = plot_sub js = js""" $(WGL).then(WGL=> { WGL.insert_plot($(js_uuid(scene)), $plot_data); })""" Bonito.evaljs_value(plot_sub, js; timeout=50) + @assert !haskey(plot.attributes, :__wgl_session) + plot.attributes[:__wgl_session] = plot_sub return end +function Base.insert!(screen::Screen, scene::Scene, @nospecialize(plot::PlotList)) + return nothing +end function Base.insert!(screen::Screen, scene::Scene, @nospecialize(plot::Plot)) session = get_screen_session(screen; error="Plot needs to be displayed to insert additional plots") if js_uuid(scene) in screen.displayed_scenes diff --git a/WGLMakie/src/picking.jl b/WGLMakie/src/picking.jl index 93bf7a56d9d..bd8c36b0355 100644 --- a/WGLMakie/src/picking.jl +++ b/WGLMakie/src/picking.jl @@ -56,7 +56,10 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) # E.g. if websocket got closed isnothing(session) && return Tuple{Plot,Int}[] selection = Bonito.evaljs_value(session, js""" - Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => WGL.pick_sorted(scene, $(xy_vec), $(range))) + Promise.all([$(WGL), $(scene)]).then(([WGL, scene]) => { + const picked = WGL.pick_sorted(scene, $(xy_vec), $(range)) + return picked + }) """) isnothing(selection) && return Tuple{Plot,Int}[] lookup = plot_lookup(scene) @@ -68,6 +71,7 @@ function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) end function Makie.pick(::Scene, screen::Screen, xy) + plot_matrix = pick_native(screen, Rect2i(xy..., 1, 1)) return plot_matrix[1, 1] end diff --git a/WGLMakie/src/serialization.jl b/WGLMakie/src/serialization.jl index 64adb5bda0b..527e361750b 100644 --- a/WGLMakie/src/serialization.jl +++ b/WGLMakie/src/serialization.jl @@ -300,18 +300,20 @@ function serialize_scene(scene::Scene) light_dir = isnothing(dirlight) ? Observable(Vec3f(1)) : dirlight.direction cam_rel = isnothing(dirlight) ? false : dirlight.camera_relative - serialized = Dict(:viewport => pixel_area, - :backgroundcolor => lift(hexcolor, scene, scene.backgroundcolor), - :backgroundcolor_alpha => lift(Colors.alpha, scene, scene.backgroundcolor), - :clearscene => scene.clear, - :camera => serialize_camera(scene), - :light_direction => light_dir, - :camera_relative_light => cam_rel, - :plots => serialize_plots(scene, scene.plots), - :cam3d_state => cam3d_state, - :visible => scene.visible, - :uuid => js_uuid(scene), - :children => children) + serialized = Dict( + :viewport => pixel_area, + :backgroundcolor => lift(hexcolor, scene, scene.backgroundcolor), + :backgroundcolor_alpha => lift(Colors.alpha, scene, scene.backgroundcolor), + :clearscene => scene.clear, + :camera => serialize_camera(scene), + :light_direction => light_dir, + :camera_relative_light => cam_rel, + :plots => serialize_plots(scene, scene.plots), + :cam3d_state => cam3d_state, + :visible => scene.visible, + :uuid => js_uuid(scene), + :children => children + ) return serialized end diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl index c44683adca5..11ac0e34ab7 100644 --- a/WGLMakie/src/three_plot.jl +++ b/WGLMakie/src/three_plot.jl @@ -72,3 +72,20 @@ function three_display(screen::Screen, session::Session, scene::Scene) connect_scene_events!(screen, scene, comm) return wrapper, done_init end + +Makie.supports_move_to(::Screen) = true + +function Makie.move_to!(screen::Screen, plot::Plot, scene::Scene) + session = get_screen_session(screen) + # Make sure target scene is serialized + insert_scene!(session, screen, scene) + return evaljs(session, js""" + $(scene).then(scene=> { + $(plot).then(meshes=> { + meshes.forEach(m => { + m.plot_object.move_to(scene) + }) + }) + }) + """) +end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index a2e349a9981..89a9c13a822 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -21210,7 +21210,26 @@ class MakieCamera { } } } -const scene_cache = {}; +function update_uniform(uniform, new_value) { + if (uniform.value.isTexture) { + const im_data = uniform.value.image; + const [size, tex_data] = new_value; + if (tex_data.length == im_data.data.length) { + im_data.data.set(tex_data); + } else { + const old_texture = uniform.value; + uniform.value = re_create_texture(old_texture, tex_data, size); + old_texture.dispose(); + } + uniform.value.needsUpdate = true; + } else { + if (is_three_fixed_array(uniform.value)) { + uniform.value.fromArray(new_value); + } else { + uniform.value = new_value; + } + } +} function filter_by_key(dict, keys, default_value = false) { const result = {}; keys.forEach((key)=>{ @@ -21223,117 +21242,6 @@ function filter_by_key(dict, keys, default_value = false) { }); return result; } -const plot_cache = {}; -const TEXTURE_ATLAS = [ - undefined -]; -function add_scene(scene_id, three_scene) { - scene_cache[scene_id] = three_scene; -} -function find_scene(scene_id) { - return scene_cache[scene_id]; -} -function delete_scene(scene_id) { - const scene = scene_cache[scene_id]; - if (!scene) { - return; - } - delete_three_scene(scene); - while(scene.children.length > 0){ - scene.remove(scene.children[0]); - } - delete scene_cache[scene_id]; -} -function find_plots(plot_uuids) { - const plots = []; - plot_uuids.forEach((id)=>{ - const plot = plot_cache[id]; - if (plot) { - plots.push(plot); - } - }); - return plots; -} -function delete_scenes(scene_uuids, plot_uuids) { - plot_uuids.forEach((plot_id)=>{ - const plot = plot_cache[plot_id]; - if (plot) { - delete_plot(plot); - } - }); - scene_uuids.forEach((scene_id)=>{ - delete_scene(scene_id); - }); -} -function insert_plot(scene_id, plot_data) { - const scene = find_scene(scene_id); - plot_data.forEach((plot)=>{ - add_plot(scene, plot); - }); -} -function delete_plots(plot_uuids) { - const plots = find_plots(plot_uuids); - plots.forEach(delete_plot); -} -function convert_texture(scene, data) { - const tex = create_texture(scene, data); - tex.needsUpdate = true; - tex.minFilter = mod[data.minFilter]; - tex.magFilter = mod[data.magFilter]; - tex.anisotropy = data.anisotropy; - tex.wrapS = mod[data.wrapS]; - if (data.size.length > 1) { - tex.wrapT = mod[data.wrapT]; - } - if (data.size.length > 2) { - tex.wrapR = mod[data.wrapR]; - } - return tex; -} -function is_three_fixed_array(value) { - return value instanceof mod.Vector2 || value instanceof mod.Vector3 || value instanceof mod.Vector4 || value instanceof mod.Matrix4; -} -function to_uniform(scene, data) { - if (data.type !== undefined) { - if (data.type == "Sampler") { - return convert_texture(scene, data); - } - throw new Error(`Type ${data.type} not known`); - } - if (Array.isArray(data) || ArrayBuffer.isView(data)) { - if (!data.every((x)=>typeof x === "number")) { - return data; - } - if (data.length == 2) { - return new mod.Vector2().fromArray(data); - } - if (data.length == 3) { - return new mod.Vector3().fromArray(data); - } - if (data.length == 4) { - return new mod.Vector4().fromArray(data); - } - if (data.length == 16) { - const mat = new mod.Matrix4(); - mat.fromArray(data); - return mat; - } - } - return data; -} -function deserialize_uniforms(scene, data) { - const result = {}; - for(const name in data){ - const value = data[name]; - if (value instanceof mod.Uniform) { - result[name] = value; - } else { - const ser = to_uniform(scene, value); - result[name] = new mod.Uniform(ser); - } - } - return result; -} function lines_vertex_shader(uniforms, attributes, is_linesegments) { const attribute_decl = attributes_to_type_declaration(attributes); const uniform_decl = uniforms_to_type_declaration(uniforms); @@ -22301,37 +22209,20 @@ function lines_fragment_shader(uniforms, attributes) { } `; } -function create_line_material(scene, uniforms, attributes, is_linesegments) { - const uniforms_des = deserialize_uniforms(scene, uniforms); - const mat = new THREE.RawShaderMaterial({ - uniforms: uniforms_des, - glslVersion: THREE.GLSL3, - vertexShader: lines_vertex_shader(uniforms_des, attributes, is_linesegments), - fragmentShader: lines_fragment_shader(uniforms_des, attributes), - transparent: true, - blending: THREE.CustomBlending, - blendSrc: THREE.SrcAlphaFactor, - blendDst: THREE.OneMinusSrcAlphaFactor, - blendSrcAlpha: THREE.ZeroFactor, - blendDstAlpha: THREE.OneFactor, - blendEquation: THREE.AddEquation - }); - mat.uniforms.object_id = { - value: 1 - }; - return mat; +function create_line(scene, line_data) { + return _create_line(scene, line_data, false); } function attach_interleaved_line_buffer(attr_name, geometry, data, ndim, is_segments, is_position) { const skip_elems = is_segments ? 2 * ndim : ndim; - const buffer = new THREE.InstancedInterleavedBuffer(data, skip_elems, 1); - buffer.count = Math.max(0, is_segments ? Math.floor(buffer.count - 1) : buffer.count - 3); - geometry.setAttribute(attr_name + "_start", new THREE.InterleavedBufferAttribute(buffer, ndim, ndim)); - geometry.setAttribute(attr_name + "_end", new THREE.InterleavedBufferAttribute(buffer, ndim, 2 * ndim)); + const buffer1 = new THREE.InstancedInterleavedBuffer(data, skip_elems, 1); + buffer1.count = Math.max(0, is_segments ? Math.floor(buffer1.count - 1) : buffer1.count - 3); + geometry.setAttribute(attr_name + "_start", new THREE.InterleavedBufferAttribute(buffer1, ndim, ndim)); + geometry.setAttribute(attr_name + "_end", new THREE.InterleavedBufferAttribute(buffer1, ndim, 2 * ndim)); if (is_position) { - geometry.setAttribute(attr_name + "_prev", new THREE.InterleavedBufferAttribute(buffer, ndim, 0)); - geometry.setAttribute(attr_name + "_next", new THREE.InterleavedBufferAttribute(buffer, ndim, 3 * ndim)); + geometry.setAttribute(attr_name + "_prev", new THREE.InterleavedBufferAttribute(buffer1, ndim, 0)); + geometry.setAttribute(attr_name + "_next", new THREE.InterleavedBufferAttribute(buffer1, ndim, 3 * ndim)); } - return buffer; + return buffer1; } function create_line_instance_geometry() { const geometry = new THREE.InstancedBufferGeometry(); @@ -22403,116 +22294,274 @@ function _create_line(scene, line_data, is_segments) { attach_updates(mesh, buffers, line_data.attributes, is_segments); return mesh; } -function create_line(scene, line_data) { - return _create_line(scene, line_data, false); -} function create_linesegments(scene, line_data) { return _create_line(scene, line_data, true); } -function deserialize_plot(scene, data) { - let mesh; - const update_visible = (v)=>{ - mesh.visible = v; +class Plot { + mesh = undefined; + parent = undefined; + uuid = ""; + name = ""; + is_instanced = false; + geometry_needs_recreation = false; + plot_data = {}; + constructor(scene, data){ + this.plot_data = data; + connect_plot(scene, this); + if (data.plot_type === "lines") { + this.mesh = create_line(scene, this.plot_data); + } else if (data.plot_type === "linesegments") { + this.mesh = create_linesegments(scene, this.plot_data); + } else if ("instance_attributes" in data) { + this.is_instanced = true; + this.mesh = create_instanced_mesh(scene, this.plot_data); + } else { + this.mesh = create_mesh(scene, this.plot_data); + } + this.name = data.name; + this.uuid = data.uuid; + this.mesh.plot_uuid = data.uuid; + this.mesh.frustumCulled = false; + this.mesh.matrixAutoUpdate = false; + this.mesh.renderOrder = data.zvalue; + data.uniform_updater.on(([name, data])=>{ + this.update_uniform(name, data); + }); + if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { + connect_attributes(this.mesh, data.attribute_updater); + } + this.parent = scene; + this.mesh.plot_object = this; + this.mesh.visible = data.visible.value; + data.visible.on((v)=>{ + this.mesh.visible = v; + }); + } + move_to(scene) { + if (scene === this.parent) { + return; + } + this.parent.remove(this.mesh); + connect_plot(scene, this); + scene.add(this.mesh); + this.parent = scene; return; - }; - if (data.plot_type === "lines") { - mesh = create_line(scene, data); - } else if (data.plot_type === "linesegments") { - mesh = create_linesegments(scene, data); - } else if ("instance_attributes" in data) { - mesh = create_instanced_mesh(scene, data); - } else { - mesh = create_mesh(scene, data); - } - mesh.name = data.name; - mesh.frustumCulled = false; - mesh.matrixAutoUpdate = false; - mesh.plot_uuid = data.uuid; - mesh.renderOrder = data.zvalue; - update_visible(data.visible.value); - data.visible.on(update_visible); - connect_uniforms(mesh, data.uniform_updater); - if (!(data.plot_type === "lines" || data.plot_type === "linesegments")) { - connect_attributes(mesh, data.attribute_updater); } - return mesh; + update(attributes) { + attributes.keys().forEach((key)=>{ + const value = attributes[key]; + if (value.type == "uniform") { + this.update_uniform(key, value.data); + } else if (value.type === "geometry") { + this.update_geometries(value.data); + } else if (value.type === "faces") { + this.update_faces(value.data); + } + }); + this.apply_updates(); + } + update_uniform(name, new_data) { + const uniform = this.mesh.material.uniforms[name]; + if (!uniform) { + throw new Error(`Uniform ${name} doesn't exist in Plot: ${this.name}`); + } + update_uniform(uniform, new_data); + } + update_geometry(name, new_data) { + buffer = this.mesh.geometry.attributes[name]; + if (!buffer) { + throw new Error(`Buffer ${name} doesn't exist in Plot: ${this.name}`); + } + const old_length = buffer.count; + if (new_data.length <= old_length) { + buffer.set(new_data.data); + buffer.needsUpdate = true; + } else { + buffer.to_update = new_data.data; + this.geometry_needs_recreation = true; + } + } + update_faces(face_data) { + this.mesh.geometry.setIndex(new mod.BufferAttribute(face_data, 1)); + } +} +const scene_cache = {}; +const plot_cache = {}; +const TEXTURE_ATLAS = [ + undefined +]; +function add_scene(scene_id, three_scene) { + scene_cache[scene_id] = three_scene; +} +function find_scene(scene_id) { + return scene_cache[scene_id]; +} +function delete_scene(scene_id) { + const scene = scene_cache[scene_id]; + if (!scene) { + return; + } + delete_three_scene(scene); + while(scene.children.length > 0){ + scene.remove(scene.children[0]); + } + delete scene_cache[scene_id]; +} +function find_plots(plot_uuids) { + const plots = []; + plot_uuids.forEach((id)=>{ + const plot = plot_cache[id]; + if (plot) { + plots.push(plot); + } + }); + return plots; +} +function delete_scenes(scene_uuids, plot_uuids) { + plot_uuids.forEach((plot_id)=>{ + const plot = plot_cache[plot_id]; + if (plot) { + delete_plot(plot); + } + }); + scene_uuids.forEach((scene_id)=>{ + delete_scene(scene_id); + }); +} +function insert_plot(scene_id, plot_data1) { + const scene = find_scene(scene_id); + plot_data1.forEach((plot)=>{ + add_plot(scene, plot); + }); +} +function delete_plots(plot_uuids) { + const plots = find_plots(plot_uuids); + plots.forEach(delete_plot); +} +function convert_texture(scene, data) { + const tex = create_texture(scene, data); + tex.needsUpdate = true; + tex.minFilter = mod[data.minFilter]; + tex.magFilter = mod[data.magFilter]; + tex.anisotropy = data.anisotropy; + tex.wrapS = mod[data.wrapS]; + if (data.size.length > 1) { + tex.wrapT = mod[data.wrapT]; + } + if (data.size.length > 2) { + tex.wrapR = mod[data.wrapR]; + } + return tex; +} +function is_three_fixed_array(value) { + return value instanceof mod.Vector2 || value instanceof mod.Vector3 || value instanceof mod.Vector4 || value instanceof mod.Matrix4; +} +function to_uniform(scene, data) { + if (data.type !== undefined) { + if (data.type == "Sampler") { + return convert_texture(scene, data); + } + throw new Error(`Type ${data.type} not known`); + } + if (Array.isArray(data) || ArrayBuffer.isView(data)) { + if (!data.every((x)=>typeof x === "number")) { + return data; + } + if (data.length == 2) { + return new mod.Vector2().fromArray(data); + } + if (data.length == 3) { + return new mod.Vector3().fromArray(data); + } + if (data.length == 4) { + return new mod.Vector4().fromArray(data); + } + if (data.length == 16) { + const mat = new mod.Matrix4(); + mat.fromArray(data); + return mat; + } + } + return data; +} +function deserialize_uniforms(scene, data) { + const result = {}; + for(const name in data){ + const value = data[name]; + if (value instanceof mod.Uniform) { + result[name] = value; + } else { + const ser = to_uniform(scene, value); + result[name] = new mod.Uniform(ser); + } + } + return result; +} +function create_line_material(scene, uniforms, attributes, is_linesegments) { + const uniforms_des = deserialize_uniforms(scene, uniforms); + const mat = new THREE.RawShaderMaterial({ + uniforms: uniforms_des, + glslVersion: THREE.GLSL3, + vertexShader: lines_vertex_shader(uniforms_des, attributes, is_linesegments), + fragmentShader: lines_fragment_shader(uniforms_des, attributes), + transparent: true, + blending: THREE.CustomBlending, + blendSrc: THREE.SrcAlphaFactor, + blendDst: THREE.OneMinusSrcAlphaFactor, + blendSrcAlpha: THREE.ZeroFactor, + blendDstAlpha: THREE.OneFactor, + blendEquation: THREE.AddEquation + }); + mat.uniforms.object_id = { + value: 1 + }; + return mat; } const ON_NEXT_INSERT = new Set(); function on_next_insert(f) { ON_NEXT_INSERT.add(f); } -function add_plot(scene, plot_data) { +function connect_plot(scene, plot) { const cam = scene.wgl_camera; const identity = new mod.Uniform(new mod.Matrix4()); - if (plot_data.cam_space == "data") { - plot_data.uniforms.view = cam.view; - plot_data.uniforms.projection = cam.projection; - plot_data.uniforms.projectionview = cam.projectionview; - plot_data.uniforms.eyeposition = cam.eyeposition; - } else if (plot_data.cam_space == "pixel") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.pixel_space; - plot_data.uniforms.projectionview = cam.pixel_space; - } else if (plot_data.cam_space == "relative") { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = cam.relative_space; - plot_data.uniforms.projectionview = cam.relative_space; + const uniforms = plot.mesh ? plot.mesh.material.uniforms : plot.plot_data.uniforms; + const space = plot.plot_data.cam_space; + if (space == "data") { + uniforms.view = cam.view; + uniforms.projection = cam.projection; + uniforms.projectionview = cam.projectionview; + uniforms.eyeposition = cam.eyeposition; + } else if (space == "pixel") { + uniforms.view = identity; + uniforms.projection = cam.pixel_space; + uniforms.projectionview = cam.pixel_space; + } else if (space == "relative") { + uniforms.view = identity; + uniforms.projection = cam.relative_space; + uniforms.projectionview = cam.relative_space; + } else if (space == "clip") { + uniforms.view = identity; + uniforms.projection = identity; + uniforms.projectionview = identity; } else { - plot_data.uniforms.view = identity; - plot_data.uniforms.projection = identity; - plot_data.uniforms.projectionview = identity; + throw new Error(`Space ${space} not supported!`); } const { px_per_unit } = scene.screen; - plot_data.uniforms.resolution = cam.resolution; - plot_data.uniforms.px_per_unit = new mod.Uniform(px_per_unit); - if (plot_data.uniforms.preprojection) { - const { space , markerspace } = plot_data; - plot_data.uniforms.preprojection = cam.preprojection_matrix(space.value, markerspace.value); - } - if (scene.camera_relative_light) { - plot_data.uniforms.light_direction = cam.light_direction; - scene.light_direction.on((value)=>{ - cam.update_light_dir(value); - }); - } else { - const light_dir = new mod.Vector3().fromArray(scene.light_direction.value); - plot_data.uniforms.light_direction = new mod.Uniform(light_dir); - scene.light_direction.on((value)=>{ - plot_data.uniforms.light_direction.value.fromArray(value); - }); - } - const p = deserialize_plot(scene, plot_data); - plot_cache[p.plot_uuid] = p; - scene.add(p); + uniforms.resolution = cam.resolution; + uniforms.px_per_unit = new mod.Uniform(px_per_unit); + if (plot.plot_data.uniforms.preprojection) { + const { space , markerspace } = plot.plot_data; + uniforms.preprojection = cam.preprojection_matrix(space.value, markerspace.value); + } + uniforms.light_direction = scene.light_direction; +} +function add_plot(scene, plot_data1) { + const p = new Plot(scene, plot_data1); + plot_cache[p.uuid] = p.mesh; + scene.add(p.mesh); const next_insert = new Set(ON_NEXT_INSERT); next_insert.forEach((f)=>f()); } -function connect_uniforms(mesh, updater) { - updater.on(([name, data])=>{ - if (name === "none") { - return; - } - const uniform = mesh.material.uniforms[name]; - if (uniform.value.isTexture) { - const im_data = uniform.value.image; - const [size, tex_data] = data; - if (tex_data.length == im_data.data.length) { - im_data.data.set(tex_data); - } else { - const old_texture = uniform.value; - uniform.value = re_create_texture(old_texture, tex_data, size); - old_texture.dispose(); - } - uniform.value.needsUpdate = true; - } else { - if (is_three_fixed_array(uniform.value)) { - uniform.value.fromArray(data); - } else { - uniform.value = data; - } - } - }); -} function convert_RGB_to_RGBA(rgbArray) { const length = rgbArray.length; const rgbaArray = new rgbArray.constructor(length / 3 * 4); @@ -22525,24 +22574,24 @@ function convert_RGB_to_RGBA(rgbArray) { return rgbaArray; } function create_texture_from_data(data) { - let buffer = data.data; + let buffer1 = data.data; if (data.size.length == 3) { - const tex = new mod.Data3DTexture(buffer, data.size[0], data.size[1], data.size[2]); + const tex = new mod.Data3DTexture(buffer1, data.size[0], data.size[1], data.size[2]); tex.format = mod[data.three_format]; tex.type = mod[data.three_type]; return tex; } else { let format = mod[data.three_format]; if (data.three_format == "RGBFormat") { - buffer = convert_RGB_to_RGBA(buffer); + buffer1 = convert_RGB_to_RGBA(buffer1); format = mod.RGBAFormat; } - return new mod.DataTexture(buffer, data.size[0], data.size[1], format, mod[data.three_type]); + return new mod.DataTexture(buffer1, data.size[0], data.size[1], format, mod[data.three_type]); } } function create_texture(scene, data) { - const buffer = data.data; - if (buffer == "texture_atlas") { + const buffer1 = data.data; + if (buffer1 == "texture_atlas") { const { texture_atlas } = scene.screen; if (texture_atlas) { return texture_atlas; @@ -22565,14 +22614,14 @@ function create_texture(scene, data) { return create_texture_from_data(data); } } -function re_create_texture(old_texture, buffer, size) { +function re_create_texture(old_texture, buffer1, size) { let tex; if (size.length == 3) { - tex = new mod.Data3DTexture(buffer, size[0], size[1], size[2]); + tex = new mod.Data3DTexture(buffer1, size[0], size[1], size[2]); tex.format = old_texture.format; tex.type = old_texture.type; } else { - tex = new mod.DataTexture(buffer, size[0], size[1] ? size[1] : 1, old_texture.format, old_texture.type); + tex = new mod.DataTexture(buffer1, size[0], size[1] ? size[1] : 1, old_texture.format, old_texture.type); } tex.minFilter = old_texture.minFilter; tex.magFilter = old_texture.magFilter; @@ -22586,26 +22635,26 @@ function re_create_texture(old_texture, buffer, size) { } return tex; } -function BufferAttribute(buffer) { - const jsbuff = new mod.BufferAttribute(buffer.flat, buffer.type_length); +function BufferAttribute(buffer1) { + const jsbuff = new mod.BufferAttribute(buffer1.flat, buffer1.type_length); jsbuff.setUsage(mod.DynamicDrawUsage); return jsbuff; } -function InstanceBufferAttribute(buffer) { - const jsbuff = new mod.InstancedBufferAttribute(buffer.flat, buffer.type_length); +function InstanceBufferAttribute(buffer1) { + const jsbuff = new mod.InstancedBufferAttribute(buffer1.flat, buffer1.type_length); jsbuff.setUsage(mod.DynamicDrawUsage); return jsbuff; } function attach_geometry(buffer_geometry, vertexarrays, faces) { for(const name in vertexarrays){ const buff = vertexarrays[name]; - let buffer; + let buffer1; if (buff.to_update) { - buffer = new mod.BufferAttribute(buff.to_update, buff.itemSize); + buffer1 = new mod.BufferAttribute(buff.to_update, buff.itemSize); } else { - buffer = BufferAttribute(buff); + buffer1 = BufferAttribute(buff); } - buffer_geometry.setAttribute(name, buffer); + buffer_geometry.setAttribute(name, buffer1); } buffer_geometry.setIndex(faces); buffer_geometry.boundingSphere = new mod.Sphere(); @@ -22615,8 +22664,8 @@ function attach_geometry(buffer_geometry, vertexarrays, faces) { } function attach_instanced_geometry(buffer_geometry, instance_attributes) { for(const name in instance_attributes){ - const buffer = InstanceBufferAttribute(instance_attributes[name]); - buffer_geometry.setAttribute(name, buffer); + const buffer1 = InstanceBufferAttribute(instance_attributes[name]); + buffer_geometry.setAttribute(name, buffer1); } } function recreate_geometry(mesh, vertexarrays, faces) { @@ -22634,17 +22683,17 @@ function recreate_instanced_geometry(mesh) { ...mesh.geometry.index.array ]; Object.keys(mesh.geometry.attributes).forEach((name)=>{ - const buffer = mesh.geometry.attributes[name]; - const copy = buffer.to_update ? buffer.to_update : buffer.array.map((x)=>x); - if (buffer.isInstancedBufferAttribute) { + const buffer1 = mesh.geometry.attributes[name]; + const copy = buffer1.to_update ? buffer1.to_update : buffer1.array.map((x)=>x); + if (buffer1.isInstancedBufferAttribute) { instance_attributes[name] = { flat: copy, - type_length: buffer.itemSize + type_length: buffer1.itemSize }; } else { vertexarrays[name] = { flat: copy, - type_length: buffer.itemSize + type_length: buffer1.itemSize }; } }); @@ -22707,11 +22756,11 @@ function connect_attributes(mesh, updater) { function re_assign_buffers() { const attributes = mesh.geometry.attributes; Object.keys(attributes).forEach((name)=>{ - const buffer = attributes[name]; - if (buffer.isInstancedBufferAttribute) { - instance_buffers[name] = buffer; + const buffer1 = attributes[name]; + if (buffer1.isInstancedBufferAttribute) { + instance_buffers[name] = buffer1; } else { - geometry_buffers[name] = buffer; + geometry_buffers[name] = buffer1; } }); first_instance_buffer = first(instance_buffers); @@ -22723,7 +22772,7 @@ function connect_attributes(mesh, updater) { } re_assign_buffers(); updater.on(([name, new_values, length])=>{ - const buffer = mesh.geometry.attributes[name]; + const buffer1 = mesh.geometry.attributes[name]; let buffers; let real_length; let is_instance = false; @@ -22738,19 +22787,19 @@ function connect_attributes(mesh, updater) { real_length = real_geometry_length; } if (length <= real_length[0]) { - buffer.set(new_values); - buffer.needsUpdate = true; + buffer1.set(new_values); + buffer1.needsUpdate = true; if (is_instance) { mesh.geometry.instanceCount = length; } } else { - buffer.to_update = new_values; + buffer1.to_update = new_values; const all_have_same_length = Object.values(buffers).every((x)=>x.to_update && x.to_update.length / x.itemSize == length); if (all_have_same_length) { if (is_instance) { recreate_instanced_geometry(mesh); re_assign_buffers(); - mesh.geometry.instanceCount = new_values.length / buffer.itemSize; + mesh.geometry.instanceCount = new_values.length / buffer1.itemSize; } else { recreate_geometry(mesh, buffers, mesh.geometry.index); re_assign_buffers(); @@ -22771,8 +22820,6 @@ function deserialize_scene(data, screen) { scene.backgroundcolor_alpha = data.backgroundcolor_alpha; scene.clearscene = data.clearscene; scene.visible = data.visible; - scene.camera_relative_light = data.camera_relative_light; - scene.light_direction = data.light_direction; const camera = new MakieCamera(); scene.wgl_camera = camera; function update_cam(camera_matrices, force) { @@ -22790,8 +22837,17 @@ function deserialize_scene(data, screen) { update_cam(data.camera.value, true); camera.update_light_dir(data.light_direction.value); data.camera.on(update_cam); - data.plots.forEach((plot_data)=>{ - add_plot(scene, plot_data); + if (data.camera_relative_light) { + scene.light_direction = camera.light_direction; + } else { + const light_dir = new mod.Vector3().fromArray(data.light_direction.value); + scene.light_direction = new mod.Uniform(light_dir); + data.light_direction.on((value)=>{ + plot_data.uniforms.light_direction.value.fromArray(value); + }); + } + data.plots.forEach((plot_data1)=>{ + add_plot(scene, plot_data1); }); scene.scene_children = data.children.map((child)=>{ const childscene = deserialize_scene(child, screen); @@ -23278,8 +23334,8 @@ function pick_closest(scene, xy, range) { const y1 = Math.min(canvas.height, Math.ceil(px_per_unit * (xy[1] + range))); const dx = x1 - x0; const dy = y1 - y0; - const [plot_data, _] = pick_native(scene, x0, y0, dx, dy, false); - const plot_matrix = plot_data.data; + const [plot_data1, _] = pick_native(scene, x0, y0, dx, dy, false); + const plot_matrix = plot_data1.data; let min_dist = px_per_unit * px_per_unit * range * range; let selection = [ null, @@ -23319,11 +23375,11 @@ function pick_sorted(scene, xy, range) { const y1 = Math.min(canvas.height, Math.ceil(px_per_unit * (xy[1] + range))); const dx = x1 - x0; const dy = y1 - y0; - const [plot_data, selected] = pick_native(scene, x0, y0, dx, dy, false); + const [plot_data1, selected] = pick_native(scene, x0, y0, dx, dy, false); if (selected.length == 0) { return null; } - const plot_matrix = plot_data.data; + const plot_matrix = plot_data1.data; const distances = selected.map((x)=>1e30); const x = xy[0] * px_per_unit + 1 - x0; const y = xy[1] * px_per_unit + 1 - y0; diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index a54d8ebce69..f97f4463979 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -22,7 +22,7 @@ excludes = Set([ "Image on Surface Sphere", # TODO: texture rotated 180° # "heatmaps & surface", # TODO: fix direct NaN -> nancolor conversion "Array of Images Scatter", # scatter does not support texture images - + "Order Independent Transparency", "fast pixel marker", "Textured meshscatter", # not yet implemented @@ -52,6 +52,8 @@ end session = edisplay.browserdisplay.handler.session session_size = Base.summarysize(session) / 10^6 texture_atlas_size = Base.summarysize(WGLMakie.TEXTURE_ATLAS) / 10^6 + @show typeof.(last.(WGLMakie.TEXTURE_ATLAS.listeners)) + @show length(WGLMakie.TEXTURE_ATLAS.listeners) @show session_size texture_atlas_size @test session_size / 10^6 < 6 @test texture_atlas_size < 6 @@ -91,7 +93,7 @@ end rm(filename) end - + f, a, p = scatter(rand(10)); filename = "$(tempname()).mp4" try @@ -124,4 +126,4 @@ end @test events(f).tick[] == tick # TODO: test normal rendering -end \ No newline at end of file +end diff --git a/src/interaction/inspector.jl b/src/interaction/inspector.jl index f5e166ef393..8e3baed0fd6 100644 --- a/src/interaction/inspector.jl +++ b/src/interaction/inspector.jl @@ -184,23 +184,20 @@ mutable struct DataInspector root::Scene attributes::Attributes - temp_plots::Vector{AbstractPlot} + temp_plots::Vector{Plot} plot::Tooltip - selection::AbstractPlot + selection::Plot obsfuncs::Vector{Any} - lock::Threads.ReentrantLock end function DataInspector(scene::Scene, plot::AbstractPlot, attributes) - x = DataInspector(scene, attributes, AbstractPlot[], plot, plot, Any[], Threads.ReentrantLock()) - # finalizer(cleanup, x) # doesn't get triggered when this is dereferenced - x + return DataInspector(scene, attributes, Plot[], plot, plot, Any[]) end function cleanup(inspector::DataInspector) - off.(inspector.obsfuncs) + foreach(off, inspector.obsfuncs) empty!(inspector.obsfuncs) delete!(inspector.root, inspector.plot) clear_temporary_plots!(inspector, inspector.selection) @@ -281,9 +278,17 @@ function DataInspector(scene::Scene; priority = 100, kwargs...) # We delegate the hover processing to another channel, # So that we can skip queued up updates with empty_channel! # And also not slow down the processing of e.mouseposition/e.scroll + was_open = false channel = Channel{Nothing}(Inf) do ch for _ in ch - on_hover(inspector) + if isopen(scene) + was_open = true + on_hover(inspector) + end + if !isopen(scene) && was_open + close(ch) + break + end end end listeners = onany(e.mouseposition, e.scroll) do _, _ @@ -306,33 +311,31 @@ DataInspector(; kwargs...) = DataInspector(current_figure(); kwargs...) function on_hover(inspector) parent = inspector.root - lock(inspector.lock) do - (inspector.attributes.enabled[] && is_mouseinside(parent)) || return Consume(false) - - mp = mouseposition_px(parent) - should_clear = true - for (plt, idx) in pick_sorted(parent, mp, inspector.attributes.range[]) - if to_value(get(plt.attributes, :inspectable, true)) - # show_data should return true if it created a tooltip - if show_data_recursion(inspector, plt, idx) - should_clear = false - break - end + (inspector.attributes.enabled[] && is_mouseinside(parent)) || return Consume(false) + + mp = mouseposition_px(parent) + should_clear = true + for (plt, idx) in pick_sorted(parent, mp, inspector.attributes.range[]) + if to_value(get(plt.attributes, :inspectable, true)) + # show_data should return true if it created a tooltip + if show_data_recursion(inspector, plt, idx) + should_clear = false + break end end + end - if should_clear - plot = inspector.selection - if to_value(get(plot, :inspector_clear, automatic)) !== automatic - plot[:inspector_clear][](inspector, plot) - end - inspector.plot.visible[] = false - inspector.attributes.indicator_visible[] = false - inspector.plot.offset.val = inspector.attributes.offset[] + if should_clear + plot = inspector.selection + if to_value(get(plot, :inspector_clear, automatic)) !== automatic + plot[:inspector_clear][](inspector, plot) end - - return Consume(false) + inspector.plot.visible[] = false + inspector.attributes.indicator_visible[] = false + inspector.plot.offset.val = inspector.attributes.offset[] end + + return Consume(false) end diff --git a/src/layouting/transformation.jl b/src/layouting/transformation.jl index d1ee9652edf..1bb59e62a8e 100644 --- a/src/layouting/transformation.jl +++ b/src/layouting/transformation.jl @@ -15,11 +15,13 @@ end function Observables.connect!(parent::Transformation, child::Transformation; connect_func=true) tfuncs = [] + # Observables.clear(child.parent_model) obsfunc = on(parent.model; update=true) do m return child.parent_model[] = m end push!(tfuncs, obsfunc) if connect_func + # Observables.clear(child.transform_func) t2 = on(parent.transform_func; update=true) do f child.transform_func[] = f return diff --git a/src/scenes.jl b/src/scenes.jl index 5903e5a9e7c..fd1f12d16de 100644 --- a/src/scenes.jl +++ b/src/scenes.jl @@ -498,15 +498,13 @@ function free(plot::AbstractPlot) Observables.off(f) end foreach(free, plot.plots) - empty!(plot.plots) + # empty!(plot.plots) empty!(plot.deregister_callbacks) - empty!(plot.attributes) free(plot.transformation) return end function Base.delete!(scene::Scene, plot::AbstractPlot) - len = length(scene.plots) filter!(x -> x !== plot, scene.plots) # TODO, if we want to delete a subplot of a plot, # It won't be in scene.plots directly, but will still be deleted @@ -523,6 +521,46 @@ function Base.delete!(scene::Scene, plot::AbstractPlot) free(plot) end +supports_move_to(::MakieScreen) = false + +function supports_move_to(plot::Plot) + scene = get_scene(plot) + return all(scene.current_screens) do screen + return supports_move_to(screen) + end +end + +# function move_to!(screen::MakieScreen, plot::Plot, scene::Scene) +# # TODO, move without deleting! +# # Will be easier with Observable refactor +# delete!(screen, scene, plot) +# insert!(screen, scene, plot) +# return +# end + + +function move_to!(plot::Plot, scene::Scene) + if plot.parent === scene + return + end + + if is_space_compatible(plot, scene) + obsfunc = connect!(transformation(scene), transformation(plot)) + append!(plot.deregister_callbacks, obsfunc) + end + for screen in root(scene).current_screens + if supports_move_to(screen) + move_to!(screen, plot, scene) + end + end + current_parent = parent_scene(plot) + filter!(x -> x !== plot, current_parent.plots) + push!(scene.plots, plot) + plot.parent = scene + return +end + + events(x) = events(get_scene(x)) events(scene::Scene) = scene.events events(scene::SceneLike) = events(scene.parent) diff --git a/src/specapi.jl b/src/specapi.jl index 8f4bf7dd11e..5d03e321363 100644 --- a/src/specapi.jl +++ b/src/specapi.jl @@ -326,12 +326,13 @@ function distance_score(at::Tuple{Int,GP,GridLayoutSpec}, bt::Tuple{Int,GP,GridL end end -function find_min_distance(f, to_compare, list, scores) +function find_min_distance(f, to_compare, list, scores, penalty=(key, score)-> score) isempty(list) && return -1 minscore = 2.0 idx = -1 for key in keys(list) score = distance_score(to_compare, f(list[key], key), scores) + score = penalty(key, score) # apply custom penalty if score ≈ 0.0 # shortcuircit for exact matches return key end @@ -353,8 +354,15 @@ function find_layoutable( return (idx, layoutables[idx]...) end -function find_reusable_plot(plotspec::PlotSpec, plots::IdDict{PlotSpec,Plot}, scores) - idx = find_min_distance((_, spec) -> spec, plotspec, plots, scores) +function find_reusable_plot(scene::Scene, plotspec::PlotSpec, plots::IdDict{PlotSpec,Plot}, scores) + function penalty(key, score) + # penalize plots with different parents + # needs to be implemented via this penalty function, since parent scenes arent part of the spec + plot = plots[key] + move_to_penalty = ((!Makie.supports_move_to(plot)) * 100) + 1 + return norm(Float64[plot.parent !== scene, score]) * move_to_penalty + end + idx = find_min_distance((_, spec) -> spec, plotspec, plots, scores, penalty) idx == -1 && return nothing, nothing return plots[idx], idx end @@ -564,18 +572,21 @@ function push_without_add!(scene::Scene, plot) end end -function diff_plotlist!(scene::Scene, plotspecs::Vector{PlotSpec}, obs_to_notify, reusable_plots, - plotlist::Union{Nothing,PlotList}=nothing) - new_plots = IdDict{PlotSpec,Plot}() # needed to be mutated +function diff_plotlist!( + scene::Scene, plotspecs::Vector{PlotSpec}, + obs_to_notify, + plotlist::Union{Nothing,PlotList}=nothing, + reusable_plots = IdDict{PlotSpec, Plot}(), + new_plots = IdDict{PlotSpec,Plot}()) + # needed to be mutated empty!(scene.cycler.counters) # Global list of observables that need updating # Updating them all at once in the end avoids problems with triggering updates while updating # And at some point we may be able to optimize notify(list_of_observables) - empty!(obs_to_notify) scores = IdDict{Any, Float64}() for plotspec in plotspecs # we need to compare by types with compare_specs, since we can only update plots if the types of all attributes match - reused_plot, old_spec = find_reusable_plot(plotspec, reusable_plots, scores) + reused_plot, old_spec = find_reusable_plot(scene, plotspec, reusable_plots, scores) if isnothing(reused_plot) # Create new plot, store it into our `cached_plots` dictionary @debug("Creating new plot for spec") @@ -601,45 +612,57 @@ function diff_plotlist!(scene::Scene, plotspecs::Vector{PlotSpec}, obs_to_notify @debug("updating old plot with spec") # Delete the plots from reusable_plots, so that we don't re-use it multiple times! delete!(reusable_plots, old_spec) + if reused_plot.parent !== scene + @assert Makie.supports_move_to(reused_plot) + move_to!(reused_plot, scene) + end update_plot!(obs_to_notify, reused_plot, old_spec, plotspec) new_plots[plotspec] = reused_plot + end end return new_plots end -function update_plotspecs!(scene::Scene, list_of_plotspecs::Observable, plotlist::Union{Nothing, PlotList}=nothing) +function update_plotspecs!( + scene::Scene, list_of_plotspecs::Observable, + plotlist::Union{Nothing,PlotList}=nothing, + unused_plots=IdDict{PlotSpec,Plot}(), + new_plots=IdDict{PlotSpec,Plot}(), + own_plots=true + ) # Cache plots here so that we aren't re-creating plots every time; # if a plot still exists from last time, update it accordingly. # If the plot is removed from `plotspecs`, we'll delete it from here # and re-create it if it ever returns. - unused_plots = IdDict{PlotSpec,Plot}() obs_to_notify = Observable[] - update_plotlist(spec::PlotSpec) = update_plotlist([spec]) function update_plotlist(plotspecs) # Global list of observables that need updating # Updating them all at once in the end avoids problems with triggering updates while updating # And at some point we may be able to optimize notify(list_of_observables) - empty!(obs_to_notify) empty!(scene.cycler.counters) # Reset Cycler # diff_plotlist! deletes all plots that get re-used from unused_plots # so, this will become our list of unused plots! - new_plots = diff_plotlist!(scene, plotspecs, obs_to_notify, unused_plots, plotlist) + diff_plotlist!(scene, plotspecs, obs_to_notify, plotlist, unused_plots, new_plots) # Next, delete all plots that we haven't used # TODO, we could just hide them, until we reach some max_plots_to_be_cached, so that we re-create less plots. - for (_, plot) in unused_plots - if !isnothing(plotlist) - filter!(x -> x !== plot, plotlist.plots) + if own_plots + for (_, plot) in unused_plots + if !isnothing(plotlist) + filter!(x -> x !== plot, plotlist.plots) + end + delete!(scene, plot) end - delete!(scene, plot) + # Transfer all new plots into unused_plots for the next update! + @assert !any(x-> x in unused_plots, new_plots) + empty!(unused_plots) + merge!(unused_plots, new_plots) + empty!(new_plots) + # finally, notify all changes at once end - # Transfer all new plots into unused_plots for the next update! - @assert !any(x-> x in unused_plots, new_plots) - empty!(unused_plots) - merge!(unused_plots, new_plots) - # finally, notify all changes at once foreach(notify, obs_to_notify) + empty!(obs_to_notify) return end l = Base.ReentrantLock() @@ -786,9 +809,7 @@ function update_layoutable!(block::T, plot_obs, old_spec::BlockSpec, spec::Block empty!(block.scene.cycler.counters) end if T <: AbstractAxis - if plot_obs[] != spec.plots - plot_obs[] = spec.plots - end + plot_obs[] = spec.plots scene = get_scene(block) if any(needs_tight_limits, scene.plots) tightlimits!(block) @@ -853,7 +874,7 @@ end function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::Union{Nothing, GridLayoutSpec}, - gridspec::GridLayoutSpec, previous_contents, new_layoutables) + gridspec::GridLayoutSpec, previous_contents, new_layoutables, global_unused_plots, new_plots) update_layoutable!(gridlayout, nothing, oldgridspec, gridspec) scores = IdDict{Any, Float64}() @@ -869,7 +890,7 @@ function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::U if new_layoutable isa AbstractAxis obs = Observable(spec.plots) scene = get_scene(new_layoutable) - update_plotspecs!(scene, obs) + update_plotspecs!(scene, obs, nothing, global_unused_plots, new_plots, false) if any(needs_tight_limits, scene.plots) tightlimits!(new_layoutable) end @@ -877,7 +898,7 @@ function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::U elseif new_layoutable isa GridLayout # Make sure all plots & blocks are inserted update_gridlayout!(new_layoutable, nesting + 1, spec, spec, previous_contents, - new_layoutables) + new_layoutables, global_unused_plots, new_plots) end push!(new_layoutables, (nesting, position, spec) => (new_layoutable, obs)) else @@ -888,7 +909,8 @@ function update_gridlayout!(gridlayout::GridLayout, nesting::Int, oldgridspec::U (layoutable, plot_obs) = layoutable_obs gridlayout[position...] = layoutable if layoutable isa GridLayout - update_gridlayout!(layoutable, nesting + 1, old_spec, spec, previous_contents, new_layoutables) + update_gridlayout!(layoutable, nesting + 1, old_spec, spec, previous_contents, + new_layoutables, global_unused_plots, new_plots) else update_layoutable!(layoutable, plot_obs, old_spec, spec) update_state_before_display!(layoutable) @@ -913,13 +935,16 @@ function delete_layoutable!(grid::GridLayout) end function update_gridlayout!(target_layout::GridLayout, layout_spec::GridLayoutSpec, unused_layoutables, - new_layoutables) + new_layoutables, unused_plots, new_plots) # For each update we look into `unused_layoutables` to see if we can re-use a layoutable (GridLayout/Block). # Every re-used layoutable and every newly created gets pushed into `new_layoutables`, # while it gets removed from `unused_layoutables`. empty!(new_layoutables) + update_gridlayout!( + target_layout, 1, nothing, layout_spec, unused_layoutables, + new_layoutables, unused_plots, new_plots + ) - update_gridlayout!(target_layout, 1, nothing, layout_spec, unused_layoutables, new_layoutables) foreach(unused_layoutables) do (p, (block, obs)) # disconnect! all unused layoutables, so they dont show up anymore if block isa Block @@ -941,6 +966,16 @@ function update_gridlayout!(target_layout::GridLayout, layout_spec::GridLayoutSp GridLayoutBase.update!(l) end + for (_, plot) in unused_plots + delete!(plot.parent, plot) + end + # Transfer all new plots into unused_plots for the next update! + @assert isempty(unused_plots) || !any(x -> x in unused_plots, new_plots) + empty!(unused_plots) + merge!(unused_plots, new_plots) + empty!(new_plots) + # finally, notify all changes at once + # foreach(unused_layoutables) do (p, (block, obs)) # # Finally, disconnect all blocks that haven't been used! # disconnect!(block) @@ -962,9 +997,12 @@ function update_fig!(fig::Union{Figure,GridPosition,GridSubposition}, layout_obs sizehint!(new_layoutables, 50) l = Base.ReentrantLock() layout = get_layout!(fig) + unused_plots = IdDict{PlotSpec,Plot}() + new_plots = IdDict{PlotSpec,Plot}() on(get_topscene(fig), layout_obs; update=true) do layout_spec lock(l) do - update_gridlayout!(layout, layout_spec, unused_layoutables, new_layoutables) + update_gridlayout!(layout, layout_spec, unused_layoutables, new_layoutables, + unused_plots, new_plots) return end end diff --git a/test/specapi.jl b/test/specapi.jl index cea645640aa..9c0a6f23ac4 100644 --- a/test/specapi.jl +++ b/test/specapi.jl @@ -50,19 +50,19 @@ import Makie.SpecApi as S plotspecs = [S.Scatter(1:4; color=:red), S.Scatter(1:4; color=:red)] reusable_plots = IdDict{PlotSpec,Plot}() obs_to_notify = Observable[] - new_plots = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, reusable_plots) + new_plots = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, nothing, reusable_plots) @test length(new_plots) == 2 @test Set(scene.plots) == Set(values(new_plots)) @test isempty(obs_to_notify) - new_plots2 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, new_plots) + new_plots2 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, nothing, new_plots) @test isempty(new_plots) # they got all used up @test Set(scene.plots) == Set(values(new_plots2)) @test isempty(obs_to_notify) plotspecs = [S.Scatter(1:4; color=:yellow), S.Scatter(1:4; color=:green)] - new_plots3 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, new_plots2) + new_plots3 = Makie.diff_plotlist!(scene, plotspecs, obs_to_notify, nothing, new_plots2) @test isempty(new_plots) # they got all used up @test Set(scene.plots) == Set(values(new_plots3)) From 743a443e6e4c2f033275ef30218806f02c646e96 Mon Sep 17 00:00:00 2001 From: Julius Krumbiegel <22495855+jkrumbiegel@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:54:09 +0100 Subject: [PATCH 38/80] HTML video size annotation (#4563) * Annotate inline video with logical video size for correct scaling * add test * changelog entry * use `size(scene)` * rename `root_scene` to `scene` and add unit test --- CHANGELOG.md | 1 + CairoMakie/test/runtests.jl | 9 ++++++++- GLMakie/src/display.jl | 8 ++++---- GLMakie/src/picking.jl | 8 ++++---- GLMakie/src/rendering.jl | 8 ++++---- GLMakie/src/screen.jl | 20 ++++++++++---------- GLMakie/test/unit_tests.jl | 18 +++++++++++++++++- src/recording.jl | 7 ++++++- 8 files changed, 54 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74cebd149f0..d73e4b9b8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Expand PlotList plots to expose their child plots to the legend interface, allowing `axislegend`show plots within PlotSpecs as individual entries. [#4546](https://github.com/MakieOrg/Makie.jl/pull/4546) - Implement S.Colorbar(plotspec) [#4520](https://github.com/MakieOrg/Makie.jl/pull/4520). - Fixed a hang when `Record` was created inside a closure passed to `IOCapture.capture` [#4562](https://github.com/MakieOrg/Makie.jl/pull/4562). +- Added logical size annotation to `text/html` inline videos so that sizes are appropriate independent of the current `px_per_unit` value [#4563](https://github.com/MakieOrg/Makie.jl/pull/4563). ## [0.21.15] - 2024-10-25 diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index 01b2f4283a9..e4a15c9c8d9 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -122,7 +122,9 @@ end @testset "VideoStream & screen options" begin 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),)) + width = 600 + height = 800 + f, ax, pl = scatter(points, axis=(type=Axis, aspect=DataAspect(), limits=(0.4, N + 0.6, 0.4, N + 0.6),), figure=(size=(width, height),)) vio = Makie.VideoStream(f; format="mp4", px_per_unit=2.0, backend=CairoMakie) tmp_path = vio.path @@ -132,6 +134,11 @@ end @test vio.screen.device_scaling_factor == 2.0 Makie.recordframe!(vio) + + html = repr(MIME"text/html"(), vio) + @test occursin("width=\"$width\"", html) + @test occursin("height=\"$height\"", html) + save("test.mp4", vio) save("test_2.mkv", vio) save("test_3.mp4", vio) diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl index 0763cf6023b..e477f0f42c1 100644 --- a/GLMakie/src/display.jl +++ b/GLMakie/src/display.jl @@ -2,13 +2,13 @@ function Base.display(screen::Screen, scene::Scene; connect=true) # So, the GLFW window events are not guarantee to fire # when we close a window, so we ensure this here! if !Makie.is_displayed(screen, scene) - if !isnothing(screen.root_scene) - delete!(screen, screen.root_scene) - screen.root_scene = nothing + if !isnothing(screen.scene) + delete!(screen, screen.scene) + screen.scene = nothing end display_scene!(screen, scene) else - @assert screen.root_scene === scene "internal error. Scene already displayed by screen but not as root scene" + @assert screen.scene === scene "internal error. Scene already displayed by screen but not as root scene" end pollevents(screen, Makie.BackendTick) return screen diff --git a/GLMakie/src/picking.jl b/GLMakie/src/picking.jl index cffc12dd73b..cde1cd4d59f 100644 --- a/GLMakie/src/picking.jl +++ b/GLMakie/src/picking.jl @@ -12,7 +12,7 @@ function pick_native(screen::Screen, rect::Rect2i) glReadBuffer(GL_COLOR_ATTACHMENT1) rx, ry = minimum(rect) rw, rh = widths(rect) - w, h = size(screen.root_scene) + w, h = size(screen.scene) ppu = screen.px_per_unit[] if rx >= 0 && ry >= 0 && rx + rw <= w && ry + rh <= h rx, ry, rw, rh = round.(Int, ppu .* (rx, ry, rw, rh)) @@ -32,7 +32,7 @@ function pick_native(screen::Screen, xy::Vec{2, Float64}) glBindFramebuffer(GL_FRAMEBUFFER, fb.id[1]) glReadBuffer(GL_COLOR_ATTACHMENT1) x, y = floor.(Int, xy) - w, h = size(screen.root_scene) + w, h = size(screen.scene) ppu = screen.px_per_unit[] if x > 0 && y > 0 && x <= w && y <= h x, y = round.(Int, ppu .* (x, y)) @@ -67,7 +67,7 @@ end # Skips one set of allocations function Makie.pick_closest(scene::Scene, screen::Screen, xy, range) isopen(screen) || return (nothing, 0) - w, h = size(screen.root_scene) # unitless dimensions + w, h = size(screen.scene) # unitless dimensions ((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) || return (nothing, 0) fb = screen.framebuffer @@ -106,7 +106,7 @@ end # Skips some allocations function Makie.pick_sorted(scene::Scene, screen::Screen, xy, range) isopen(screen) || return Tuple{AbstractPlot, Int}[] - w, h = size(screen.root_scene) # unitless dimensions + w, h = size(screen.scene) # unitless dimensions if !((1.0 <= xy[1] <= w) && (1.0 <= xy[2] <= h)) return Tuple{AbstractPlot, Int}[] end diff --git a/GLMakie/src/rendering.jl b/GLMakie/src/rendering.jl index dda559f5ec3..18e41427383 100644 --- a/GLMakie/src/rendering.jl +++ b/GLMakie/src/rendering.jl @@ -1,8 +1,8 @@ function setup!(screen::Screen) glEnable(GL_SCISSOR_TEST) - if isopen(screen) && !isnothing(screen.root_scene) + if isopen(screen) && !isnothing(screen.scene) ppu = screen.px_per_unit[] - glScissor(0, 0, round.(Int, size(screen.root_scene) .* ppu)...) + glScissor(0, 0, round.(Int, size(screen.scene) .* ppu)...) glClearColor(1, 1, 1, 1) glClear(GL_COLOR_BUFFER_BIT) for (id, scene) in screen.screens @@ -43,9 +43,9 @@ function render_frame(screen::Screen; resize_buffers=true) # render order here may introduce artifacts because of that. fb = screen.framebuffer - if resize_buffers && !isnothing(screen.root_scene) + if resize_buffers && !isnothing(screen.scene) ppu = screen.px_per_unit[] - resize!(fb, round.(Int, ppu .* size(screen.root_scene))...) + resize!(fb, round.(Int, ppu .* size(screen.scene))...) end # prepare stencil (for sub-scenes) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index a6782635144..0add2c5279d 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -180,7 +180,7 @@ mutable struct Screen{GLWindow} <: MakieScreen window_open::Observable{Bool} scalefactor::Observable{Float32} - root_scene::Union{Scene, Nothing} + scene::Union{Scene, Nothing} reuse::Bool close_after_renderloop::Bool # To trigger rerenders that aren't related to an existing renderobject. @@ -400,8 +400,8 @@ function apply_config!(screen::Screen, config::ScreenConfig; start_renderloop::B else stop_renderloop!(screen) end - if !isnothing(screen.root_scene) - resize!(screen, size(screen.root_scene)...) + if !isnothing(screen.scene) + resize!(screen, size(screen.scene)...) end set_screen_visibility!(screen, config.visible) return screen @@ -442,7 +442,7 @@ function display_scene!(screen::Screen, scene::Scene) insertplots!(screen, scene) Makie.push_screen!(scene, screen) connect_screen(scene, screen) - screen.root_scene = scene + screen.scene = scene return end @@ -612,10 +612,10 @@ function Base.empty!(screen::Screen) delete!(screen, Makie.rootparent(plot), plot) end - if !isnothing(screen.root_scene) - Makie.disconnect_screen(screen.root_scene, screen) - delete!(screen, screen.root_scene) - screen.root_scene = nothing + if !isnothing(screen.scene) + Makie.disconnect_screen(screen.scene, screen) + delete!(screen, screen.scene) + screen.scene = nothing end @assert isempty(screen.renderlist) @@ -875,8 +875,8 @@ end scalechangecb(screen) = (window, xscale, yscale) -> scalechangecb(screen, window, xscale, yscale) function scalechangeobs(screen, _) - if !isnothing(screen.root_scene) - resize!(screen, size(screen.root_scene)...) + if !isnothing(screen.scene) + resize!(screen, size(screen.scene)...) end return nothing end diff --git a/GLMakie/test/unit_tests.jl b/GLMakie/test/unit_tests.jl index d62843af195..2da385dbbe3 100644 --- a/GLMakie/test/unit_tests.jl +++ b/GLMakie/test/unit_tests.jl @@ -288,7 +288,7 @@ end @test isempty(screen.px_per_unit.listeners) @test isempty(screen.scalefactor.listeners) - @test screen.root_scene === nothing + @test screen.scene === nothing @test screen.rendertask === nothing @test (Base.summarysize(screen) / 10^6) < 1.4 end @@ -438,6 +438,22 @@ end GLMakie.closeall() end +@testset "html video size annotation" begin + width = 600 + height = 800 + f = Figure(; size = (width, height)) + + vio = Makie.VideoStream(f; format="mp4", px_per_unit=2.0, backend=GLMakie) + + @test size(vio.screen) == size(f.scene) .* 2 + + Makie.recordframe!(vio) + + html = repr(MIME"text/html"(), vio) + @test occursin("width=\"$width\"", html) + @test occursin("height=\"$height\"", html) +end + @testset "image size changes" begin s = Scene() im = image!(s, 0..10, 0..10, zeros(RGBf, 10, 20)) diff --git a/src/recording.jl b/src/recording.jl index 7f01de597db..db4f705f399 100644 --- a/src/recording.jl +++ b/src/recording.jl @@ -179,13 +179,18 @@ function Record(func, figlike, iter; kw_args...) end function Base.show(io::IO, ::MIME"text/html", vs::VideoStream) + scene = vs.screen.scene + if !(scene isa Scene) + error("Expected Screen to hold a reference to a Scene but got $(repr(scene))") + end + w, h = size(scene) mktempdir() do dir path = save(joinpath(dir, "video.mp4"), vs) #

;Z2RsMb5N?Y7JTuG4=-<>M~I2g}^81 zlXl|WZyICb0LR}>oTjH>w0EWpjW_F{@AAD#<;W#8hl+y(M2#FkU%A0pco6 zq{o&*jR(e>(s8=b)>H~^>uOOkOlSuX?Za?Wds9^zG+rl}M1r*~6R^X$dX zP`)>OCJtaubmv6tHt#-ESoec0K?UxDh)@@e7+yhZt?#b8U#o8GqT2QIZF^e*^#!9Y z(ak$~Zq~Ezk%<1P)iZ-(Viy8Xp8-L{w6%iDY5Tbd=`#1ii-W;=vqG(P0Ut3w2RqYt zzs!*wT}{;PR)ivv#Aq~u$l5M11kn$0LB`ghKHJddqm%cAHZ~@w6boTd?6&fSVCDde zpA@f^icVu>2DO(CwBr(RTKQHu7r5MlxYHm|Alr>n9wwxo$on#-+r8efOML21*)x%# zkQC8jcaf!O1-aiUZ*$u@V;3MC>90`Nvm4%v4^)4)(Nx`s=L_B4L5bjE)~4h{Cj%^JQNz zGfz(eL~T`|;--&O840lpXgEYvV6G?ar0E}hbs~)WYh0~9$}q&!(>Rfxbb~$~BhHYw z0dFcTdoRY9P4cfvpi57Ub2ZDsXlsSR-`#QoDWpQ-8$tlIlSW7wAxh{)5oA18wC*Dz z%(x)f)}{a%9hxhs(R$P}!n*S};*1dRpvSkAU{>^tg&6f)0y0W7M8eq)3T;WadTPNb zWKy@Yi2Ggb z?IZ@|D8$wzM;7s}v>?XwikniQc4ZA&gjY*-8GCXoMU0KigT>YG;(LMq^47OR7M-Fu890|=TA+pILy0b}O^M$yE>x(kPmH;O!~&FN+r;L{{o=G?Ej=TNu9*j? zl{cbo~?xL!$bT;mc^o7D2V_o+!9s-(N3N#_D1%|Yim8*mAuR$idj)eQ`s_2RE zS`*0|lp6)L2W~d+F^mz^A%+qnZcCL3L{8M(l{Q**>}RZL@)YOB34E6%o;72Q5s{)W z@f1M6iN>+!q>|n>VoPg~q0YTEW{-Vy06dWXs_?E$volxpQA>&_E$psPwah&5qJSPX zh3Ka-@%^+{4D0t>@1%KFWN6)dO>*JFjrnWTQ^?Umyl9pwJSeKthaG$n<4x)mD`1q~ zsVS9R%5nr6_+Wbyhz(ZpTFolL$!sX=#;h;Kxo+ycs~Y7NuTSc%IV*#FE0>h}>zPTV z7w|YxBG(oeeO1W7s1zu|h`?rR;sEF5wxYhD3gzZLHvM&Wf#j9WbdP-_%EghcQ&+;d zM=f(Wv}B5FnBRwm6+E?T3dDg|arYYa8oQI&YSR5ii7z1L5dQ#GawnZ5T0HEuhQ)Ch z{Z(P4aK^5{Tg62id_Fb($|g1HaJ>~!>KcuxXaQ@SAn>jgY%0T+tS?-_JgF1gS1M}4 xiZpxc3cfXWRgl&eu5bk;Z3&qMxjBYw`)S3v@S+^hi;9XV>7s`9jr00H|Jk89w21%! literal 0 HcmV?d00001 diff --git a/assets/sponza/01_St_kp.JPG b/assets/sponza/01_St_kp.JPG new file mode 100644 index 0000000000000000000000000000000000000000..3e8b2f5717de0124b972b6d2f58def6a8028628c GIT binary patch literal 6137 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 X)G5h@Sb*w_chg=B?)r%Payk89J$qrm literal 0 HcmV?d00001 diff --git a/assets/sponza/KAMEN-bump.jpg b/assets/sponza/KAMEN-bump.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc9feca7035c17660165c9762f58c810aa4934ba GIT binary patch literal 55036 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 H-(UaPJU!qp literal 0 HcmV?d00001 diff --git a/assets/sponza/KAMEN-stup.JPG b/assets/sponza/KAMEN-stup.JPG new file mode 100644 index 0000000000000000000000000000000000000000..f3616c87dacccac454ade536894e9d13ef10435e GIT binary patch literal 85214 zcmb4qWl$VU(B|T9iv+h_oZ#-du()e*mIQ*kyZbIqfZ*-~2qZ{wO>kRYG(aE_f;(Kk ztGl}2_e|B)RP{`E^;AvQJk!%JD=%9BVl@a90zg3l08sulzzY(f2*5x?M@L7)_}5`z zU|?e5V`KdT5gr~6J_!*JNJ2zHLP|mVij<6+oP>muiISR*j)8%J^c6FhnI23_&p`h_ zN>KhS#lplQ#KtD1CnF)F|Nq8IKY#=qM{sQ~(Al1}geL7yb>gP*BlG0O&v_eo}c1W?dT(fe11XyhNep+w5OV zy=`)^;F+zbVkDM63&L(-?wG~DgU2c`Dfstgr4O8i2j-XKc-PgnE2&UfgU!x zFKYmNw0|>6{#la+JcSbBeuoujvle*@3JT=w-f^NRtx_E85~YP`{7 zU-f08yqzZ>EGeRb`M2|5DAN#+m;aR>A4J!s2;q7yo&IK(z*l@Ujs8EpVJ_wppB5%+ zc@YeYAe$cXy@=H}hxCSvz3?}|mV$h5;cd+Rmg3uwBRae8mdPrF2bpjLpgXE6tDSsZ=W@u24Op3zs6g3~WmQaK>0(YgP3xWdhN~`C z;uor<75P~*PzDxj8DXe)#8e0Bcw{(D!f%8Go#{?pGGZ66Pu)AdZ$?jBrsKZ!(wfTP zf^v6oic18BBXvN@4mOa;u~92c3`vWm3!-Fgw&YvE7eLLy3qX_Al}bv15;RR|@OhS0 z5Q9Kb6F~rQJ4sZ2&ot(sZTL390dv8-|AMzH?IiXYRqQ|H-w`fAB%Si}myaDZ2&%W) zzVGb$Mh0Q8gOVCXMLR2n#&h$%v&AG(H1(yM&2~?snAVE0;X^j7S7`gv!^fI7zq8|2 zefG;!bUU5quwDQ*k5+Uop1Jp|&Kr)f5QD^D7Mb_yy-t4!Z$E0(fZ;=XWW@K`63ZbA zH?;RQ--=TeYB=utm$hzW9#U2Y1u{$J3Rt*f%xb2 z;Ru_J>C@Ox{C8-=ydj_KnEQeSAizEgOS#IV8jBP^^oK3>HCUvkC(gm|?g3rR%l@{e z8^-GukJMyR2+TFJMd~lRF$6FxOJ7C``ySf?GNjq;ISZS|D9k6xC=|rWHISoPV>JV~6z-WJyK~{r zEhKw3Vi;E=9X}Ly<9QqYkze2twra$*|3HtuVzU1f%Ozg^=BtZ9LEQxOuqU9W+TwmY zJkZi~1xH=qs1fJpsd;JwlFw`p->8>p0l5xYW9ID(dn8alo~WGq2M0!i)n>1RaXWve z+#Gu-5m1$&&wV2cQEMLNEtej3532-{m+P61xnNE-SdCWV@dKy+VkL0O3ZmlCzbkxj zyXd7JCksg7Im$NCU5l&g0AufDe)+QBCV)zQSna(GD^U!y_`Av&;kss=E^HbP6g?>X zYwAJs2`Hka4lzoWD(1RBLMu$eIw&C0P3)G^&OspGx}T13e3SG;o5$w2bC{yM6d>+a zEx>q4YzbY_VU}9;%Nx@*%Mu5j#zaaaN{~2~6a&rT_fKzo&h)+z6-U!Ydj%Kvh-?DU z|HRj;sMLzMBG5}Y`=v){?Hs0Kj&wZg%-t-JVN#e5ucr32bgSUudgyU40LIrX?t|{` zp4jvdO`nR6x8M{hmyondVs!~B)VeQ6$O-o9JE^T*M@s%!N(at-3Fj#vnuDlOF=t}1 zhd^<7EEqwF`aUhpS))?SFz=Exo8mvs*KcDE5bpv{n(g?rLq2)WWxS=!_?{Tl=e3DGMTS`-SmAPjG*-t)o6*8ch&~r_J;Bw z7EpE?;@PftVa*s3iH4!>7nyJ&KFz1j1W88=K}g ziJeCG zy@1M~?Dm*|K$P_#=zGbh&%^F&h_dhC^yl{iOlnNpO-&^00~sV1|;Bvt1~>= zQNpBK6hSH+e!c{h7d(sQfB6bVcUbsa1zQhs*9>$iH0gTI(r(K!N?6u9c{n3hE>1?U zA!i74rA?g$Ex5yjwY|<8KEuPbGH4Hg;HeouFK1dpo=X+oPK{eJq?0+$E=tjtvXbN&{9Gz)S?!`p-Fnzgo=GXx6<$eb0F`R@jbP~*E}Tn}o69)$^`dGFC`p|`Nfb{LFsOb`iNvB8p@7qQuLs!@%I3Dq7G1i# zL}al0xzORUDL~wLl_J+CKE0A`?|e0UEVM|j4pgPt@mZouY68oLWRy5w8!MjFsduk5 zEp2)pJQ6JjiK-s~DyM053QA^`m^btta(6=s(cFpkdEUCUiN(ck_ggzL@KpuGI;z>H zv;x8e4Ug7W#mLO=hb1f_dhvT#hsWV1ZG9KnJzK@;t51h1qw1RahhI)Rm=5z85i%9m zxXL=$gBFH+wiZjj=&tK=0pT?%^&InRVh>w_sX~@SaK1(ZrR4anF*EfGv>N}7(MC>j z|J})nZo64#wQgZP`cP-IzOK_m&%3-0#{xk}{#$2)vk#N^-R>Fo0_wi%>U%{mDzjx|zsdBZfI?69O=3^TA z*fYJMSzawMdl(KxPF@e05wz*i!J5ilt3m_2&oa2r(2SAM;Wnw06-~@5|G+cMIz{zPi?~^zz>&^rA2xTfmpm^VC}=+^vrd%q z`Bq|uQg<>e(`=78aL!f$L>4;a8VCEtGVJY1*tEp%<9w~LUj?1S3^3;KHD-Wb-(~5v zryO(qQIWlfn6B}KCCu|Hq;oDko%Z6Q6RW1+uU4C98{!#nq=5>QroOUzoBlNAQpMAP zJXeMK(O5gApkISXG>N&IYP~zk!Zc?^#P0?QrfHeQcA+>bs2~nQyOovG(?n~8pEkER zHGqZ@m3uKVzQp2)o>p0uvSM!4V&pSlXub+JTwMn^?7&PC! z07!rwb+Wybq!X(V_=(8h)Q55>Dp6+%f_ziy8Yx0L_0UDm^B9(QRy0Sr9cO$cC?ds`A<`PcO`JU+22wJ zEuWDUR&qG<1GeDA8oP0=f+S5;9;v3jyrH2bbE7dK{XvLSJHo9a#9TkU8Srwiezzz{ zKmx_F6(y7=>g>f5%_*#OB(7^;YOs`(y}!5_sHA0f+>NHgSnT&{SjD{YJBiF3vIBlw zoCpOcfOB|(UxZavdlX&jgDe;0?cc18)X_VTqhnZC%Y#GCOfm$*(QZxE*l{h?vSdN+ zVNzzzP4}Nu$~g0C@bi-cne-~!8LBbSZu+4+g~psVF1|j1g8KshZzxlFRzmpE0m~}z zUI;rEvFggc9n9!n$*lLAw%bPyI;q!fwE`zJ>2})FF!-h4X3!r#FaNJBiOM?x{HU(@ zh)q5ILEQ#0smim*cBaoVW1^_OQr6rMkq@nPA3+X$CX_-TFff5r^+w27%pp}8FZqXN zU_|488cre7+Rg$0(Fn{`n#!eS)Yt>GWoc~+xiU1Inw8C?m{(**ZpE-lm@y#|(^>eG z!XdvNca&ilDA8W=Ml4GciWVFHr7oT(=l(#$O5+hW6LUcJc>F-_;vozn00qs9cbcwP zvS1tW1lEkYN;IyaMFFRa_m+UZ( z*Q4)A>Z|zMYg_!J$(%9hZIl44)Rd0wYtw&_rgC@Jl&B6gQKGa1ObF#U7390BRUHXX zA6R)sUP7tEWn2K1Yxp*t-4H@E*1E=H`)W~nQU4#E(RS$^jdms}r*qO*Q(#BYCJ){a zFQO99v(-3mRd_o|0_4`iG`5I$005BgSWRM{-{&XpmZnC*YQ>bh=^v-MOJ#3$`!bsA zab_~qRBeMNr^p(98|tC-Ob1(gc`$iSFSrkME61i|1=bC`04S=@9oT$`u_tAQ6WFX} z7Ek?}W1tbc=ZVt=-+7M$D9 zaa?@)!5OiT9y5n?{LWdLmtuG^&pYEe4?ZnptpS(f9&@3jRQR8s0^SLUg#BNf`RN9h) zJ?pL2m16Nj*^}|TKjvOgszqrOH|wZWvAJ&CLM*|_j;#|`I^0ukp?+JTD~Nq>xF!bn zR*J@@L7J;Omkv7@n2??5b$d{O_Ut5gp`%>_zI~HHY0?jf2w;p)8V`ckvDjviv`kRfRhd&OYyE_%aH$OIs`?L@y+8nDQlcf<1ZhHz`SY%2jf&*UC>2|bi#d_kTkM>{*1Oz5AZ$*x zy*ykHp$OUj)`-D}ZbVyuOm?X;P1@RQYnLq64*tVWkyq@SOPD8FM+S(x9!@Q}9Ixod zpYHlEW<~V(Nldw(4}uLh39NQsOLnc#+ZQcorx=W1`|5 z+bz1CCIOf1{W`)mlxK~hSwHjtVYV#*AjUzku)M=qNlSY}8wiK0q-U?X9dgBX%l*Pr z#b)?Zx`|8VOm=5|=a-5jT&Svg47j3;rKQfx6GDX<;thIodvsu$o^p5QH1S*LvYbwT z)ZR-l!`Tf$Wh@@{!|;PO+1iCDzJUQtB?eJ&o0JAPGNC zZspJs{-e5Oj65;P;hrO2wEoO0%%$=EGeaZSJT7b;kD1W{u>e3}og@l5Q#Qx<;)ajq zmuCxG1DG7O8AC?TN93u{9WP8)L+;oP3aEVGP=cpW#0w%W{FV4?@w6>boMX+%ClzRMN$&SS>^RK-EU`_?1#-H z%N{|_3|;`bFMw(#TMo}bE_yGB4wmhIa`{C2j7)|RG`Pbu7+Z&1vWEqI(Tck=T)R=6 zLoa}ZMlX>Hmx|zk~tpPQ+!hshKcH*Y;;;2+L~?2Lsdu9;#8Zz zPEzM&)_uY1=)L(P_F#jIVr?(=Mi?dNxfWBD!4JO!qu~&M0c65S4)b|^iE=IRDr=3; zD6M3OS55VZcqd-ME#qOAyn1C;s=IX6ZXGeKa=01NJSa9<+Ef53&0GeNK)f_)>$pan z@oGDt{cqys3#VX98Z9pRd|kA__MjPhr1gAKSs~>F6Fgf0<=sMS!&B{da-otVAvqMe zFK8{Jgi=NCCXO?PQkcWhW3@%#O0zQN(tSmeU1AC0jJ68Ri^f%3q;Wp}Ij?g$Eq9IE zD%3r!XLqRzrnqasqI0(8Bes(F)P2ERl8lp>tW*#S%RT?Hr7f$!#pno4g4q5HBz^=W z6eIsl*97ZcG=hxdp5WA(PAixc%E-r4qemqnKx(~_zBKA26lCSrZW`cuj>0@85x5Fm zuUlaR@&;mcT~T(EpggJ+aD<^LXpI4mU8M6G+q}8J~PoHDIp{%OCea}Uhm!3dYQ}!lBCCxJ6+gKQR4topb zL}4d&THx)_tW!tAf8~X=5IJ)l@1wQP#crobP#}a=7l=tA!ukR*AEKSfPywfKjak&_ zakWZ6ONKO`{+6f!5{<$Pu$Rv!6iO z$+sGTAXbz~EpTJaoUOpHEU}#oJKNfdtt_M>`5^}fiE62@#)LbEeWKTP_IkxXm#w6Y z`+8QFzP`SCD1TkJGsmf^Xyxd@Uc7SB;RS%XS}=qd>Jd7J(&`yi;uD~~X~JyzJ~^5D z0|e93^WT{q`%SL#>4=E66JNz`lU#QpVPMFl1ig7KBBAalzBc#1$tf@M5$-n&0GJq? z_9RRU9zB_HNVSuUpfWXXp&LH!#iUy1Y@*0x%VhGD@Fr|h^bQ#DsW+|knSfQ~2<9p> zlg@kVaKAq6y)kis3KG~=3jsO57y%qgp+>;tIrI&3$r>(m<5!!@82A>-tBYzVa;g|l z2}Ae@y?LP!R@?GOt*!i@)+}#+>XyaN+B#gwAij;Q^(cqtR*b*rkI&^qvU`d&`wvi{ z=ST{Tu-uy(WsjAFiFV0+IEf}ONo4L92*XXOw}FET`5L%K+6fF^C#LRpkoA!9RCL!< zIg-mN_BB;w}9r43dFtO4tz_{hVtTxh~^Hp7-z6X7xzMgFN~&V!B^A2o2*6L-Dj zh?Ysz>7B<;S-mQ7(1l>4x>X;S!-7U3rn>#-jahC293?*jpF)0<3LJ5R6Q!Lrvo`HM z*PU22>yyL4DH5bpjLzR!aO?icQE0ccm`Dw^sM%#hagU4yDAEytKVdBco&W{9WYwut z1V!W9(w^KGh+w@ z)Yr;m28r2clqMdNsyeXaNILVnw0w+3VM*R+!PJ?qH#?*s{rGpQOd@kAUXY76HCkte z9e(eUd^okHFf8(USvAcH&DiFN?-gj^QF8En15{|L$HZz&u2KZO&tHUEd(-7W?LMy~ zpzAyJ-=-bTIWpshJ`9=`;!hJc@!<<&V0H*8UyN{NzXHZTLJ1Vg#HEgpS94VudQM!6MzSR@cR{LZNEy(ND?XxZ2AUA|z!0;X#4kZ>m_uJv#1h zB%^e98P;wtDH|IfcG<+mTicovpk$^Hoj;ZeC4ip{%>zZD>G_KJd5ZTO#j-zw11!!r zb2CCMYUZ+@f_9wCQ@EvJQ{T9GMxm*q_iht@*9{Llz16n+rn^96I20u>MD9+EbOZcL z=b2frnMw|3R41u(ux!gWHIB(4!QZW8SSPI5H|oqJhtOnwQ>HD)S$DVer1H7N-x;IK zuKH`X_Z?X~gW_R~1g2<`2*-(m(yx z@SNsSpvA!6e5TN}QAKzAw%I4$F_87Kx){aH&qY-9xecVRtHt z)=y$Sdj-|`F(PGz(o~=Kr#GQY`f~Hl&1g~?10jauw9UFCmDCC&@Bt z7pJ!i)_qc`lxEZEv(VK@R){rb({3{s%P_To6px5gI^)|j^`*-8;($06sDw-;zBg-z z^qets^uJ9ulACtM7l|}Yj^ z=!hfUU>spU;Iij@KcxeG8&X-Od-J>n1!cLvHr5B)<*+oY7fTrMSI)OICGBFi$L3BA z|M%ER{Zn|^#6CS6AwV`FaxVn;R>LV?>B`(#M)t1EXQKd2 zQNV==3Gm0$hpo$7D2w9zBU4R4*Q(oX&L(!Ob!?s@qisy)sn0kqzHtR^?WYe>`E@Mc9DM!8~akTwU{4V{ks5MBfRs;eEQ(x62VC z;&-&w>3FvJ!h~9n!FJ=h{`zKub*t=0yLOv4$65rY+Hw5o^%)~ps!~qfY#k5(2m`?s zCx%{YWO9FfBCnfekX;d58Xp=CbA~DS5HxL0YB%?5Co@2TDJcsbw{bbnNNR5MHqex+ z+7ZR*X4C7fGjtzzgyms`0)lT|4z_t7u_?uT)3u!@>JT;(jWi6xQnCAvE=xH6V?OD% zi1m2;6>64OaJWj~PD}8==oOl3Xk%=FN5x_xcWNbeK-8=nB-|9p#h z^Y}521Vc6v#`tw?nyQpMTsW|kEGRt0jmjld6EI19R`0P^uUksPiP>MY|7G`shz;ow zzFtYHF#!8=l}zi#;^E#%g8miCr2U4phQfq*xK?p7fcs;}zca&%thME?Q^mP>zY=Ab<8fZy~2NHd#t)3=OTCN|jG(;wQQ@?WF!LRIeI zVxFNN7(vpyn%9m^^>d~x@|%NBt_R8;p>yZz;O|SiDZl9B?zWZ%ImbX#g3L4kjMk7| zE{b|)hk|{3#GCJy?`AM2hbfG{XIxR(eQOC1{iKwgrp|Pp_ew7n88J4jG1Hscs>$3~ zQrf||7;Gz$hyHXp2?ut-WuSjme}VbzElArqhVl-%1;$zn2?| zL#?ZdE4uqXs)|R(W)rgVD)|zPIW9ZP072){e9dsQJYuZ3BDPBp=iW&t$QOV~!fCGa z7tVfrN70UAosWSu#%>vrDLHSvoaXW&>6j0HM^9rZSC?2abJ=SR`8WIW9S_?@%q{X#PLZY-rD&C^=$)O;8gr4q`Hdr$lLv_Op=k?ry`$un zirKW~>OdYN<7f!lYjZB&2Wtg*Hc>gG!;~4v%)Ij-6~7yWF|#TzPksuzGbmJ6Hl*IU zduqvmZQE2`7|KaB%KxAt>g8Ps{Z3L?HCZr3Jk(_;#M#y2q*YbbcGxlM(JP@p;@Qi1 zHnF2XiF8(6Z5ajNQm%ZJ<)Sx}>Sji*x&WjYxU8gUo>dK=Z?7Yrj$0akKH3h~=Npzv z%tnr(0F0iCK3}Q9=5x@~3D@p~FwPRe6CVe$ci;-w3rM1r#=jktq#%Xv3iMZD(k3(y zK|2#2nB0TRVAJpzTg)1*V$p}tTl?D90Lnr7Y;-~B6+vAx(e`$LgiVB|P;^vihx>{^ zroF)=SL-Zc(w>_+@+7V-=4kfkYf{WsHB^;;scq7ZG+w!^W00)E{fCpYxP$UZ81tmm zOuff`8;G}tq$qcl0t;YhDcriHkt_&kX7N@iNb747Yf&rUfF;%BK;}8%1~tB1^Iu9jVqyZuBtiZxlz{@HgO@{}OH)NkUS|Gk&OmT`Rz@J{S~oy*J+( z3St8UYFCL{Gp7_zXg{6+Iqc+C+ZT8rYzkkcded{}xdAh{SgnLusli8~^g1PUHI!Y{ zCigL{EzVT%T&nggFA~^uiNuuMt2(G2mOA%kirGq&l)TLS`PMbnAp$#+G7-er;s_V7T;&czBbl} zoU#42%RLR$2b6V8@Fwj7F+)R>1~Iz~|BE-}oC&lz=F1h-`93i&FfSSL9+p_hO+>S? zJi>NXQ0XVUEnZ)sarQM`LYXJsfYjP7Y4%kUqUaZ9Qt3Bl-^W#H{%gr>1Sl~TJNnMYh&p? z0zL6E2BE&h6^ab|&H#c^LcJ8Fy6Y_CC$qpaG=cYLzmp?5N(pI%CCB-#9&5`TUyUFw zo>vGhTuKp*!SVa`7SeAVK z&u!xjC%3~?p4ZeSKMi1*nU)V{Df(B8%JyuN)d=lrzkze!>}jD4p7(6IhhHFbBR;&D zOCL5Pu^WBGN?k2A%B1!w_kW{R1EMMD%Qr+AS-$UWC2=3_*%jB=^jL&-T=!R6VrG() zv>me71M6L#zP}!qRi2Z>abibtDmy)i<(%4#+H;5mrtO&W=S<;fZ(V3&XGR6uGYn<> zS>E>8CNRBgSF7P9TWUS@3VrS^%6&8UURf)@6-`b3Jm~o*`Dg&=t+CZ>)Az4VQhrl9 zSdf5Jj0@BG+n9`l(7t$TtvKJm?)D_W?{7BjcIv)*^V%WMw&l~Lya8h?m)F{9u;SKT z`41jfjjsZ%+Puv1`C_d71f~vLrJ`2Y^PXHB3*P&%A+AH4DSO1K zNhmnVMCM6KW8jfcFJF zwg$&+gIy~)vvu8Dd@RHlyCBIgRtEuSBpLz+Jhjnce+ z?~*B;)QGe;7UbmQQtfwNl3L=uLQ)jadJ)KVBNPttWs`3XdhzNwaWw_F-&dr5T$TGt zi#@GaI#SWl5AfY@j>wDGIA+0KQj4-Cnol-sarUf(m_ij)-wG`%MjVH@=cRBvsc0tg z(-4cE{N(?Lwb#)iuD$he9Tfto4b{mbdMyU320fLtgrJ#>Uu|x`mG# z_ARhI0m{llVFuCVx`o*xsAO5|2vTI!1_?il%2xKj%-#5uCjagFTNRBI!cjAjM&A_O zaWiRPUc{1o#vc0^U`KEo+8L3pWA~<0(bO6;B^6tbck=rprZG6VaZY8<5Z_Skm~buj z!FoOsy`{TUGgs+#4Z+Zo3>W;~&c!g@DU9!v-DlxvOSU8dIg{NkIA%o^z4;43(q>e! zcqAFYe7eWYd-cTcH1btGPM6G?FrT*1I;4dW2I=yB-^ABUGefJ--9kdiugUpANb{_- zk!|`)!h_L3wcW&}Zr9~cvmx!&wMFHW+?Xzc3O_$uq<|!yCWj5`w#PIav!Tx2IgKUy8CiiTZwjhKb`#J>X{B7T~7>s{`EoFVE?!dl8>%9BU zMGB?mHlHqb0`P1F8aI-1d&y*Z3UjnI_>uQ7BC$k1X%CUxr>541v2VH+K#6wL9jK*& zoE7Ar(U@$VYq8I_eOz3w!jh)V;g*XJOH7lYoWlM;vnZ+Xm{Yxd`!?w)?Hw>O@iu#J z5`_Ci*yK?ieOm1^M|?raq*5`f@Y(3NBOH6uU)r)__G*2xc=_Q|Zby2WPpK%SvFdQC z@W%9TC%itJO=|lggzEA|;T1$-J=)?SFr`IH5B5r`s8Ap;}gKBkhFq!WF}=#c0Ii3HNFc{BbV`z|c*#@Q6o^?H-`@z@mIb2Y*9tvJ}F zQ?0^ZIUjQzOd49SD_motO=QTvXQE!cBJqpwkMi{kpny+4JLMRH(;M3JqY^wJWBCp} zL8je>%?EU=Vi%`Y%Zy;PpsrCnyy^Y#Z)8Fy=O_EaQdQWzK~U-_l60>mVILYJmD)Jl zjCxtLc~D8MHcG&UTib2)XJF$#&Z{rvZ+&mHq03Ahh6em*-Q{7IMZ}7KPPI69Fl&mv z!zi794qEyyC2fR5g%|kS8v$9LBT;Cs5Up9yCDC1l+ zd=ubm&{BS}Ng_2oKiNX1alA+e1#>5sxOF*T-qE;9UXMmXZv!aDcep2tW2c!LKWwUG z*aAxI?2h~f1@@Ns@v!CJZB9%!jDF0L9SlQaa_4pSmpjA$ZjTDi!;MgXSn0P5z=P7|)*piH zRB}XEVCYBdM2ar}N^0RGoLALmhUm@d;eNMQ4b4?qLlbo+s7?!&4E*4fD}r#;>rF2t z892XO6_!9OURL_&^nG71+k{d{V9AE!TVF}|^V$&ENT~@7+?=YsMa`TRIB2F?k_ZK+ z>zL}6H%F(wzpUpmR!Y-#y1-zOw>z_1rjpD+6fk#``vNeUxpCKR2t^b3J=cE5Y-@be z5gs6rMD>-4#ueeEPvgeN%Y0{hcv&kSs3&M%`~q-qa#sKF_jW&9k+rKxuu&LS;hHR9 z^Ob+B>5fJ{*N0gAB|Awn{xN}Rj)na=xT??>a|_G;II6<;9-D_eFrE1Ow~-*Do|{dK zya_3f1oS_}MsRP{TTlTnKk!F|JR=5E67`~;tMm(?NQ%5XAH=Am8}BA&GYYsF$#XFD ze4=|dKjwXzM!k~q#Q9g0(SQ6=9#)K^&3Ssc*>p4Vj*GfDz7nCivU|6kKs)qHh&+7m z-$%R9TSW4J(hT+*_6&(Lw3~6`)>PalQDpK=zzeGtPz;v#b=W0s4vTE3LwuIFmSn}rq}6B6R>Tl5Q)gJ~M6 z4D@V=W(AHF-q4_e_dfInK>nEAL-I+NBbmpY#Z<)Q1!8x%7&ZAFm?cl?!x#LKI@w1B4zPwr4J2 zBdG=a_@8|ier5KkpB^fG*jxtgD9+PfH%{e#dH{%|nv=e82#>^%-k| z;!jWCReagsTl?=y9}}nh^OIA6QS)zguUeSD_w(#m6SVjxaIsn#Kvli#-jS&ygH8?; zygYth4=dKkrN%w`_B!EQ5Gz@GoMyXk!Q*_16QhU&VkKJKE)F z;z#NTPDXHGdUgEh5dtBAl``c0c4R9y)6p=u_~RA}?bn1tKc-Has6DIacEwK0Yp=@Y zp@``i3VKcB7r@Dn-^t@sZ38ss-NbA5I;ekqA8hgsr*y#v5%1z?BBdW5Y&w;4vv#1& zO6e5wBE@)gj8WFl#kK{lMISIYfxnKND92}QCIUWHH(Dh{0y(?UYH-kt`ggtKJSm8WIPT@J zmLKniOsONQyK6^|bGnQekc60G``nFjrmdV!o6mfsDdmX*co5o96Y@5{?$e0Ctik$B zC?|?;=jQ=>_+taNr{IZGNm%+-zf|(s0!-bhTQc$X*kj*{Q3t&s55T&9jYRiGH@B6u zWk>01bO*Ik<&di5eBdincOb*gEkRRdDsSRg*Eu-xuM%D$X?ATP=qFQ^uHUlLy|q?s zBjwFAnW2v+dV_*T3p0pel~>l5I>r<|`&d&3bQrPYmL)zP=7kvXA4+_}k}!9j!brX5 zTQ)_wvk5Xx0$xdOpEy7W&l`ICk5x_+E`UdHAz>6nb^iBaoXK!6Oyc_f+M(nJO>_SlB!N2{58 zym?W$E$tCce1aSoJ>iv+aO5(^T$K^K9P{P%SK;W1Pbr$UYpbe_$Q@|6R-ig18)PUj zEiBkSt`eOf6J8Z0JCC2L&{cjU#^N_$th-6<{{l#&&FJ^sAk9AmBP=st08tcYq=V!w z@9j;GMYn9sWJl4@R0aNC{XCjD(=1umz;D6iVg!e%DH}EXsAM_A&oOKRsXhNwzTO2Kq^ZGgAhtG&2RVTj?1V;nS++OB zV|K;|fPCIVG2e3^Ok_8my>pY>B)^|hGdqb4ehd!cr%E9&U{|qDX$b$ag|Z1SG~_f{ zs>fwsC|XI3hri`w&-|oO@=Z=u|JzsmW_f-ckUe9#df&&Cl;^M3Wdh?hsEF|6-v(Um z)`>&ZKUs2}g;{s8(KRA?G&#G~B{^E9zhk$JprlP#rTZZ}D7ry%;fI0qYN`x2o4Xjy z&8g}Egpo2?3{#~YyU31&vNivk0Zz=i9N+;Umzv~!CSh6z3X=2QD5i3{J&faCy zCc$$~tdPlM)z*!tnDa-YV8!a#zMkp6|NW6bTp1&$!Yg}I%yhX0)YiM<6r-<^Secoy zr5{YM19J^GtC}wRVOqBG6~BMTA>^ABEqcxl@8PH{Gq~*>@1?xVv}x_k`1QFXAxb1M zi)w(fW4?kRlv2H)AG|%@c6Th^P_n?ywv!=rCxw~BEVmG*{xs)=M5`~hRm3QbhWjZ&P(Bi2=bmZs{%3){7+iV1L;b06xd&8 z?*^LqIhkg3V%mJP<CF@~>_vrAE}qQY4k<4h5OUNy^_T@QPW&ZhhZ?I;WLblJX}RX19!iG5^H-0Z~%C*=4!dVM$i zB-E9A`6*L&?W@}nR>z8mlipMy`q(f_N|D$sZVO6vzI5D4%)x%%E<@bZDD5OOfuLo2 zangcHd$O?Gzls{uVp01#R9Yw2cu~1cXHhm*7oSn^2{D`AA=>z{Gz8VYIX2fs3Afj> zKh0UQ#$aMw@iR*`aCE+dinYR~K{%a*KBCX;5({tK$D-VSFO@F=mWxr0b$XG2B}hGm zs&*ug5yS3cxOPiv55?OvG>Fg2jrBBEK0_V`R07p~Nr$%8xz z!W|&pt3-It;}KZL_{ca(t{waDUJu9dfTst+cP3RSlwUQKDAyv;Il5(g2Whj2WE=w8 zC*3FtC4Od^AP$QND9o^O|O98#7Y{gOEGj&_%i>4Ua&F!;h@bW|C*{v9Tz-Z4q zSd4+F;==^qh_9ILjV)O!C@Nj2kiYSEKECktCkl9{MPO#cudqs*0%k{MZ+KaXb2hf? zvt+WWmKT$HcvDr1q8GA&C9rz`HJ2IzO{;Iu6OkAcBA`kIP03PF2~)@2IK93ZyylEm z@d@ex1jscW^b)DR4IY$q%fXU!<7e`Fb&+5bU>{gm0=l(}2V!4=|N89`a{taTnIa(r z5TDThK7EVq>m_SsoD%XF#ZM#?oV|CmX+G$^&GZ&%nb1AC!gSa5iRmx|5Wer)M56!w z)bd+T*r_f(i@Nu*u!R*mwEFdrRv6K=oWML}R;`?JA3sV5Eqy}!d|2R_UZ5sV{?@-7 zoyVrj7Mq5C|EyAz-@(k*-u^W&yijY1E*@*1bUG&yrRohAW1taS(UHx?{2X&AX_E@9 zBN*N#*brOYFQZ5F0_YICtMEW&b$GY$!&J0R1wy+GpF^Q>)xy(A{k?CKEu3;e{9h|4 z>Hc{KVsM6Dr5Hpxuh9C5m1+#9x=p0QT6}#&ju~SCZa1(wQHxtMmSX36SZ%7p`>07ta!+)44;v|wPJU5Wrj!||V zu!}wM74Bfq$FWR>$IGJZjngNut2qT$KKH{&BM*eY4z`7KUV9nqe z|9~*&A14)Z`FTP4{rAdQ^YzGW^4V}U7B3v(Jf_Vo0f_0{4E0b`&WRcFfSJH{-uea3 zK*K7WU8X-iZBwn-*;E+Ip{ajz$i-X7$MpU*r7mN@`j!VoL5PrN%vxu7&vrQ^ScgqM zL;#M+{+Y~y>z3qZF8jXT0t72D+4~@NrUG>0mgCgxi!Zz`>kB4RFkIxeMKEG>-?>{Z0-BrqU%ZMQndAL+E7 zU>kA4+!P5n3L{=FpLw3OevckBh1^IS;Ut^i=!NEB(7n0gZ!F`#;aZCdX`3r?!Sv}{IiCW#;2^fF$>Gdg`Y&U2_M~Ria@`;7 zK5IcF^XzkknKbn=O1)z(Rix`}`1c&9xz$;1bTM72u-}a-s`!-+qBU=BQs>mK{-ZGo zY|teG|J8iee1Uir)~q1@S6$swyAdydGfeHPC9A>GsjxCHdJ?t-MS&~OcRBk+gI#&* z9JN_C9nn3!Mt{^`*$#0JSo}Tj z;CV5`T?ZRK=Ni#J?*9YWMBkJD_lXfhpmj!f`eQH?dFJm_C^vx+22s9fVp%|{DcNYw z#8}h?0OMPq!h2Uq>#HXG)G-Vz6C*G&<}z}VhHIMjO}!^S%z99cLPYZHG+M3O=Ut+B zxQ3hvDu5ms(MAijj}ch!UmRqBMZ19_>|S~0_a86&kId({G>KV%4nd++q~O{ zOqP68N0kNEfv1m}&jQZDL;J+1=+QmY$nUA;!!5ojy)MT zde35@&h)%-H6bPv3bI5K6i}HD*!7m1B2Q+{#AvIHUH5!{XDfSE8TKTxLOM9JXl5OOyyJnV2m6Fu+C(o*%&p zF^Z=AX{Y?J9CSC?dqf!NXrwU5HKvyTifZ$8lX4+Oq9vT@^}0*P3K7oVcjra?JI~xw zUL(O;+OKA`6s11|w5}51syuaLRA3%QjiV)tr?)9{wti`tgMaZIadtA&qV`7>N0DE- zxW9k;tE6{Qck&&$Hps^Z7>+V(XZX>JQD%hDo@Cy`j3WKf)YOaWwUSa#QmxaDcIUf^ zItj`{-Kb?yT@U@R?KDYjM=7M8+b|=u90|?Z5KV+YMLTrlZ7j<=z?TCiC8~{| z-D0MQA#n!Lm8I;3(|&_=mrN+aB6U(l>pKC(#`>Nz*HV>@Pkj1>dYxuqlyJ65zs^Q! zZt;NA`nMMLrUdhn1l1`cWJz4a_6`!kWfz2tF90z=sQHuzw92cd`eWJ~#)63L=7-SKkQY0NhnOR(zVn|IWxHMM9nOLT=P3{ELq{#U|PqR3SK(P{OJ2`Hh(1(m;D zuF;!u4ti^ubk4@ln;85{-@6NXd+S4W_efC{ibm?gq*!r^Z1Gz;PX`IoR<8T)4a1Fo zvu0sgkk%xi=0=JiW1QVvCy&t9%#zJ###J^HOGU4&1C^-{Ue)NUNPf~SX!bnSjYfw0 z1mh0fNllqHi3yM_AV_O4E4)3+|9g_rM}9s{9veGnE=f2)=Cy1^DHU6J0c6G8dPC8n zbBP*V3uD#wDb&TR<^K!EKsdiAoxuI~?r|kDC|Ms!R&9o+A3!4o37H;Q3aLQKpog+{ zqqYVCBS#CVmNn`-HeNsmo%TQ13`Jx_kh*F|vO(VJxclNsWQkf;rePSd(gat@?}WT@ z{vp*ezTFo?%u=+ztK?b{4!=Y=M<5r%Q6SS$}dhi-7Z znr39qN@sPYm4bBQDhg;7tA2SGfVyzQ9PD!1jEvFQjhJb#am`?*ifB>XSu(1+=0y6_!^L_EGXHu68x;0xNKTB`h1>!Fp z*|OPm3W}&w+}iu`ciR+9tsG;htnLXP`Fx|=yH_}#aHOduvF>^Q064^3;u)lkmPu*S z=%THGwF<0rGZV;=!#HP~G_WubYbu9&Hbw9VHU;CQn2}`5G_J9)n3AlWyBu<|x#FHA z7CCw@Rm?&(I*0&SBy22u@%m!7s!k}$gb~O2!1-5Fhb{pl{l}a(cUc`!XjhY9LLJXw z4;UPbEtZN*uo6xY+2fU+MX9r-0mvP_2N-5x5}H@4jT%rcMwpFkK=$VXPEKTH+9+R3 zzM-f_3nhxNb}!P!VcD3B@GK1?lwfTTl?}Ik_m5tj+L&O~Xh@Z6!w+uv`(tx5<>1O^ zGo)q=QNbf52?1$jDx{QE8jb5JL-wd7e5(!+8g{2@n z6ZxBeOhiM&Iay_zWEp5?i~v#98Fnr|hkdV}-LO74;#m(3$mVmtC!UT7AX7GJq+y~Z zQL9n+u0R)9QOOY@QbdS2QWK_Z>AP#b_z0w_L1@H)f(3DXuznrl{uSb$7QQ9m-X)ar z45ncT%jPn&brL`cQ>3q>0toH;;c=N0ES*G;sI;W5w#1#u_`!ImMV62H+FviX1B|2@a+UbfcZ?oMu&|QrZ{|!)hoF!<~)~r{HrY zQq2^p8BIaT>*QUE^{h?Qjudx6r46z5Zr!=`!?SH3Z!Idyg^DmX1$jxn20ale9HJQ% zDURRCUH#9c;wD00L~XWW%CJ`i4LX27rvCsqSBbt)D6>hLh@?S@jH(a?pW+^*;?{(X z9VME10+wR6!8coSZ}!5xL0;KW^GE^`9Z0@L^}qN=5Su1TLabmgwQQwrh^_V)TW^Z< z673C43cV6uG5{5St~r@v&}Lzd2;_xaat*O-=I?9g9Uii>ktDM_vDT#Qp!@!~B{n*+ zOZh6w+5-sKtnrORp47X3m|kW#S{B!;$W>!Q+l#??buLFL(B@)N6x9txTN8hw#!I1w zVKb7furq1%OQ32$N#E(d)wSb)82Upza+Ow0!$YdjSOes3V-3wPMFfceJf3v^qi%2y zK36dMl*$#G_>kQVkI%@(S`krYl0>uw)7=kpdtgWl#Lc0z1Z)uQHvPYBH!TvOWRRC8 zhzY^>z!Gg$voY9BHFdG=>)#B>#~Fntjmav!jtIZs5venAzgp2-Un5boeZ8v+%D`R* z)v8tvsxm+Yh4-#&7v=zGI(m`>Vpt-QYP(l_NZL%|HCf3AV8(#`JK&~QLKwqEnk8Ks zf-o%YeqMIt`kMH_D@oKzQRQ(ivXRL^Rb89h9kvIYHaCZ_4qjR8y_S!4Vs`F3VMwyJ z&*fc2#zdv4U!WF#*wZ9tW0UajJS>I%C1y*rR5m~7wkOU{re_)0rfHQ{GO)zb0^ICW zSswW6%eh%hMOk*IZ9v$rC|BPLnm3JMmQ)T4tcglNAPuaK-`55mK$*8_L`{{DklGUV zRWE1+wgh!c zr{}`b7Pi(js;>KcR^Rc76sI-SG_2^X4^Xzuy?{Ti{`d^DYMyJ!!wW2h9Xgd3(`sA z66Vqu@dO#>>J>2M>=?*8buqE3{ZDU9HaO;a0{DdCgA4L5+Y4QR^&f0(ux5@yCd*|I zKUB(H+=62)1FhcU)8F*Pkw+0{k1sS5Myd+6n;UV+vN*AKi4QBAiR5z@UrM3!)Im&} z3n2CuYkJ`J+~!kDDH+V9CIy-^5h`UPP&OF`=W*~T9@z9T^neKRR*|SOfn@r9uuQTt zvXzoD40Ow}U^oYFe%NJZo0l=gqQDGfdIXTAfBKDq_vWz+G)xl4`|v>L z=LUg9(Mpr*73Nh>Q(K?cd^ShvM$R(|dPq8kz}wW}kTW4NsnpPxR~lH8zT^Nm`(eec zhAE9V5v+G+S8ET075BsYNX-dk>wIzcf1knPvRP(>s}0H66UUY_6)?ReE< z8u&7w{B0UQsW+Kfo-1-oEV>;F< zKmleF#-W4U}Y=iIov6*rehO#vgxM72pql~!>Y0!XU0BK?C>5Vd6hDW9? zXc|UfRXcYZ@HoXY$Pys)OpXI8My$SdRy9}2 zTjvwzOtG`YmQmCqm(D!L)8iM5)e9P>KSgGc@8>sO^l*@{352N7hi412_O2_86lHV= z%xXp>%oVTig4Z02A4n3UYqLjK^!@Q67SKuEnI^amfeyVhw3V<~F_o_U8j9Q;XoI&GX0 z&cgz%j<4rcz0ro?Z@KsT;G!S)!Z4~Rmb<&(VT3k9#IC5?!6wR@KPR>prZj7Cs!M^S zu{$31{&9_!9(kD0H6f!z~3pdg~_kz_T$mdx^vBj87$PK4lOSsrq zwi7c=Bw(4k$N^Fq0KhkD9fl^!K569f5t)-O7f&?8C4yIwtX(+iTrjOf>ey{nxx8JL z%jL92zn8z6fY|Eq{{XBsB+F$C5FD_X= zSqm783!+iW^{qZE3X)59&No|QywfdYEH2{w<;yR--sYnG%!liG0hLL|iY)j&q zSZ9kc%`k&FnLNT}^D73aa`L=H2{$`(54gjLZ~A~~S|x3I!7W;b*K|3@QMb=a(o1s_ z7~(Fr%2)FMvD{ZXUMMS%;8|$$o)O_081r&QkYu2VgUPKk1!7gRV$Ie19&t~JL)9OK ze5y>PZI5tyR=&h?54I{1i6(i!4V5z-tVr@@Ur^A$PngxHzn%EN=ggUDJ!&PW zHo}hH*lLasgJkml58=`HZ-&Al$wErO3~6E=M6n~jZfuWF<%Q(CyYq2wa`1>k}L!4PWTLigis7msA(#pz$4!A5i2P|6Q+c})1>~lhQknwWoDZ~ zYkCVFp;R_L<8$}K`PUs5k``1d+6ZBMpI+GTW~o+K+3kF-r~)_R_ZX4pXw(T1%CUwO z6Ch12XaKr*uXDxWIi!&jLPFuimbbD2t?&Hf4*~M}B$8NxDpUix2u*|W+t(9#x`(X4 zRKo0jth;%Q9tG@p{&=c_hc6&en2Efa0b(kF9G_apN;zYXEL@kB)QH+e04Q46HSP~! zN8cRmsTzU}m1(szQLk)QuP{tyRv9M@e-9C(sQca+D(?!8LE-ScxoKZe2bC*g#@jNb1PB=igODthyYa@eR)rFBynV3Z+k|dsC zro!smu{DJ|q|WYS*s79_YOC+>iX=$D(v49Mts?|A)qrlcyh)nNWiv8~vpHmP@`jMe zfx`eOkPm-D+Z8ZvoyxQ%yphCpX-jR~_FNP>_#K^Lm#(np3`JNQjnoSkc@{?aNRKX( zTqF?gTosC(4%T=(VC2&NXM$ZUXd z$0D?*J!!E+SEjx&ndQ~uA&z3A5d=h7&Y*WT+Ny^izc`mC7G^x-=8k$NjaWP}YCn|2 zWBfeb_dNVyD^h5NTolO27pPM~Dm2$|XOE0DFwMwTIP`+N79G6_J8|0_>CRp@hs4d# zm{|-;!VubGV?cv;D)$^$>5JvcYJQ$(67rw~9RiqE%q;ME9q}Q?BI?UbkI_<&Ne8E3 zH4+ZnQTVJgDCai)D5Z3CDm0V;?krK*HyoaK!m_ab9aJ=NRcu3jH`=hrD)6*wBXAxv zQbFYuO_8?z{;d*ZxI%~-y2}wsW85hxWAW+EDj>@%GcY7x#)ku(6uEYikgBA<1y~+OA3D;cV zBY52DyHmyUftWZ_X_`%eCibo|6Cln;l1Ut_jVmhJg$96b;<2*JC}sS~V@7Ri%G_SV zald?Ig~?eYj?RD@jaU3**?5#XGsNt>f;Tj7z`OnNCd8>JF6^ubASSA#WD|Y=0OJf$ zx(I1oS-(m?!`B`JBQr@P0H3LJosX#dVNk|Cf+DSFQz$x!y~^MDSb>FMi!zR(9m)^^ zKz6@wF_sBr3I*xYr^*d)Uf-?($m+LH9U}CAEEViX?0#_Igve1e8=wu)0>z)w@TGCj z26i9BMJySFmR4|MV<1*jrGY-g5$#xy#OFw$l3DXHMGOkhD?0gOQOUEw#hL9u(mNI# zD-S1>?^WC1_rh~7B+ImYXr)&`lB9@v6=2@Pe%Na;g^AIM#>66;K2yzE-@iEW7bzR7 zxn(+t)J6LmZtbz)HnuB#DJDme=(-QxYXhd1YiqKT5AValSlab$h7{Wat8b z+t%=6Ry0F^wNysK+X{YyI-z=S+M1m0??)0)no12S7Pim^zv5spfiaj>zcwk%qeBdAg1WDRviKmyMn z7-KmGjEZQ5+vE4bCYnUZ88W)wMFao4O-LA?cc3v< zJr9E;490x2Bz=6zc$6^udQpw*y@UE<*(PmDvHn_~<}lm*$WlnHjaA_ZGqEbp83ckmQmvzHBzF~zh77w*CTPTrI+Vus08s8dcdfoK=|hDp z2C%oX6Se;UPWT8`G}7|eXz#h)dm5lV_&$+q%ufSOOwS>O6ujX>S?osN%K@H+%KCQ^}Z1jYvE&v3p+_bXjOJ!>1I*){2Q4Xq~(I@_%ez zuL+qnLXxUDA3!=u1de}R-xxTTFwrFCyP6t71whyzd@RJ?P`aN}+-ii-zgso;+Z8O5 zW)5NF>9kkB@Q{6L*J{NTK{{pgaYpm0Efl-8n%Hb>j{M-@PzxfxAv9@de)wceHO8No zsoivLMl&e0MJ0twDn{#I?|gHA+W{jxmx36b{{TnlA}+lUx+cXOQ#4stX%Nerj29)NT*E8x*bsQ!_xQpI66R%0)T}x&$3B?Gt1_=m zP1o?zy^Y{H;pbS(UW8Vz;2!?~wk66LTQ}lrvFercS_F!k)G&>7sQ})lP49~a>tI}F zRedVvF}MMcp<%Ti=8#9tz z#35%^GpGvNdF!5e?}14mLmWzAW|L$`kQQPGWj|LJ_QVC|xTP6*0yxtkmT|2J0N#Y3No_|KhgTENQ;!v(~P>AHrHf-; zvTpTO=8a!$7FuzX6HT2k<>OF8bRVb`D7|`W$v%V7Vtl)1&w}OiWz^3gADXcr%->>T zCe?`Cj@OAU%Me^h@<*xYDQ_sf>W6v&{jp3=Bw0jyB+$TlHwL?pY)JrL!X%Lrv8X4e zhyxY=8r8LS`}<;yfk0t^XpTvIxp?JZtZZJ9TN6OyiikxVvoy~jv{J!YNLI2oKhG5A zG)PybO-M@@Vr(y`&LGTYAdLq~N0Tuj0LBiYvF}VrqgKb*?~aW5X_h#hGU~C?mpbU~ zH?Vjf{`f^pBM}{{uCRcqZNT5uVdXa2VL{DK-`5^?DK!R5asjc&vB7ISV;Y!% zwbVqEKKPO;T3|}1i4L19r~pXy^*H9mCVw?LSX{aYiLRk$7?c=p`A)u zJe8{V7?j1Bn>Hdr8#bEJRK))Py6=y+GFpiw03mHrUZ9$TX>QCnuJ|okh18EL$tzhJ zZoW3}UbqB8HNLXNs8KhjzxVrLomB|H`B;j*#edEuR!(4Mkz~wDG`_^$sjcmK$4DSZ z34%Ax$4X5x1OwdN`gX)Qc8VOrPRCP`weAVV&-f&oDaLAOZY6?>hB zJ7I#IIgSJpPBVU(M`e^U=7)0}{ zBgrgj(=uvyVosC(>piNCeBn8))DMzuyt$b&>zTM|HfB~Em1@}c9+-7u@kI8j{w(hb zJxl|lnqYf-0zYqD3dXX**DvOkOEBrvdJDf`Fv(_)SlUU;F$txSU=-nd6W;dU3h^A~ zRuv7No-C72>ZsB-m1k{{RDAZtlVw%IH~$sj7TGnIoXTpk7D3z76036ga}=twXk6mJ$V4+rP1~ca3~!>L$!qxx%8cg-Jw{sYh+Y z5qE|7hh3!9V`c`A)&p_eOv$1bNYWps4Zivg=+TMNZ1u;9=vfd*8o zDDuhzgttiyx6)@$9QqayWZUV4*5bvD(xN=J2V3 zngIwiNmi`ditSr&JM)it4pS^MGNQvQ8bB?n^|g~)j{WdMT{405Iu=_h-dai=dyB+= zFBGO2qKV*LZT|q}b{_+8bB=^*Do)6ctd-ba{{YJqJX$AY>cF}VmTIl}*qmyJ5Xz2F zkc8|NcTrsPhDHOUTDt>QR=EQcGD}KP;4ZCuDf4>o!P^DbTYj8#F%3i^1D-ZNd>V0` z1&yVN7{~=YMWmxzLE^c)!jv?S!g|sGMxrPN8~kHpM?(Dwj7Y7iiVi+-!4rclO|!7O zS#<)p?_Sst7HJ@UipisI-hS2W zJ7JO|#O2pPAP}PcH~#ohFC3jNs36LySwlCLM)V|oApPqEGcjCRm<47ELNNee6mNVi z$sHj~lw(!4pnRy^i+r8-2M0fhNhHq{(6T$w4yYLql%#(kYtsl6E#h zCz0!2vFN4Cg_V{^5ybi&(*g;5GV#Q@q;-u< zwWI*&fmTi3e=GULnXJxs9hPA;oUmz1ZPhUmAtHb_KTRGdVAGw|&t73@IV z{0+}+D>%@)8Nlk!6UDa^;w_ZF`n1Sh3`u*uE8Bkz!88@d_Jd$_y@7t)+BWljW_>_&1Hk5?&+k&g#1Ds_1QF*gj-BIP#Gl=?dwVSg; zbGGDvTvLklqO-1fggHRXm(|i{fTDT+uRV~&bK8S}&t#|1W#Uwg5_t{vUZhlGU5#B( z*kWw{c_oP=f+y%m%>Mw0iGwOF77n0ZtKzxy6YGP@Wgjh(N7N^k&~+!vxLDjD<5Z(W z4#VSoNwN5{oV156nfy#KURo%YE9trx4Qzz5vOPA(M=P6)IpHkbcqLRQlOt{s#^h1l z5o4Sp6-gt?$k~&Z5w}t^9AymJf!)9ZkV!T<-)h^UvIkdK5FQ=Ks%ri(eSpG~`24Wt z{4z$CWJNM)a0?Iv4IpY8{{R;_>t(W;MDVPxM=h6Va@klCEV9WXNXUQ%7R+t??sxup zk1&kQ*2|!k>4-rg0jzfhl+fRQwmL3LW#Z_vrl1-^5Za38ZO_2N8LoebhU!AjQ39KO zq9|57fB`w1FypNb|O3cd56B;M_RxCvIWKN?zvgr(>OB#2rhTQLn znW9++;?2+^)E{qrT+To40THPzq_`Sb{jnR(7fg#&L~7)1jMkuX?;?Pt$EYX_K`KwR zW7&pUZBhtUwE{>44@@#gp{NK>!hl0$AA9ZTg|xB$Bo&tWK&F6}pfz2N{@*x}N`**Q zm0g{{7Ih6Lo2(XVE+mXv>KcIrQ15$Th!8MDR*{(q)X7Ca=8wPhz{NgJT+I1+rHV=8 zjCEkw=ZrVfNbSMFhexN(HB%pK0tpF`W7wU;6IfiT$f_^NM}JHXQ>N_Uq%5I8RbCIK z=e{B^;0pnW>}zrM!x2CV%vE%n^BS^nnOqG=Lqs|AI66Sv=LZzAq)tk?YYYu?F#`-? zZ6ss?NL2`Ik#}dX_{WjfG3Jp%AF21XJKt|?0_I_oF)+0{0kVw5ZfIBEioyh~n9R6Y zB%_y&m1wGgY(A5?Z%}a|ftsH*OpwqBt{AD(K_mhXuGjY%t)q2idyYeY@G$&;g(k@Ri~sT2;CJ<0X z)|2n;jI=C>%{D?0`io@_U`9U>`FP4kWbdZQC4DasRcA$xL2a>df*|!1q@wTfdcv6h}YL4vksk(4a9S>==Nf6GJ73B&gIk z(v{!S7D9DuA~4%sRbSr}6afOqB%KKwjW)Z`dtbg4EW%}1I<#vH(5sX$TUCHhJMHQA z!{li7C{~cuQm-T8y>Pt(NZEp+rz)iE5cc=Nn!QDK{MkY=yDfS_;2nppaf}jXng=~k zGcKBq_bLw?jquo1tbJIBN?T`XbyZ!RJbK_jT@{vNr~*}1$s^zV?T8hbRD;8$F+6b2 zVk*w++BYV*9+oU)ojg*$S}35-G*W?GR#3Z-N(StE40NQBMbs@TNV;z5{{Sojm2{|v z1$8U}pF!*If|!`2l#eX(`5BJ7Cwm+Edw<3SA(u!95h6yW(u5E_M)<3s71C885-D|m z5%<16*j%t9I?Wg+;DdI!G}Po>gg0w9Zq!zv3K1_J^SO)MEz#h8bYv=qB!V) z0i*CfFyobJL3Z2`eN9$&;}m3~Ot~g;dx7OVny_(V)hduppsKF0LV&|}UT@Xv8Hy}tniXGfjq!%lDW_!Ul}fU# zmR9@x8o#z5m2jD9j#_1aO0=v{y0JgSS9E&GYz~G!4!MR3 zK>#gL`8?u^w3(P06Z%3n7kMqP+oB*sPhUMWlHg0qIw{Y%xQQtSXL ze_R)b+REJ(0JZsp3qO7^eMh4!xoH)PHx3xC)yCUl6QJrb0bDa%7drqsKQ)RKTp?L{ zhfd>dzSxh=6T1{;BV($@pVJKyEE$DK!%Ah#26G@@u`vN#P~a(04SJJ?2;@m;=fYWbM*yJHAgU>MMgFTfnfR_z3|x3yoyX_MjynR zlkH>qVVXo!Nh2fCqSgtz^z_7&^;<%Kq^bfyBe%b<3Mq_iI!x?uU{g(P>)RUbl?85g zW^hU=P#Np8{{Stpa>A@!RN1wwUB`drigHmc zGfdXcWG%ZKFo>lkI0}?mxB|siFrG zIZ%zIH7PIOxHt^Nst-~@7*a1%(trnlL50rUBcv&IYF{FfKj2_eS>iXKtX;H4uFm|| zIDx(;9Bbk7brH%kgdh+_o$k3JxgVxE>79hf4AbVMQuJb2pk?fBExJs+ZE?u zDVLKmoXlnO`OLYN9g&nef>lFPAN*^%J?Ilhg0|F?<|P1zM=Ey|PRHPbwi=*egD}d% zW?amw?b8Y>_uAmifXzzP#LA1!XPlZpaxPvS8t2Gg>8=s!Em6lBvPFuXdTJEeh01=`FyC%wq_i> z9alQ!DGua;M}J!T;?sx2mvXEVJo4FRK+ZIvKf$MBcON_AHp#Y6D-n(iv1sB83O%B( z=D;TCaa22D@~a%!ev~wzv~o2yDHK6HiT1(D8(x==IDm{cfWe!ePfElP!)zLP&RZ`s zfYWG}E?LRMia>=)2+wf6MI@|c8VGc#ER ztlHV4KW_Mea~6%_g&Ixh%E1&0(oWmh-o1T2$4JT&Gzj__pa#YAJAa?g9M2I*Vp7vI zivp70O9C7CXoKte=-H5sg2!!zqbOERhvK+R4Q8^`(@Ex*>o!OnlY+J8z85g2@R=XjMRMOhKc`*!vHBTO4t;31ZHW-s9Nfom8e{ z8X5$x_ZT%~bl6j})ebN%(vvLCWn#*{RMojYzZh&{*eb@-6 zxw^B5Ms#apr$(BP@CT*)bAWi`=&|d zd}Wts0i^0M1Uj(+Yn7_+T=Tv&tm>sfy}=;%!4w@pSqj+}HBmRl_{tEHWmPFAmc6x} z#BwVIPSZlNF=&(C)C=4J$o3bFpsK4OP`7HKwQuWu{jugCIJsCyce3(-=Nd`#4W?LA zU*v=*K~t;J=;2VOm{|o&am628HYZU8D9+mG+j1Log4mq|DNR6~BCqe*V=X3-ERt$r zdj|lelke&~{NRzb2`xhy$rL%@9C;_q7SWo|wsJ(B#zMCiM9G@_Lx-i|MFFA!cNF`581?zPYMz{i(X&(AAOp{CMVs_fMpuea%s zMC(CfmN_FHn|AfVA!y_qC>QH^lOeepSBW(A&=%F^BYo(4)*Cv*K0_|j2G^9#E@llP z)G!2jh3+WuaEaCtBZO%KRTFjg!y!_cGpayXve(bYrWsX^WLgH^sQ~a zGD8^>sL?ct7PbjruW^lN-Z%+J%z;|bQZO9*>b?H}Oh2WgNNc0D>Ef`dqDdj?%C1C_ z=zRr|md5l!vHbBZDgar)AnQX_tB+0V4-$EaB!&zq9l$-f-yX@pnP?-*lO&O%&}VYY zPrDt@&Nc{=1&^mnYi+w8aV-+*nm6joN)tpah6mdNn1?SRk4mbbbtXfo=@mrM{agP4 zd^jSMWhZmS+?-@;8UWErA_2Q3erO) zVm4M!%#t56osW=$N2<4%3zIDaXJO7mmW^H)Ge&o52bW3E;;Uo*Fx^=&=2(;)0UONP ze7=C5aU*0`T%*oIW?iB`mY^29HMhRp?TEt98H+I-O!I8T1cj*g*l}M24U{HfnrPz< ztOCs;sTPjH#wh4UH3UX9wg5F}^~7NTdaI^fz00hS@+cum-p8e5V+yDTM5`7ETUGD< zy|8PPZnRTLmy^vCCZ44VBwc~Y*nfu)%=Cmh%n?S+e}%WY^u>cfWK~v1FR6D>ydUp` znSw?^LOi-b1hxMFp1rX2nN0vv>aFa5JOa(7LTU<6Bn$7`5@N}#%#7NL0$9~Asow(q zUZ;48D~(5uSaeVp>L{5N zC^Cm5QrExlg*4HDVSv-c?|2o=1)aKuP#F0%0reQTI?TqYGWX_?hN7prKg$6$1{pMr zvW9RK$OBHGXo25vUidK{sY{a!#~h#Sv+M{s2@y=B79l_-X^!{&BhdTcrLQ71N*Wm1 zU?3365V!5W?}awF<}I=TtU)v6BvlrDkB9?}sV*83eQ5Z?fW>OYraU^dg z1PW2W^&p$=g6W3AB$7&S;VcuLs`|HaMCz3cwzq zrt*)FdE1}c9?Z_!d6Y@5o<;uvc(GNvznlxmk36Xv4I;`zF0j?}Tx>=)GZ|)CAdXaz zGKg7BY&Bo0ALlq(FE2QCc?M-d>N;2pR`uWhFcSEHX&4D&kx&O~ln*NsF22WZ_+(jh zi#gWK5X|d(TWw^G?!A4s+Yk>9Wpf)&(`A-I<_cXTkwI>*hc(UnZHTH_;%F2`HGnTt zo=3N)85$&|B$F%Gk!1^|NF{G#e!;B!97QfxT)O4-7I+kBGzG8wZoNDEV%Li=H-{Kn zXrm1x0MGR+D+Ua^_8@KFz6t8S8`CmOQV?Wy17l%!{Gu{x5^?ESOOlBi z>b+_SJNo;5uU{=gUKq`GKVwhML zrgwD+Lau-}J@36>#p;dOjk4KYg^}O%?SUhT85b(qc`{i=j>uFCZ6YZ0lY3s=A52O3 zT`??~uAyNPi5x_sE6eK?+2u=&;74OnOMAt(WgqYY)rD$cLvO zCuC-l^*BDY?Tnfpj1oHKn=nkBLl_hd@y`@LmI+WRnIVoXY*$+YSGVuB1Zp&~=veDp z3I|y7f)78-Y+4x-F&R~m6w#!g*yEmzUofQ3sBX%YCf2rJ*ALD}JcLK2jmamt!zi&! zPZ`WBm9mHxe>3Rc&8GdVVrWbc)ksyXXn|14L`tp3p@Az z*ye{f#A>9e`fnp{-49ta5q7Yptl)$KMJ710;v0 zJff7@Cb{2_wl2+RE~KjYg`ZK0H7Q#Nts8Mwwfke8bb^LN4Xi{68v-od_Q7dI%ylDj zj=@!}*p0Y4PQ#wqo)(B|B#6T?AoR!=Rr=wY(G(!FU=5Ex=N7Da(Uw(BoT#l{#|ICY zTsr7oK}$7?JdOMPu#wd#=?Hr$JJtqUc%ubenBKuB>Ds>hoJBmDsSbK1gDM1U<(c&j zBiq!U<$y6ml*~sahl$=ml%QEVw{G0?#y2#JHn_A)mSmx>w!c6-;UmK`F?fU-)0S7B z6e^OrEu?L?KDZOFBS^~8va2PBDo-?QSE(2nly?vbDxlsEfWU(+Zk5j&T|?N+bqGSD8kFC;$hc z7;GZIR7k;gvR9GcoCP>Q)3d6`PLjGq((*fB`FM9r&nHbJRMZFI?HA~PZh0W=cG z=eRY3D$f@P7#lZ4{{Wxg5*y3YK%-jfLn@Y9AP#uKzY>g&;LO@n%nIy%IpZFAS|m`d zD;+?H`2b$zd~=K~6q0FBBv)GoJ**x#`NWe9!0Nu4SqN1Qs@4>X{SNpzVx6Vxm|RDz zOnQ$yY~J{8kt_)tq7p`#NIaj6CE*b=1!D|IhlwZ=r0ji%^T0%7oh7B1C_q@)=N6T8 zh@dRQjfeo*??&+sUMS^g#Iphm3jxR`h(D$Nyknggk1vbmJY~EO!1EcylFmtxba~bd zax8sINdX`WD!163_`?AIKmflWwcce2phpaWi28u~{m;GvLK@gf(xF6=SmtGu54J5NgQI$C(dLD+kI5{MaIN0liMbp1CfLDG`?}050gF=L8 z*aF&KhTY9`fhJaB9@Gm25=9@-V`I^+Sdc=V>2y1-d)6KKYD{{BmMYHnfAx)2sMwXg zWU1|VmV@!=Bw&PTEESQM5)^~xtP0DPGQ9+%)InRZ{Rh4@WnSG{B?5u^#gkZ)C7Yw< zD#cFAs)7EVxHg=Q%&ir&gabrnh;yl9+mL_Vj|}%#IKa6awvc*7i+Tdw`wV>iv&fUu zzN#8n0Cw;E{{WsJ4r)G=CSfGf%mE3>Q^?zJNZ|Ix`w!LF&8(5f9EqV+SyB`*kAG9? zzdw9T!Z3sb5kR-4YVY}+5X^$&CWc`8h$OHI5I2wzd+Dn^=$v~x`P3zvCHy!94Jkx0 z7f0>!_r|D>bhZM?m30hD1W%;3kG}h1kvW$pa{@L78Ey2{A)~+TjwG3!o?AHC$4u6_ z<`A+fmu=6Wx*K1|+Z|q{G3BLfsc7RC&(sIZq=RGAxZfCUdWCG+Qi~R!qT~Yp_VPcO1pSB>#J2qx@mn}#nZk&xNkz8KXy}muM)@hb5tg)w6^a2&V z`QzgojMqLt3x$!HSz1#+(IoCQAnrG%-)$qEv61C+>biOdTgX`gpe0xX<|e*zTmejs zT%rQQNo(5w0GRm0ZG$;wYP&ibn{(J=z~yFEEi4@CL2=@vlo8fZUb_{hbnjve; z$hs%BVuocz-&?GUsGw@Gz|-s6@ht8W%*^ZvA5t3kMqRSZ^&>D(;YK76W#`k3 zTCu(fE^2(CZxqYrGTDb_4w-UUC?7AFfqq)Og?+I^>m$sPEM>_wO18ufclDv$z5!k+ z1cq4Ul0}fCXc~krYfXjizbEg7PZP*`0s@ZU7I(cna6r2pcJGA|;rTYmPnvgOHbP0! z;DqMf{v6hm6}KyP!`#(uG3ryU^z;IeHyu z$$^3yon#_q>M3QVKO>bM#~iaDRuDE?K_v0(bqYq50;^EiRq#E}smDVhp363GF#|Is z(ZY(#nvUFow!n}_y%2EEMNv3jsgfhq&<4PI z{#FdiQbcJ($}|~SRY@cc+jp#UqLK3%NPN(<%H4Q_CW6uGu-1ElUYm{w7?H`H*PV+e znU^;XV8s*4S1RF1bzq>{jU)v+hblPbH|>U5*14TuUjtQ=AzJg$iBiyoEpZMQpmVJ{gnBti&+5DbNy$UN{p z#sXkm%z0?RN=BT=8?N_js1`Nl6b|QZJ#bFpky=(Q=mL@ODSKjlIXScGoo0Nrq9=<; zkm(?i%H0#T$B}2(4u%QWH4NdNIGGuUqQKo%p4DHzFW_WyOw%v`#Ji0o-5+0%Y!o^I zo%XGbVXP1tH9GYJQz_n#77fBQk0J7w)5oFp=MpMbWRN_PtU+VgY&9F?{{Vb7PMYa8 zc}Vkb{LT?N14luTfmO0=eeZ9+D^U=Er7F(rMwksJeg6Qj?TJ$Iu{ew!+5Vsb000Fa z*9D$Qq=YOi%2`bo^Jz8dk7Xo{GQv2r>kGz%n}m;xwrIq&{f zFN^3rGB$@V7FOB8#w7`i4^H@;XqJ1z59<&_l`s|BvVo$XDrOm>(BPb zXDIoJBMu|IDoPenO}6V})LtLNSBgQ?NQ8r`4xr40n;&h)JhDh3QRQ%0#@&E5V`KNm zNvWV+G!%}ot(j1Y03V&PlKD$;7&4I=9%kE%;~EFTs3S*}0V>+`0$7vkK-eFg5;_Jf z@w%*g>pN?|TRYeF!c?h~O-w9{(m20=?}jv;N!m!dniv&e)ImFb_(A!EMr3th7*?Y3 zhDEW%S2c{wp++yb67j0QEH*R`3U(lS;R=pX@|x74`ePdwQN^EMabFn7a{0 zV8mCg{Nm8CC|N)VMGrRC2d>@mk*modMRs)8sag0R3*YaBw9F%EN>U<)A%^DtJ9=PD zF~XpRF4e19prDQ_zpfCUHVKi{t*MWnS3hr?#1kk8Pfb-daq0KRKsk~-k(uR!h z{*_i4p z{6>fN!qjHu&5b~;XbBEn0)EwdVl8=EQyF4(0DOijquB51{c+;)rm}0DD&Wo;3{dfi%dhNo6tqquT;r{W51U$lXVKG)UYHf%U^_p`{C@&6f(U7WE!Fm2CMmCBkH&exs)oTY^l!sslZrc*-<1==WvAbzSv|_Q;|;P zH3AFhCwlb2^;o^QRU+9KG1|GkzA>uo>=?>LSzvFI&)eS;QJ?E1GDXsr)F5RM(-{Hm z81hGR>4~KgW*v*SGuM=-3;2ozHG@s>u?*Za8#@WIBLj(-n0`o$Cn6!=f8p!%$}PNn9W2ir-dZ zWe&iXK5b3jw#EQ@D4uOcg8UI*=Qztujbsy|B(|zfr3wv?oHH*QE9sL_CuJsuk56jG zMjgy{ir@fEn){3li}|l{>eQPm&B*@%Ho}>haT*qCj1mR+tHjxP4CrhH-GwRVo$y(N zw9>J4E{h7j)m80bcPSabYhBW5p}w#>K!DN zSN{O(WY@kn86QH;Da*+sKho@~z);@3Imb_!=j~4>Yc`Sp0Ncul%*RzpYCt}2DD$^H zyWqO=wAqSfR=u>bHfw%w8{z1=ETn4&RtQ&9ml`Zvv34iri8u*nXUz2)0JIp|sR7UR zI0%k*3J%Lk2kC(U(jnA@5WPS=?ZxxA^2d=N5yLt#4Jz9WKls4s=X@4j&yn>F zU=>^6Poekik3xY_TFXXF0$dPH_85@7{XHPH>kPC<7<_bKksDcw+^V}R8ruH=sKnvQ znAMVlDPV8w>?`!a#!=&rGb4{H9k-?@>OS~FX({I>N%f+NPzL~e{9;91w5rh;Z^Ovi zxZHn(6;i{jgJWG5Zb|NOG>%F+A$;PGm-%{ej8@@IByR#vArfj1KCq|AN`b$vY<=;Q z#aVSCHVoc(j?|+8+WVX1jxdKgX>zfqWgYbfX6dN`fG2`IhW`M6TvL;L*@8C9Lkq(C{f}>qDT+K3KjHI!9`X%2jI_>; zO552Wov2+8UtB6T&INdk@mEu(;LO*D|s5=TOAH3#l*YuIBY$%B%3U7kq>f=f8u1_b{AY+Dp&FoD{UYz3~@ zxWas9J$ZI<6QeB9xacvk1gJ%xNdEwLsJs}pYDW~A*<_3+kjLs>>#?kEV=t*5T)1dt zlDb-)u&bu9YaTPMS*4w)5p^dDuhr9K-hi@ea(KW?m`0hJND76wD!_sW+k?I|0ASC$ z8RV37(s2;!<&ThxJJIuP-(mc)W+~o2qvx{$l68?-NuySqq~mqpae*1B!{{VQRNI;G# z;mpdvnzqd_jX34WAIp2L*vU{jzL>(FsFoyN`+@%e7;Zo;nK1c0##(f(M2lfpzT?vb zRWyNKNn;x0A&R${FkxoJ4#%I<++vvyTxI8$Mk-Z8r7LPJKmyR;T88rgEbwo(^^MDTip`gHf-^_~}KYc#~!dYIZ>hkbz}X(yzxS z?}k;1vr$YJIULSni$G0L5?S?wWozlvw&w8xiy;SxGUp>{;f4x+x78~ufU+M=z#_i; z;_UvIrYjagD2Qz?5q1m7YbU=S<>3zUE%f1l$v%~)8<08Q6{0|Y)ALNQLb_i$Xd!;QYME$OoM}2yLEB;2 zdtL{_*DkSUGPMkHWv6)>i3U=j($*TL}sM64=WmrWJBL#P@z{02UGPF=aWmN(<(sx_jA9^3QB3#i2 zfMtePE-2Xrb_Da=o^a?R6ic%P1W^sxad>O$XxZ3NUiIJO84Jj|g6Rgq)KnU-MPdcY z&Vx$|syjDqW?iCuuTbV1T5_zYHMRHEUjG1Lk7f$yB4m{4EQrgk^&2Bt0LB zva%J;7~_^zP!7#ne#C8l*!E&f#KX?zHKA5j9#$jz@qvX_(Ih~U0jc!;HZbcr zs&c*%j=0)0VHgin?0aCLDbm@Xlp92aSPg6# zf(NO@`A-z^?-ASMORz@nI5*F+C!zOicCQd0M2m;tFZvFS?d>}`nMb^5uy0Qa)m?@7iZ#t<` zSb{9o;X&9unvm*fmvA;hZ)1nMepykaRMnvaZ)4vOJenAdCDnqVi~s_iTetb?g0%|F z5%`J>OAUY{WAA;iG7GkjQqQEQ{{U_KkMD-&k`RSh%b+-O9BM6!{Ox$1Gf<<24Hs5Y z&SRl%M|KtKf8QBsN~{W)$te4sO=KVa;)3oGTp}f@u+j$9Jw_(Wq{*#62bGQ|tpxo} z8p|p8C)T&XMulSq)u*XTH9Pj#IB#SVM zs#)0eBMhz4(hdA@aPqLMBUt>_wMtvqZ~X5bj4_4SmmrP7*nFHJD#C3~P?l))m1i_o z?(6#F(8l_sN#&>DmEpeKyJ4<{>!f!C zin4ERYYmyCRZszugOw_$_uuuw25q+iM*DX@w}F~j1R~u5x*-p5d_*PLNJAN7MwSe0 zFS+y>G>jC`U{VxQ6+3+4eNd7r^_tp)?ZNHOBMfG_^qCV%(Im!x{>K7i!SXfy zdW~PAb(LaiuYBW4nLUMYe)#=%w~0X}6QSuM5M z+P3#{yi+7{O0!20eFOqZnxj`<_|Qib^2SSPVH4~SDFB-9d|(>m> zF-M2T66K>giL*JTW{~PMq-t`&Q2zjE^80~&I8;{Is< zm}Xg}GsgPRWmO0}?pXQ{(*!4FVUc8Jh@?m^o%?Ou0*mI+AdWQ_6pg?8aqEXjV>(k= zQi8z-tor<5Nt^QTE`rH#F6WFW#g9rvt41^BLya8ukFJ;m|%*=0Z{r><=AZO)h%060VCG@4zs**Xgu^zRJImrx@7ldWVoQ_?*8Mm5>)`BVqWM#39bLEF=Wl_Mj{s`1r*c%}<@3$#!4~ zau9*P1RAa@5qOKFl1Ps(mQ4B$lE;eTW+KeXT9}mtkA%} znXXqM8B~q-_V~j(V9aEFW?pTa?XVns58n|VQ5P`@3Jk+ks{K7dKxMTod!E<(<4$1= zE;XYl4q81LNdtNnKp)!@Ou`=&gna>N#X61!*o*M+uGV6ta63j^}<)h8*xIAL$^D)bpsEEyZzcK~cm4|W< z^{flk8CdeMWfBk^2!@7DMaW`K#C8MudtrK7NeNJ@RQZNB0^fuUGrXs&Td=TAf$%T( z9N?N<##(v*023T>KtM2%bu-^Ow%dLy*8!YQBuyT5Rv{9@D!<}1nr35Ke_ijt7@Niu zIUI?b>%$wu@q^T25Jb)dfO>g%7klHaGv-EZA(lVG!W=2g>s=w)w1dve}4RIMB-u zVm7ery3-C#eb4WRBFwA8lXS&PFHb1lWh*A3YIePi_TIP0MkSB_pg|#2CXpCB1Ho&n z-_wn;1AIzl*7^jEEK8}OR$ik6bzA)UaB(cnD#etYgGP@YsYI1=fW#gN{-YIVV#!3x z1gzw~l`N{HKKDbNhxNiI2~9IenNVpI#sw)o$GF8*FFBsd!5Ei2BymDuX#;BqT7l*y zdI3joTrv?nZX4Um`1{hC(NVY)Ql94?8??cxAAEkpG~ngtY~Hi+5Z0k z%M>?6K#nD61X}b|uwQ@u;y}#mF%qiU?B1hBg>m-BmD(WcC6IQYYh&DudQ!v4B5$rbK8}*$w+bIMv8xSyY70c@j)s=`<`oe~xUt?aFk)k`1pQfEY_az$OH{WbU znus~nVpe5+fRO3j3bDZ$cmcmH5VEk`%Eh!-V%*>S;WKJ3vQjKy)el{}dVArjIUXYD znaO&yvgKn!^Q363l|#zrytX?YK^2a_hc#s*(WW@c4I|Kfw#N_sPZLR%cw%RCkhE_S z#$T(e_zv^|y0hvx814T6r%|&09|Q+3@Er=80kIGQwDN3@ImH8{j_QOBhLUUpV4nC! zP^eaO8XH>V+I!co3S-pWLvFXZ+#26(3=t_*WpSv1K&JRnwItt6he%zG17ijXv$oEx zTT*GQfbMS|Naj*>fYd>)0ANueGsBJaH%GE=20B8hSVF#K-YYW zJARn+$Oxk|<5xc370cwmtTwW(b4Q_V?Pz0ZBP*H;7HZ06^3ioT? z{xHHAvop&;4WtWe^XrXm$5uJ1DQR>l18>&<0BmILix`cHGVVdDuh7;s6Fiec6lLS< zt7?BB3RDi;_dn%eW1lsu{{RV~1Sh8Q4=m^%&iPHWqvT0G`+)=0#P} z7SVu*M(U%s1opXGMx~6#$afcN#P;^A01qwE3#oT3c)`jUS5q@C)kJ+G2!cg+^C^22 zt@MI;`NWwcIa*a4sN26jxO97}mSys_Y`2NCPoy@Wh{tva$C20$J%3CT67=A8>1rp; zq@DKdgXzvFaH!=O`kZM15lg@8^MqzsPFcpMRCVMg;i&ItYW=WrPR|f-gs8=LR8gx@ zCt+uu{c#~+sIUG2n2EJFx3$FWb;X%%o!x;#Rv*ZR{LGq_N}=4 zih z5b;&p3XC~d(U_4C05pam7WK!Y>d!8cjOamajd9-aaY#(6R7T4o#@H+;~6Y6O*w)i6l&o{q>SlH1&cW+xuLgn@3XV{Uk)3yNg0&wBvUdU zFJYwb-x!iaa}mb9RtTbC05dFF!C=evZCXcfwjRu{D79j(z$0QgTlW32YQttAWxBD))R9wYPTfQQ z0NC;Ah2}Flq>>Xjlrh=5La(+Qt*Rgj>NUdFpNjSN#wxC&cVIUF3MBmD5i8Hsk;quX zInZU&MOSWk#Bzf2tc#hCEhUBu^cHP4bRDdYO$s|>uQVcOex6<9jDV#`v1Ysa_r!Ua zTPVSVQ?o`jQpRLsx1ikMvYBIxB$=GfZYd+5DK<`beQ!jyp|Ge%)EE)C;CkxV@UIiTqbXrV#WO(`bq5I%0t zt^&SC%e1~o`b^KM;x0;(djZ?y8q!O4(?-vG>s>0V*kkkG_4EG#sQe0Xm_8i-GvU4= zn$a$I!}5|oXC?@wGZ5f}UhI{1e4YN!z>($f_!lvl{A(y!&T1oKdR^I7dmHRe7~Po6 zvC%R{tV57geT8G$nIp_b%4U`$N=}`)Aoo0fOlP0+ndgv5heUxQP>eK!IXCNx{6{g< z%FQe#rF2x;PVH=~`(N#WBbO^AvrY`sl`~v!1=v{&1^KE3;+ZGPvlfdpJI<37%&eJn z7)Ky5RXi2n>fh$U7_73%D-;pPs+)-4Z*psKgrNuWqgo|K%r_h2B&K3~yDU90QB9J6 zjr~u){{S1~Gjr>kRc5Z~k+SCNy>17+@mGU%SDIBw#Cnj_W;b`c73RJ_&s-`$U`@03S2fMAGrXV^$)Lf_s8UqwC&=C48XGW#>Yvp$Y{g8&?ge zSlW$%eese-Gv=d`IWqxNvZ^FHMO86&;i&c;UOO)e$0JCwMz714GNV^}w5#dJ|wx%4U$g}F~;zICY8~hSSGJl z(nkWj^M)^mJXsx6J5EwHXnrMMuG^tx3JnuM&V z@w?wA>i(ubTaRJ~3P!Hgl#%)Fhy#NF?*O+Y)Db z>llM7GU7;`O}Q$H8=g5lZSRWUR$#KoBQCyBP?7*Vk9-K<5uHJ7!#pB37bJ|pDD<=1 z_`)Q250-MK*#jX1jmWdyy?qk zVVWr4&ew&9mx4cFXaN>XL%(nL^leQVPjID|+wIu#4G8~rf^nW&)r>9b*B ziMotYH5F0L{{Yt;l+5L`fu+k#C5%YwKt08G?S&}|sh57Bp|w{30MisYIW5y*3HJ2j zu`+c{Dxp>)_G_4%#y<-h5^%XIM7L=Hb1@L#iL&=r39`)C(KgDpf?`;bBJ>>M=o?q@wZYf zlnX;_2v$~Vj*%^-O>IMBGfHXNf^Nr4CVS%EG zS)rabfH9Ff1Pw-+j@R23tXlI}@~E&kH-@oc`H?vkZ<_rv&YLr$BFi1G4QA*b z*NRc4>5Wiqr+EoG3$QlYt87w?QyjGD!>Gy_wu?S%{{WnEf6{qh70LKsV%OD`>t?%Qt;G%HfPWIFes;}D)j5LK- z>KR=hmZt=KV`O-a>}`dEr14mvb|lmK5Es90*bQxL9NTZHKmZ@@i%uynkt>B^w!j_- z#uYloe8rTJN>1Yb++i%p`Ej9is(?+9NZ+tM>kQYqqj5D5N(=ke4Oq#+V5Ir`&H?uOJvdGAkf?D_eBN^`t8Ok6#^5bS}<5LlETAPZCh=&GCwMSMWxZ- z>BN2ymxn3gIV_0kFpN18GajS)*p_cy$;G3Z4D7u?#61D;bx;dxth2nGWL4JDwFRzh0toixeXy*k5T24|GZSdc$U{o)BPf41olH%rE#-7env7VAbjMqJ4kwgL1gD) zMDNd~`(p!XO={94BV*M<04IU>C-pcOT!~3p5r8}C9DcZwkWQiH)0I%g0yt1NKkgO2 z*eLT6I11W}Ml~Hip1k3b2%}<5I#l17N#D18POl!cD7uxw3>WGSPQ{ z6|y$l31(?0X_nm=c5Q`|u)yg9DkIVsWsup`XGZi0_}c#X#R`!*k$kMXqjsZ-V!sSY zBV>_OQoS)7>FsCx;qu556lRjOZiE0-o_9U4tfZNFGWnKgSc@vc{W8m|P~55Ys@!pf z=3Xf?5;Sq3(d9aZ+l)R&eV`eLlToQ2RRFjGnvcI>iv;7RqR=B*BXV^S>V5{p9L7l1 z+D@wKXux6!)JI`>qb)5#(T>iY>bGxleXvSwE_nK>_=A^eBhnY^3!`p%z5f8NGsAw8 zoi#)ps{N|b#qYh)QEUP@Oji$Phf)siRuYOJ{iH^N6 z$sCi7!n5hEKT)bZzrl)nw~b_h92d&w(4VML+P&}u!6T}v6DAqt5_xR@psztKL6mf~k(HbqgpshRl4sdL)I`0%r zI%-*zNv}v7+4lFuQO_$C=qs9EFjoL5^*^ULqAf&f&1K-qW^8K^W*MV(JCZ!3Q3JJe zwiKr&Gtoa73f6RimBvy$JlqqMqzlQo*?qOwy@V^DLZaIrU6W?BI=CDfYAalNwHtH*mYxxn9k;-@SK6l zW$2hxDr+o$owqxY?}TKBk9dYoX(P)+mYv@$BZoHR6B5jgJ9%%E)E55$imrjYGr-q5 zn#g!#DLHv%$i*b_<|6upfrgsDD=8a~k?3*sFyvYi8c4)aM5SYEHp#cIG;e==Sc;BV za*U5NGBBF-!n&@-dARi-7^5{b0x;rI(W5bDC`ASv4|DDP@vf?pFHd7(R0i3Hp#4b3 zOA<6~3w4nfcLGuf+||*rHb0@llegjLiBL&ZxFqfDaBh&Y%OoX$epE1B&fDP%h;xgS zGH234H4wRHc=#ttatI$c>4#1-=|S@bAo2jr{!qt&KjdJ=shmrf$RP8T&ET6mIWU?go! zq&M)m?d{xbdBuJo62v4^1aY$s4WIxOK{{+62Q}N8!WQi`uu5ISB9_*w2q1gi6_iR#+|83irR;LfIn;mJzlfSq=Hl{04-^5w%~Gpd)*9C(`Je;qY%N|Bcc)w z06-yl+quFNtnkYWvPC56q}yv%8tsc}#cFc02*zN=E}b$egaj#G z`}fDj_>y55>iC%Od6^ zY}}o70YWK^mWRtklYQ@Qe|@@M37B(hN{+*AQ4kK^pVJ>b;hrYGC)blDmCPu;AWvE* zq5}}Z$t&sZKHdC=YbQIp<(cHt29_X&eYethaqce}%PHAnu`rd3B!NxoA~Ac8=idqr zkEGd*AEYoVsQh=v9KeaX0!T!cWtBX{e2u?d@KMPlLs~1;B@oG<(nIv_rw5YQ8A@-WVj4E?L6F5FvL8q`4pbK=14J!(ot{*W>_Kivt^{-pxo0x@NGs%ZaL0{-J$!=joY;yFB|lf^~Ek>#RYBTBM^ zba!7c9ftMa3>DL9Rr;EwkkwGr!6z8%V|BIzN!3ckYCqTf^v6HJGdVNi*(q|d--k5P zLuWCs@a~c{ZxoDeg9ij~R29?BVZ+37E#mo~2J@W$Qz@0s&1gVavkvJP-idF^W?@4} zuX@%|vCjVhSw$3{96H__bL|_eYKeM|0U!~iY(;KH)mZBIE{O6Fq>wm9b}XhLx)!mq zsx)YiTsB<^4w8$cvE2w={Y_x<45UOlg~>{D7zBRj*9NRPNmOV~W=TMh$VF@bPY?iP&?O3KHR*j@>U{DNMj{gAD5=$6t`ID>}Ry}}<$NvD` z_x}Jl#Z)ge7JT4F&dlvWf{}~8lJ9}+mpV+$0r^pL=n_dp=8vd^sbXtr(@52GpO$>!lyxIG_tKI z{{ZGl?kfpGlCVQ83haw4U`PY95zTTp#7lS_D1jC20C4?M#GdMG?mxkcpjo2JtH&c3 zDs?WRpn4m`>Kw?_;DXwHaL5(Vl`_8VMF!XR?l2=_Ek&~sLzbSvVFb*`!0%(qEZ1yc zvm-Z9y1$pqPXqekLb#FCO44eO$>xxdr1l$ZHscumFnrQDZCZ+I*dLrVL)3ZNoq#pl zd-OOdNeDwpps}*2xcBtL$tcv%y^CM0YF~13x|&69p+U7V4P<}DBx1&l9O)rJ2gr9A zG)V+Ng_ySClmVoBUwluPLD687%3XlA4i@+1liU0lVbdIi4yHS9lGtsz6^IxDs_Gh8 z1Ha$X+W<&t!pK#u0yf&W+wFwthD@rId10HRMXP{Q;E+Z9jBiRj#4Nd$M~X!;y1Lk4 zM+ABv_>qD=O5x#P5U~V-exqVWuh$X5CVMj$UpRa|OoGWHp^Gsnc~}jDD#ln*_cw>c zBW)3X`}eu=Lvk zx{8+S>b3H(V#AH_=$)cSprlk$zXgwRxcR`QVnUr{VWEhzWm}Jm#!Diuv0V#f`4+h+ z6w2ttva<&iw_WipQij@O%&N7DRu}tmhO~;&f}?N%0E(gRH{ah3Ojy`VRyKD7>9PA^ zs8%@t0Pb|4XH;8Ib=V{ou6RG8#D+N}WR6KzIUCFXCrw{={+M#Eq>ecwfE1Tfx;C}| ziZ!sW^Vbwr&XYHqlRXJuie<}|cibBn!qpA?-V-)&BL;gf0hR~M$o@#yU`ZRDV0vE0 zy>VxV{Y!dYCxUL;m@KAcz^bG8KNW*y;I zc_oceY5e;Gz18;L605mJkVRwy#ZYSR!Q*^Tq)(T4rHIRA;*QOq-S2FJ+oi5hAd3f7z*75F`bwQR?eXKJ{^%MLe z{{UPBs-rTRR*;4OEZdXs@%?avXZ)~Pqg7b{0F04;cJ#t?ETRk2%r$R9kLr71WvJm$ zMv%bjA5er20r~ac1ggCt(GdP>BsJHzB&)-TXr+whVQ`Wc)ZgtOeJXb&87xI(Nb==X zMbHZI*>4A>m0hgD{McKnMMdR3BZ2HaQ+tkjl_G1Qb~qD=^;c_Qtb} z{YYe#F#iC=K^Lb{#y7&mzi<6urY_JLH!S8xa0eTj+->*t!j!Ivm7Vq{s#Qogz@u9Ws~9M=EWRMI zq*0WGFRB5vsDVPej!o7)h>I~n)MCwVshF*k+-yz|9L`)z7vY*ca<>f|GF5I)$9=K# z{y*V9A)C!KX`9oNIjD?Li5fENLsyx6#k*F*uKm8kb-39~?I=(P)WnMRyiJ)9sS6Np zs?$kH+>Pt+it$GuDVLl~=m?Al>6qb7BS_mwyW9S8c+1MN%!!zM!D-CHt1)1VNVOYm z02Bwg#U?Qtp>XAOSU+vR{jf=vjzJQLVtCQ)qLPF$zI{1At_cmKHeL}Vj?=QRA-XDd zLv&60)qk5|Fh`(CBS|En<`@9km1!59$Q3~L@5R>ms#t$LCMwZ5Z6OnIEPDE2BNA(o zdXdP+J7z9G3ws}(xWtQ{x=SR88&^*0&Z+?6Lo|}X8Hpv#EUdbEU=mr4U2WwX-?k4i zpvtI{tZ|{Y0d*?+X*+vf2P>J(rgBW)ayVv>F6c7mAXUtJ9WA*2BYzcurJ1wFMo4@k z#1E3yl~?mXV`O=&*6-5?0L=Whupm(P#z~oMo>FO-IV=nuM9Hv2;Z;2QFmdbt)!2B;NkR(vPR8%CE zWny>%t4{s9W1$;Kn?W|XnmMD@6NUj@H>0<`kJkf`NZ-P8O*;~Ff@RiJ5JiDazu5X# z25}rwBz+a>jiZeK7Ciy%d+FnxG)ByzWzp$bB)#2P|S$Y(4=-DvEa)iy33gw zPKQp7M)WGi`xDQgH(2p(oUJ0OkihL|MIHO%AjzmyhB9OPxWcNem$2i0apIA~PQk3* zD;|H!=LU|D=?6eJs5J`T-`CV(nRY{!Q#7eKnOK5{{{SYFy5a<8vdkblAY^uoXe?{Y zcqjD1!jVg^FzRGN1BE<%k4zfR8HOfYq>3F0Qsi8F)pLCDSo0xyWGEKN)Jm@tyNt*nu_owWos9OoV z3^TD&DoH3PO{hSo$F~)}GywUS1BjIrP9v>7L#YP#{{R@1JSS|E0)G!MTR+F7pVIML zNRcUxW{qA))2NVapW(g^{jeS-9C_~!lP{T`UotdI@xnnsMO}}ca1>ccUN?ndXx}s- zX=F4cbXC)+-GWqDvIW^2W0B(HC!EPeoQp38b}2RAUT_t6I#>ZMfyNKnk(H0B=}4qy`w|%JiKKa?VD-%nGd`WFKI_SMZIy zV(@y4{W^vWqQjQ(xFSNtIz=rsN4ejUcH3&(bK3Kli?5e>ppwy_R=tLzIH9wCzZ_%G z<^=h5iY%niw2!7lniC&SZNk5QYCHDD5l@;%m02L^X$>1FI=};LXD_e-KpOB z+QskbjBMU_H3nWOGr2h45UrV5kjgbsG_SBA`jK4s7?5+GE0)SCN3CNF#Az+L0m2eT zZbk}yOFV`1vzXqI5e2epHDLD_{%wJi!n`v&9WyayGfcsM5Yj?W(KZ-0Z@TUQ{{WhD z@S;o$lxZ49(CQ5jQa{=;B)NN?x@(rqXw=GQIjW2hYf1hf+g`%A*x~t%tP_dy%M_Vs z%`hn{6zT{$)#d=5z{Pod(#Se(1hJcRJy%_;aM$R6Ts88`EGW|EJkvU$)&SDX zXbbIsrXS*PKhfD-mxuUNJ{yqDLh!|!%u2AS@~b{(Nn|N@MM)tcNjw4QanT>@9|Ow& z0IDYc07^*7FNw{Ri^dL8IAtj=Sj*5dmsC6Jp}8DxD~LbQ8H1kisJuYs=E{uHyG;v3 z2|1YMXruv-`c;t8;EE$1S@Lnej%2(~@iK{_%jPohW*Q~{#O#JZ+I-Yq5b5t)t82!- z^`D#;h~;?NdE`{aPsS&wnB=L?8{q=@Vm(xpK>_ZBMxB5cA0eBzE&iwBof z(x%|Zs$RVX)|%7z!-`NsV|Nx^!ffaGcI+_`(7jB;9-NYF%*&)5{GcA^alRa>I}F># zB%d%;YL{6gg&-X~DuQaj0`a+OpF~-lyCST`<6D5;G{TDcoY!iVzaTh4@l)1EWb`>et+?G(07cO|;j_gQ8M6IJ zTk|9|q%M$XSGe1sbBSatJP%VmPAhdI>AvR#_NgRbMLjz z85{Pku>fXr^E57z2TA}g*Hz!&2QE zt%AC?z>Ua0{9@wkp^}joD;`*cU(I_vmJ9qQxc%`nLmHqm^(2uL)`7Z@j{gAhk2ZEa zPO(e~WGS)PkEz@C!3k1$T$PqIjfIqA!j1E{wl&HS8EIZZtUyD4TIwHvpIkMjUpk1K z#2ZKao$ zT`488-nXr;_}h+gK4H~@1e6^n{KQuFeK6MQKAmc$M)tLOPT#4-?+mi$36Yc_^N-9u z`&J5~{IH~&=r({uAl#1P{tgxA1}PU(yWJLR3%3TjJ7ZQ@k<=8iWt|S_Pp%Swh@Asd zlBGuD*kVnQSY_Mu86A$n(#v~og=Qt1DOxtlHetmgvK1PQUHQE~Ts3CXZRA#6fdG<4 zZ_VR6)MNANrwkO2r_&NCa~ZK4TuMVd7kAhS+={*aaK@K6)R4}I2$YmDD_cjfU02Dg zJ7Y_<}AX^dR8RWhc&IFO+667mDrnWHC5^T@VIjm zG>+3rl@&Xx6;>$5jetnp`qvbFFl@8QnmR+W8Z0a7AJcp9FruvWY9rIrqa*BWE4i7xd!lue@ULmbdMNP;&2=+kEV6Z-p%G(q(8^W`BCA_Ul}SisTgzc=$$jEo8;76o&? zw{d^DALlq?JTPWMlCy;R5UKNf9rr%?PE7v*{WzHAXcZ)t5mea>9*NagzcugQ5g%C` zwwaO8ujR4;L9bphKZd0FXMDu@_+}QuG8mqmY&TKm9lw?=%JlWkP+5-n$53DX^bH0AX=SQdQE+g z9=Nt>^)!lNexR^Ht-g1}YcEGF22yDyon(u75lWpv@k%HPJ^cm`H$GxHQaCbq4#D55 z00W&xP2XYbez=m@)1rkLV!a>-4S#GUvvA2Kj=U+Qx9T`nP}HDqZ*#E5!QG6}i670W z_W3$TZU#9%HQ^at`0GamABmkF8JI8Th}m?#dKxqvmAe>IvzmremU!Mph^Q(+0+ZiQ z=yA)FrN2f@Hf;DJnV}8SoL!FN{^1zu+sb$^Eu?iIZRbIrYcfE-p&i?>xQt`swDW#2CNUSK8b!cKJl_&Z8;nTvh9;_j@umdp~#opuB zVfVz6C~VOw3~~UYQ)xPZ>^|G!@Omip^va1sa7hM_x7!O5&@a+bS+z2z*40<wU3}2YZ^S1^r4mOZnx0~MuAza_YmWQg!wFoxjCHY*wCo95 z{V`fHM;Rw4`NQZg2nD-Y?emXBi1OPsuH=FZ9VGt%OgJ^o@d&e0mxqh$rMFPH~?y+dwbp(<4JCsE61@cqmO=Z;@vpqLg?(^ z#)>Y4T3f(nelJk`ttzIw%VC+Yu}BOR(x`2`1-7Y9825VCADy&zyaUBG`jm z*4~(=PMkeCB$jP%2}MX=O+vXLL7+W*SiVLFF3Ty?_@tXiFB+VyM(Q~8>J_=VJ6Piu zEewIwx+_Eqf^Vy+0yq_kdJs#~IjGlOp)QWvfxFxTSH3G^ByB7dhH_l8+MSO3cfU+4 zDD(A|WsWxrfeW-mYHHlAlS}P>zi}NDO{pEcj>COFPW$nLcmaT}r;1%HvCclI*z6f@ z#gBXxnv`jMItjna6u&!r;aQ15bWpJquGyW+m0CYYJlU^K`{55XOB6vBC1}RrNajOc z>~|-=Jx8dlqQMou{{S4}lE|zLhfuMQqKEH<#tBQ2jgeYO794KBHrP4gB(^ACy8pdjwNoRC>prKZX=W+Xfm;;=2glK6cjzdL}Y}h;BrsK8( zDB5!_sS-h{OC{|?=lWoyMU5E%jKi$RjMh&3k?nXSDoQkxZlg$2P4CaO;GGp*palN_ z5G;PLj7Te)j+tSOI@?fMWdL#uVXLQ7YM9!vBDUWW#UJN8fJF+>2EdKU^f!hW^9|D1 zGwA>nVh7dS?dypjGq^%yW_di`UI7QO_QdlQL`fx>^@}j`F**Pi5|1^5Y^yu;vgI37 z1GBIePnhw6iZ_{NnC4NFRTC&7jh%oaY(WfJu=#h)M>MSoEueImaCbU+Z=o z>m6 zRg8-OpKLBfT)u0hLKw?XvvvsE`r?4~nf$obxZK?X&m+DdiI__>`P=f;8&xDoS&P`A z&5zLShO-w0o;=r1HtEK@h87BK!6xquZ4VkWS)(zc%V)O0*X@r+!YM8+#Ibc_&?D$A zMEtX@e-XOp73EM`$dmN6F3Z>+Hub_EKPa5$wG#3ID59{d{{VNWul?eMl%y-AVJy-p zB(0xP4G?(W9T$ZJY~Thvn`QtUbLXBIq=}u>gd_kY+kdt*nAYnulR1&tR~4j?K^nQP z`&K%cT}`xkhKwrP=rkMC9IT4vniY7ZSJZVSolF5c8}2*n0QHnl~Ki-=b-jX#*(G3EZ&Te!v_6o&3edlK%kz05Oi$ zw{K1HSBVx`_2pI3Lmwlkuz4hVW0^dO8Y@gEa?V%i28s3GVS@3g(`0D+ayDR~C_&UW zHTLHTnsrSXv7gX=7Fv02*Uj>^VR4iVzKDdZ2lA0OE=G z^~9Y(slt{P0|yL0Yrv>k0@Msp0YH&;TkdxE-w7FsR#Z?BfCjKR+lmJsmE0#%L?SrB zGQU*$R=W@4?ce)jOpc?GU?t|3L^QAXnpqU6t%k&1_uG6=;#1yY2+V>fb@IgYrn@%Z zef_aEUN?UZ$uTj+0szR1KvJ3lxZb|lY=xsm4gTGPea8E5jM`*lW#T?(=@!%pC3S8N z#Qy+%P?=nr&8*p|)TZpmLX&l|9^=>33Jm4}BPN4qPqq#zgXb7u{x;KH*2cj8xS=v6 zh8Y!5gQON?6k0CM_uG7Av8>WBQ!M9QkhWVvs{XiehE<3jlgXyVY8zjFw&(d{CRYsl zB-B>PRX{%NS%*MybO(kZQD#*6#tCmMC>)MyMhV8Zh*_owo9^+!J-_hu}>fVZ+AhH&@SE zLF3n-(+Z0G7!up5$j?s>p0@i$jV$~R5J3SwCaG(jG>V-4u& zX%AF^tTPREqiU)>zpf2Ff*}hdrdu+~qD1`Bve4YGum12UnDG|OOO^arhKT_LM*BR7 zPna?F1ADFr=X?`9_xUc0t}LB5_ODDs;n^8-bLKNShL^-=%*qx?nGpi0*jAosZNI1L z#a$=PeiOp8GG-=Bq>hBAK@95~-jx9W410d38=C6FmVxC+kVv4;6-IUZ!6QAG|)Jb5V?mOd?k#bmmi(X>O$t^jPwV?) z)0hOxtnL*+CrT1YAnbid{9;NbFvgNRomN?7-BB5}_TIPy)Zuc)HivNWmIM&Q7t%Y5 z8cKm zSu)XOBFp5m=^2f3zyc2h5Ki_0_3w+=vm$^9dj9~(%d(TfKHu9K`Fzu&oal&101RUV zN67TQOh~w3%4K4T2y)TGmySZ}p(rUNllzm8O_q$ja!Z#o#H2b03c(uwut2bPtG@pD zHp}Qah{eXUZpT-cimi`7rV!46xQH-yDy-TF)HkjB_r#r?#Ma9kOoGjqB$4btwj*^j z%vq)un*hn%zQ>QAaICw_D`{QH;)n3xrWlSMX(Ewnugc!1zAe$1V=VGbOEa=8Ris8d zzW00J>_|bHSxr9oTj0@HOw6%{Aw>!Uz82F%Dj3xy(hDyDTMu8?Y$z|*NyLjX$(7PU zjKOWH_F*QisBNac_&mr#09h8CNnG82o8Sr+ky0q+Dijx1VLTesg8L}6UhP9%~?c+f}-5Q;rOzHe+ync0?7xuy#X zxuQsGBaU~vy}gbWxq+E&d3hBeDi$_LffEKAf&T#FeXwbD=bf2KhiOi(0iaJf^jW!| zIu#?0BU>eP5=p(c_TOsW`&K1{n8ZQ<04%CdW@1DA$A5e?Etra2wx;SsZsAscsIrVxrAmcK~sO9K$NfAOIGK0;?f@{-j;Ay_>s(On3Tff%S^3keV(hM4ZF~mz`{SECO_y2HAaxemLupp*e{bo6wHcDnCTIu_q!Eb{XAu;$ z$N(SG@Od@MtZIl~rm<4ZBNulUd*A8K7XBj`Q}~9B^Gd~1CuL1!0>Ek<9U{lu4$1X^ z7R@LgZl1dW_pf|NaH_!aGR9>Uv34Sj-TdN=yt$@=NgSYZT8Od~6R@LufkN+oXmN?3 zFp$JNv$!%Ul!%ie+W}{>0_^j!IIfm#!m3FTaMd6@C>$SxH^l`WRi(?XNv3H>UPUV; zg$k0O&?D;jJ9q7ZNnK-(B-Q1NfOZ3O-w>v1EYlgXnW!^4M9xo_X;9gUNa!}gHkjqn<;yfVsSp{XGOGyN z()cy&hoE4{&hoC7-Az#*n_$DNkOSpaARW%GxlPWwtuD`(m+5TE>zskID))Ua`etG%Z<@DzHbGALWNj9Bd47n567O zdXD>HGO%;H$Tf{h5;|SEz)g)osU38Nl&_QnTh-PaArB$Vr7Jj4pJINP=g zRAtPQD``U3YL4gI0V=*+H=mKVp?~K7nAD8X*I{fBIJ4UxNtlxclKx=CiXaa6i4&5A z;}0NBC;tHPtyP;QufMJaOx8;)nNP$`23|?oRx(^6VogxJyNq6FGm+dICy)5Vd5pYm zm|0nal%pBbbZ?J=i9J4HlPqPMJOaX{B+NzzqN;q_hZf&SG(D>s{+X6>9(0(piDus*42n~kmF9TW z2sCt1{&=ovoUN=nm6-Ivdhg!1z~#`aZX>xlx3BU1toamTJ4vp~oa znOF;=NW>*0OyJDR5Hgi9Gq$A^HvocYbIurK*0<--9)kh(SpIl5Vxv4oOELvKSvOd)Ahsh{Yx=O6;C09UHcuesI_k-4(cw*8&_^Nz~&~)7bqAZVpNj#zi;39 zU`&=aXc{Rp^Mz$GlFeF0@~jV+ZsUwq63v!63qojsN z_Tw9uS2CK-RBU)~8n*p^d@`(6oMHHwqH(zX5;h|fsI1msE)~FaBG~HGS8?~;Z{Gte zvLh-oMthAW+uwWrF;smKGe{9$kSj==8z0+z9G*)*2`Wb%UK0|73Mr{HfCU=| z)0}i5B2e1oD&n;7WbA7F@iddHr(7G|b?FkV<{Xay0Bm4(R8u0kC4!=;1V}wiabMpB zic+Iskh-iX=3>po^Nj(UR;D2Wl@_3oNTKpES57z57Ck1x+xh%qW+isqB7<}{1N6m< zP{)#JVu;2}ZyW+QYuZ6tLlqkn#{BFtL=cFg%fp#_90Q|$AZ~Ee6-Tz23W_SAf_?u0 zTx3za&?_=@NpgsiHI?6!U5_Ka_^}2&tWl(k2Tc)O>BZe^?%vmC4U3w2@uJ}L@Bh4heV(ZG7i0b5h|^_kFVWHHvQXq2 zBi{&BEJP_~mK`7)R$3N!ZaCi%=aD?creKCOXtLu^OcWsO2)ie}_s2pxWf2vVQyQl% z2H*{V$1}up2GA^X5r05lE0TQ%D^xmI$d+V|JkEkZV8(#}-LKsJ@$B4jDhVf6R0mlS zE$a2LU(W@YWn!SgBgSJS6BeLA=8!Y7nTI~f@SDB$WoRV+R`e_RCXCTmnViV9#VIIlY{Y8GaU z#-b=M$`p6gwvu?O+J~+k;?Ap$9hxOvww4}BhzQX|dT~Pmo`=LBIb#H~qH6(((pRTj zpsmf24yN31>w%Ii!@(C#q?T1aZ4v<6Y)@f^cwSebkw?s|(@36HcX6!WW4#|=wg{?} z&C6=Jo}gE-j-hn2ji_!-fwtqeGAQz~Wsz1~=9tB-jA`1<(Y^jh#yRIR_#C@Qn42>K zb%bjqg-c0oK#do%`%pX9;8!5cnWP1dhFv9){U-1H+X_$N@=qpvF$?A-mLV`9rP_%B zxf}9oxE~m*0_qw1>n+IUk*i`TUGw(DS$xh;8FMP@5@tG;ENoP{P{rCN_l$u7& z>4~+hXH=Syiu42b_{8z#rZ)t-xBFOzDpY^83iQLDGD)3iWQ)P{<;BaYR3W1*Kw#DQa!NW^kj9sN(gwj;}wT~cou05fS8T=B*q z7fd-tk(iPUgr?w;V0&#?Vyb3hldCin%Dpr6kf3`T@3r9z5Ym%Hmv{`yWU38dfC<~a zueK_Qq$WcjSxQK)5D2Z*S{JMRv~T8yJgUnhIh3r73bG$Ah{pA{#GTLSjbkofj~TNu zn6p`|#4!S;dQpJn`&aL`^1)2eqR9))Bqbf$Ls@k@k}H4H6=oH{X@zwa^l1&)j($Dy zE^j$9bp#7)hMnVEc*R>46iHw#g#g*FTm-WpRbi1cZ$XJjz zB#|U?G^C|VYV4#a;PJ8e#ral$6Ui`GWr8UIM|WckjD&q#Zl<@bh(4IizlMmNg%qg5 zBWo;lgS9c<$mbRdf?G&*Cg zTLiLA4XiD9u9i|63?W;f@gV8Ov8I5r{{XX2{{XHVmxvC7f79A>1h1)7E{#h~WSy$T z4}H7fd_D-F%EufcK%yX|u_c&sWzW=Wy_4&Ram=0wX3lj)k|Ts3P-qYs9({KEi^rsz z85$`B$l8kplI#t|gIitt`}^X#+Ysh_Q!$s288VrPRfRAwP@D;vOP1F8(!`mm)$2iafbQSilEx)TEQ@ z&D(r+ne!_wxg8`TgmsorrLOHp_hNzER{sDv3E_)Sl1~qlFIl&jRfQ5cP^>hAxE+Dp zYaOsds$|reaY09$ResoSqD3WBGP<;o$_S;PLHRwffQ1&C^<5Buo;=b-7{U~XB<>q< zG3eu1h*b>Dc1t;L=ZPO15`_T@6q47#zW9?eG{|)gUTdo+hqfNlg0Zn7i5_htuh`%K z24>YFv(W=d9$-DORT4WeWih;iLlz6B{{Wvj88ZP2#@X0mW*?NUyAnmv9nYv8u|rI! zGFPYtsnWqU1t48uo($4UDjyM?+(D{nVsc5YG_3}%4cDkRBs!7>kO+ol;h4POx8$8c zO${S)-p~1X1(6jU7cl}9ve5?F6qBb_?g<^c99f%;wrK(})v0cUHT(Q~Vs}=VLd+X( z2G(oW*VhFik1Ygw=gmmab)=1B)f+J(MV>f2lfO78Vbasn(!`N^z`yk4)wYm*d;DWmxRGm4%x;I&Jq|J!5@i&}lu;#A zqIcY199aI?TZ0Rl5-I*QIavISGJyZ#fwwG1+vPj_IA_(Z~bCS+c=mah{FdeY#X95 z*)p>%i1Nra@2%vJD=+^5ZG~ZRz}+xfG}N|0y+_>E3e+X!WQk*uUIZq=C|H6q(jCL?k60O2^EG>(hhI0a8gB*=s`is5Kh)^}_@JbrV!gbaf~z z_V0lK+IuS#6$NP@kWaYAf80BT0y8oI9g5Ikdyn&MIwehi`Gj?2Z#UlXa{#PNa;rA= zweC0n06uXfR7fN^Cby&C1mb03tw`XKvC-ex(+2bA84>hE6oakw(L>mQ-}bC|^I6$t zF-aRru4}6sSsNZf?PIaUhgyjs^J#NP$VpR7dmpwRmDHLMQmOQ@E6Mf5a`lw-*G#`j z7id%w_q+?|Vaw;dD=#DxD`zvKE|Mb}2SYi(KvhrPu|a%v;xdg-87=10N`MaDyZhkd zFCwf3^+KalimgYB$BL=AnV<~pH60?x)+n4OGa5XRw2+{cPPo=r*;{dTJ&7NDX_Mq| z?8Ze>#a1xOvH&l)udWqo8wW-zv>2hyGB?5ivDC^)(PUiFrc~EWz30Yje(G~ zVN>ZEKqK|v?}(o61AqZW-uLZ<##^i8XF5UHf#x^2kxTD|S4~2wm z%0VOPDz$5YFsW8}*Q5{?D(X$K-<#v(3y@2wpqemMHK-$Sdm0_E0w(;+R(WJBNjw`I z{NN&t#E~f>Lavr#e9kZQ?}-dFpo^SF4xmXoiJag59~ zC0;f$&SunxCU#o`xUM$9$2Mw6R4Ik_)^0t5p2QP(_Q#t$Dt=`0!wyz4Y~WQn*$sCU zs9$Zl#DM^VFO?BM9b-dlCYO7_K>(c6l|If?*LR;|&_bjIug{<~l1O0M*g0 zz4*knutHptuNx>JFapT+8+XU2R1$QU-l|oPasl)>R$(&j2op4+1ff7fxg_m%+WEsv zN%%nP8)z&-k;$XAdK^W?@yyYNAaeN$GqJ1bbdB`85wX#zR<0|4$I}zS`4{W-M9i^2 zPk4AW<{GbhKNY?@S&XJmT*4V+%>ZW;kVq;TsOmlMgMo$!vYB?$!VJ2*OscD(Zamuo zWRt!g05nY9O_%7eEf5yuaP zMVo=oUTKv?X{ecLMUYsY{PF9~Ez7&4QO3P8f;K1~%dsR_0Qb|Y^!LP*XO})u$k~N> zpsGcOc_@N)5w@*}6++JU#tu&qm3-;o{wFH{9YkoQsEXRnaf(M-^qh=Qv>AB>ELpTx z)+hjPV@KQ^a#9hL&OBOX9Z8O0n`36dVX^c)VoRpXoUTb{gDkq7g5i?+ZSK2_Jra2v zsR-%mVv_4Xb*UQ>=x@Fy{{T|L+0P0O26MzRK_rVb8!r$n-8T)OgW0Nx_c-ch*EL!O zQ)y;;NHk^b^whSl(LaXBk zFwH{~OzF@NicLf_Aq4I~JNnSZWxbGO#3Ue>z7^Awm*0 zDvslBYx#ZfC>B*&4C%QtBSr~FjGdcfxvIQP6zc@?!8=o%zP5r&6E> zAX@^)fc74|V=^W+a=kIDYehmUw$?qlz?XqVH3xFn^vVfdHa+hJd3AP14b^h2Ev)VR zHp3JvM9mnFr(<5H+W!Et#;7FRZbE4)0X_TIwl={z7^W-ub)J0LLRd+d((gsMv;lUy z8+OLalaS`6XkwLUgQzIn*RSb{`qMu8maZu^gpF|o@Gx}0WOPMy{A7#bt(dyF}w zak*Tvmnx{iU6c@L*(96ohh>sUvTPnrJ1{~3y(62wuL6xb<)tX>@`cniZUz4Uy14t{ z8GOK!GbAdr>m5m~X!%*`8NCSiSKeiX1S5#spP)hDlQU3te zC?VyZisjYVf8}62q>Z`TY9_t$Jd(76IaBcxEUE~8kkPo{5wRYazA5ThRy>$Um6Qjb zLKb1v3`zrNJ05rR+YgVe(~VRR6+ETM8c8?%<0DAX^mWKXAv>PO`r}eIk{$IpU4w79 zzCAD!GoVG1mob@8BIz;m1Gxm)+nzWW<>xbUWHONFOnHS;QN&cESGN0e&gZ^6GY8Po zi6xK9H&)(-hq!I6B1(o!_lj)5aaTv_>;4RkD@)=m2E&v*qy|%nX zjAuxr%lTsB%EmQNYvlGkd~rtyn}lYd(9#z`n!-fXED&sSz8R_|4bxSK>7!s9`;X;~ zDjiCaVXzrP5Ic>o{{U=29w(hlym^_KqL4#5)O6DghR)sV)bKtrOxl9DD-?_#%WAMT z?0LfGG07W)E~{V)p5m}lM(mkkJETYzjQu^?n2#t2u)6iXY#>t*#O376<(N1PRb-J= zer$h-4OtaNmefj$eYR8TaV}RtV6zz>EjnCD%6qRBzpdetO(cRtt)Exa4SA2gEm$T3 zM_E=~({D0!@O?k$3J{eBkTPED{e~D8XyJJx%RXQrEEX~~5C;PJKYUH$jp7-P41Csh z5tDgkVH7UknvwR@2HKAM1F*%5UQ^R>%yg5hZp3M-IP&!0EKyh|kXTfq_V@P3izu2q zbrBIFv|%lemt(5&krF^1R+C{%zzx`50x2VcX-Lzp00&TFH4lG7k4}x#jZJEHr5D?_ z_~ZE3iTLJ6#XLM^JSQ!k$!28{=Ot{xji6Q|^AG?*3NFF+u1$33VK+_Qh#q9p-&KDV z`hU(O(2NiUCt^w|HAf1OpQf=f6pdi}VWclvev_yQ0>+y4`Nm2Zs;MimqeElAI1DsK zuN6Llw`Zfj7=hW+Qwql@Bm%^n2jciYwkVc25cy&^!D{s5d}QK4>g`WT16Z-Qd=&*z zmROnI6B~U%*(S|>j`*qtO)go8)HD@0TNC^5fy*jKQs|7TYFPz<0l$BJu+qgNE@gKt zXpo@azx2ZuNtv1TDzocV;Z%9K^z^_cV^eAdJbg3nR#i@?nx98iwIg`ZZZ9xSn6hPjq zzA-ZsLNv!EIdv+nM6m|;9^Cz~e#;%)(2e6wJ2(K4cxyhtbA;?#cS)j`(PpJ zIAI=rN-H#mEn!PDHl`#I>Q8;|6Xw4P9(fg{hcJjmI_WYGVSL}N*wtn^@0*Hz#F;rF zb&(0zAR2uKma5Mif5s@vF+LwE$j*+AT0Kk18*loIa%Pu4B{9dCjs}iit4%Tj8Zo(` z2&=l^(EDQS#%R7(<&lKul*da!S(t1+u8uO=`iWT)2Ixqt8~WAz;4@u7W052Z&O7$n_r>^=2cDjB^$pSe$I?gyHU9uzuoE=l;OGTb zhdCNZ+cjF3-FZE~Tq$JT88fXSw0fC=Xrx8hzV2_WnV!&MAq^kPIhg*Di(OFQ7iQahO#JZ?&o28%b_ zf$iHE78w_Ik~91y-8Z)QpaejW>oLBf)RB&Nt?@B2DNj6&ra%bNe5c&@z(&S7B2gSF zcM8w1ar&L{1hBbXi!YyTCruv2f!m+g4$Q1E7i~pB+$h|dH-eRXhEbRDIkmSIJ-NY5 z49U94h?Ye;y~BE+Tp^*3V#zFO(H5;Nb~U*9-wlabkhqATJb*!O)ZmH`g~na}oxIw2 z9P|Dh1)(Ai@<$MBLPfuhXHm2U_HS0!aOoYX3DFE5$R$8arJ;my9a(h;~olT zVrCh6GVzTiNF;6!&D@IKxZ@mHJW>p5@jK_c-uO(gRQJY$|#dcHs9YBBxTC1N)Q36>*^=C?^vNvnS?}| z*aE$Vq7T};;&`&o&i+EmT7i&~RBg8BY*jdPEovNA?#xD?j0R#cp}J}SAZcPxm(%9~ zP5%H)mSQ@YiDCfxM1D||NG8WNJ$UU$40*E2Byh&l79xuN8IG0LLu%X5f5sMaZjp%@ z&s9g52^azoa0bNvusNs98RCj8!LeFYv05g@-5pc`I(@GQ9(OeqvLqVgmPC-eQFV+m zP&7zIqWm42!LIvZ5l)g!C5{=HWCdNzupe&R@<$l0vg~2$%O^|%8&X1}OAW~z9`SIx5s!awwj~hwij2wX<{$9u5e~8(+>4@zItIjl@!NP!(&ia|JNUq z`}|`v(>j%=NkiL4`c|m$0Uq9%r|~kfg3&W;U?mZ$VwYTc;t3?l{v;L4^;mAq(Y}&` zj`!cT*rUUYii_8mQPM57Q9kv)_>;kMz9kojfuDT5leS|p^#IsJ(?3X`eHt&`@z8^) z(|M4EN?Kij`{Ko_;>~$cEHgTQ30t##`;(1rLP?})NI*cfQUzAy2$72BqspR6&a7k_ zBvtYcsfa&To8#P89x@P3n9H<`6+qP;PqWzUFMqx@)_B1_h6vLbS=6X_#p?Ipj2>=8 zchssv4>F<+{+c)2wc%?Db!3o`sC=VY)uo5VDa<6Pmqn2mjoE6)y?c6L9785|rvnEJ zpthL8I0WAS`tOFmXvGn^bVQ4wg1oNWSE1hUS=_!!a=?JIsUpseiw{pq#7Q(N5F^A! zlER}FJ@*(qGpgW{E{w6qA$cQ=`B)nTj>O-=#eGtmV3RV#F0t(rso2*X@%mzDWREcd zvpA4}{2(bC@3{CM)4mQ_Tk%mQUrd_aQ%!S_aGOR_eT*1Kb)ro2{`%c32xPEE48m znI6DyAK_lGJ9=l*OIX9Lbo@Xx%(IBz|6aaEJ6iEly1!@hmE?|!^lXT-rT95!* zR0Co69B+VvT*R4Wm#H*jKwGCHAz&|$F5kJr7c&xPAyp8@7Ddp~HpS@B0lDYzeenFe zOqqY{9(-!oBh5l?wR~vN5VW?Y9`3Qu~%iqAr&9_r#B&%nLkWfC{ILHQx)2 zbkv$*wGFGb06HTmWd&-<7JtKylmvEXQaIRMd~b-Km0~p+Vw583>B!8zH@noJUE0-? z+Q$6go;Q(IAz*cE96riNr}^Iu(6QtaMFeU?Y}~(luE!Fjk)&ol5B~u31foKteXEM? zg_6u=%xtAjfW!*twSQb`ETKz8#faG5oITvj8b&0EU3SvP>I2w&VpdSgWmn2}^{?~! zU?GrFokoclMC?63^@O0(Ly|qbynG*AJfP|9r4>n%?~lKx6w(;uQ>kdM7o>_j<29os zXpWUAp%2zsw+Eao=ux3q>}tbp#p817vt{I(853(7NGwVFU>Zkfa6<(Hg;{ zG_mB-o2{&C1j2dLO;@QFH}>tmC=ndSQBfVr1k_7zLwb|RBM4Q@LljY?q%5K7bP_{Z zH+}D)z8R6pN0^Ey%ruJbW9w5qN-1|DjqT~*1WMI1{{VtP6QpJ(A*2KnLH-&%*K7mM z$(DGVKK>$Mkf&AJGaz6sk@NvvfmOv|pTpvgLFI+PmeN4Qg#ocDc-yuyNvI1$Wm8w076^c?BUZRNr3%Mj%BJY2EN0mks zN!5&$GHeaFU02%iL%I;kz@r_kf;htk3YeKpEekj1mjzg>1IFIo*a*9*m?G*TMyoAd zy}KMPS3ve8Q0zh6lYZD7q><%`26G#k6$5U3#PPRpd_+0hNVV#Wv2P-S=CA3t4mWj* z8FZDHHmKZyLa^GvAoj6^c%Enw7z98b$^QUto^a@;K^tn2t?uP_EC}ZN93M}>I;+j5 zF=feO5DLGj@yBz68FOt@wnJCRP|PTO4!X^{*n1e7HpiKA^= zfb<+=L52t;g`Jp&1dEN26~Mmt>yH>ZY>I`85q@*O?Tl>1MS71urX)}+YPIWL*v!Ph z%Nu29ifFokbs9|oD{uu5F}?;49*SJKX6n2_i;W=bp|_`@#FHd2BLvonPL~ARUF=Ek z&MT9u>lkZ7z-sC#C^3~|PBU7E9Kufk%tW*4W2k_f=6WO}#HUHp#MN(CfAfb!wsKFI%gkCg{K&|41OTIg2_D_=3{;(9%*Pi{ zG)o&u8p5z(ZGZVyS?|F(lP=F5Su)UxO46=_DROq;yKcwl4Oh%IVs#lUaOsS!-B3h@ zg90nsh5F+_=%Gi2val8*q9QjaTFBi008Am*GaED$#h8AF<@Kl{G15H3jaK!FqlMK} zJX8rJlyaPpe)#9b245o*<&^Zo`Cdt-X zigf2#^bklMR;~a&yWk>jq}>&HiHvGv8nVzkZ`hA-ae|vJ$q+w`&a{Z6C@d8uCvaE} zz|h;a0&FZos)e8uyytW6X91avNf?i;;w)XFjWkzF38Jc;XnS^Q>SR3gFfB3@l z4$;dSOB77SPsSvM2qVpy+EmZL?%INk$@15&au23G;`6K?CE+<)4sTv(HPR0h zMUpZQn{&dcs^8ROp$dUGa4PpB={3P4_5JZwfs;A-Kf`7ui|c&OtB!{Y_xZ>zFN{Hmbe!hCGja6S`^8%_5DpNNVqpyjb+dQ!U|81X2N2V92neX=Nm804Vh0 zhpr*c%a_Px#%1#Lm#_=CEF|r>sk_6KPstH^RnJGKE0kwO>kl<>HXC>Kuf8b!K@pt< zqwyPvivVkqJCAD^9}3TYB1q0n3qb^u$C%6n)u?F&3^p`K2MtDn1Rcf;$Gj`?ivvH3 zc&1(qj%GaE&5>|~kT-@!qqA5#M*9zJU#R_?9EF{s%ejqo>WCB@9`p_gGmt|h0Oq5G zrjn!7eyZ(*o2MM;P4x9@MU<6wQa*v-o%h1>fgUdm8V~A;{)a!t!|>tU1#-GRDZvqg3k1!mRfOo8f)& zYRA$Ua<1oT9YD4$_6MbmY0YKi%hPNQ!J|q5_W8#GO#WdbnHMb54zK1PQ`<=7iu5&j zEQ{x4StK#!;n!~rV7qBS%SaC3_pZlpd|$HtU43D)Js&J%PL{L5zDKR&h+&hif;lt< z+LFA&_x*8Q#UU~4oS!c57Lk4X;Sr)1^B__IHq1hhFN0hBV7+Q^+^p{;k0a$L%7lKL zVpa!Mor%&rA6z%YCCuVn_yN`vmLv7W57a)L6^|gS^oaB{OCKx9@M zo7|8w<__T+Ccpm3Irhd$)>j=$tMhxGj7Yqs@jDElnkJJi7)Oh%t-`rFSdQ06=CJ(9 zWQ(O6DwYFEXuV%PhXf;J%0^^hSgxj2;ai`p_<-zxOk`6$af{6;nOv|}Rz6zh{%;aN zkP;lOuf@ffkh|-6qS6;h(pR3xZ;^+lt!|~;P&7BK>wG&T@}^xx1O<$c%!)gkB>nIb zJnuP?!nTmW9ju>YT!;Wb(R#HK`vJ zc%Lv`l%AWj>d*-`JMDNWgu=20&=IHtSg(JPhr(A#Tc>bOtyUuvPR@m7bO8qZbuRAj z(uNE&WuNggLd!a>EuI&EN6rS8DnfusIA7Eg{rq!1d(^8vv3#xn_4Q5iQ75xuQ>g4mP) zZFPUhVYNu1Bwz^-!+xUiE=i=Ji7}38Hl^A}DID_c~9f{ERt;qN~Ok*!k@2Jq|d&8u`=Xzv>K(*+xMZ z{XSV|X<1t}A%%;W!;%$fBG9e%cNpxZWg-VDEGj0JGbAyn21*11d9zkG_r|QoWz^9w zR%Le*u8||5rQDS?Z`XWmYf4p9Qy;6(Rkr6Bh}Kk%fn;yuW;I9O9O-0!p+}&-$>$lf z47y%n5GxRkdWGv2t=jj-f{vb0Ben&Fa!2&R=`H19sY#&3fEJIwF;qOHwV?xn#s-U( z^2j4cQ5bhF2UB3xyGSfvAqdPijV(lCZ#1Ug%B4LlyjCf+-jVeUUcmE)s;aEeE|rm7&^rJ< zk9<>#&2u~}!u&tMqiFJt;%^%fs85`eFiE~{RzAJ4Plz;Gv@y!!NYCZ0ffG`~X_*7;?mUfmkF272FLQg?muNCy;mK5s5$gbi&4(jD-?BtF~G(B(`jJh~3 zV*$0P2s;pYAI}-E!U0huz+q-!b;#R|$=c6;IP5FbVaqIJ=A9LmU3AOlqH4B03OSko$5Y-e=Jb2ep=F70p)&htjjiDF>Il(mwIRTJ&r66KZRxUO#~8n zgFaqt)N4CNexWE$EoXu#DF6Y*N@k8o^%jr-ODL=0Z@+D~#524yWtus% zl9JHYhd@Exjla(LwkVo%lejR)7}SGFVMeTW9Ix+$5G0Lbc;J}{c&P-62u;V+T#uYO zIa!xVu3U8(M!=fX>+ExVJVTM z-0!5(2X8~S>5hcamC30iE*(ko!bnx$(C*%s`(jCA6DF&O%db>(G*)A6I&1=HY=57w z0>sI@$1R$g7R$OhcInQx+YwjYMgGGU#~s*Z);U%jSEeO~9K1PxizqPxus65A&L19Q zYm+e=M5|=W<@gw)rsOhgzl%nGq-Dbmag}BCH@?Rd0z643$;xxB3Jp#J1OR-kUmR?E zw&==@v5Oa?K+#GyEbNJjYepETf?OBJcfHM`2PSA$hK&7gv~NS@RBxYoC{VRiMy@&IDTPsId8`PCo?0@ zm|n1wX!?qh=~ePp!D&IER7)G*7R+P+0H?4yb@W?iYjHiITN7)%f?KR=#Ye1$edvwogm7|;wEpy zb-^v6jCMt!dL(l~`(n(r5$7{sgw8fkNOqnjIb$OPAg!H7f#1~aV;kd*n#{o*LJaKf zJgB;IAS}aEjSY4w0s6f`^t?iocwdEN^4W8mizkyrDHJ)J!IhD2j7;opfICs+VcQqQ z_0oqk8hC?vGmNy%k(s~k<*g#cTAt^6_&Dihl*HwlGXpBy+@Dv+smI1c{{TtKf2jOR zFE>LeI)o9;mS#tgKRVA0L81xc+aFn+Ni*;vj%bRQfmSEX3Z0M5j8-pOE5^ytY?U(qBPdu&z>>JhHntd=6?w~D_FIeWZkI$0JIDA-;MEL%}+E>8>ErR zx7QufqeONmz5;t3w04i3@b#6bgqA=VM)!1hBvtmtN?ri6u3suABrB>gbwYL|ll2O( zOnWW($Y5ymG)pQu(5eog03oaRw&VaRw%ZEvyv$K$9!p4+FH~t@HXl8(hd!QeR7MmMO%2;)>Ax8Hp97OH`Bq>il*uU( zfGeh_ZLYQ(O#^dzFx%FfT3?$5~O1m!IJ+AlXx9f`* zyjjpFwM76DPu1LCt`R0>l4eMWXH-@$5NROlp?%2jfq^6mqwy>mKo_80cQi)l`{G~- z&t?`!c_Ubn709i8on#GxHb&l<6Q`2QGsfal3k9_rsC<5=u+Rk36=MpOOW2*gM;-8- zx-b%;g^`p8B$Ym1^@F1>x)uS_xeL%iisR4pu+OZ+U}J?|a5Znn!@etV6pzXoINkNBrTzxcPq`T+UK9@kjCe zr-XP`MgyA8LjD+MlS)R~h+yG|W3rRC8{-o&*}U#fPY&@-anF}b@r__shw#iil3vb* z$I^K6n-($D@kGmJTpY|&OEM{CnIi?%o(*=`*50^&S1|L3m145!6^R-T+!MY308AV? zTs+K(h1)Y8YeI$d2#kYP{{Zb6HHrx{2_rg$CLObKD<6j5a>BZv#V>b%E6AK~=qN?fo##mE36#?25IJ{&0SNqA_8q?9~8w8~4NxnHloNEkrR|)}LBmqhT&2d2A#qj7ZR{ zbUkpvCWKhnG+=?n8uzjMuxi%s2r;!)=^%mk!ja1qsO{B7^4T$gL6_s>2nrmmO$tns zf~Tm(D{YS-YTKN3j)@(bOvAfaTPt=p?~Y&kqcXl70ch7VFAxH|hJMVvS$Nb?w1)mw;2}vI*o6E6F zc;?HP-8Un3v*~*i$itZuIqqF-bSMaRU!PEawhP1Qv1f$RvVLT_c$^@ckjA$PIX?d5 z)M5Fk<1wXxW_$Ms)?EeS76!*Ana?OOjuq? zGMzObG2c-aL=Q|67c?-ZTtS@+Gc+`kQ|VD*{{WE#(zwMriDMHbvz9#3yK)_Ea zFgRoN6n10)D;*$T`>*~m$cPaZMlk||O)lgfcESXY)44jd2^&>|{l0LwS)GV!Q2zk+ z9AJrwsI#-PiVdcMoOm5ynBi25?^byG;z_110CW^cBi!%L+X{|iWfYq1X^}-)^u?K; zkZ>6tTCRt`d`{3a(?$lQdm6UCV}io~dWSI097uQag1c5d9C?{7UoQ(r(+Ge{q*L`U zW47JLZ$pJFs?!#-2qj3;K?mGk-p9T=Sy{D4a`H%JNl?Wg*lcR+(EIQA#T7zf(*#AL z%0V=eI4GHeGKxBNpCF=kG%SBS0||Bh8!akHEgAtC^)MGqe}oZ7d^5%KIc&5`ESZ#5 z6GsZwfW)%KloS*ETpzg=jB*BV%p{L4tg7zJD3VCj2-^1a@4h7y$(WK{xy&TgO0C7x zd#zO8rvCt?<8o1{kExqdz^;Xpsx5cAKRbSy-10KV*IuluR;?dSz}@f99ASE+(G=FSBKy;ueH4|~5(!yJgD zf=S{sIg_N}*j4Ibxvs#5{#)Ya)A&V}qLt=ySb?OwamCfw^1~>xhfJ~?kRGcS4pN4W zK`MB}@x>5t#XKffo=q0NQ_$qsHA5SioQN5>Cs05Kar{(Ipx*}(WzMUJ5?u;(7Xr|o ziz)1X=K%;w07D98#A@SHmb=?(9k;Kw;oT)M$suwAu+&QpCA)xodtneo4o@izSzMGc z0JIxVl&CZXcB{tpP{^)S1&oGT76u>^tV^_U$TUXO0}JQ;`Nll6LzI$8XggvBIfnc3%5Pc5>45#p}{My_H=*eqR5AZk(?iLu<* z7>C5!WQ4^!LeCrdOP(lSJ+StR#2if|v%x&_Nugv|siujsBDG zff49}Oa$RI)xbJfEr7#=z9Ie&BRoOMvmq)x!|LpL#~N8o{ygah zVCN9J$(-tHRfTK-rGVM28m?&HzhTB>qa27OWL2WCCW6ht?Lb}=;`x6Pk_KFtj{gAD zGpJN9X>tQJuBVU?(DdBmkydy*_?hthnek@H$KpA4nWTJ{w$W$E0C8aZorXJJ7X*SR z7ABK0LIq})Fd6sy*nwB;Sd+pal~Ri{O2L@Q;O}PG(Y>#F?ZCw{B*~Eu%;l|=ylhke zeQ(!q=Z!{b{uZzZ*1 zusLfH#E#+6AO*D0x;qo=h`e7bogNdIia6$Y;Uc13$qwa?;aHo!_OrLPAA{>MnM|0@ zH;lAiAtbYv4l@$XG;oy#8S?S7bl_$Oa;i0wN!;VCt*t9TDge-ofEE1@Y;b%N#B%ZY zen`#L@VJ!80%h}zWR^1#D#6;$rZi32ByI`EU&6E59~jFnVHdO?AiiLKWi_j{-eq9ng!tahZ!aQLj&*c^d%w`p1ZmXCRMM$S&0}U;~4yw&cT7xQsLdA2=DK;y5PHQ!l@XcpA;+cO6$;PbNnO;zRjf3qZVRipE7NQ6#*GQLGI(lO)i>Cf54iRa0@$s6XijitI(-+OKMukD1kQR0$Aoa<%!irOK{ zPF6I!+^=)6-{%$O(j7xguNaPy$jl0ABJ@$-=!5TL8ELsVvVwm!vp!&wNz^LXgZzbG zwiTO1XxlDX*br0(3ugU@tZ0fkJcc0f02M+2XpY!3(O8xQc4b9RE67RUkDN(1QXK|3 z-!qv~kE>X&-L9&!coIHl2(sci4Ecbj`iU zuE2dZ6gK^c$66_aM@Cs*R%KQZ%zrS0V#gJJxQ{BW3gzRB0ka*3*Qk@X$=}@KmJ*Un zDLP0b06lR300;F(M?IE!wPoj?RWiC3nmq(aj6*FDDu}JQ=X<;fmKhnMm+6%)+h$v{ zu>Sy^_{0pd#h7lRM;*^#sNC!Z=e{DyHf3{?Hf|-7Nf`9x3Qm3{9AecYv!W@A z3M%pgLaUEcgUW^z^%0Q?s~a9T{#Z-vc*v4emBR(CI!825KYR-2Us2SdPNT5zi&Ih@ z$}=mOR1KEa=z4r((6(W90z#-#$Vj_yOf@DmHN@-*HRd!0?S-I~rvqAvBPCgs-EU+0 z-XvmXvnr8H#1!AmQh{pY=KvVUWR!>Ms*NhURh{rgCRjvzPer6I7`O2oCdaMsfpuu1 zux$N0k8_C`K3npbGL$LN+BrPH=fe{cg$P{jSk8hk)l$WUj&E*A!Tc0r%j#EzU zKmJIKkf15vz~22vHYudi#1JovU@O-KoK-aGpaSy=Xl|qhVZ~$FBOG#(AhrX_U3F<6 z(-6#Eyv3sn>~|Qd00vd0>X1cj+*#OH>x))!nP-k{(?u3yBPvl~gl&C@?X_aal(VXw z!^gNK(Lf+q9)w@=#JRYp4jO2es7Gbqxb1|tXd~!zrFb@IcCCi^Hx_H5Gt1PWJF#8V z6dIUzIQFYLn^gq`O$)QmB7EFNMOfc5Wntw#BS&x8UK+Dl3T2B|(_88!ay>8C?}Z&S zs;Ih1SpxY(4uTicKr_KPBWQ<6f-s@)4K*MrpPEuVaYFxyH zRpczA(#hWaM+1)7kn(cMGYlk_Syf1ONM>QI72Ht+jrPJPg3y27X$VNv=%Y|4%A_Ao z2d)|)q)d*`Ak;5n31T)D2XkK7^sZsgq*D;fH|5$5VTe-IRk0iMhu}dRV=GXjBIvPE zt*Bp3yB|z0Sls3V8Nx(K)kk(D_a}Y0+uIlB6t1HgVps;&R1HJ7`R|3!r=Vn11?QRXBw3pf_YprxnUcSWh;8SRvMNlksC4kNKkmj^`tPBnToRj+LT_RPW$}evd{;R zIC|W0pwRWK2&RT10CnmhQ>ofGMZe7pII1;~LIgGl*xws>!()p^T?@!ngkgNg_alqP zgY3FqJJ8L|ZaBk(TO>i_D$&>~F1@>B5kQV5x4n8dCu+U^Frw_}ta>}BB$9dbtU5Oy zQ6hPQg(|0Eg(q1GnAOIitFbmaewcK!m65cN5uiTz-GcT zq?(mv>3{-*ME?Mt@F66GKAjPTHAaP!apr+t0Ci^C_WnZ?A#{>9X!Z`gwR+)^nB9q0 zK!gG=LTdrQoP%oA@t`F}?Dd66ihw(+y=}8u%p{do3<& zTEi@eWmP1#QnUPf@3u0~B}V8>Qds88ZTW2&Hb*ywM)O*g%)-tTg+-3}@Ak&aDk)~o z&SIRhq9iJ24Q)hF8|}UJ0G=-ieC(6tfrRyXpopy)16D&42Fhz=!LjNvrCD@A;#p0i z%H`wgK4Jv|^A2Ni_S}Z~U%nmT$q4ZLx8bs;Jt{#~T6L}oz4zzm3ClN6Hva&Yh2V@c z6Kt$VBWvy~*!LK#!^-rEyH46e79;B>y>V#96cQ#`*odMsbxxt?Dn$dv#{F>d2h>2= zlVH7AudMr%SoBV*BPxj`G^6X-?fY$mAZTETB!W{{b60%?@ht1AyT`+|-}->v{i z8b*)vWQ~HFKorN#lk0!e6e|W*Y5_yi$Ht*s@7n>Fnkd{PaJ%&ZA&J$cf%;C~n2H?k zatyw0vk*g?LlSFKxlKa$7vIwR);YQN@o7YH!$0|~?HrKtabN%o=T)b8~F_Q6=1;ILfl#{j< z7I|Wt`iN>b4y@jDd{O;zxyE})@vMSoygxUTpDopnRb682Y`}^g_XBJ(L}NEBl7jMC zMw3R8eXqF14C-VIjcDFr)q1@~G8I;lh?S(=8}+dRPNt6!mSV`K>)7AF2mGfz#%L~BxwUXunbTTKqmIS{kQLfS)&S3 zmqcK1=it@}l}t|Y=~)33N*g1v;}ICkBE0JmEJ;}rwR6~>$J+`~8E68{BH3~TU^%dN zARo}<&@Nvq_=%D!UGtKOCTV9cY(;Q(+P1DY#PKKLdSDGOL_k=|k`vQxJ}{CQe+vpo zBVlN_vvauS>wHlPMpDjXQrb$UmpYb#Kf~&AK5r|M%&dZ3ymN=L%OVZ$W5y_!I1H@Y z^$8?`NZ;S%f72NWhFGDSE{T{9x`U$@T{q2xRfRLco@};dL5DBVP=X^AAtV)hF*_e) zheep0EUiP~(IXi{zve30W9Mo>F2~%T*9e*l&7m_oMvwWKaKI~jS>CUGy*)9iLrJ&> zn3HC#cKxpw(T;{yk}TnX%JHi;g`!T6e}9Z|b5g~b@a&w~a?+VY7wSTEl_2S$csE?) zp_o@8mDx?*6SaDK;eYn)9+fJ)ZSUNT{&8BQ#zsNP<>Qh!Q3Zt&fC7r*?`|hkqCtI`P+MSt(Ttte??W(r^{cR(ff-`ryg<=ZtIBW2fvSH7PA0Mh|; zJVPy$&NNfal<0m;`~kXtB`)P%91x)K<=O^3T$#WAP;kn z^u1;`IMo)00Bz3HPxQp4a{mDJ?8E`s=KlZ&iKVD3C?&NgtrNNS#GK#yJV}$1D8!IA zRLdBU>Jj@3JRaViu=#TN=aFVe;)*#D1nBHoAzLApcn!bR-j@%{OP9=M5WnJ5Cy5n< zMnh|&NFee4d}5!?{3LIck~x@6b(xqc(h2>?1LF~Me-Dt%XLD^57tM6nstm+Ww_~Qj)q7oCZN?l)kjEl4O&p{abf&cl1=k?f!!v|MI6AYE2to9-0$gu8F10OPSLNBsiIEbKi3-wX(#-o zcM+*#%yt7|jH#ePBv54^UWo4Y-)scP(glt^OAL{V);dPp*M8V`STeD-gsgGsR|Dx= z`gg9?$L)=b%BReX9R{KYF15f0ujF7+Ci2HK&zErFok&wkM$1P30BmRqHBfEQKqtt$ zMx{bHyY}DL7R0EKuoiU7bVce~*bDyv1{FZFlG`XGYpqApzHo=ky12Ul(JOaGu=qVH zY@|Pp8Ic1Jhf4iK3-%TT{{T!cJC>WN5sOO3BqfM#&d1C9{@At|zM@ST5Z5Drw`>Nq zk~P$!Yyv#je)te3fY~V;zN$nuTl$0jaX8Ad#Oh?w!AS&-WRu_L7RMz?7>Y-B1(dHy zkWFnzfzLSC%}7$2Se=!+i~z5;EQ@JEJ#ogeBS?d5E6XbaKKou6T~cfOz|Ulc9M>o3 z`{K1_nS#hQEmbMld>mp#)4}+(4Q0x1{Ncf=rFAJJsnpg6;mr~3PizE?&li-GYv)aE z&0l;_*?mc*6;mH(H`FBVN%r-@bOwMNfI-_yyTylMfBaC$(g2asZB9T$J6JpOjzl?F zBY9+46uY+aE9|@9@9EzkMVo>)fJL}wCw3Nh?OxcIE#c9nw^_!KW3uh-g2gL<@tnS3 zOEU<u?U+eFULkzRZlI6^Tysi}2#f^<`wmye3lgou>k@Y!S09AvxBh!37 ze1fBC3=o1v*2cU3*uPqTX!)sEGFaYfNSCNItqnC_PW}$ zvB>0kAy!Y~ae9ZQCCedbiRqM2C8JPa5AcrIF=$SyT(q(lA!}i#hTpC|980N57Hglj zDNM1Ba~WqPiAr)Q4G6h7mNZxY02?Uteqe7X=eGw6%jHux{R#5%O&hBT z1m8g$Qn$$_z78msM;WElB8c6AkjNA$93AUmKTWZwe!2Y!n>7??FwN9PdR7-ocpG8K z6cGa9?tr2iwPC~$#pUh@8=zg4)%5|WRqg68pImJDi3D9H%cE&JbcoKOTz|$4J4-SF zNYI%ht58kR#Hp+uT?J#wq#A+w_5S#x0^==d6B4Oqf|3CzkDaittkK5ZMUo<2l#rBo z@9BVs*e;Nat*1z4C1@UMuoFgFoDhiT%G&(Ry&Lq#hU#V*B;~+r7r)qj@Uxd8S#+#c z*JE>cf2IPV07j8r*ac8QVn|`$y(_*kqZ(s%CD@Rmfiwa2_QI9kIRJMotPZ9G4ZSP+ z;Gvm^Q&dtk%+^mM)3(?{G#BbA0Dz<*_wRqUH6~FSyNBI)t?vyBs7eG*h_VpucopgE zglF1i1yN*WY5=dH9M^0GLdxK(^$wJMw6FvDVm#9cHdbZRC9^5BSBfG9;6^CMP*>)Q%J?zvXQu3uEP@OXN|Rk(`y4sqxQ!;#9ugb zrX(|%qa#=kdKIebwPm_R$9m9tuV3F6L6M9kb4?_Zx&u-b(|&;0@ZIx-6AaM_m?4#mEf2~f``#BoDtvmGqg{+`BL!{A z`=6Y21EgY@f=%){JYon^I1=ED5q4F4ru}f@WKbJ&cNJH?`hS;=PBT+xsib)V){#ID z-|vK(8&a`0#1JoH1|QW7Lprjv5k*HkV|5cw(8a9BT8aK1+xqsv)F&?C&Y3E%iS+ap zf{mjjR=Xz2UO?Zm=LQjEPc&)^X$Q-9N4_8Ua=@DaieN&6wb%2kShe$C>K_iwX0mfb z5$VY(>MYL0k)Vwxf#0Yu*?kG?vc6<{?O*~@8A`)WLz z!ySgACQ#C;(g-(Oaf=p}EM`o=00%o7H(QJ*KBQSoG*5#L59z43yjP`{HDk z8ZZO?aABoI64&YLit>UvLCW>BL`15|suCVbCdQ&D@xJ$7nAB#oOhQz+lnCq+wkTJB zd@?xW=qXtNEy&|X$F1>#bdWR1#35o&nZYD*f5U}Fsi}^&3mDYI5kz`e+V2QY;}NoW zE_P%?EYU(_i2@d8BDmk5Y$U#4Cz+lzmsnO-4m7h=tX7K)9$%+{>3#6|rE&m>pB>P7r5TdjrJo2{{Rp4d1>WYI?=_b0aSRd zEPCM8W}W2K428w{qir|u^M$YtlUZOcLr19e*_+Hgjp6YQm@_e%5oq3~-K%T<*rKIJ z3&*H7prtpqwinyy*9y!!h<^&%a^gS~v{Dr)R9%m6QQsBl61P`bjo8~Q>b0(K=K&Nc zGPx44XBtgy!P{&|5+oAFk~9oU0u@W`TzOAF3brRjpKs?}PyTJ*$`m`rh7`PBde_-ecR z;H7kB{4%W3z{BbU5Jh(O=WV_)cq44-6tk3S?b+SJy@Fy&dS)Vv$-;PEW&iW0b|`2UdW@ zA8>va7M+S@eJ!N z?qPC*yKryyKi?OJ)lug1vF0O!R?>${1yO82{YKzl^2eY#ftGVNT0FeNF+^YG$TfXJ ztnqw-z5^$wDI!>xeufD;MH(2=m97X~ER&~qD`HcZgf<{mf8v=*;Zx77nWQ#F8kC~C-B~>FOcCmNG-PppEu3~YQc)}E7 zz`02cg&ZC|h5)h5Yo0*p?L$bA>Du@oe@rqWRRAbXpf5vhvF%tFnIVy;k&+k>1%UGU zl6wq6kjR~RBc=g-rOD^m9_#P>W6~80#6i?7b_H+r-wuo(nVNKnBQ?dMO+nMwZZV*l zRa7C=z-bG+8}|JNzc_JgUO3%A{A(f+W-u#%O7y@YO%s+NUzmAHiUfP}zos@uGDnzg zIeA%ML`560tM|M69C{K=gcdrK5(4k`AKwxfh^$$2EjKhHaN2y_5mY|dKMxvmEuq&j z6J{myA+<);y8XAmf71%;Y6_~3H``TncnqpZ4q$Z1ROw2^0HDrkwSPa?5T;9^qbi{R zv9~_I-x(KNxFy1$q^f|T{qXRik&2P55%7I}D+&Ng0gdwPH8K`iY$FYg;!M{tg=oAe z3A$llH-kltvbd&o1%N6@JM2w+?~Dde#(5ITutC1SSEsH#$WV>k0=5GHb|?FeF(tDS z11!?Sg@Ec6UPv}SwlwBc!z)M$F4~IMAN+^TB%iC82#lz7>Ew>XxWXol;meT0h;kQL zC72trz8x5{M?(>gI6!l~?fwJpi3XPjIhjHg+?A_7{{Y_}kPf59b_@=nPnnNCa#`qWjj67)`fcmRI*j~e*esRs224gi0$s}&T zGKMk{YQ!Cfeg_!27)LCAR%O&S2YUC$b*v}{Ov1d4OGN5bA0N*Y%Mvz8B$9an9b38u zwyGz$H-5Mll{$$W$cPx}I!Uo-ZhPS!7XkFlWOC+&jqk+)Sd-@S{v#~DCmvTTnU+^V z27=@OYjq%ddhNGteO_8QQ>M}7;&yTn5xBAeBl{e1qwx5TinBl%rSs}*F~SnzI-916c&T83=|^%f|UY%FYjF{vXq&}DL`DL9gZoPtQ-daf|p zvWqbvSuC1#Y_@#{3-gAUn_YqC1t<`A{l+sg-e}p9Gyv#UvTDbsG|r*SWsGc6G<0pW zi3c9w;#|z7=s1zx2=nks2l?XJUN6K&5Rs58vofIBw)f+6e{3RO!Yl}(CsVKlwSq_Y z#%ob9h!-(AcA=>nM<;&!0l({lOq_8cbecs9%E+b3^H|_>-yUs3d{#JMB`=zn}omd!<*%84Rl(sSf?cBlc5)Xg1 zW2u(RM-+)EK#Z!?MG#LG!?>b&-vsKrA3NgS7-R7_mN?=w2+{vqpw z^*Vw~sp*D%$c_@369LVi%bdOAA2LB&*UWO)mY2^sYuP7om8iiIA~~xd*7-5hWcU`5J+(`L5Vh zo+%M1lC-SLvll*K39VoKdE4g}g_A7=F*31D0T4!|j-J2)y#cn^=PMqsMRM8ZyCk+% zg%E0vz3gKc+55$(eMZ3beH39hhF$Jw_phN!+9%S*^DGkT>}l zptfR2$ciZ($w*Oa%HQrcL~)7=k#cj*AqHoSKn|s3Vn*CmRb!Xq9uu9+%u_8eL=2&s zizL_{Ftap`AfkNc&XP#30LOPp4b*KdsDis#2cOjMUwlQ?DJjtGRE->RIP~WgnIexO z`EZP8Gl6B!>(~z8`^6201)X!+C59!67-O0j*RDsAnZt?{P$H@kMCvZU)o(k}#$-f| zb(?KG-+#Bh7Xm4YM+~T9k)zTMttt;5x_z+OQFTlBR-Mv@+DaNc**^HFfa}Wytnrxo zs;5q~<=0%-HH6D7l1w52>7=B4T`bxOVeM3L?EG}rLae~sq3{0Fzs4hn#WKH*8GrhY zP%6k4R-5UKh%_&PeX)Os{dOaA(SX^a0=Nv64)y9d_{T#jGpp;39gqU3EM5AHaO!;a zQCTwiR#un=C5kMI-n-Rrd*a-g%5@Vd58;)fVzZ!P-68-wZqD>U7tP^})o8NH$5Jq5 zWmF6WGaI4p$0rGbLo~8Ph?*KRsZQ2=?tAUV6D0XKr9l*?C7fNW02~et_us$2I0!=tRBGIbjz)cTq`U;N?JX;X3aGRSnHH6axH_N)2dz6->%&SOb*#SGD_uCUtdleJhC zwxE7-Jeka2KqHqk z11lDA2=uMU*#7|Q6@UU1{8Z$*HowK}Pi#shl`>I8@pPj;V2S3?0Nmdv^v2~U%jPF_ zFVbX1A&S=5SHC|UzWAc5B8xV$x6pfRwZFZ8oMCb%W@$Qdv};~%MUJgPw9v2f`EFA+ zG8Hm}jig&q(*n2Odct1Wpq_b6^&0GYaB(A@I!uVG3$n&3S-CMu^(h}5dtqN2{7G601`R zMml|O^xF=sQkr9g>I+H@soYl>vS|o>yy?+ajX@fGw)I{6V~-@oi^DsH5+aYwBv~en z96A=wG97WMs3MrgTmn6J-xR9BmJbTCb*2`G7wR|lUt@!vnmCL)ghZV)s9p%z07tL3 zCS24?)X>(AlWGnAZ;IW6z*k$TJE&^o+~SEgY>c#0Dm;=$NT!DtcIVpv089o^*`(7v zT}dpmGob^fJACo`;aT&laGgg)B}0oENgIR3-Q#ju<#mojp6lqiT}n2f!)_~KvBC8# z)<|6>>5vPJN1@n*>xE|FJ%0_Ap>Ih@cY+3__{b^!z%-nljY6|6c0iy)Jz2xKn7 zm)%WoUGUj@12E36$_W8xXU;(w( z`YiF!z2I`?<|z|6AQ}|X03nEjuW?6}iPJ!g5Ov zTf=gh9Ha>jUn2rT3{jenD3DSr-wLB|Y&I4KRcPaSbs~~6OYy(o1g$EaM={d?m2QPK z?cV!gP1Ai2M1|^seTz`;0NZQ{Td|;#7J8+2J6QLwF*^+K@ywQGGcgQ;W%DbUiQ%`J zSI{6TS7ZQ}yV{TGMnMqgQZUGnxw07v2W|-Wz+`hJGl}PRip`BRu=hRtV^CFOMV4bC zl~EOzd0SGb?|$U>!j+?Un^S4qVba*Qd-mL6$!RQXu|$rfFkmcd?YE}s{{Rs+hlxgNM-iolYkfV*{{W0nAD%{%Kvh84k1_V! z3`vb^Ld-($NYiH#rZiFoKbTWgff;}nRCeRr4RtA&m0_AlrB@--Pwz*#7$upcg`2Gk zy8u~EI(FN9MSQL8;)4w%}zw2HS zJV*YIB>9;0#vZ)6f5WjoGQb0>w1H=UZ|#8ajI*m|vl7p48FU_*B<Q6Oj!^@2{^ZT@Y7 z>q-_yf#r=!*{+TE+P{B%9JBN_W(S=?0fK;a5n_dIeXxWRhqRHLvk*21zj0l@d*KZ1 zlUZ06$+}&E`5@xioT0v&BNPlnC?I$GxFZqBWiym`BMb)MKNrR`T8U=r9OEvCSu_S^ z00Z9709PLPCs2&te7Fh)u?1{j2W(T2j!_fKtyx%q29P1{kshh!*bP?Sfc5ID5yMF`+f%)M~G$0$X&JtOAuDHpHEy{;nB2~%c3)T z(wbrEV~iek<8q|5PatbM5ku1Z;?520pZ@@CjTxFXDz8d3aNUT+!4;*JL7MzePE9Qh z=@GY}{{VFY4*s}dQmhlJ8g&WdQw_M%J^ui{4iLj|`D!v~DABUD@nrFUl?y{2r_4tq zC0RW|seV7s*r&tib&|6FKp57lI&6NGft3s8z>ZFvlc<(Z+YU!K_C_^2hf-8mHK>kH zehrLKRf*OLy%!ORt+sLVT&V%ko0o( zOKhq`Su{xO4iMP{GDjwu)Bw8ls@Ap~3;lP;&SzwV_^aiaOXd_a1!W-*cCt3SeI%OI z*bM~i5P6n@^;@GY}BgH3^O|!BWISwRp}sUuH>oQZ^e!A+iffaf@h6T z2BZo>zUR}AYzU}e4H;M>GHDviE0rm|QTGSaYzA*JAcjSj7ho8%O@^jCoj~7i_$gLN z^0LD5C*f8xrbd(V0lOeA*y8MZ_QXjPI-N@EXF`ESpeXm}x9x)P&W!kj6*8)2-QyOv zI&4jC!2R(50Q#P2#g{S029Q^#Bqe)Y?diDp$2>z6z7LbqbfdzWAsv%{DQe!a)5~RA zL`2k9#mGYqhtI9z>ftgujU$j3Vn(WINIQ~$!S=uj-SXX4jhT9jq9Xck_APu}Z(h}a z@w~g7jG1{RRaX_e62yAnsqOu6jOT>rp zyqRG%di2RWjPgkx8oJ#5UA^(spDX4dXBt;fbR%#Fug(y&<<)LH=2jPy%RmJma+xCk7noRNk<_tu*1IrOTLaVf!T7vg7=Ve}ohMPfs_^{L_XrY1CF>s(m^4u7V1vrU zo3D?2QuC%_7tBg$%d>!EhgR!gb6=*|JWR3=W3`e2c^}#gp zrld0@JMXyb;;L_T0+B7LJtHFYzu~&W^vO(nnbmKveUByhzP5hFzu5%hv8R+T#_0CvX9m{}pwEOIzi)Vpk=>wmpt#iUnt zXR#uq00xi&18;m!l9lrr!2yd1boU!l#sjN`8?XcrFR;L{b7n+ERAsYAR>}lcH~Z{A zrWDYtsLL>Q)=M;LAq9QyPWYq5p)qC*VtJS;8#^znqZ>WRob+>Fa7c&%*Ebo|k zhG}G5kqIgin_!~%?f1p-D2S^mX8e;VF2#sCh~)fX3zvx}8F*riSN{MfpHusu{{TN8 z_>(7m`2k}p32Sz7t4Qxv-nhfc7~d~6j`GVLtWN3|P}|Bt?em1?&*OYIFX9nA*==q? zjUK_H$+3@1A)ZMf1=Ub1f?Cd{ApV;Tv3R`>PpH|v+)A<}nwL!65}bg(o9gC>H|>j3 zMKao#+FjY=zbGf2TOGgSwi6U1E1Hrpbp$p^yJG%gPW_3+QY#fpk+k%I)7#_Y3MP4B z%QP^qNJ-?jnR7Kp&0nV8hXQ0ql*t!{%d9U&gUSdX1J3uS`{UWXu3i}8%R>rB3`OGz zw?{|>6L-D}2^~XD8MPI*-;v#ZHpC!fUbNA%$Uy{=$6Z<$FOYw3F`6NZGCEN*1sZnY zd(p1n-ww($=5slK(Wq>^y)`@UY7PGNhGqKmFhh_m^Vlz2l%lKv(1@ux<__UDTG;>@V)n8aZrRyylW zqigPYyhwQ&VumLPoJIw8IFiqr#~WRHW6v;=s$|uf-l%Vzg2STh z{GUu0+7Xefq@cP;vDc=byPu~z_T6hJo_Cxhy7 zxp^0)8$s&o1i3fc(*zx!Wq^_rVN|pzild1uRaHj{iK|sGlEWSS2pF~~$byZ?Q zMy0L0gSYR9rmVhbM_oZAoinfJoISGuMI=cO3Nud;wkGzo`r}Mgo-ZThGR;1?Mx7y5 zq|zH!$Pk`)JM&nRH1V>M=n%<$$~RHv9ObQ8=rsE+tV2OmAu0Kpc;j9wgzEr7GFKvUk2*; zz{&tfkp`WqyV?H$4h0#Sh9)t*QXMVL?r-~Gr7DpS%A{!|hN>gd@G#|sOAe_dlnPU* zjsE~aiu3WSK^C7VaL7kIR``y&k>(erq~+1s_Or*@uyGL9g+e}^NlR5+gZhq6Bh2(f z8rl%qs(n7BzX;$A=FurRUg=SMQ^dvy)D#914Z-4EL6UAW1ETur8#i*0t zoI&`VGrFuu2w*WWXjo9_aOT!|XJiJjfkpy|!VKGIaqfG$OYdVeI z`+RrCr50f@$Cb%=Mpq}BmMqg~VVHU7q=mf9H1Dyx1AsQc48}t)QH)5Li4KuCqp;t7 zApN+*^BHnnRGL#mNE1k_P)PaQVO|N4eBXv;GZ~50%yQ7Xl#D1WK`plRXWth`6J(w^ z3$#TL2@M3-VZrD2^nRPPM}H9x8BG3-vF=(4LO(hvuGm!7u=d^sGr%9k@Ts6lPvYZ3PxJ^$;*w zXyTd~lSY+>L!<-40(`^J*uTCHH0=&$fO!}*8w}Pq7=8ygHze^ye-AK6)C#u8egO9X z;Xvy}Tegk0TaC#-%YWM!=A%f$B8`HQKcDr)Fh$g#s9n^)l0u$tx%jMRwGMeAW}Y2j zMiq6GVS4D;s`ek-0UHq-2cAW;0rCaZFW0}NW0M|a!{Sn9LjM5fmC0+|ZomHk)+@_R zBgdG`f&h*uK&3(5Sgqe;b~qEOreiGXp*lMqYRk9-f;};LW;_rkL| zW^B53B`@UeqkWDu#3l=r8HPabE@2}io%rV(WW=6cv#6N)s{GV=@3+n(Q!LXcd7%bV zq!}S+R6eG;#%FU6muT|KWJ@I$y~fA;V&;TJn2VxJVSyk5e7g=l=K)DPau*^;%e!?A z7?7n}0o7UroPnvPA*fZF8~*^t3oOiJ(<8KsM!?k! zNUda`(<4ZLlFrOV$Nn&RPsGQV5={)!BUl9;Yf0mCxb1@KAVSR^unlP@f%n@36e%hf zUnO4L@J0laE|H{h#ST*{s1k&#ShQelBURw9CicfcVR<7FPn$rbn??*MkB|LevRNS` zurgJ#U6#A^iss8MUKqiaS-NLSmm=kI)vVCi(BM$^!;04{GGmJ=AdY2VHc&%s0q#!U zZrC}ZbjppEGRz3RM{+i?Sj^kI6hiH#rP3DsqQJ9V@A_fML0y2dmKRp_dw0f3#z4^q zFB=5_YZEdOf7(52?*09-&mks`RJe@gSg%m&AlD*~KF1a6>c=daA_yaWE1-EzQ8&)} cd}0_XESzhZRLvb%ZY#MXk4=Z$6{0`?+0N|Gr$ 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