From 9af06901b1bff043e93343779d6eef3174431879 Mon Sep 17 00:00:00 2001 From: 1-Bort-1 <323661610+1-Bort-1@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:33:12 +0200 Subject: [PATCH] Spread sliced sections evenly over the span, not over leading-edge arc length station_indices now places its targets along the quarter-chord line with the chordwise component dropped, so a tip whose leading edge runs aft no longer gathers sections, and wingtip_distance is a spanwise inset. march_edges drops its arclen field, which only station_indices read. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 4 +++ docs/src/airfoil_pipeline.md | 6 ++-- docs/src/settings.md | 2 +- src/obj_adapter/obj_slice.jl | 47 ++++++++++++++-------------- src/obj_adapter/obj_to_yaml.jl | 9 +++--- src/settings.jl | 2 +- test/obj_adapter/test_obj_adapter.jl | 19 +++++++++++ 7 files changed, 56 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c00c754..de2e0921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - The Makie `plot!` methods for a `Panel` or a `BodyAerodynamics` return a `Vector{Makie.AbstractPlot}` instead of a `Vector{Any}`; for a `BodyAerodynamics` drawn as flat panels it is one flat list rather than a list per panel. +- `obj_to_yaml` and `perpendicular_sections` spread the sections evenly over the span, + measured along the quarter-chord line without its chordwise component, instead of + over leading-edge arc length, and `wingtip_distance` is that spanwise length. A tip + whose leading edge runs aft no longer gathers sections into its last centimetres. ### Fixed diff --git a/docs/src/airfoil_pipeline.md b/docs/src/airfoil_pipeline.md index 7d8b7a58..9aca652b 100644 --- a/docs/src/airfoil_pipeline.md +++ b/docs/src/airfoil_pipeline.md @@ -27,7 +27,7 @@ The conversion runs four stages per spanwise station: ## 1. Slice [`perpendicular_sections`](@ref VortexStepMethod.ObjAdapter.perpendicular_sections) -places `n_sections` stations at equal leading-edge arc-length intervals and cuts the +places `n_sections` stations evenly over the span and cuts the mesh *perpendicular to the local span*, rather than along a fixed global plane. On a curved kite tip a fixed-plane cut would smear the profile out and exaggerate the chord; a perpendicular cut keeps each airfoil undistorted. Each slice comes back as a @@ -113,8 +113,8 @@ For each unique airfoil id `j`, `obj_to_yaml` writes into `output_dir`: directory A tip that tapers to a point has no airfoil to slice, so the outermost stations stop at -the last slice that still has a chord; `wingtip_distance` moves them a further arc length -inboard when the slices just short of the tip are still too thin to analyse. A +the last slice that still has a chord; `wingtip_distance` moves them a further spanwise +length inboard when the slices just short of the tip are still too thin to analyse. A near-vanishing slice that does get through can shrink-wrap to an implausibly thick blob; such a degenerate section reuses its nearest valid neighbour's airfoil and polar while keeping its own edge positions, and a warning lists the reuse. All floats are rounded to millimetre diff --git a/docs/src/settings.md b/docs/src/settings.md index 3b2fba46..4eb611e4 100644 --- a/docs/src/settings.md +++ b/docs/src/settings.md @@ -53,7 +53,7 @@ wings: n_bins: 60 # leading-edge stations marched across the span # rows of the mesh-to-slicer rotation, whose x = chord, y = span, z = up rotation: [[0, 0, -1], [-1, 0, 0], [0, 1, 0]] - wingtip_distance: 0.0 # arc length the outermost sections stop short [m] + wingtip_distance: 0.0 # span the outermost sections stop short [m] clearance: 0.006 # shrink-wrap offset outside the cloud [chord fraction] min_concave_radius: 0.02 # shrink-wrap rolling-ball radius [chord fraction] diff --git a/src/obj_adapter/obj_slice.jl b/src/obj_adapter/obj_slice.jl index 729895f7..fd3f5b43 100644 --- a/src/obj_adapter/obj_slice.jl +++ b/src/obj_adapter/obj_slice.jl @@ -270,7 +270,7 @@ function slice_mesh_at_plane(vertices, faces, point, normal; tol=1e-6) end """ - march_edges(vertices, faces; step) -> (; le, te, point, tangent, arclen) + march_edges(vertices, faces; step) -> (; le, te, point, tangent) March the leading edge outward from mid-span in both directions in steps of arc length `step`. Each cut is a vertical spanwise plane (both the chordwise and vertical @@ -278,8 +278,8 @@ components of the running LE tangent dropped from the normal) so a tip that curl downward can't tilt the plane toward horizontal, where its min-chord "LE" pick would jump across the wing. Marching stops when the leading edge stops advancing spanwise. Cuts sample mesh *edges*, so the picks are robust to vertex density. Returns, ordered -along the span, the LE/TE points, each cut's plane origin and tangent, and the -cumulative LE arc length. Build the airfoil for a chosen station with `build_section`. +along the span, the LE/TE points and each cut's plane origin and tangent. Build the +airfoil for a chosen station with `build_section`. """ function march_edges(vertices, faces; step) ys = [v[2] for v in vertices] @@ -318,7 +318,7 @@ function march_edges(vertices, faces; step) probe_mid = prev_le .+ mid .* tangent here = cut(probe_mid, tangent) # Reject a near-degenerate tip slice whose min-chord "LE" has jumped - # chordwise (an artifact that would skew the leading-edge arc length). + # chordwise (an artifact that would misplace the tip station). valid = here !== nothing && abs(here.le[1] - prev_le[1]) < step && build_section(vertices, faces, here.le, here.te, @@ -340,12 +340,8 @@ function march_edges(vertices, faces; step) center = (; mid_cut.le, mid_cut.te, point=[0.0, y_mid, 0.0], tangent=[0.0, 1.0, 0.0]) rows = vcat(reverse(march(-1.0)), [center], march(1.0)) - arclen = zeros(length(rows)) - for i in 2:length(rows) - arclen[i] = arclen[i-1] + norm(rows[i].le - rows[i-1].le) - end return (; le=[r.le for r in rows], te=[r.te for r in rows], - point=[r.point for r in rows], tangent=[r.tangent for r in rows], arclen) + point=[r.point for r in rows], tangent=[r.tangent for r in rows]) end """ @@ -429,12 +425,12 @@ end Extract `n_sections` airfoil cross-sections following a curved or swept span. The leading edge is marched into `n_bins` stations (`march_edges`); the airfoil is -built (`build_section`) at the marched station nearest each equal -**leading-edge arc-length** target. Each section is +built (`build_section`) at the marched station nearest each of `n_sections` targets +spread evenly over the span ([`station_indices`](@ref)). Each section is `(; LE_point, TE_point, span_dir, contour3d, x_airfoil, y_airfoil)`. -Stations closed to a point at the tips are skipped ([`station_indices`](@ref)), and -`wingtip_distance` insets the outermost sections a further arc length. +Stations closed to a point at the tips are skipped, and `wingtip_distance` [m] +insets the outermost sections a further spanwise length. The slicer assumes `x` = chordwise, `y` = spanwise, `z` = up. Pass a `3×3` rotation matrix to reorient a mesh stored in another convention before slicing. @@ -455,19 +451,24 @@ end """ station_indices(march, n; wingtip_distance=0.0, min_chord_frac=0.01) -> Vector{Int} -Indices of the [`march_edges`](@ref) stations nearest `n` targets spread over the -leading-edge arc length. Stations whose chord has closed to less than -`min_chord_frac` of the longest one are left out of that range first, so a wing -tapering to a point puts its outermost sections on the last stations that still -have an airfoil to slice rather than on the point itself. The remaining first and -last targets sit a further `wingtip_distance` (arc length) inboard. +Indices of the [`march_edges`](@ref) stations nearest `n` targets spread evenly over +the spanwise length of the quarter-chord line: its arc length with the chordwise `x` +component dropped. Stations whose chord has closed to less than `min_chord_frac` of +the longest one are left out of that range first, so a wing tapering to a point puts +its outermost sections on the last stations that still have an airfoil to slice. The +remaining first and last targets sit a further `wingtip_distance` [m] inboard. """ function station_indices(march, n; wingtip_distance=0.0, min_chord_frac=0.01) chords = [norm(te .- le) for (le, te) in zip(march.le, march.te)] usable = findall(≥(min_chord_frac * maximum(chords)), chords) - arclen = march.arclen - inner, outer = arclen[first(usable)], arclen[last(usable)] + quarter_chord = [le .+ 0.25 .* (te .- le) for (le, te) in zip(march.le, march.te)] + span = zeros(length(quarter_chord)) + for i in 2:length(span) + step = quarter_chord[i] .- quarter_chord[i-1] + span[i] = span[i-1] + hypot(step[2], step[3]) + end + inner, outer = span[first(usable)], span[last(usable)] d = clamp(wingtip_distance, 0.0, (outer - inner) / 2) - n == 1 && return [argmin(abs.(arclen .- (inner + outer) / 2))] - return [argmin(abs.(arclen .- t)) for t in range(inner + d, outer - d, n)] + n == 1 && return [argmin(abs.(span .- (inner + outer) / 2))] + return [argmin(abs.(span .- t)) for t in range(inner + d, outer - d, n)] end diff --git a/src/obj_adapter/obj_to_yaml.jl b/src/obj_adapter/obj_to_yaml.jl index 10dc038f..72180949 100644 --- a/src/obj_adapter/obj_to_yaml.jl +++ b/src/obj_adapter/obj_to_yaml.jl @@ -42,14 +42,13 @@ end Convert a 3D wing `.obj` mesh to the native YAML geometry route. -Stations are placed at equal leading-edge arc-length intervals and sliced +Stations are spread evenly over the span ([`station_indices`](@ref)) and sliced perpendicular to the local span (see [`perpendicular_sections`](@ref)), which keeps the airfoil undistorted near curved tips; each shape is then shrink-wrapped into a clean airfoil and evaluated with `aero_solver`. The leading edge is marched -into `n_bins` stations. A tip that tapers to a -point carries no airfoil, so the outermost stations stop at the last slice that -still has a chord ([`station_indices`](@ref)); `wingtip_distance` moves them a -further arc length inboard. +into `n_bins` stations. A tip that tapers to a point carries no airfoil, so the +outermost stations stop at the last slice that still has a chord; `wingtip_distance` +moves them a further spanwise length inboard. `aero_solver` selects the 2D-airfoil backend: [`NeuralFoilSolver`](@ref) (default, fast) or [`XFoilSolver`](@ref) (viscous panel code); pass `aero_solver=XFoilSolver()` diff --git a/src/settings.jl b/src/settings.jl index a5aade8c..4c486b5a 100644 --- a/src/settings.jl +++ b/src/settings.jl @@ -38,7 +38,7 @@ slices as an unconfigured call. trace (default `60`). - `rotation`: Rows of the mesh-to-slicer rotation, which brings the mesh into the slicer's convention of x = chord, y = span, z = up (default the identity). -- `wingtip_distance`: Arc length [m] the outermost sections stop short of each tip +- `wingtip_distance`: Spanwise length [m] the outermost sections stop short of each tip (default `0.0`). - `clearance`: Shrink-wrap offset [chord fraction] the contour holds outside every cloud point, and the radius its convex corners are rounded at (default diff --git a/test/obj_adapter/test_obj_adapter.jl b/test/obj_adapter/test_obj_adapter.jl index e2f0ae91..317fe88b 100644 --- a/test/obj_adapter/test_obj_adapter.jl +++ b/test/obj_adapter/test_obj_adapter.jl @@ -28,6 +28,25 @@ obj_path = normpath(joinpath(@__DIR__, "..", "..", end end + @testset "station_indices spreads sections evenly in span past a raked tip" begin + # Straight LE over |y| ≤ 1, then tip caps whose LE runs 40 mm aft per 3 mm span. + cap = [(1.0 + 0.003k, 0.04k) for k in 1:20] + stations = vcat(reverse([(-y, x) for (y, x) in cap]), + [(y, 0.0) for y in -1.0:0.05:1.0], cap) + march = (; le=[[x, y, 0.0] for (y, x) in stations], + te=[[1.0, y, 0.0] for (y, _) in stations]) + half_span = 1.06 + + y = [march.le[i][2] for i in ObjAdapter.station_indices(march, 9)] + @test all(isapprox.(diff(y), 2half_span / 8; atol=0.05)) + + wingtip_distance = 0.3 + y = [march.le[i][2] + for i in ObjAdapter.station_indices(march, 9; wingtip_distance)] + @test y[1] ≈ -(half_span - wingtip_distance) atol=0.05 + @test y[end] ≈ half_span - wingtip_distance atol=0.05 + end + @testset "obj_to_yaml (alpha,delta) matrices -> loadable Wing (NeuralFoil)" begin @test isfile(yaml) @test isfile(joinpath(out, "polars", "1.csv"))