From 6f76c5e7c4d246a03a668f4b099c8dc03bdc9df4 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 14:19:30 +0200 Subject: [PATCH 01/30] Allow using CairoMakie or GLMakie using the menu() --- examples/Project.toml | 1 + examples/menu.jl | 3 +++ 2 files changed, 4 insertions(+) diff --git a/examples/Project.toml b/examples/Project.toml index 2fa79483..5b5059b5 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -1,5 +1,6 @@ [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" diff --git a/examples/menu.jl b/examples/menu.jl index d4324927..5c5962f7 100644 --- a/examples/menu.jl +++ b/examples/menu.jl @@ -2,6 +2,7 @@ using Pkg Pkg.activate(@__DIR__) using GLMakie +using CairoMakie using VortexStepMethod using REPL.TerminalMenus @@ -43,6 +44,8 @@ end function example_menu() options = [ [("$( splitext(f)[1]) = include(\"$f\")") for f in example_files]; + "GLMakie.activate!()"; + "CairoMakie.activate!()"; "help_me = VortexStepMethod.help(\"$url\")"; "quit" ] From 51b420377fe386245e443bfaad97dea53006eea0 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 14:20:43 +0200 Subject: [PATCH 02/30] Update News.md --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.md b/NEWS.md index 4bc02d87..dbc9a279 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +## Unreleased + +### Added +- allow using CairoMakie or GLMakie using the menu + ## VortexStepMethod v3.1.0 2025-04-19 ### Breaking From 245e39a90d50052ea99d029be4569c213f891f74 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 14:41:09 +0200 Subject: [PATCH 03/30] Update docu --- docs/src/examples.md | 53 +++++++++++++++++++++++++++++++++++++++++++- examples/menu.jl | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/docs/src/examples.md b/docs/src/examples.md index e3ce46f9..1315cec4 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -154,4 +154,55 @@ Choose function to execute or `q` to quit: ``` You can select one of the examples using the `` and `` keys. -Press `` to run the selected example. \ No newline at end of file +Press `` to run the selected example. + +## Plotting Backends + +The examples in this package support three plotting backends. Here is a comparison to help you choose: + +### GLMakie +**Advantages:** +- Interactive plots: zoom, pan, rotate 3D scenes in a native window. +- Hardware-accelerated rendering via OpenGL — fast for large datasets. +- Supports animations and live-updating plots. + +**Disadvantages:** +- Requires a display server (does not work in headless/server environments without a virtual framebuffer). +- Heavier dependency: needs OpenGL drivers and a GPU. +- Longer initial load time compared to the other backends. + +### CairoMakie +**Advantages:** +- Fully software-rendered — works in headless environments (CI, servers, SSH sessions). +- Produces high-quality vector output (SVG, PDF) suitable for publication. +- Lighter dependency than GLMakie (no GPU required). + +**Disadvantages:** +- Plots are static — no interactive zoom or pan. +- Slower for very large or complex scenes because rendering is done in software. +- 3D support is limited compared to GLMakie. + +### ControlPlots (based on PyPlot / Matplotlib) +**Advantages:** +- Simple API, easy to learn for students +- In addition, the Matplotlib API for users coming from Python/Matplotlib is supported. +- Works in headless environments; can save to PNG, SVG, PDF, etc. +- Very lightweight Julia-side dependency (delegates work to Python). + +**Disadvantages:** +- Requires a working Python installation with Matplotlib (via `PyCall`). +- Can cause issues when multithreading is enabled. +- No native Makie ecosystem integration (e.g. cannot use `Makie.Observable` for live updates). +- Interactivity is limited and depends on the Matplotlib backend in use. +- Extra setup complexity when Python or Matplotlib are not already installed. + +| Feature | GLMakie | CairoMakie | ControlPlots | +|---|---|---|---| +| Interactive (zoom/pan) | yes | no | yes | +| Headless / server | no* | yes | yes | +| Vector output (PDF/SVG) | no | yes | yes | +| GPU required | yes | no | no | +| 3D support | full | limited | limited | +| Load time | slow | medium | fast | + +\* GLMakie can run headless with a virtual framebuffer (e.g. `Xvfb`), but this requires additional setup. \ No newline at end of file diff --git a/examples/menu.jl b/examples/menu.jl index 5c5962f7..50a948a7 100644 --- a/examples/menu.jl +++ b/examples/menu.jl @@ -51,7 +51,7 @@ function example_menu() ] active = true while active - menu = RadioMenu(options, pagesize=8) + menu = RadioMenu(options, pagesize=11) choice = request( "\nChoose function to execute or `q` to quit: ", menu) From b129ace7e783c35cff896c3879f708ab36d061cb Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 19:44:00 +0200 Subject: [PATCH 04/30] Use pdf as default if CairoMakie is active --- ext/VortexStepMethodMakieExt.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index e4e39c8e..fd12b661 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -247,9 +247,13 @@ Save a Makie figure to a file. - `title`: Title of the plot # Keyword arguments -- `data_type`: File extension (default: ".png", also supports ".jpeg") +- `data_type`: File extension. Defaults to `".pdf"` when CairoMakie is active, `".png"` otherwise. """ -function VortexStepMethod.save_plot(fig::Makie.Figure, save_path, title; data_type=".png") +function VortexStepMethod.save_plot(fig::Makie.Figure, save_path, title; data_type=nothing) + if isnothing(data_type) + cairo_loaded = any(m -> nameof(m) == :CairoMakie, values(Base.loaded_modules)) + data_type = cairo_loaded ? ".pdf" : ".png" + end isnothing(save_path) && throw(ArgumentError("save_path should be provided")) !isdir(save_path) && mkpath(save_path) From a4fa28c3e98ede852c0ea19405a2812dd15e97be Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 19:57:12 +0200 Subject: [PATCH 05/30] Add output directory --- .gitignore | 1 + bin/install | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 6c974991..a1c01817 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ data/ram_air_kite/ram_air_kite_foil_cl_polar.csv .gitignore data/ram_air_kite/ram_air_kite_foil_cd_polar.csv data/ram_air_kite/ram_air_kite_foil_cm_polar.csv +output/ diff --git a/bin/install b/bin/install index a395b862..9005330f 100755 --- a/bin/install +++ b/bin/install @@ -233,6 +233,11 @@ $_julia_cmd --project=examples -e 'using GLMakie, VortexStepMethod; @info "GLMak $_julia_cmd -t1 --project=examples_cp -e 'using ControlPlots, VortexStepMethod; @info "ControlPlots extension ready."' $_julia_cmd --project=. -e 'using Pkg; Pkg.test(test_args=["settings/test_settings.jl"]); @info "Minimal test smoke check complete."' + +echo +echo "Creating output directory..." +mkdir -p output + echo echo "Installation complete." From 0e2af14f4ad4ae2a47a60288030038a66027e25d Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 20:20:24 +0200 Subject: [PATCH 06/30] Save as pdf for CairoMakie --- examples/rectangular_wing.jl | 10 ++++++++-- ext/VortexStepMethodControlPlotsExt.jl | 3 ++- ext/VortexStepMethodMakieExt.jl | 17 +++++++++-------- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index cbf8b6b1..f3df6d4a 100644 --- a/examples/rectangular_wing.jl +++ b/examples/rectangular_wing.jl @@ -2,7 +2,9 @@ using LinearAlgebra using VortexStepMethod PLOT = true +SAVE_ALL = false USE_TEX = false +OUTPUT_DIR = joinpath(dirname(@__DIR__), "output") # Step 1: Define wing parameters n_panels = 20 # Number of panels @@ -60,8 +62,8 @@ PLOT && plot_geometry( body_aero, "Rectangular_wing_geometry"; data_type=".pdf", - save_path=".", - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) @@ -74,6 +76,8 @@ PLOT && plot_distribution( [results_vsm, results_llt], ["VSM", "LLT"], title="Spanwise Distributions", + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, use_tex=USE_TEX ) @@ -87,6 +91,8 @@ PLOT && plot_polars( angle_type="angle_of_attack", v_a, title="Rectangular Wing Polars", + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, use_tex=USE_TEX ) nothing diff --git a/ext/VortexStepMethodControlPlotsExt.jl b/ext/VortexStepMethodControlPlotsExt.jl index 5b08aacc..bab179f1 100644 --- a/ext/VortexStepMethodControlPlotsExt.jl +++ b/ext/VortexStepMethodControlPlotsExt.jl @@ -56,7 +56,8 @@ function VortexStepMethod.save_plot(fig, save_path, title; data_type=".pdf") isnothing(save_path) && throw(ArgumentError("save_path should be provided")) !isdir(save_path) && mkpath(save_path) - full_path = joinpath(save_path, title * data_type) + sanitized_title = replace(String(title), ' ' => '_') + full_path = joinpath(save_path, sanitized_title * data_type) @debug "Attempting to save figure to: $full_path" @debug "Current working directory: $(pwd())" diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index fd12b661..4fef786e 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -257,8 +257,9 @@ function VortexStepMethod.save_plot(fig::Makie.Figure, save_path, title; data_ty isnothing(save_path) && throw(ArgumentError("save_path should be provided")) !isdir(save_path) && mkpath(save_path) - full_path = joinpath(save_path, title * data_type) - fallback_path = joinpath(save_path, title * ".png") + sanitized_title = replace(String(title), ' ' => '_') + full_path = joinpath(save_path, sanitized_title * data_type) + fallback_path = joinpath(save_path, sanitized_title * ".png") @debug "Attempting to save figure to: $full_path" @debug "Current working directory: $(pwd())" @@ -504,7 +505,7 @@ end """ plot_distribution(y_coordinates_list, results_list, label_list; - title="spanwise_distribution", data_type=".png", + title="spanwise_distribution", data_type=nothing, save_path=nothing, is_save=false, is_show=true, use_tex=false) Plot spanwise distributions of aerodynamic properties using Makie. @@ -516,7 +517,7 @@ Plot spanwise distributions of aerodynamic properties using Makie. # Keyword arguments - `title`: Plot title (default: "spanwise_distribution") -- `data_type`: File extension (default: ".png", also supports ".jpeg") +- `data_type`: File extension (default: `nothing`; delegated to `save_plot` backend-aware default) - `save_path`: Path to save plots (default: nothing) - `is_save`: Whether to save (default: false) - `is_show`: Whether to display (default: true) @@ -524,7 +525,7 @@ Plot spanwise distributions of aerodynamic properties using Makie. """ function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, label_list; title="spanwise_distribution", - data_type=".png", + data_type=nothing, save_path=nothing, is_save=false, is_show=true, @@ -659,7 +660,7 @@ Generate polar data for aerodynamic analysis over a range of angles. literature_path_list=String[], angle_range=range(0, 20, 2), angle_type="angle_of_attack", angle_of_attack=0.0, side_slip=0.0, v_a=10.0, - title="polar", data_type=".png", save_path=nothing, + title="polar", data_type=nothing, save_path=nothing, is_save=true, is_show=true, use_tex=false) Plot polar data comparing different solvers using Makie. @@ -677,7 +678,7 @@ Plot polar data comparing different solvers using Makie. - `side_slip`: Side slip angle [°] (default: 0.0) - `v_a`: Wind speed [m/s] (default: 10.0) - `title`: Plot title -- `data_type`: File extension (default: ".png", also supports ".jpeg") +- `data_type`: File extension (default: `nothing`; delegated to `save_plot` backend-aware default) - `save_path`: Path to save (default: nothing) - `is_save`: Whether to save (default: true) - `is_show`: Whether to display (default: true) @@ -695,7 +696,7 @@ function VortexStepMethod.plot_polars( side_slip=0.0, v_a=10.0, title="polar", - data_type=".png", + data_type=nothing, save_path=nothing, is_save=true, is_show=true, From 016be7a322b61b0b91317c072baf986e63b39b08 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 20:28:44 +0200 Subject: [PATCH 07/30] Add folder to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a1c01817..0df6f6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ data/ram_air_kite/ram_air_kite_foil_cl_polar.csv data/ram_air_kite/ram_air_kite_foil_cd_polar.csv data/ram_air_kite/ram_air_kite_foil_cm_polar.csv output/ +output_cairo/ \ No newline at end of file From 01ce01eeb7f361e67181d3517c73a4d87ba63410 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 20:32:48 +0200 Subject: [PATCH 08/30] Spaces in title --- examples/rectangular_wing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index f3df6d4a..fd0f7372 100644 --- a/examples/rectangular_wing.jl +++ b/examples/rectangular_wing.jl @@ -60,7 +60,7 @@ println("Projected area = $(round(results_vsm["projected_area"], digits=4)) m²" # Step 6: Plot geometry PLOT && plot_geometry( body_aero, - "Rectangular_wing_geometry"; + "Rectangular wing geometry"; data_type=".pdf", save_path=OUTPUT_DIR, is_save=false || SAVE_ALL, From fb44d6cc816f7b88c83ff881f8240a78e4194927 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 20:32:58 +0200 Subject: [PATCH 09/30] Add save logic --- examples/billowing.jl | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/examples/billowing.jl b/examples/billowing.jl index 98b0760d..abb27b27 100644 --- a/examples/billowing.jl +++ b/examples/billowing.jl @@ -2,7 +2,9 @@ using LinearAlgebra using VortexStepMethod PLOT = true +SAVE_ALL = false USE_TEX = false +OUTPUT_DIR = joinpath(dirname(@__DIR__), "output") # Data paths (all within this repo) vsm_src_path = something(pathof(VortexStepMethod), @__FILE__) @@ -132,28 +134,12 @@ println("Billowed: CL=$(round(results_bill["cl"]; digits=4)), " * "CD=$(round(results_bill["cd"]; digits=4))") if PLOT - # Plot polars comparison - plot_polars( - [solver_flat, solver_bill], - [body_aero_flat, body_aero_bill], - labels; - literature_path_list=literature_paths, - angle_range=range(-5, 25, length=31), - angle_type="angle_of_attack", - angle_of_attack=angle_of_attack_deg, - side_slip=sideslip_deg, - v_a=wind_speed, - title="V3 Kite: flat vs billowing $(BILLOWING_PCT)%", - is_show=true, - use_tex=USE_TEX, - show_moments=true - ) - # Plot geometry (flat wing) plot_geometry( body_aero_flat, "Flat wing geometry"; - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) @@ -168,9 +154,30 @@ if PLOT [results_flat, results_bill], ["VSM flat", "VSM billowing"]; title="Billowing comparison distributions", + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) + + # Plot polars comparison + plot_polars( + [solver_flat, solver_bill], + [body_aero_flat, body_aero_bill], + labels; + literature_path_list=literature_paths, + angle_range=range(-5, 25, length=31), + angle_type="angle_of_attack", + angle_of_attack=angle_of_attack_deg, + side_slip=sideslip_deg, + v_a=wind_speed, + title="V3 Kite flat vs billowing $(BILLOWING_PCT)%", + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, + is_show=true, + use_tex=USE_TEX, + show_moments=true + ) end nothing From 9c418ce91e62dc07c5fefd67655371152f62ebe0 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 20:53:31 +0200 Subject: [PATCH 10/30] Billowing can save now pdf --- ext/VortexStepMethodControlPlotsExt.jl | 2 +- ext/VortexStepMethodMakieExt.jl | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/VortexStepMethodControlPlotsExt.jl b/ext/VortexStepMethodControlPlotsExt.jl index bab179f1..46d5094e 100644 --- a/ext/VortexStepMethodControlPlotsExt.jl +++ b/ext/VortexStepMethodControlPlotsExt.jl @@ -56,7 +56,7 @@ function VortexStepMethod.save_plot(fig, save_path, title; data_type=".pdf") isnothing(save_path) && throw(ArgumentError("save_path should be provided")) !isdir(save_path) && mkpath(save_path) - sanitized_title = replace(String(title), ' ' => '_') + sanitized_title = replace(replace(String(title), ' ' => '_'), '%' => "pct") full_path = joinpath(save_path, sanitized_title * data_type) @debug "Attempting to save figure to: $full_path" diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 4fef786e..b815f2e9 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -257,7 +257,7 @@ function VortexStepMethod.save_plot(fig::Makie.Figure, save_path, title; data_ty isnothing(save_path) && throw(ArgumentError("save_path should be provided")) !isdir(save_path) && mkpath(save_path) - sanitized_title = replace(String(title), ' ' => '_') + sanitized_title = replace(replace(String(title), ' ' => '_'), '%' => "pct") full_path = joinpath(save_path, sanitized_title * data_type) fallback_path = joinpath(save_path, sanitized_title * ".png") @@ -448,7 +448,7 @@ end """ plot_geometry(body_aero::BodyAerodynamics, title; - data_type=".png", save_path=nothing, + data_type=nothing, save_path=nothing, is_save=false, is_show=false, view_elevation=15, view_azimuth=-120, use_tex=false) @@ -459,7 +459,7 @@ Plot wing geometry from different viewpoints using Makie. - `title`: plot title # Keyword arguments: -- `data_type`: File extension (default: ".png", also supports ".jpeg") +- `data_type`: File extension (default: `nothing`; delegated to `save_plot` backend-aware default) - `save_path`: Path for saving (default: nothing) - `is_save`: Whether to save (default: false) - `is_show`: Whether to display (default: false) @@ -468,7 +468,7 @@ Plot wing geometry from different viewpoints using Makie. - `use_tex`: Ignored for Makie (default: false) """ function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title; - data_type=".png", + data_type=nothing, save_path=nothing, is_save=false, is_show=false, From 2994e4b809e280d6994ab5ea80ff8f7ed0165208 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 20:57:44 +0200 Subject: [PATCH 11/30] Update pyramid_model.jl --- examples/pyramid_model.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/pyramid_model.jl b/examples/pyramid_model.jl index 42bbfd8e..cb8ee74e 100644 --- a/examples/pyramid_model.jl +++ b/examples/pyramid_model.jl @@ -24,7 +24,9 @@ results = VortexStepMethod.solve(solver, body_aero; log=true) # Using plotting modules, to create more comprehensive plots PLOT = true +SAVE_ALL = false USE_TEX = false +OUTPUT_DIR = joinpath(dirname(@__DIR__), "output") # Plotting polars PLOT && plot_polars( @@ -37,8 +39,8 @@ PLOT && plot_polars( side_slip=sideslip_deg, v_a=wind_speed, title="$(wing.n_panels)_panels_$(wing.spanwise_distribution)_pyramid_model", - data_type=".pdf", - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) @@ -46,10 +48,9 @@ PLOT && plot_polars( # Plotting geometry PLOT && plot_geometry( body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, + "Pyramid model geometry"; + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, view_elevation=15, view_azimuth=-120, @@ -64,8 +65,8 @@ PLOT && plot_distribution( [results], ["VSM"]; title="pyramid_spanwise_distributions_alpha_$(round(angle_of_attack_deg, digits=1))_delta_$(round(sideslip_deg, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(wind_speed, digits=1))", - data_type=".pdf", - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) From 0a44cff75d2e1da985bf85328b3f9a1cc580f30b Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 20:59:18 +0200 Subject: [PATCH 12/30] Update ram_air_kite.jl --- examples/ram_air_kite.jl | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index 6ffe00e8..02574124 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -3,9 +3,11 @@ using LinearAlgebra PLOT = true PRN = true +SAVE_ALL = false USE_TEX = false DEFORM = true LINEARIZE = false +OUTPUT_DIR = joinpath(dirname(@__DIR__), "output") # Create wing geometry wing = ObjWing( @@ -74,10 +76,10 @@ PLOT && plot_polar_data(body_aero) # Plotting geometry PLOT && plot_geometry( body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, + "Ram air kite geometry"; + data_type=".pdf", + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, view_elevation=15, view_azimuth=-120, @@ -96,8 +98,8 @@ PLOT && plot_distribution( [results], ["VSM"]; title="CAD_spanwise_distributions_alpha_$(round(aoa, digits=1))_delta_$(round(side_slip, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(v_a, digits=1))", - data_type=".pdf", - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) @@ -114,8 +116,8 @@ PLOT && plot_polars( side_slip=0, v_a=10, title="ram_kite_panels_$(wing.n_panels)_distribution_$(wing.spanwise_distribution)", - data_type=".pdf", - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) From 9ffda88cb714ddf923fbcf17166288597e88b9d5 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:00:46 +0200 Subject: [PATCH 13/30] Update stall_model.jl --- examples/stall_model.jl | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/examples/stall_model.jl b/examples/stall_model.jl index 87c380e9..8e1301cb 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -5,12 +5,12 @@ using CSV using DataFrames PLOT = true +SAVE_ALL = false USE_TEX = false +OUTPUT_DIR = joinpath(dirname(@__DIR__), "output") # Find root directory root_dir = dirname(@__DIR__) -save_folder = joinpath(root_dir, "results", "TUDELFT_V3_KITE") -mkpath(save_folder) # Defining discretisation n_panels = 54 @@ -67,10 +67,10 @@ set_va!(body_aero, vel_app) # Plotting geometry PLOT && plot_geometry( body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, + "Stall model geometry"; + data_type=".pdf", + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, view_elevation=15, view_azimuth=-120, @@ -89,15 +89,13 @@ PLOT && plot_distribution( [results, results_with_stall], ["VSM", "VSM with stall correction"]; title="CAD_spanwise_distributions_alpha_$(round(aoa, digits=1))_delta_$(round(side_slip, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(v_a, digits=1))", - data_type=".pdf", - save_path=joinpath(save_folder, "spanwise_distributions"), - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) # Plotting polar -save_path = joinpath(root_dir, "results", "TUDELFT_V3_KITE") path_cfd_lebesque = joinpath( root_dir, "data", @@ -128,9 +126,8 @@ PLOT && plot_polars( side_slip=side_slip, v_a=v_a, title="tutorial_testing_stall_model_n_panels_$(n_panels)_distribution_$(spanwise_distribution)", - data_type=".pdf", - save_path=joinpath(save_folder, "polars"), - is_save=true, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) From 604579b0291b7a8a51947b61933ddc1ba56d11f3 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:02:04 +0200 Subject: [PATCH 14/30] Update V3_kite.jl --- examples/V3_kite.jl | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/V3_kite.jl b/examples/V3_kite.jl index 8c042db1..34f2db62 100644 --- a/examples/V3_kite.jl +++ b/examples/V3_kite.jl @@ -2,8 +2,10 @@ using LinearAlgebra using VortexStepMethod PLOT = true +SAVE_ALL = false USE_TEX = false DEFORM = false +OUTPUT_DIR = joinpath(dirname(@__DIR__), "output") project_dir = dirname(@__DIR__) literature_paths = [ @@ -118,8 +120,8 @@ PLOT && plot_polars( side_slip=sideslip_deg, v_a=wind_speed, title="$(wing.n_panels)_panels_$(wing.spanwise_distribution)_from_yaml_settings", - data_type=".pdf", - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX, show_moments=true @@ -128,10 +130,10 @@ PLOT && plot_polars( # Plotting geometry PLOT && plot_geometry( body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, + "V3 kite geometry"; + data_type=".pdf", + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, view_elevation=15, view_azimuth=-120, @@ -147,8 +149,8 @@ PLOT && plot_distribution( [results], ["VSM"]; title="CAD_spanwise_distributions_alpha_$(round(angle_of_attack_deg, digits=1))_delta_$(round(sideslip_deg, digits=1))_yaw_$(round(yaw_rate, digits=1))_v_a_$(round(wind_speed, digits=1))", - data_type=".pdf", - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) @@ -193,7 +195,8 @@ PLOT && plot_polars( v_a=wind_speed, title="LOOP solver", show_moments=true, - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) @@ -211,7 +214,8 @@ PLOT && plot_polars( v_a=wind_speed, title="beta sweep", show_moments=true, - is_save=false, + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) From f3934ad0f134b2c0430edad1da75f590f7fb8154 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:17:26 +0200 Subject: [PATCH 15/30] Increase test coverage --- test/plotting/test_plotting.jl | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 53ded13b..f51ded1f 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -423,5 +423,62 @@ end ) @test fig_no_moments !== nothing safe_rm(no_cm_path) + + # Tests for save_plot function + if backend == "Makie" + body_aero = create_body_aero() + fig = plot_geometry( + body_aero, + "save_plot_test"; + is_save=false, + is_show=false) + @test fig isa Figure + + save_test_dir = tempdir() + + # Test 1: save_plot with explicit data_type (".png") + VortexStepMethod.save_plot(fig, save_test_dir, "test_explicit_png", data_type=".png") + @test isfile(joinpath(save_test_dir, "test_explicit_png.png")) + safe_rm(joinpath(save_test_dir, "test_explicit_png.png")) + + # Test 2: save_plot with explicit data_type (".pdf") + VortexStepMethod.save_plot(fig, save_test_dir, "test_explicit_pdf", data_type=".pdf") + @test isfile(joinpath(save_test_dir, "test_explicit_pdf.pdf")) + safe_rm(joinpath(save_test_dir, "test_explicit_pdf.pdf")) + + # Test 3: save_plot with data_type=nothing (backend-aware detection) + VortexStepMethod.save_plot(fig, save_test_dir, "test_backend_aware", data_type=nothing) + cairo_loaded = any(m -> nameof(m) == :CairoMakie, values(Base.loaded_modules)) + expected_ext = cairo_loaded ? ".pdf" : ".png" + @test isfile(joinpath(save_test_dir, "test_backend_aware" * expected_ext)) + safe_rm(joinpath(save_test_dir, "test_backend_aware" * expected_ext)) + + # Test 4: save_plot with title containing spaces (should be sanitized to underscores) + VortexStepMethod.save_plot(fig, save_test_dir, "test with spaces", data_type=".png") + @test isfile(joinpath(save_test_dir, "test_with_spaces.png")) + safe_rm(joinpath(save_test_dir, "test_with_spaces.png")) + + # Test 5: save_plot with title containing percent signs (should be sanitized to "pct") + VortexStepMethod.save_plot(fig, save_test_dir, "test%efficiency", data_type=".png") + @test isfile(joinpath(save_test_dir, "testpctefficiency.png")) + safe_rm(joinpath(save_test_dir, "testpctefficiency.png")) + + # Test 6: save_plot with title containing both spaces and percent signs + VortexStepMethod.save_plot(fig, save_test_dir, "test %efficiency metric", data_type=".png") + @test isfile(joinpath(save_test_dir, "test_pctefficiency_metric.png")) + safe_rm(joinpath(save_test_dir, "test_pctefficiency_metric.png")) + + # Test 7: save_plot creates directory if it doesn't exist + nested_dir = joinpath(save_test_dir, "nested_save_plot_dir") + !isdir(nested_dir) && @test !isdir(nested_dir) + VortexStepMethod.save_plot(fig, nested_dir, "test_nested_dir", data_type=".png") + @test isdir(nested_dir) + @test isfile(joinpath(nested_dir, "test_nested_dir.png")) + safe_rm(joinpath(nested_dir, "test_nested_dir.png")) + rm(nested_dir; force=true) + + # Test 8: save_plot raises error when save_path is nothing + @test_throws ArgumentError VortexStepMethod.save_plot(fig, nothing, "test_title", data_type=".png") + end end nothing From 2bb87b84cbe3d148c9fa1d5ea25817dbd81d7263 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:38:56 +0200 Subject: [PATCH 16/30] Update menu --- docs/src/examples.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/examples.md b/docs/src/examples.md index 1315cec4..39c18298 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -150,6 +150,9 @@ Choose function to execute or `q` to quit: stall_model = include("stall_model.jl") bench = include("bench.jl") cleanup = include("cleanup.jl") + GLMakie.activate!() + CairoMakie.activate!() + help_me = VortexStepMethod.help("https://opensourceawe.github.io/VortexStepMethod.jl/dev") quit ``` From 815475b6af1fb31a84aafda1f2baa01a89048c8b Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:50:36 +0200 Subject: [PATCH 17/30] Check if Cairo is activated, not loaded --- ext/VortexStepMethodMakieExt.jl | 40 ++++++++++++++++++++++++++++++--- test/plotting/test_plotting.jl | 24 ++++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index b815f2e9..de9d265f 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -247,12 +247,46 @@ Save a Makie figure to a file. - `title`: Title of the plot # Keyword arguments -- `data_type`: File extension. Defaults to `".pdf"` when CairoMakie is active, `".png"` otherwise. +- `data_type`: File extension. Defaults to `".pdf"` when the active Makie backend is CairoMakie, `".png"` otherwise. """ +function _active_backend_prefers_vector_output() + isdefined(Makie, :current_backend) || return false + + backend = try + Makie.current_backend() + catch + return false + end + + # Makie versions may return backend modules directly or backend callables. + if backend isa Module + return nameof(backend) == :CairoMakie + end + if backend isa DataType + return nameof(backend) == :CairoMakie + end + + if Base.applicable(backend) + called_backend = try + backend() + catch + nothing + end + if called_backend isa Module + return nameof(called_backend) == :CairoMakie + end + if called_backend isa DataType + return nameof(called_backend) == :CairoMakie + end + !isnothing(called_backend) && return occursin("cairomakie", lowercase(string(called_backend))) + end + + return occursin("cairomakie", lowercase(string(backend))) +end + function VortexStepMethod.save_plot(fig::Makie.Figure, save_path, title; data_type=nothing) if isnothing(data_type) - cairo_loaded = any(m -> nameof(m) == :CairoMakie, values(Base.loaded_modules)) - data_type = cairo_loaded ? ".pdf" : ".png" + data_type = _active_backend_prefers_vector_output() ? ".pdf" : ".png" end isnothing(save_path) && throw(ArgumentError("save_path should be provided")) diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index f51ded1f..43b6f5f0 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -448,8 +448,28 @@ end # Test 3: save_plot with data_type=nothing (backend-aware detection) VortexStepMethod.save_plot(fig, save_test_dir, "test_backend_aware", data_type=nothing) - cairo_loaded = any(m -> nameof(m) == :CairoMakie, values(Base.loaded_modules)) - expected_ext = cairo_loaded ? ".pdf" : ".png" + backend_obj = Makie.current_backend() + backend_name = if backend_obj isa Module + nameof(backend_obj) + elseif backend_obj isa DataType + nameof(backend_obj) + elseif Base.applicable(backend_obj) + called_backend = try + backend_obj() + catch + nothing + end + if called_backend isa Module + nameof(called_backend) + elseif called_backend isa DataType + nameof(called_backend) + else + Symbol(string(something(called_backend, backend_obj))) + end + else + Symbol(string(backend_obj)) + end + expected_ext = lowercase(String(backend_name)) == "cairomakie" ? ".pdf" : ".png" @test isfile(joinpath(save_test_dir, "test_backend_aware" * expected_ext)) safe_rm(joinpath(save_test_dir, "test_backend_aware" * expected_ext)) From b1b6748554cd2fb87047a6e538aeffef60aedd21 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:52:44 +0200 Subject: [PATCH 18/30] Don't hardcode data type --- examples/rectangular_wing.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index fd0f7372..b576b026 100644 --- a/examples/rectangular_wing.jl +++ b/examples/rectangular_wing.jl @@ -61,7 +61,6 @@ println("Projected area = $(round(results_vsm["projected_area"], digits=4)) m²" PLOT && plot_geometry( body_aero, "Rectangular wing geometry"; - data_type=".pdf", save_path=OUTPUT_DIR, is_save=false || SAVE_ALL, is_show=true, From c7cc6f1357d30369e4091360b4749cf2f823dac7 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:54:26 +0200 Subject: [PATCH 19/30] Don't hardcode data_type --- examples/stall_model.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/stall_model.jl b/examples/stall_model.jl index 8e1301cb..8b9676e2 100644 --- a/examples/stall_model.jl +++ b/examples/stall_model.jl @@ -68,7 +68,6 @@ set_va!(body_aero, vel_app) PLOT && plot_geometry( body_aero, "Stall model geometry"; - data_type=".pdf", save_path=OUTPUT_DIR, is_save=false || SAVE_ALL, is_show=true, From ae8d3b7e1e55e8636f72ebd41155d2796141ba51 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 21:56:44 +0200 Subject: [PATCH 20/30] Don't hardcode the data type --- examples/V3_kite.jl | 1 - examples/ram_air_kite.jl | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/V3_kite.jl b/examples/V3_kite.jl index 34f2db62..7e4caea5 100644 --- a/examples/V3_kite.jl +++ b/examples/V3_kite.jl @@ -131,7 +131,6 @@ PLOT && plot_polars( PLOT && plot_geometry( body_aero, "V3 kite geometry"; - data_type=".pdf", save_path=OUTPUT_DIR, is_save=false || SAVE_ALL, is_show=true, diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index 02574124..8deb6973 100644 --- a/examples/ram_air_kite.jl +++ b/examples/ram_air_kite.jl @@ -77,7 +77,6 @@ PLOT && plot_polar_data(body_aero) PLOT && plot_geometry( body_aero, "Ram air kite geometry"; - data_type=".pdf", save_path=OUTPUT_DIR, is_save=false || SAVE_ALL, is_show=true, From fcbd71bc2698597abc2cd64443f654c6c504a32d Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 22:01:35 +0200 Subject: [PATCH 21/30] Update docstring --- ext/VortexStepMethodMakieExt.jl | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index de9d265f..f339e390 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -236,19 +236,6 @@ function Makie.plot(body_aero::VortexStepMethod.BodyAerodynamics; size=(1200, 80 return fig end -""" - save_plot(fig, save_path, title; data_type=".png") - -Save a Makie figure to a file. - -# Arguments -- `fig`: Makie Figure object -- `save_path`: Path to save the plot -- `title`: Title of the plot - -# Keyword arguments -- `data_type`: File extension. Defaults to `".pdf"` when the active Makie backend is CairoMakie, `".png"` otherwise. -""" function _active_backend_prefers_vector_output() isdefined(Makie, :current_backend) || return false @@ -284,6 +271,20 @@ function _active_backend_prefers_vector_output() return occursin("cairomakie", lowercase(string(backend))) end +""" + save_plot(fig, save_path, title; data_type=nothing) + +Save a Makie figure to a file. + +# Arguments +- `fig`: Makie Figure object +- `save_path`: Path to save the plot +- `title`: Title of the plot + +# Keyword arguments +- `data_type`: File extension. If `nothing`, defaults to `".pdf"` when the + active Makie backend is CairoMakie and `".png"` otherwise. +""" function VortexStepMethod.save_plot(fig::Makie.Figure, save_path, title; data_type=nothing) if isnothing(data_type) data_type = _active_backend_prefers_vector_output() ? ".pdf" : ".png" From c9c0dbad02ff03928a1c28d2e0dd20667c732788 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 22:39:13 +0200 Subject: [PATCH 22/30] Increase test coverage --- ext/VortexStepMethodMakieExt.jl | 6 +- test/plotting/test_plotting.jl | 124 ++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index f339e390..4bd98fdd 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -236,11 +236,11 @@ function Makie.plot(body_aero::VortexStepMethod.BodyAerodynamics; size=(1200, 80 return fig end -function _active_backend_prefers_vector_output() - isdefined(Makie, :current_backend) || return false +function _active_backend_prefers_vector_output(makie=Makie) + isdefined(makie, :current_backend) || return false backend = try - Makie.current_backend() + makie.current_backend() catch return false end diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 43b6f5f0..1d088278 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -1,3 +1,92 @@ +module FakeMakieNoCurrentBackend end + +module FakeMakieCurrentBackendThrows + current_backend() = error("boom") +end + +module FakeMakieReturnsCairoModule + import CairoMakie + current_backend() = CairoMakie +end + +module FakeMakieReturnsOtherModule + import Base + current_backend() = Base +end + +module FakeMakieReturnsCairoType + struct CairoMakie end + current_backend() = CairoMakie +end + +module FakeMakieReturnsOtherType + struct OtherBackend end + current_backend() = OtherBackend +end + +module FakeMakieReturnsCallableModuleCairo + import CairoMakie + struct BackendCallable end + current_backend() = BackendCallable() + (::BackendCallable)() = CairoMakie +end + +module FakeMakieReturnsCallableModuleOther + import Base + struct BackendCallable end + current_backend() = BackendCallable() + (::BackendCallable)() = Base +end + +module FakeMakieReturnsCallableTypeCairo + struct CairoMakie end + struct BackendCallable end + current_backend() = BackendCallable() + (::BackendCallable)() = CairoMakie +end + +module FakeMakieReturnsCallableTypeOther + struct OtherBackend end + struct BackendCallable end + current_backend() = BackendCallable() + (::BackendCallable)() = OtherBackend +end + +module FakeMakieReturnsCallableStringCairo + struct BackendCallable end + current_backend() = BackendCallable() + (::BackendCallable)() = "CairoMakie backend" +end + +module FakeMakieReturnsCallableStringOther + struct BackendCallable end + current_backend() = BackendCallable() + (::BackendCallable)() = "Raster backend" +end + +module FakeMakieReturnsThrowingCallableCairo + import Base: show + struct CairoMakieCallable end + current_backend() = CairoMakieCallable() + (::CairoMakieCallable)() = error("boom") + show(io::IO, ::CairoMakieCallable) = print(io, "CairoMakieCallable") +end + +module FakeMakieReturnsThrowingCallableOther + import Base: show + struct OtherCallable end + current_backend() = OtherCallable() + (::OtherCallable)() = error("boom") + show(io::IO, ::OtherCallable) = print(io, "OtherCallable") +end + +module FakeMakieReturnsStringCairo + current_backend() = "CairoMakie backend" +end + +module FakeMakieReturnsStringOther + current_backend() = "Raster backend" +end backend = if "plot-controlplots" in ARGS using ControlPlots import ControlPlots: plt @@ -10,6 +99,9 @@ end using VortexStepMethod using Test +const makie_ext = backend == "Makie" ? + Base.get_extension(VortexStepMethod, :VortexStepMethodMakieExt) : nothing + # Resolve repo data directory for ram air kite assets _ram_data_dir = joinpath(dirname(dirname(@__DIR__)), "data", "ram_air_kite") @@ -426,6 +518,38 @@ end # Tests for save_plot function if backend == "Makie" + @testset "_active_backend_prefers_vector_output" begin + @test makie_ext !== nothing + + active_backend_prefers_vector_output = + getfield(makie_ext, :_active_backend_prefers_vector_output) + + + @test active_backend_prefers_vector_output(FakeMakieNoCurrentBackend) == false + @test active_backend_prefers_vector_output(FakeMakieCurrentBackendThrows) == false + + @test active_backend_prefers_vector_output(FakeMakieReturnsCairoModule) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsOtherModule) == false + + @test active_backend_prefers_vector_output(FakeMakieReturnsCairoType) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsOtherType) == false + + @test active_backend_prefers_vector_output(FakeMakieReturnsCallableModuleCairo) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsCallableModuleOther) == false + + @test active_backend_prefers_vector_output(FakeMakieReturnsCallableTypeCairo) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsCallableTypeOther) == false + + @test active_backend_prefers_vector_output(FakeMakieReturnsCallableStringCairo) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsCallableStringOther) == false + + @test active_backend_prefers_vector_output(FakeMakieReturnsThrowingCallableCairo) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsThrowingCallableOther) == false + + @test active_backend_prefers_vector_output(FakeMakieReturnsStringCairo) == true + @test active_backend_prefers_vector_output(FakeMakieReturnsStringOther) == false + end + body_aero = create_body_aero() fig = plot_geometry( body_aero, From 6aa190f752c31096ddab6648e97e50fd2154050c Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Sun, 19 Apr 2026 23:24:07 +0200 Subject: [PATCH 23/30] Increase timeout to 60min --- .github/workflows/setup-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-test.yml b/.github/workflows/setup-test.yml index 1bfc2b1e..da4db545 100644 --- a/.github/workflows/setup-test.yml +++ b/.github/workflows/setup-test.yml @@ -12,7 +12,7 @@ jobs: name: Test end-user and developer setup if: github.event.pull_request.draft == false runs-on: ubuntu-latest - timeout-minutes: 40 + timeout-minutes: 60 steps: - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 From e2acd33e7de732eae7a4015b2975da2886cb08cf Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Mon, 20 Apr 2026 00:30:59 +0200 Subject: [PATCH 24/30] Increase timeout --- .github/workflows/setup-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/setup-test.yml b/.github/workflows/setup-test.yml index da4db545..ea60c709 100644 --- a/.github/workflows/setup-test.yml +++ b/.github/workflows/setup-test.yml @@ -12,7 +12,7 @@ jobs: name: Test end-user and developer setup if: github.event.pull_request.draft == false runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 90 steps: - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 From 8f19fd3cc412b15cc347f347a12ec6b2bc94ec93 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Mon, 20 Apr 2026 06:20:39 +0200 Subject: [PATCH 25/30] Next try --- ext/VortexStepMethodMakieExt.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 4bd98fdd..5ab2c87f 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -336,7 +336,7 @@ Display a Makie figure. - `dpi`: Dots per inch for the figure (default: 130) - currently unused in Makie """ function VortexStepMethod.show_plot(fig::Makie.Figure; dpi=130) - display(fig) + isinteractive() && display(fig) end """ @@ -531,7 +531,7 @@ function VortexStepMethod.plot_geometry(body_aero::BodyAerodynamics, title; fig = create_geometry_plot_makie(body_aero, title, view_elevation, view_azimuth) - if is_show + if is_show && isinteractive() display(fig) end @@ -661,7 +661,7 @@ function VortexStepMethod.plot_distribution(y_coordinates_list, results_list, la save_plot(fig, save_path, title, data_type=data_type) end - if is_show + if is_show && isinteractive() display(fig) end @@ -881,7 +881,7 @@ function VortexStepMethod.plot_polars( save_plot(fig, save_path, main_title; data_type) end - if is_show + if is_show && isinteractive() display(fig) end @@ -940,7 +940,7 @@ function VortexStepMethod.plot_polar_data(body_aero::BodyAerodynamics; color=:blue, linewidth=0.5, transparency=true) end - if is_show + if is_show && isinteractive() display(fig) end return fig @@ -1312,7 +1312,7 @@ function VortexStepMethod.plot_combined_analysis( colsize!(fig.layout, 1, Relative(0.6)) colsize!(fig.layout, 2, Relative(0.4)) - if is_show + if is_show && isinteractive() display(fig) end From 5d41ba4d59983287c9e1639a628476c885808db5 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Mon, 20 Apr 2026 06:57:20 +0200 Subject: [PATCH 26/30] Improve test --- test/plotting/test_plotting.jl | 40 ++++++++++++++-------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 1d088278..350a3591 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -558,6 +558,9 @@ end is_show=false) @test fig isa Figure + active_backend_prefers_vector_output = + getfield(makie_ext, :_active_backend_prefers_vector_output) + save_test_dir = tempdir() # Test 1: save_plot with explicit data_type (".png") @@ -571,31 +574,20 @@ end safe_rm(joinpath(save_test_dir, "test_explicit_pdf.pdf")) # Test 3: save_plot with data_type=nothing (backend-aware detection) - VortexStepMethod.save_plot(fig, save_test_dir, "test_backend_aware", data_type=nothing) - backend_obj = Makie.current_backend() - backend_name = if backend_obj isa Module - nameof(backend_obj) - elseif backend_obj isa DataType - nameof(backend_obj) - elseif Base.applicable(backend_obj) - called_backend = try - backend_obj() - catch - nothing - end - if called_backend isa Module - nameof(called_backend) - elseif called_backend isa DataType - nameof(called_backend) - else - Symbol(string(something(called_backend, backend_obj))) - end - else - Symbol(string(backend_obj)) + backend_aware_dir = mktempdir() + try + VortexStepMethod.save_plot(fig, backend_aware_dir, "test_backend_aware", data_type=nothing) + pdf_path = joinpath(backend_aware_dir, "test_backend_aware.pdf") + png_path = joinpath(backend_aware_dir, "test_backend_aware.png") + expected_ext = active_backend_prefers_vector_output(Makie) ? ".pdf" : ".png" + + @test xor(isfile(pdf_path), isfile(png_path)) + @test isfile(joinpath(backend_aware_dir, "test_backend_aware" * expected_ext)) + finally + safe_rm(joinpath(backend_aware_dir, "test_backend_aware.pdf")) + safe_rm(joinpath(backend_aware_dir, "test_backend_aware.png")) + rm(backend_aware_dir; force=true, recursive=true) end - expected_ext = lowercase(String(backend_name)) == "cairomakie" ? ".pdf" : ".png" - @test isfile(joinpath(save_test_dir, "test_backend_aware" * expected_ext)) - safe_rm(joinpath(save_test_dir, "test_backend_aware" * expected_ext)) # Test 4: save_plot with title containing spaces (should be sanitized to underscores) VortexStepMethod.save_plot(fig, save_test_dir, "test with spaces", data_type=".png") From 79e4909b28bc455638b3921020d09d40b1d4e259 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Mon, 20 Apr 2026 09:02:31 +0200 Subject: [PATCH 27/30] Updated. --- docs/src/examples.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/examples.md b/docs/src/examples.md index 39c18298..849e76ff 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -177,7 +177,7 @@ The examples in this package support three plotting backends. Here is a comparis ### CairoMakie **Advantages:** - Fully software-rendered — works in headless environments (CI, servers, SSH sessions). -- Produces high-quality vector output (SVG, PDF) suitable for publication. +- Produces high-quality vector output (SVG, PDF) suitable for publication for 2D plots. The quality of 3D plots is not yet suitable for publications. - Lighter dependency than GLMakie (no GPU required). **Disadvantages:** @@ -194,7 +194,7 @@ The examples in this package support three plotting backends. Here is a comparis **Disadvantages:** - Requires a working Python installation with Matplotlib (via `PyCall`). -- Can cause issues when multithreading is enabled. +- Might crash when multithreading is enabled. Start Julia with `-t 1,0` to avoid problems. - No native Makie ecosystem integration (e.g. cannot use `Makie.Observable` for live updates). - Interactivity is limited and depends on the Matplotlib backend in use. - Extra setup complexity when Python or Matplotlib are not already installed. From c556cf96359c6ab729a4997210456b404ed0c5a5 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Mon, 20 Apr 2026 09:32:46 +0200 Subject: [PATCH 28/30] Removed this lighter statement --- docs/src/examples.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/examples.md b/docs/src/examples.md index 849e76ff..15731868 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -177,8 +177,7 @@ The examples in this package support three plotting backends. Here is a comparis ### CairoMakie **Advantages:** - Fully software-rendered — works in headless environments (CI, servers, SSH sessions). -- Produces high-quality vector output (SVG, PDF) suitable for publication for 2D plots. The quality of 3D plots is not yet suitable for publications. -- Lighter dependency than GLMakie (no GPU required). +- Produces high-quality vector output (SVG, PDF) suitable for publication for 2D plots. The quality of 3D plots is not yet suitable for publications, though. **Disadvantages:** - Plots are static — no interactive zoom or pan. From 0b7bed6cad830cf64ec9b5816090565691d5e9c8 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Mon, 20 Apr 2026 09:37:08 +0200 Subject: [PATCH 29/30] One more change --- docs/src/examples.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/examples.md b/docs/src/examples.md index 15731868..36555701 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -172,7 +172,7 @@ The examples in this package support three plotting backends. Here is a comparis **Disadvantages:** - Requires a display server (does not work in headless/server environments without a virtual framebuffer). - Heavier dependency: needs OpenGL drivers and a GPU. -- Longer initial load time compared to the other backends. +- Longer initial load time compared to ControlPlots ### CairoMakie **Advantages:** @@ -183,6 +183,7 @@ The examples in this package support three plotting backends. Here is a comparis - Plots are static — no interactive zoom or pan. - Slower for very large or complex scenes because rendering is done in software. - 3D support is limited compared to GLMakie. +- Longer initial load time compared to ControlPlots ### ControlPlots (based on PyPlot / Matplotlib) **Advantages:** From 3fe65f820f476721b3bff5cfd39ef0f38b54b0c1 Mon Sep 17 00:00:00 2001 From: Uwe Fechner Date: Thu, 23 Apr 2026 16:35:24 +0200 Subject: [PATCH 30/30] Simplify code Co-authored-by: Copilot --- ext/VortexStepMethodMakieExt.jl | 25 +-------- test/plotting/test_plotting.jl | 92 --------------------------------- 2 files changed, 1 insertion(+), 116 deletions(-) diff --git a/ext/VortexStepMethodMakieExt.jl b/ext/VortexStepMethodMakieExt.jl index 5ab2c87f..fd37e52e 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -245,30 +245,7 @@ function _active_backend_prefers_vector_output(makie=Makie) return false end - # Makie versions may return backend modules directly or backend callables. - if backend isa Module - return nameof(backend) == :CairoMakie - end - if backend isa DataType - return nameof(backend) == :CairoMakie - end - - if Base.applicable(backend) - called_backend = try - backend() - catch - nothing - end - if called_backend isa Module - return nameof(called_backend) == :CairoMakie - end - if called_backend isa DataType - return nameof(called_backend) == :CairoMakie - end - !isnothing(called_backend) && return occursin("cairomakie", lowercase(string(called_backend))) - end - - return occursin("cairomakie", lowercase(string(backend))) + return nameof(backend) == :CairoMakie end """ diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 350a3591..447176db 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -13,80 +13,6 @@ module FakeMakieReturnsOtherModule import Base current_backend() = Base end - -module FakeMakieReturnsCairoType - struct CairoMakie end - current_backend() = CairoMakie -end - -module FakeMakieReturnsOtherType - struct OtherBackend end - current_backend() = OtherBackend -end - -module FakeMakieReturnsCallableModuleCairo - import CairoMakie - struct BackendCallable end - current_backend() = BackendCallable() - (::BackendCallable)() = CairoMakie -end - -module FakeMakieReturnsCallableModuleOther - import Base - struct BackendCallable end - current_backend() = BackendCallable() - (::BackendCallable)() = Base -end - -module FakeMakieReturnsCallableTypeCairo - struct CairoMakie end - struct BackendCallable end - current_backend() = BackendCallable() - (::BackendCallable)() = CairoMakie -end - -module FakeMakieReturnsCallableTypeOther - struct OtherBackend end - struct BackendCallable end - current_backend() = BackendCallable() - (::BackendCallable)() = OtherBackend -end - -module FakeMakieReturnsCallableStringCairo - struct BackendCallable end - current_backend() = BackendCallable() - (::BackendCallable)() = "CairoMakie backend" -end - -module FakeMakieReturnsCallableStringOther - struct BackendCallable end - current_backend() = BackendCallable() - (::BackendCallable)() = "Raster backend" -end - -module FakeMakieReturnsThrowingCallableCairo - import Base: show - struct CairoMakieCallable end - current_backend() = CairoMakieCallable() - (::CairoMakieCallable)() = error("boom") - show(io::IO, ::CairoMakieCallable) = print(io, "CairoMakieCallable") -end - -module FakeMakieReturnsThrowingCallableOther - import Base: show - struct OtherCallable end - current_backend() = OtherCallable() - (::OtherCallable)() = error("boom") - show(io::IO, ::OtherCallable) = print(io, "OtherCallable") -end - -module FakeMakieReturnsStringCairo - current_backend() = "CairoMakie backend" -end - -module FakeMakieReturnsStringOther - current_backend() = "Raster backend" -end backend = if "plot-controlplots" in ARGS using ControlPlots import ControlPlots: plt @@ -530,24 +456,6 @@ end @test active_backend_prefers_vector_output(FakeMakieReturnsCairoModule) == true @test active_backend_prefers_vector_output(FakeMakieReturnsOtherModule) == false - - @test active_backend_prefers_vector_output(FakeMakieReturnsCairoType) == true - @test active_backend_prefers_vector_output(FakeMakieReturnsOtherType) == false - - @test active_backend_prefers_vector_output(FakeMakieReturnsCallableModuleCairo) == true - @test active_backend_prefers_vector_output(FakeMakieReturnsCallableModuleOther) == false - - @test active_backend_prefers_vector_output(FakeMakieReturnsCallableTypeCairo) == true - @test active_backend_prefers_vector_output(FakeMakieReturnsCallableTypeOther) == false - - @test active_backend_prefers_vector_output(FakeMakieReturnsCallableStringCairo) == true - @test active_backend_prefers_vector_output(FakeMakieReturnsCallableStringOther) == false - - @test active_backend_prefers_vector_output(FakeMakieReturnsThrowingCallableCairo) == true - @test active_backend_prefers_vector_output(FakeMakieReturnsThrowingCallableOther) == false - - @test active_backend_prefers_vector_output(FakeMakieReturnsStringCairo) == true - @test active_backend_prefers_vector_output(FakeMakieReturnsStringOther) == false end body_aero = create_body_aero()