Summary
march_edges (src/obj_adapter/obj_slice.jl:284) stores with each station the tangent it entered that step with, so row i carries normalize(le[i-1] .- le[i-2]) — one station stale. build_section hands that tangent to airfoil_frame, which builds the cutting plane from it. Near a closing tip the leading edge turns aft and upward, so the plane swings back towards vertical while the tip itself curls down, and the cut grazes the surface instead of crossing it.
On data/ram_air_kite/ram_air_kite_body.obj with the default n_bins=60, station 79 is such a cut. t/c below is the sliced section's depth over its chord, straight out of build_section:
station | y_le | chord | t/c (recorded tangent) | t/c (central difference)
77 | 1.5807 | 0.7273 | 0.2194 | 0.2089
78 | 1.5944 | 0.7074 | 0.1946 | 0.2081
79 | 1.6254 | 0.5055 | 0.8040 | 0.1776
80 | 1.6440 | 0.1683 | 0.1738 | 0.1816
Station 79's recorded tangent is [0.811, 0.564, 0.154] — the only one in the tip region pointing up; its neighbours are [0.305, 0.25, -0.919] and [0.695, 0.337, -0.636]. In absolute terms the section it reports is 0.407 m deep where 78 and 80 give 0.138 m and 0.029 m.
What it costs
A section placed on station 79 fits to a blob. NeuralFoil then clamps its whole table:
alpha,delta,Cl,Cd,Cm
-1.0000,-1.0000,0.6374,1.0000,-0.1099
0.0000,-1.0000,0.6277,1.0000,-0.1074
and its deflected shapes come back empty — Skipping the delta=1.0° overlay ... for airfoils/4_d1.dat: no finite coordinates.
obj_to_yaml's degeneracy guard is built for exactly this and does catch it at a realistic section count: at n_sections=10 the blob fits to thickness=0.428 against a median of 0.122, so it trips 2 × median and the section reuses its neighbour's airfoil (Reused the nearest valid airfoil for: section 1 → airfoil 2, Cd back to 0.0094). At n_sections=4 it does not: two of the four sections are blobs, the median is 0.27 and the limit 0.54 clears 0.43. The guard is only as good as the fraction of sections that are sane.
What I would do
Give each station a tangent from its neighbours rather than from the step that reached it — a central difference over le, taken after the march, leaving the marching itself alone:
le = [r.le for r in rows]
tangent = [normalize(le[min(i + 1, end)] .- le[max(i - 1, 1)]) for i in eachindex(le)]
Measured with that in place, every station from 71 to 80 comes back between 0.177 and 0.21 — the outlier is gone and the rest move by at most 0.035.
I have not committed it anywhere. It moves every station's frame, so every mesh re-slices and every generated geometry shifts; that is a decision of its own rather than a line inside another change.
Where it came from
#350 places sections by spanwise position instead of leading-edge arc length. On this mesh with wingtip_distance=0.05 that puts the outermost section on station 79, where arc-length placement landed on station 80 and walked past the problem. The defect is older than that change and reachable without it — any n_sections/wingtip_distance whose target lands near station 79 hits it.
Related: #272 (the panel normal built from the leading-edge step rather than the quarter-chord step).
Summary
march_edges(src/obj_adapter/obj_slice.jl:284) stores with each station the tangent it entered that step with, so rowicarriesnormalize(le[i-1] .- le[i-2])— one station stale.build_sectionhands that tangent toairfoil_frame, which builds the cutting plane from it. Near a closing tip the leading edge turns aft and upward, so the plane swings back towards vertical while the tip itself curls down, and the cut grazes the surface instead of crossing it.On
data/ram_air_kite/ram_air_kite_body.objwith the defaultn_bins=60, station 79 is such a cut.t/cbelow is the sliced section's depth over its chord, straight out ofbuild_section:Station 79's recorded tangent is
[0.811, 0.564, 0.154]— the only one in the tip region pointing up; its neighbours are[0.305, 0.25, -0.919]and[0.695, 0.337, -0.636]. In absolute terms the section it reports is 0.407 m deep where 78 and 80 give 0.138 m and 0.029 m.What it costs
A section placed on station 79 fits to a blob. NeuralFoil then clamps its whole table:
and its deflected shapes come back empty —
Skipping the delta=1.0° overlay ... for airfoils/4_d1.dat: no finite coordinates.obj_to_yaml's degeneracy guard is built for exactly this and does catch it at a realistic section count: atn_sections=10the blob fits tothickness=0.428against a median of0.122, so it trips2 × medianand the section reuses its neighbour's airfoil (Reused the nearest valid airfoil for: section 1 → airfoil 2, Cd back to 0.0094). Atn_sections=4it does not: two of the four sections are blobs, the median is 0.27 and the limit 0.54 clears 0.43. The guard is only as good as the fraction of sections that are sane.What I would do
Give each station a tangent from its neighbours rather than from the step that reached it — a central difference over
le, taken after the march, leaving the marching itself alone:Measured with that in place, every station from 71 to 80 comes back between 0.177 and 0.21 — the outlier is gone and the rest move by at most 0.035.
I have not committed it anywhere. It moves every station's frame, so every mesh re-slices and every generated geometry shifts; that is a decision of its own rather than a line inside another change.
Where it came from
#350 places sections by spanwise position instead of leading-edge arc length. On this mesh with
wingtip_distance=0.05that puts the outermost section on station 79, where arc-length placement landed on station 80 and walked past the problem. The defect is older than that change and reachable without it — anyn_sections/wingtip_distancewhose target lands near station 79 hits it.Related: #272 (the panel normal built from the leading-edge step rather than the quarter-chord step).