diff --git a/.github/workflows/setup-test.yml b/.github/workflows/setup-test.yml index 1bfc2b1e..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: 40 + timeout-minutes: 90 steps: - uses: actions/checkout@v6 - uses: julia-actions/setup-julia@v2 diff --git a/.gitignore b/.gitignore index 6c974991..0df6f6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ 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/ +output_cairo/ \ No newline at end of file diff --git a/NEWS.md b/NEWS.md index 07a48b16..8e9598ed 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +## Unreleased + +### Added +- allow using CairoMakie or GLMakie using the menu + ## VortexStepMethod v3.1.3 2026-04-23 ### Fixed 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." diff --git a/docs/src/examples.md b/docs/src/examples.md index e3ce46f9..36555701 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -150,8 +150,62 @@ 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 ``` 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 ControlPlots + +### 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, though. + +**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. +- Longer initial load time compared to ControlPlots + +### 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`). +- 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. + +| 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/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/V3_kite.jl b/examples/V3_kite.jl index 8c042db1..7e4caea5 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,9 @@ PLOT && plot_polars( # Plotting geometry PLOT && plot_geometry( body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, + "V3 kite geometry"; + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, view_elevation=15, view_azimuth=-120, @@ -147,8 +148,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 +194,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 +213,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 ) 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 diff --git a/examples/menu.jl b/examples/menu.jl index d4324927..50a948a7 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,12 +44,14 @@ 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" ] 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) 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 ) diff --git a/examples/ram_air_kite.jl b/examples/ram_air_kite.jl index 6ffe00e8..8deb6973 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,9 @@ 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"; + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, view_elevation=15, view_azimuth=-120, @@ -96,8 +97,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 +115,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 ) diff --git a/examples/rectangular_wing.jl b/examples/rectangular_wing.jl index cbf8b6b1..b576b026 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 @@ -58,10 +60,9 @@ println("Projected area = $(round(results_vsm["projected_area"], digits=4)) m²" # Step 6: Plot geometry PLOT && plot_geometry( body_aero, - "Rectangular_wing_geometry"; - data_type=".pdf", - save_path=".", - is_save=false, + "Rectangular wing geometry"; + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, use_tex=USE_TEX ) @@ -74,6 +75,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 +90,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/examples/stall_model.jl b/examples/stall_model.jl index 87c380e9..8b9676e2 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,9 @@ set_va!(body_aero, vel_app) # Plotting geometry PLOT && plot_geometry( body_aero, - ""; - data_type=".svg", - save_path="", - is_save=false, + "Stall model geometry"; + save_path=OUTPUT_DIR, + is_save=false || SAVE_ALL, is_show=true, view_elevation=15, view_azimuth=-120, @@ -89,15 +88,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 +125,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 ) diff --git a/ext/VortexStepMethodControlPlotsExt.jl b/ext/VortexStepMethodControlPlotsExt.jl index 5b08aacc..46d5094e 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(replace(String(title), ' ' => '_'), '%' => "pct") + 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 e4e39c8e..fd37e52e 100644 --- a/ext/VortexStepMethodMakieExt.jl +++ b/ext/VortexStepMethodMakieExt.jl @@ -236,8 +236,20 @@ function Makie.plot(body_aero::VortexStepMethod.BodyAerodynamics; size=(1200, 80 return fig end +function _active_backend_prefers_vector_output(makie=Makie) + isdefined(makie, :current_backend) || return false + + backend = try + makie.current_backend() + catch + return false + end + + return nameof(backend) == :CairoMakie +end + """ - save_plot(fig, save_path, title; data_type=".png") + save_plot(fig, save_path, title; data_type=nothing) Save a Makie figure to a file. @@ -247,14 +259,19 @@ 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. 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=".png") +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" + end 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(replace(String(title), ' ' => '_'), '%' => "pct") + 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())" @@ -296,7 +313,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 """ @@ -443,7 +460,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) @@ -454,7 +471,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) @@ -463,7 +480,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, @@ -491,7 +508,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 @@ -500,7 +517,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. @@ -512,7 +529,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) @@ -520,7 +537,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, @@ -621,7 +638,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 @@ -655,7 +672,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. @@ -673,7 +690,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) @@ -691,7 +708,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, @@ -841,7 +858,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 @@ -900,7 +917,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 @@ -1272,7 +1289,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 diff --git a/test/plotting/test_plotting.jl b/test/plotting/test_plotting.jl index 53ded13b..447176db 100644 --- a/test/plotting/test_plotting.jl +++ b/test/plotting/test_plotting.jl @@ -1,3 +1,18 @@ +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 backend = if "plot-controlplots" in ARGS using ControlPlots import ControlPlots: plt @@ -10,6 +25,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") @@ -423,5 +441,88 @@ end ) @test fig_no_moments !== nothing safe_rm(no_cm_path) + + # 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 + end + + body_aero = create_body_aero() + fig = plot_geometry( + body_aero, + "save_plot_test"; + is_save=false, + 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") + 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) + 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 + + # 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