From 9694a76be1b9d1e7548c790e3d1b9f9d784935bc Mon Sep 17 00:00:00 2001 From: aeronauty Date: Sat, 13 Jun 2026 17:18:10 -0700 Subject: [PATCH 1/2] feat(rustfoil-cli): add --flap option to polar and faithful-polar Add an XFOIL-style --flap "hinge_x:hinge_y:deflection_deg" option to both the inviscid `polar` and viscous `faithful-polar` commands. The deflection is applied via the existing rustfoil_core::flap::xfoil_flap before the body is built (faithful-polar writes the flapped section to a temp .dat and feeds it to XFOIL). Enables flapped-airfoil priors directly from the CLI without a separate pre-deflected coordinate file. Co-Authored-By: Claude Opus 4.8 --- crates/rustfoil-cli/src/main.rs | 41 ++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/rustfoil-cli/src/main.rs b/crates/rustfoil-cli/src/main.rs index 7c0333f8..fcdd3a09 100644 --- a/crates/rustfoil-cli/src/main.rs +++ b/crates/rustfoil-cli/src/main.rs @@ -149,6 +149,10 @@ enum Commands { /// Angle of attack step (degrees) #[arg(long, default_value = "1.0")] alpha_step: f64, + + /// XFOIL-style flap "hinge_x_frac:hinge_y_frac:deflection_deg" (e.g. 0.72:0.5:12) + #[arg(long)] + flap: Option, }, /// Repanel an airfoil using cosine spacing @@ -326,6 +330,10 @@ struct FaithfulPolarCmd { #[arg(long)] parallel: bool, + + /// XFOIL-style flap "hinge_x_frac:hinge_y_frac:deflection_deg" + #[arg(long)] + flap: Option, } fn main() { @@ -348,7 +356,8 @@ fn main() { alpha_start, alpha_end, alpha_step, - } => run_polar(&file, alpha_start, alpha_end, alpha_step), + flap, + } => run_polar(&file, alpha_start, alpha_end, alpha_step, flap), Commands::Repanel { file, panels, @@ -508,8 +517,18 @@ fn run_polar( alpha_start: f64, alpha_end: f64, alpha_step: f64, + flap: Option, ) -> Result<(), CliError> { - let (name, points) = load_airfoil(file)?; + let (name, mut points) = load_airfoil(file)?; + if let Some(spec) = flap.as_ref() { + let parts: Vec = spec.split(':').filter_map(|s| s.parse().ok()).collect(); + if parts.len() == 3 { + points = rustfoil_core::flap::xfoil_flap(&points, parts[0], parts[1], parts[2]); + eprintln!("# applied XFOIL flap hinge_x={} hinge_y={} defl={} deg", parts[0], parts[1], parts[2]); + } else { + eprintln!("# WARN: bad --flap '{}' (need hinge_x:hinge_y:deflection); ignoring", spec); + } + } let body = Body::from_points(&name, &points)?; println!("Polar for: {}", name); @@ -1097,7 +1116,23 @@ fn run_faithful_viscous(cmd: FaithfulViscousCmd) -> Result<(), CliError> { } fn run_faithful_polar(cmd: FaithfulPolarCmd) -> Result<(), CliError> { - let (name, body) = build_body_for_faithful(&cmd.file, cmd.panels, false)?; + let mut src = cmd.file.clone(); + if let Some(spec) = cmd.flap.as_ref() { + let parts: Vec = spec.split(':').filter_map(|s| s.parse().ok()).collect(); + if parts.len() == 3 { + let (_n, pts) = load_airfoil(&cmd.file)?; + let flapped = rustfoil_core::flap::xfoil_flap(&pts, parts[0], parts[1], parts[2]); + let tmp = std::env::temp_dir().join("rustfoil_flapped.dat"); + let mut s = String::from("flapped\n"); + for p in &flapped { + s.push_str(&format!("{:.6} {:.6}\n", p.x, p.y)); + } + std::fs::write(&tmp, s).map_err(|e| CliError::Parse { line: 0, message: format!("write temp: {}", e) })?; + eprintln!("# applied XFOIL flap {}", spec); + src = tmp; + } + } + let (name, body) = build_body_for_faithful(&src, cmd.panels, false)?; let options = XfoilOptions { reynolds: cmd.re, mach: cmd.mach, From 62b263e760023802d19cf1cf3bdfefcedbc7a2a5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 14 Jun 2026 00:24:26 +0000 Subject: [PATCH 2/2] Fix CLI flap validation and temp race --- crates/rustfoil-cli/src/main.rs | 51 ++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/crates/rustfoil-cli/src/main.rs b/crates/rustfoil-cli/src/main.rs index fcdd3a09..078455ba 100644 --- a/crates/rustfoil-cli/src/main.rs +++ b/crates/rustfoil-cli/src/main.rs @@ -521,8 +521,7 @@ fn run_polar( ) -> Result<(), CliError> { let (name, mut points) = load_airfoil(file)?; if let Some(spec) = flap.as_ref() { - let parts: Vec = spec.split(':').filter_map(|s| s.parse().ok()).collect(); - if parts.len() == 3 { + if let Some(parts) = parse_flap_spec(spec) { points = rustfoil_core::flap::xfoil_flap(&points, parts[0], parts[1], parts[2]); eprintln!("# applied XFOIL flap hinge_x={} hinge_y={} defl={} deg", parts[0], parts[1], parts[2]); } else { @@ -560,6 +559,19 @@ fn run_polar( Ok(()) } +fn parse_flap_spec(spec: &str) -> Option<[f64; 3]> { + let fields: Vec<&str> = spec.split(':').collect(); + if fields.len() != 3 { + return None; + } + + Some([ + fields[0].parse().ok()?, + fields[1].parse().ok()?, + fields[2].parse().ok()?, + ]) +} + fn run_repanel(file: &PathBuf, n_panels: usize, output: Option) -> Result<(), CliError> { let (name, points) = load_airfoil(file)?; @@ -1042,14 +1054,23 @@ fn build_body_for_faithful( no_repanel: bool, ) -> Result<(String, Body), CliError> { let (name, points) = load_airfoil(file)?; + build_body_for_faithful_points(&name, points, panels, no_repanel) +} + +fn build_body_for_faithful_points( + name: &str, + points: Vec, + panels: usize, + no_repanel: bool, +) -> Result<(String, Body), CliError> { let final_points = if no_repanel { points } else { let spline = CubicSpline::from_points(&points)?; spline.resample_xfoil(panels, &PanelingParams::default()) }; - let body = Body::from_points(&name, &final_points)?; - Ok((name, body)) + let body = Body::from_points(name, &final_points)?; + Ok((name.to_string(), body)) } fn run_faithful_viscous(cmd: FaithfulViscousCmd) -> Result<(), CliError> { @@ -1116,23 +1137,19 @@ fn run_faithful_viscous(cmd: FaithfulViscousCmd) -> Result<(), CliError> { } fn run_faithful_polar(cmd: FaithfulPolarCmd) -> Result<(), CliError> { - let mut src = cmd.file.clone(); - if let Some(spec) = cmd.flap.as_ref() { - let parts: Vec = spec.split(':').filter_map(|s| s.parse().ok()).collect(); - if parts.len() == 3 { + let (name, body) = if let Some(spec) = cmd.flap.as_ref() { + if let Some(parts) = parse_flap_spec(spec) { let (_n, pts) = load_airfoil(&cmd.file)?; let flapped = rustfoil_core::flap::xfoil_flap(&pts, parts[0], parts[1], parts[2]); - let tmp = std::env::temp_dir().join("rustfoil_flapped.dat"); - let mut s = String::from("flapped\n"); - for p in &flapped { - s.push_str(&format!("{:.6} {:.6}\n", p.x, p.y)); - } - std::fs::write(&tmp, s).map_err(|e| CliError::Parse { line: 0, message: format!("write temp: {}", e) })?; eprintln!("# applied XFOIL flap {}", spec); - src = tmp; + build_body_for_faithful_points("flapped", flapped, cmd.panels, false)? + } else { + eprintln!("# WARN: bad --flap '{}' (need hinge_x:hinge_y:deflection); ignoring", spec); + build_body_for_faithful(&cmd.file, cmd.panels, false)? } - } - let (name, body) = build_body_for_faithful(&src, cmd.panels, false)?; + } else { + build_body_for_faithful(&cmd.file, cmd.panels, false)? + }; let options = XfoilOptions { reynolds: cmd.re, mach: cmd.mach,