From CAD mesh to aerodynamic model
A kite or wing usually starts life as a 3D .obj mesh from CAD, not as a set of clean 2D airfoils with polars. obj_to_yaml bridges that gap: it turns the mesh into the package's native YAML geometry route — per-section airfoil .dat files, polar CSVs, and a geometry.yaml that ties them together — so the rest of the solver never has to know the input came from CAD.
The conversion runs four stages per spanwise station:
.obj mesh
│ 1. slice perpendicular_sections
▼
raw 2D point cloud (ribs, spars, membrane — noisy)
│ 2. shrink-wrap shrink_wrap / ShrinkWrap
▼
clean closed airfoil contour (cosine-clustered panels)
│ 3. 2D aero solver
├── NeuralFoil: fit Kulfan CST → neural network (default, fast)
└── XFoil: coordinates → panel code (± repanel)
│ 4. write
▼
airfoils/*.dat + polars/*.csv + geometry.yaml → Wing(geometry.yaml)1. Slice
perpendicular_sections places n_sections stations at equal leading-edge arc-length intervals 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 raw 2D point cloud together with the station's leading- and trailing-edge positions in 3D. The cloud is messy: for a ram-air kite it contains the outer membrane plus every interior rib, spar and seam the cutting plane happened to cross.
2. Shrink-wrap
shrink_wrap, configured by ShrinkWrap, turns that noisy cloud into a single clean closed airfoil. It builds a distance field on a grid, thresholds it at a rolling-ball radius (bridging gaps between points and ignoring interior structure), flood-fills the outside, erodes the boundary back to a small clearance, and traces the resulting level set with marching squares. The contour is parameterised by arclength, so the leading edge comes out genuinely round and the blunt trailing edge is capped by an arc — and the output points are cosine-clustered toward both edges. The same wrapped airfoil is what both backends analyse, so the geometry the polar is generated for always matches the .dat written to disk.
This is the robustness win: an arbitrary, self-intersecting, structurally-detailed slice becomes one well-formed airfoil with no manual cleanup.
3. The 2D aerodynamic solver
obj_to_yaml's aero_solver argument selects the backend; both consume the same wrapped contour but along different paths.
NeuralFoil (NeuralFoilSolver, the default). The wrapped airfoil is fitted to Kulfan CST parameters with fit_kulfan_parameters (a LeastSquaresFit), and those parameters are fed through a small pre-trained neural network. NeuralFoil was trained on XFoil results over Kulfan-parametrised airfoils, so the CST fit is not just a convenience — it puts the airfoil into exactly the representation the network expects. It is fast, differentiable, and returns an analysis_confidence alongside the coefficients. This is the path used everywhere in the test suite and the recommended default for generating full polars.
XFoil (XFoilSolver). The wrapped coordinates go straight into the XFoil panel code — no Kulfan fit. Because the shrink-wrap already emits smooth cosine panels, XFoil needs no internal repaneling, and repanel=false is the default: on a clean airfoil XFoil's own curvature-attracted repaneling gives an identical result, and on a trailing-edge-deflected shape it would re-cluster the hinge crease into panels NeuralFoil never sees and drift away from it. Set repanel=true only if you deliberately want XFoil's paneling. XFoil is the viscous reference; it is slower and can fail to converge at some angles (those points come back as NaN).
The two backends agree closely with matched settings (transition, n_crit, Reynolds), which is what the test_backend_comparison.jl suite checks — the residual difference on a clean airfoil is NeuralFoil's own model accuracy, not a geometry artefact.
Trailing-edge deflection
Passing a delta_range sweeps trailing-edge deflection as a second polar axis. For each deflection angle deform_section pivots the trailing edge about a crease, re-shrink-wraps the deflected shape, and re-fits Kulfan parameters. Re-normalising means the chord is always measured leading-edge to trailing-edge, so a deflected section becomes a cambered, asymmetric airfoil rather than simply a rotated one. The result is written as a POLAR_MATRICES table (coefficients over alpha × delta); with no delta_range a plain POLAR_VECTORS table (coefficients over alpha) is written instead.
4. Write to YAML
For each unique airfoil id j, obj_to_yaml writes into output_dir:
airfoils/{j}.dat— the shrink-wrapped airfoil (matches the polar)airfoils/{j}_raw.dat— the raw sliced points the wrap enclosed (for inspection)airfoils/{j}_d{tag}.dat— each deflected shape, when adelta_rangeis given. The{tag}encodes the deflection in degrees (mfor a minus sign,pfor the decimal point), e.g._d5.datfor 5°,_dm3.datfor −3°,_d2p5.datfor 2.5°polars/{j}.csv— the generated polar (POLAR_VECTORSorPOLAR_MATRICES)geometry.yaml—wing_sections(leading/trailing-edge points) pluswing_airfoils(each section'stypeand the.dat/.csvpaths above)
A near-vanishing wingtip slice 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 precision by the single write_yaml writer, so generated geometry files stay diff-friendly and consistent.
Why this is useful
- CAD in, solver-ready model out. No hand-drawing airfoils or manually pairing them with polars — the mesh alone is enough.
- Robust to messy geometry. Ribs, spars and self-intersections in a slice are absorbed by the shrink-wrap instead of breaking the airfoil.
- One geometry, either backend. Fast NeuralFoil for full-envelope polars, or XFoil as a viscous cross-check, both from the identical wrapped contour.
- Generate once, reuse cheaply. The
.dat/.csv/geometry.yamlbundle is a plain-text artifact you check in and load withWing(geometry.yaml); the expensive slicing, wrapping and polar generation happen only when the geometry changes.