-
Notifications
You must be signed in to change notification settings - Fork 0
Development
Deutsch: Entwicklung
| Property | Value |
|---|---|
| Language | JavaScript modules, no framework. Build target ES2022 (ECMAScript 2022); ESLint parses ECMAScript 2024 (ecmaVersion: 2024). |
| Build tool | Vite ^8.3.1. Relative base ./, source maps. build.chunkSizeWarningLimit: 900: Vite warns when a chunk exceeds 900 kB (Vite unit: 1 kB = 1000 bytes). Output: one classic script in IIFE format (immediately invoked function expression) with the styles inside, loaded with defer and without crossorigin (plugin classicScript in vite.config.js), because Chromium refuses module scripts and CORS-mode (Cross-Origin Resource Sharing) stylesheets from file URLs; build.modulePreload: false. dist/index.html therefore runs opened from a file. |
| Runtime dependencies | three.js ^0.186.1: 3D view, imported only in src/ui/viewer3d.js. fflate ^0.8.3: zip container of 3D Manufacturing Format (3MF) files, imported only in src/export/threemf.js. The build bundles both at the versions in package-lock.json (npm ci). Plugin licenses in vite.config.js writes dist/LICENSES.txt: LICENSE of this app, then the license file of every npm package with code in the bundle (found from the bundled modules, not from a list); a bundled package without a license file stops the build. |
| Node.js | 24 or later (.nvmrc: 24; engines.node: >=24) |
| Unit tests | Vitest ^5.0.2 in Node.js; timeout 20000 ms per test (test.testTimeout in vite.config.js); coverage with @vitest/coverage-v8
|
| Browser tests | Playwright ^1.63.0
|
| App version |
package.json version, compiled in as __APP_VERSION__ by vite.config.js. Shown in the Help dialog. Written to generator.version in the project file (JavaScript Object Notation, JSON). |
Code in src/geom/, src/airfoil/, src/export/, src/import/ and src/model/ does not use the Document Object Model (DOM) application programming interface (API).
Unit tests and scripts import it in Node.js.
The bundled airfoil library needs no network request: plugin airfoilLibrary in vite.config.js compiles public/airfoils/index.json and its files into the module virtual:airfoil-library, read by src/airfoil/bundled.js. Vite also copies public/airfoils/ to dist/airfoils/.
| Path | Content |
|---|---|
index.html |
Page shell; loads src/main.js
|
src/main.js |
Entry: store, rebuild scheduling, top bar (Open chooses the reader for a project file or an XFLR5 file), status bar, Checks tab, Help dialog, autosave |
src/geom/, src/airfoil/, src/export/, src/import/, src/model/, src/ui/
|
See Modules |
public/airfoils/ |
6 coordinate files (.dat, Selig format), index.json with 6 entries, NOTICE.md (source, legal basis, conditions and attribution per file). An entry needs a free license and a line in NOTICE.md (see Airfoil library check). The 17 National Advisory Committee for Aeronautics (NACA) presets are computed at run time, not stored. |
scripts/ |
See Scripts |
test/ |
Vitest unit tests (*.test.js), helpers.js (NACA sample project), step-cases.js (validation cases for Standard for the Exchange of Product model data (STEP) files and 3MF files), xflr5-writer.js (writer of XFLR5 project files), fixtures/xflr5/ (XFLR5 test files, see XFLR5 test files and tests) |
e2e/ |
Playwright end-to-end (E2E) browser tests (*.spec.js), helpers.js
|
docs/Flow5upgrade.md |
Plan and owner decisions for mitred section planes, a rigid tilt of the whole part and the flow5 import (English only) |
docs/Handover.md |
Where the work stands, next steps, working agreements with the owner, what the repository does not hold (English only) |
docs/wiki/ |
Wiki pages in English and German, _Sidebar.md, images/ (English screenshots), images/de/ (German screenshots) |
.github/workflows/ |
ci.yml, docs.yml, release.yml
|
Generated and not committed (.gitignore): dist/, coverage/, step-check/, foam-check/, test-results/, playwright-report/.
| File | Content |
|---|---|
src/geom/nurbs.js |
Non-uniform rational B-spline (NURBS) primitives |
src/geom/linalg.js |
Band lower-upper (LU) factorization without pivoting (B-spline interpolation, used by src/geom/nurbs.js); dense LU factorization with partial pivoting (unit tests only) |
src/geom/profile.js |
Airfoil as NURBS curve; chordwise resampling; fitProfile (curve fit and shape check of an airfoil, used by the airfoil preview and the XFLR5 import) |
src/geom/spanwise.js |
Spanwise blending of section values (linear, smooth) |
src/geom/guide.js |
Guide curves (nose line, end line) |
src/geom/planes.js |
Section planes: roll and stretch of each section (sectionPlanes), stretch limit, extent of a placed airfoil (upExtent) and fold test of two neighbouring planes (planeFold, firstFold) |
src/geom/wing.js |
Wing loft: buildWing; mitredPlaneProblem (stretch and fold checks without the loft, for the XFLR5 import); profile cache |
src/geom/mesh.js |
Tessellation, mirror, mesh volume, area and edge check |
src/geom/triangulate.js |
End-cap triangulation: strip of upper and lower point pairs (linear time); ear clipping as fallback |
src/geom/stats.js |
Planform statistics; for a tilted or rolled part the MAC position, 25 % MAC and span in the plane axes |
src/geom/part.js |
Rigid placement of the part (Part tilt, Part roll): partPivot (stored pivot, else the leading edge of the root section), partTransform (roll about x, then tilt about y, through the pivot: point, vector, matrix), transformPositions (mesh vertices) |
src/geom/foam.js |
Foam cores: proposed cuts (proposeCuts), cut list rules (normalizeCuts), segments with end profiles, wedges and deviation (foamSegments), splitting above a deviation limit (splitOverTolerance); limits FOAM_LIMITS
|
src/geom/sampling.js |
Span samples for drawing, shared by the 3D view and the planform: refine, thinParams, edgeParams, MAX_EDGE_SAMPLES (20,000) |
src/airfoil/parse.js |
Airfoil file parser; cleanPoints cleans a point list from another format (the airfoils of an XFLR5 project) by the rules of a parsed file |
src/airfoil/geometry.js |
Polyline geometry |
src/airfoil/sanity.js |
Sanity checks, importAirfoilText
|
src/airfoil/naca.js |
NACA 4- and 5-digit generator; leadingNacaCode finds the code that starts a name (NACA0014_Flap) |
src/airfoil/library.js |
17 NACA presets, bundled library, external sources; librarySource (source record of a bundled entry) |
src/export/step.js |
STEP writer; file format International Organization for Standardization (ISO) 10303-21 |
src/export/axes.js |
Up axis of the exported files (the Fusion 360 fix of the export dialog): UP_AXES, upAxisMap (identity for Z up, (x, z, −y) for Y up), meshToUpAxis
|
src/export/stl.js |
Binary stereolithography (STL) writer |
src/export/threemf.js |
3MF writer |
src/export/foam.js |
Files of the foam-cutting wizard: .dat profiles, segments.csv, profile ZIP (profileZip), template blocks and sheet (templateBlocks, templateLayout), kerf offset (offsetPolygon), SVG (Scalable Vector Graphics), DXF (Drawing Exchange Format, AutoCAD R12) and PDF (Portable Document Format) writers (layoutSvg, layoutDxf, layoutPdf), PDF paging (pagePlan) |
src/export/precision.js |
32-bit coordinate check of STL and 3MF exports: checkPrecision, MeshPrecisionError
|
src/import/errors.js |
XflrError: file-level error of the XFLR5 and flow5 readers, with a code and, for an .xfl or .fl5 project, the byte offset
|
src/import/xfl.js |
Reader of XFLR5 projects (.xfl): readXfl (windows of 4,194,304 bytes), readXflBytes (bytes in memory), startsLikeXfl, sniffXflr5; the windowed big-endian Reader that the .fl5 reader shares |
src/import/fl5.js |
Reader of flow5 projects (.fl5, formats 500750 and 500754): readFl5, readFl5Bytes; readProjectFile (an .xfl or .fl5 project by its first number) |
src/import/xmlscan.js |
XML tokenizer of both XML readers: Scanner, children, readText, toNumber
|
src/import/xflxml.js |
Reader of XFLR5 plane and wing files in Extensible Markup Language (XML): readXflr5Xml
|
src/import/fl5xml.js |
Reader of flow5 plane and wing XML files: readFlow5Xml; readPlaneXml (an XFLR5 or flow5 XML file by its root element) |
src/import/xflr5.js |
Mapping of one XFLR5 or flow5 wing to a project: surfaces of a plane (planeSurfaces: every wing of the plane; the XFLR5 fin built upright by finAsWing), sections (mapSections), airfoil table, report and project (mapXflr5), airfoil checks in steps (checkSteps), airfoil uploads (readAirfoilUpload) |
src/model/project.js |
Project model, defaults, limits, validation |
src/model/budget.js |
Warning thresholds, loft grid, time and memory estimates |
src/model/io.js |
Project JSON import and export; upgradeFoldedTilt (a folded tilt of a version 2 file becomes Part tilt) |
src/model/edit.js |
Edit operations |
src/model/wizard.js |
Wizard presets, parameter ranges (RANGES) and panel ranges (PANEL_RANGES, at most MAX_PANELS = 24 panels); wizardProject builds the sections of the straight, elliptic and panel planforms and the elliptic tip (ELLIPTIC_TIP_SECTIONS = 6); panelsFromParams turns the straight planform into 1 panel |
src/model/winglet.js |
Integral winglet: wingletSections (blend arc in steps of at most WINGLET_STEP = 15°, then the straight part), wingletProblems, addWinglet, defaultWinglet, ranges WINGLET_RANGES
|
src/model/defaults.js |
Project on first load |
src/ui/store.js |
Store with undo and redo |
src/ui/viewer3d.js |
3D view |
src/ui/panzoom.js |
2D canvas with pan and zoom |
src/ui/sections.js |
Sections tab |
src/ui/planform.js |
Planform tab |
src/ui/airfoils.js |
Airfoils tab, upload preview; readAirfoilFile (reads an uploaded file and refuses XFLR5 files) |
src/ui/settings.js |
Settings tab |
src/ui/wizard.js |
Wizard dialog with the panel table; drawPlanform (also draws the planform in the import dialog) |
src/ui/xflr5.js |
Import dialog for XFLR5 files: openXflr5Dialog
|
src/ui/exportui.js |
Export dialog |
src/ui/foam.js |
Foam-cutting wizard: foamDialog; stored settings (storedFoamSettings, key wingdesigner.foam) |
src/ui/winglet.js |
Winglet dialog of the Sections tab: wingletDialog
|
src/ui/dom.js |
DOM helpers |
src/ui/styles.css |
Styles |
src/i18n/index.js |
Language (language, setLanguage, initialLanguage), tr() and the number formats fixed, count, whole, plain
|
src/i18n/de/*.js |
German texts, one file per area: shell, panels, editors, model, geom, airfoil, xfl, xflxml, xflr5, foam, winglet, flow5; index.js merges them |
| Script | Content |
|---|---|
scripts/coverage-readme.mjs |
Coverage table in both READMEs |
scripts/check-airfoils.mjs |
Airfoil library check |
scripts/export-step-cases.mjs |
STEP and 3MF case export |
scripts/validate_step.py |
STEP validation (OpenCascade) |
scripts/validate_3mf.py |
3MF validation (lib3mf) |
scripts/export-foam-cases.mjs |
Foam-cutting file export of the test wings |
scripts/validate_foam.py |
Foam-cutting file validation (ezdxf, pypdf) |
scripts/screenshots.mjs |
Wiki screenshots, English and German |
scripts/check-docs.mjs |
Documentation check |
scripts/check-test-counts.mjs |
Test count check |
scripts/check-i18n.mjs |
Translation check |
- A tab (class in
src/ui/) callsstore.update(mutate, { key, session }). The store pushes the previous project onto the undo stack and clears the redo stack. A mutation that leaves the serialized project unchanged pushes nothing, keeps the redo stack and notifies nobody. The undo stack holds at most 100 steps. Undo and redo stack together hold at most 64,000,000 characters of serialized project (JSON). Above that, the oldest undo steps are dropped first, then the redo steps farthest from the current project. The newest step of each stack stays. Consecutive updates with the same key, each less than 800 ms after the one before, form one undo step. Withsession: true(planform drags) updates with the same key form one step however long the pauses, until the drag ends on pointer release or cancel (lastKeyreset); undo and redo also reset it, so a drag in progress continues as a new step. - The store notifies its subscribers.
main.jsschedules at most 1 rebuild in the next animation frame; further notifications before that frame share it. - The rebuild runs
buildWing(project). The build stays in the frame of the part;build.partholds the rigid placement (src/geom/part.js), which the 3D view, the mesh and STEP exports and the statistics apply. It updates the 3D view, Sections, Planform, Checks and the status bar. Airfoils and Settings re-render on every change except a selection change. - After a change other than a selection change, the rebuild saves the project to browser local storage when
validateProjectaccepts it. A pending save is written at once onpagehideand when the page becomes hidden (visibilitychange), e.g. on a reload before the next animation frame. Nothing is saved while the first-run wizard is open; a reload then opens the wizard again. Skip (open sample wing) saves the sample wing; Create design saves the created project.
| Local storage key | Content |
|---|---|
wingdesigner.project.v1 |
Last valid project (JSON) |
wingdesigner.project.v1.rejected |
Copy of a saved project that failed to load. The app then starts as on a first visit, with the wizard. |
wingdesigner.project.v1.stale |
Time of the first failed autosave; removed at the next start and when an autosave succeeds |
wingdesigner.tab |
Active tab |
wingdesigner.language |
Chosen language, en or de (section Translations) |
wingdesigner.foam |
Settings of the foam-cutting wizard: coreLength, tolerance, kerf (mm), paper (a4, a3, letter); each value outside its range reads as its default |
The import reads an XFLR5 or flow5 file and builds a project from one wing of one plane in it. XFLR5 is a program for the analysis of airfoils and wings; 6.62 is its last release. flow5 is its successor (version 7), under GPL-3.0; the flow5 readers are written from a description of the formats, not from flow5's code. The readers and the mapping in src/import/ use no DOM API and run in Node.js. The dialog (src/ui/xflr5.js) and Open (src/main.js) use the DOM. The rules for the files and the formulas of the mapping are in File Formats, section XFLR5 import; the dialog is described in the User Guide, section Import from XFLR5 and flow5.
- The change handler of the Open file input in
src/main.jschooses the reader. Extension.xfl,.wpaor.fl5:readProjectFile, which reads the first number and calls the.fl5reader (500000 to 509999) or the.xflreader..xml:readPlaneXml, which calls the XFLR5 reader, and the flow5 reader for a flow5 root element. Any other extension except.json, or none: first the first 4 bytes (sniffXflr5, and a UTF-16 byte order mark for the XML readers), then text that starts with<?xml,<!,<explane,<xflplaneor<xflwing(XML readers); otherwise project JSON.importXflr5runs the reader and opens the dialog. - A reader throws
XflrErrorfor a file that it cannot import.codeisnot-xflr5,flow5(from the XFLR5 readers, which hand flow5 files on),flow5-old,flow5-unknown,flow5-new,wpa,damaged,not-plane-xml,no-plane,finortoo-large;offsetis the byte of the damage in an.xflor.fl5project, elsenull(XML files, errors without a byte position). Open showsCannot open <file>: <message>and leaves the design and the undo history as they are. Any other exception of the import shows asCannot open <file>: Internal error: <message>. -
readXfl(file)andreadXflr5Xml(text)return the same object,XflrFile:kind(xflorxml),format,lengthUnit(millimetres per length unit of the file; 1000 for.xfl),unitName,wingOnly,planes,foils,foilErrorandwarnings. A plane has anameandwings, the 4 XFLR5 wing slots main wing, second wing, elevator (horizontal stabilizer) and fin; a slot that the plane lacks isnull.foils(.xflonly) maps an airfoil name to its base coordinates and flap settings. Wing sections keep the values of the file: the length unit of the file and degrees. -
readXflreads a project through windows of 4,194,304 bytes (WINDOW_SIZE,Blob.slice). The record readers are generator functions. One yields when its next read lies outside the window;readXflloads the window that starts there and resumes it. A skip only moves the offset. The analyses and the analysis results, which make up most of a project of 96.7 MB, are skipped by their counts. Reading ends after the airfoils.readXflBytesreads a file held in memory as one window. A file damaged after the planes gives its planes without airfoils (foilErrorand a warning). -
readXflr5Xmlis a pull tokenizer without a tree (src/import/xmlscan.js), linear in the length of the text. A missing or garbled number stays NaN (not a number) and gives a warning; XFLR5 reads such text as 0.- flow5 files:
readFl5andreadFlow5Xmlreturn the same shape withprogram: 'flow5'(kindfl5orxml). A plane haskind(wingsormesh),bodiesandwings, a list in file order; a wing hastype(main,elevator,fin,other),twoSided,position,tilt(Ry_angle) androll(Rx_angle).readFl5walks every record before the planes by its counts, through the windows of the.xflreader, and stops after the last plane.programOf(file)givesXFLR5orflow5for the messages, which carry a{program}placeholder.
- flow5 files:
-
mapXflr5(file, options)(src/import/xflr5.js) is a pure function.optionsholdsplane,surface(mainorstab),fileName,name,project(the current project),library(bundled library entries),uploads(read.datfiles) andchoices(the option key picked per XFLR5 airfoil name:file:<name>,upload:<i>,project:<id>,library:<id>ornaca:<code>). The result holdsrows(the airfoil table),options(the entries of the airfoil lists),report(lines with the severityerror,warningorinfo),errors,project(nullwhile the report holds an error) andsummary. The steps:mapSections(y and z from the developed span and the dihedral, clean-up of sections at one y and of chords below 1 mm, the position; the tilt angle is returned, and the project stores it assettings.partTiltwithpartRoll0 and the wing origin aspartPivot); one airfoil per name (file, uploads, current project, library, NACA generator, similar name); the airfoil frame (placeAirfoil; a NACA airfoil of the current project whose points are the generated or checked section of its code gets the frame of that generated section,checkProjectAirfoil);createProjectandvalidateProject. Check results are cached per airfoil object and language (WeakMap), because the dialog maps again after every choice. -
openXflr5Dialog(file, { fileName, project, library })callsmapXflr5after every choice and draws the result. The first mapping followscheckSteps, a generator with one step per airfoil name. The dialog runs it in slices of 50 ms (SLICE_MS), so that clicks, Escape and scrolling work meanwhile. A change of plane or surface runscheckStepsof the new wing at once for up to 50 ms; when it has not ended by then, the dialog turns Import off, empties the airfoil table, the preview, the stats line and the untyped project name of the previous wing, and runs the rest in slices, as for the first mapping. The candidate project is built withbuildWingfor the preview; a project above a size warning is not built, and the preview draws straight panels. The errors and warnings of the build become report lines (buildNotes) that do not block Import. The promise resolves with{ project, summary, warnings }, or withnullafter Cancel. -
main.jscallsreplaceProject(store.replace, one undo step), opens the Sections tab and shows the summary and the first warning as a message.
| Constant | Value | Use |
|---|---|---|
MIN_PANEL (src/import/xflr5.js) |
0.1 mm | A panel shorter than this counts as sections at one y_position; XFLR5 skips such panels |
NUDGE |
0.5 mm | Largest move of a section that shares its y_position with the next one; also at most ¼ of the panel |
DIHEDRAL_WARN |
10° | Dihedral above which the report warns about the thinner vertical sections |
FRAME_TOLERANCE |
0.001 chord | Offset of x or y of the leading edge from 0, or of the chord from 1, up to which the report does not name the move of the sections; every frame above 1e-9 of the chord (FRAME_ROUND_OFF, round-off of the curve fit) applies |
FRAME_WARN |
0.02 chord | Offset above which the move of the sections is a warning, not an info line |
FRAME_LIMIT |
x or y of the leading edge 0.1 chord, chord 0.5 to 2 | Beyond it the coordinates are not in chord units |
MAX_XFL_BYTES (src/import/xfl.js) |
2,000,000,000 bytes | Largest .xfl project |
MAX_PLANES |
10,000 | Planes per file (both readers) |
MAX_TOTAL_SECTIONS |
1,000,000 | Wing sections of all planes of an .xfl project |
MAX_FOILS |
LIMITS.maxAirfoils (10,000) |
Airfoils read from an .xfl project |
MAX_FOIL_POINTS |
2,000,000 | Airfoil points read from an .xfl project; airfoils beyond it are skipped and reported |
MAX_XFLR5_FOIL_POINTS |
1,000 | Points of one airfoil; XFLR5 holds at most 604 |
MAX_ELEMENTS, MAX_DEPTH, MAX_ATTRIBUTES (src/import/xmlscan.js) |
1,000,000; 100; 100 | XML elements per file, nesting depth, attributes per element |
FL5_FORMATS (src/import/fl5.js) |
500750, 500754 |
.fl5 project formats read |
MAX_FLOW5_FOIL_POINTS |
10,000 | Points of one .fl5 airfoil |
MAX_FLOW5_WINGS (src/import/fl5xml.js) |
100 | Wings kept per plane of a flow5 XML file or .fl5 project; further wings are read past and dropped with the warning Plane "<name>" has more than 100 wings; the first 100 are read.
|
Sections per wing: LIMITS.maxSections (20,000). XML files: MAX_PROJECT_BYTES (100 MB).
| Condition | Value |
|---|---|
| Machine | Node.js 24.21; Intel Xeon x86-64 central processing unit (CPU), 2.10 GHz, 4 cores; load average 3.3 (other processes running) |
| Date | 2026-09-29 |
| Input |
wizardProject(PRESETS.<preset>.params) with settings.chordSamples set to the station count |
| Functions |
buildWing(project), exportMeshes(build, 'merged'), wingToStep(build, { mirror: true })
|
| Cache |
buildWing caches the profile stage (sanity checks, NURBS curve, crossing and x-reversal tests, resampling) per airfoil point list, parametrization and station count. The cache keeps the most recently used entries: at least 32, and at least 1 per airfoil the sections use (src/geom/wing.js). |
| Build | Every profile is in the cache (filled by the warm-up runs) |
| Run | 1 call of the function |
| Repetition | Mean of 10 runs after 2 warm-up runs; STEP write: mean of 3 runs |
| Unit | KB = 1024 bytes |
| Case | Chordwise stations per surface |
buildWing, profiles cached |
Mesh (merged) | STEP write | STEP size |
|---|---|---|---|---|---|
| Sport preset, 2 sections | 60 | 17 ms | 1 ms | 3 ms | 99 KB |
| Sport preset, 2 sections | 200 | 40 ms | 1 ms | 4 ms | 303 KB |
| Glider preset, elliptic guide curves, 17 spanwise stations | 60 | 36 ms | 12 ms | 6 ms | 445 KB |
| Glider preset, elliptic guide curves, 17 spanwise stations | 200 | 89 ms | 42 ms | 19 ms | 1398 KB |
Large projects in the browser:
| Condition | Value |
|---|---|
| Machine | Chromium 141, headless, software rendering; 4 shared cores of a 2.1 GHz Intel Xeon CPU; load average 2 to 8 (other processes running) |
| Change | 1 chord edit in the Sections table |
| Values | JavaScript time of the change; JavaScript heap after the rebuild |
Linear and Smooth stand for the options Linear between sections and Smooth (shape-preserving cubic through sections) of the list Spanwise interpolation.
| Case | JavaScript time per change | Heap |
|---|---|---|
| 200 sections, Smooth (399 spanwise stations) | 0.5 to 0.65 s | 38 to 48 MB |
| 1,000 sections, Linear | 1.5 to 1.6 s | 78 to 91 MB |
| 2,000 sections, Smooth | 2.9 to 3.8 s | 159 to 175 MB |
| 5,000 sections, Linear | 6.9 to 7.4 s | 345 to 361 MB |
| 5,000 sections, Smooth | 6.2 to 8.3 s | 237 to 413 MB |
| 10,000 sections, Linear | 15.8 to 16.4 s | 490 MB |
| 15,000 sections, Linear | 19 to 23 s | 678 MB |
| 20,000 sections, Linear | 24 s | 969 MB |
- 20,000 sections, Linear: 57 s page time per change with software rendering. Opening the project: 24 s JavaScript time, 59 s page time, 496 MB heap afterwards.
- Selecting a section: 2 to 100 ms JavaScript time at 200 to 20,000 sections.
- Airfoils of 20,001 points each: 100 airfoils open in 17 s, a change takes 2.3 s, heap 546 MB; 200 airfoils open in 35 s, a change takes 5.8 to 7.9 s, heap 1.1 GB.
- Mesh export, 16 sections × 40 stations, 200 Chordwise stations per surface: STL with 8.5 million triangles in 6.9 s, 423 MB file, 2.7 GB browser memory at the peak. STL with 20 million triangles fails; 80 million triangles crash the tab. 3MF with 8.5 million triangles in 49 s, 97 MB file.
- STEP export: 81 MB in 2.2 s (1,041 stations), 330 MB in 8.4 s (4,161 stations).
- Node.js 24, 5,000 sections, Linear:
buildWing5.1 s, wing statistics 20 ms, 363 MB heap kept. - Node.js 24, guide curve of 50,000 points: 5.1 to 6.1 s per build. The Planform tab opens in 6 s with 10,000 guide points and in 36 s at 3.4 GB with 50,000.
Build time without cached profiles: not measured in this run. Build time and memory use on phones: not measured.
Sizes above a warning threshold work as usual. The build then adds 1 warning to the Checks tab: Large project: <sizes>. Each change takes <time> and <memory> of browser memory. Each size reads <value> (warning above <threshold>), for example 250 sections (warning above 200). <time> reads under 1 s or about X s; <memory> reads about X MB or about X GB. When the estimated first build takes at least 1 s longer than a change (firstBuildSeconds), the warning ends with Opening it or changing the profile parametrization takes <time>. The first build runs after Open, for the restored autosave and after a change of Profile parametrization; it checks and fits every airfoil that a section uses again. The status bar counts the warning. When a size rises above its threshold (an edit, Open, the restored autosave), a short message shows the same text. Beyond a hard limit a desktop browser tab runs out of memory or a change takes about 1 minute; the app refuses such projects and changes.
| Size | Warning above (WARN in src/model/budget.js) |
Hard limit (LIMITS in src/model/project.js) |
|---|---|---|
| Sections | 200 | 20,000 |
| Airfoils | 200 | 10,000 |
| Points in one airfoil | 5,000 | 100,000 |
| Airfoil points in all | 100,000 | 1,000,000 |
| Points of a guide curve | 500 (enabled guide curves) | 20,000 |
| Loft grid points | 60,000 | 5,000,000 |
| Export triangles (STL, 3MF) | 2,000,000 | 10,000,000 |
| Export control points (STEP) | 1,000,000 | 3,000,000 |
| Characters in a name (project, airfoils) | 200 | 10,000 |
- Where the warnings appear: points in one airfoil in the airfoil preview (
many-points); export triangles and STEP control points in the export dialog, which disables Download above 10,000,000 triangles or 3,000,000 control points; all other sizes in theLarge projectwarning. The Settings tab showsLoft grid: N points.under the resolution fields, with the time and memory above 60,000 points. - Further hard limits: ids 200 characters; airfoil source texts 2,000 characters; airfoil input 5,000,000 characters (
MAX_INPUTinsrc/airfoil/parse.js); airfoil files above 20 MB are not read; the parser stops after 100,001 coordinate lines (MAX_POINTS); Open rejects project files above 100 MB unread (MAX_PROJECT_BYTESinsrc/model/io.js). - Loft grid: spanwise stations × (2N + 1) profile points before added stations, N = Chordwise stations per surface (
loftGridinsrc/model/budget.js). Up to 5,000,000 grid points the build uses the settings as entered. Above, it uses fewer stations per panel and warnsSpanwise stations per panel reduced from K to k: S sections with N chord samples keep the loft within 5,000,000 grid points.When 1 station per panel still exceeds the limit, the build stops withThe loft grid needs P points with one station per panel (S sections, N chord samples); the limit is 5,000,000. Reduce the chord samples or the sections.
Estimate model: linear fits to the browser measurements above, JavaScript time without drawing the 3D view. The 3D view adds the drawing time of the graphics card. Phones: not measured. The coefficients are COST (each change) and EXPORT (export) in src/model/budget.js. Units: 1 MB = 1000 KB = 1,000,000 bytes.
Unit (COST) |
Time | Memory |
|---|---|---|
| Base of each change | 0.2 s | 15 MB |
| Loft grid point | 11.5 µs | 0.65 KB |
Airfoil list entry in the Sections table: sections × airfoils up to 20,000 entries (LAZY_OPTIONS), above that 1 per section |
8.5 µs | 0.5 KB |
| Airfoil point | 1.5 µs | 0.2 KB |
| Point of an enabled guide curve | 110 µs | 50 KB |
Airfoil point, checks on import and first build of a wing that uses the airfoil (airfoilFirstUse: many-points warning and first build) |
30 µs | not used |
Airfoil, first build whatever its points: curve sampling and crossing tests (airfoilFirstBuild: first build only) |
4.4 ms | not used |
Export, per triangle (EXPORT) |
Time | Memory | File |
|---|---|---|---|
| STL | 0.8 µs | 210 bytes | 50 bytes |
| 3MF | 5.8 µs | 110 bytes | 11.5 bytes |
| STEP, per control point | 2.5 µs | 620 bytes | 98 bytes |
First build (firstBuildSeconds): the time of a change plus 4.4 ms per airfoil that a section uses plus 30 µs per point of these airfoils. Basis: 7.4 ms per airfoil of 99 points in Node.js 24 at 1,000 and 2,000 airfoils, of it 3 ms by the point term; not measured in the browser. Example: 20,000 sections (Linear, 16 Chordwise stations per surface: 660,000 loft grid points) and 10,000 airfoils of 99 points: Each change takes about 9.5 s and about 650 MB of browser memory. Opening it or changing the profile parametrization takes about 83 s.
The export memory adds the base of 15 MB. Tests: test/budget.test.js (thresholds, estimates, loft grid as the build computes it, shortened names), e2e/limits.spec.js (warning above 200 sections and the title of the + button, airfoil lists of large sections tables, export dialog note and the hard limit).
The app speaks English (en) or German (de). The code is in src/i18n/index.js. English is the default, also in Node.js and in the unit tests.
At the start of the browser app, initialLanguage() picks the language:
- A stored choice, when it names a language.
- Otherwise German, when the first language of the browser is
deor begins withde-. - Otherwise English.
The list Language / Sprache changes the language. It is the first group of the Settings tab and holds English and Deutsch. The choice is stored in localStorage under wingdesigner.language. Without browser storage, the choice lasts until the page closes. The label Language / Sprache and the two option names read the same in both languages and are no catalog entries.
To add a text:
- Write the English text where it is shown:
tr('Created "{name}".', { name }). The first argument is a string literal. A text is a whole sentence: no English fragments glued together. - Add the entry with the same key and the same placeholders to the file of its area:
'Created "{name}".': 'Entwurf „{name}“ angelegt.'. A key without a German entry shows the English text. - Run
npm run i18n:check. It names the file and line of a key without a German entry. - A text of the shell that is made once (top bar, tabs, view buttons, status bar, meta description) is registered in
shellLabels, usually throughlocalized()insrc/main.js.labelShell()sets it again after a change of language. The panels take their texts fromtr()when they are drawn or labelled again.
File in src/i18n/de/
|
Texts of |
|---|---|
shell.js |
src/main.js, src/ui/wizard.js, src/ui/settings.js
|
panels.js |
src/ui/airfoils.js, src/ui/exportui.js, src/ui/viewer3d.js, library data (see below) |
editors.js |
src/ui/sections.js, src/ui/planform.js
|
model.js |
src/model/ |
geom.js |
src/geom/, src/export/
|
airfoil.js |
src/airfoil/ |
xfl.js |
src/import/xfl.js |
xflxml.js |
src/import/xflxml.js |
flow5.js |
src/import/fl5.js, src/import/fl5xml.js, the flow5 texts of src/import/xflr5.js and src/ui/xflr5.js, and flow5: <file> in src/ui/airfoils.js
|
xflr5.js |
src/import/xflr5.js, src/ui/xflr5.js and the texts of the XFLR5 import in src/main.js and src/ui/airfoils.js: title of Open, item in Help, count of further warnings, refusal of an XFLR5 file in the upload, XFLR5: <file> under an imported airfoil |
foam.js |
src/ui/foam.js, src/export/foam.js and the label and title of Foam in src/main.js
|
winglet.js |
src/ui/winglet.js, src/model/winglet.js and the Winglet… button in src/ui/sections.js
|
src/i18n/de/index.js merges the twelve files into DE and exports them by name as AREAS. The three files of the XFLR5 import hold 25, 34 and 146 texts, the file of the flow5 import 44.
To add a language:
- Make a folder
src/i18n/<code>/with the area files and anindex.jsthat merges them, assrc/i18n/de/index.jsdoes. - Add
<code>: 'Name'toLANGUAGESand<code>: <catalog>toCATALOGSinsrc/i18n/index.js. The list Language / Sprache showsLANGUAGES.setLanguage()accepts the codes ofCATALOGS. - These places name German in the code and do not follow
LANGUAGES. Each needs a rule for the new language:-
initialLanguage()reads onlydefrom the browser languages. -
fixed(),count(),whole()andplain()compare the language with'de'. -
src/ui/styles.csshas ruleshtml[lang='de']for the longer German labels. - The documentation link of the Help dialog in
src/main.jschooses the page bylanguage() === 'de'. - The
<noscript>text inindex.htmlnames both languages. -
scripts/check-i18n.mjsimports onlysrc/i18n/de/index.js. - The label
Language / Sprache(LANGUAGE_LABELinsrc/ui/settings.js) names the list in English and German. -
scripts/screenshots.mjsruns onlyenanddeand looks labels up inDE. The wiki pages of a new language need their own image folder. -
PAGE_PAIRSinscripts/check-docs.mjsand the rules inscripts/check-test-counts.mjsname only the English and the German pages.
-
Rules for texts:
- A plural is two keys, or a German entry that is a function of the params (
({ n }) => …) and compares the printed number with'1'. - Numbers inside a text go through four helpers.
fixed(value, digits)writesdigitsdecimals:1200.5in English,1.200,5in German.count(value)rounds to a whole number and groups the digits:20,000in English,20.000in German.whole(value)is for counts and limits that can have 4 or more digits: English printsString(value), German groups an integer and never rounds.plain(value)prints the shortest text that reads back as the number (String(value)), with a decimal comma in German. - Number fields are text fields with the role
spinbutton(numberFieldandnumberInputinsrc/ui/dom.js, also used by the wizard), because a native<input type="number">in Chromium drops a typed decimal comma. They showinputText(value), which isplain(value), and read the typed text withreadNumber(text)fromsrc/i18n/index.js: a decimal comma or point in both languages, the digit groups of the current language, rule in section Numbers of the User Guide.aria-valuenow,aria-valueminandaria-valuemaxhold plain JavaScript numbers. - Data numbers (attributes, file contents) stay unformatted.
- Not translated: file contents (STEP, STL, 3MF, JSON,
.dat), the project JSON, airfoil names, attributions and licenses, names of external sources, file names, cascading style sheets (CSS) classes,data-*values, option values and issue codes such asmany-points. The text of an exception insideInternal error: {message}(thrown insrc/geom/nurbs.jsandsrc/geom/linalg.js) stays English; the frame around it is translated. The 3MF file declaresxml:lang="en-US"in both languages. - Code never compares a translated text. It compares a code or a recorded field (
issue.code,build.sizeWarning), because the same message reads differently in German. The one text test,/zero pivot/insrc/geom/wing.js, reads an exception fromsrc/geom/linalg.jsthat is never translated. - The descriptive texts of the library are data:
categoryanduseofNACA_PRESETSinsrc/airfoil/library.jsand of the entries inpublic/airfoils/index.json, and thenoteofEXTERNAL_SOURCES.libraryText()insrc/ui/airfoils.jsshows them. A new text needs acasethere with its owntr()literal and a German entry insrc/i18n/de/panels.js. An unknown text is shown as it is. - The Documentation (wiki) link in the Help dialog opens the wiki home in English and the page
Benutzerhandbuchin German. -
npm run i18n:check(scripts/check-i18n.mjs, jobtestofci.yml, also run bytest/i18n.test.js) reads everytr()call insrc/outsidesrc/i18n/. It exits with code 1 when a key has no German entry, a German entry is unused, key and text entry differ in their placeholders, two areas translate one key differently, an entry is neither text nor function, or atr()call does not start with a string literal. It does not compare the placeholders of function entries. -
changeLanguage()insrc/main.jsswitches without a reload. It stores the choice, sets thelangattribute of thehtmlelement, relabels the shell and removes the notice, unless the notice reports an error. When the build holds an error or a warning other than the size warning, the wing is built again, because those messages come from the build. Otherwise the wing stays and the size warning is written again. In both cases the panels are drawn again, the Checks tab among them. The project, the selection and the undo history stay.
| Data | Source |
|---|---|
| Airfoils in unit tests and STEP and 3MF cases | Generated from the NACA equations: nacaAirfoil() from src/airfoil/naca.js, naca() from test/helpers.js
|
| Parser test inputs | Short synthetic strings in test/airfoil.test.js in the file formats of third-party sources, with invented coordinates |
| Browser test uploads | Generated in the spec files, no third-party data: 13-point .dat in e2e/smoke.spec.js and e2e/mobile-layout.spec.js; Selig, Lednicer, X/Yo/Yu percent table with decimal commas and invalid files (crossing surfaces, text) from the NACA 4-digit equations in e2e/airfoils.spec.js
|
| Bundled library in browser tests |
e2e/airfoils.spec.js lists the 6 files of public/airfoils/ and adds S9104 to the project |
| Screenshot upload | NACA 4412 computed in scripts/screenshots.mjs, 14 x positions from 0 to 100 %, written as X/Yo/Yu percent table with decimal commas; the same file in both languages |
| XFLR5 files in unit tests and browser tests |
test/fixtures/xflr5/: own work and files under the MIT license (license of the Massachusetts Institute of Technology), origin and license per file in test/fixtures/xflr5/SOURCE.md (section XFLR5 test files and tests) |
| XFLR5 project files for edge cases | Written by test/xflr5-writer.js from the description of the format in src/import/xfl.js; no XFLR5 code |
| Airfoil uploads in the XFLR5 browser tests | Generated in e2e/xflr5.spec.js: .dat file TEST 12 with 13 points, invented coordinates |
| Screenshot of the import dialog |
test/fixtures/xflr5/fixtures_v662.xfl, opened over the Sport preset, in both languages |
| flow5 files in unit tests and browser tests |
test/fixtures/flow5/: written by local builds of flow5 7.57 and 7.56 from own inputs, MIT, origin per file in test/fixtures/flow5/SOURCE.md (section flow5 test files and tests) |
| flow5 project files for other record formats | Written by test/fl5-writer.js from the description of the format; no flow5 code |
Third-party airfoil files are committed only under a license from the License row in Airfoil library check. Third-party XFLR5 files are committed only under the MIT license (test/fixtures/xflr5/uaslab/).
Sources: Airfoil Sources.
Parser test outside the repository on 2026-09-29: 1,964 third-party files from aerodesign.de, mh-aerotools.de and UIUC (University of Illinois Urbana-Champaign). Results per set: Airfoil Sources, section Parser test.
Reader test outside the repository on 2026-09-29 and 2026-09-30: 29 .xfl files and 66 XML files, most of them of XFLR5 users, read by src/import/ and by an independent reader written from the format description. Apart from the files listed below they are not committed (20 of the .xfl files carry no license). Results: RECORD.md, row XFLR5 import.
Files in test/fixtures/xflr5/; test/fixtures/xflr5/SOURCE.md gives the origin and the license of each:
| File | Bytes | Origin | License |
|---|---|---|---|
fixtures_v662.xfl |
11,092 | Own work: 2 test planes, "Fixture A" (main wing, elevator, fin, tilt angle, dihedral, twist) and "Fixture B" (no elevator), saved by the project writer of XFLR5 6.62 (Subversion trunk r1506, Qt 5.15.13); project format 200002 | MIT, as the project |
xml_mm/0.plane.xml, xml_mm/0.w2.wing.xml, xml_mm/0.w3.wing.xml
|
2,154 to 7,493 | Own work: plane "Fixture A" in millimetres, and its elevator and its fin as wing files, written by the XML writer of XFLR5 6.62 | MIT, as the project |
xml_in/0.plane.xml |
7,494 | Own work: the same plane in inches | MIT, as the project |
xml_m/1.plane.xml |
3,936 | Own work: plane "Fixture B" in metres | MIT, as the project |
uaslab/Rascal110.xfl |
397,086 | UASLab/OpenFlightSim on GitHub, commit b020511: real XFLR5 output, project format 200001, inches, with body, 13-section main wing, 9-section elevator and flap airfoils at 0° |
MIT (uaslab/LICENSE.md) |
uaslab/UltraStick25e.xml |
16,852 | Same commit: XFLR5 plane file in inches | MIT (uaslab/LICENSE.md) |
uaslab/UltraStick25e_v662_stripped.xfl |
28,418 | Same commit: UltraStick25e.xfl loaded and saved by XFLR5 6.62 without the analyses and results; project format 200002; the same plane as UltraStick25e.xml
|
MIT (uaslab/LICENSE.md) |
XFLR5 ships no sample projects. A local driver that links the XFLR5 6.62 sources (GNU General Public License, version 2 or later) wrote the own-work files. The driver is not part of this repository, so the repository cannot regenerate these files. The files contain no XFLR5 code.
Unit tests (Vitest, Node.js):
| File | Tests | Content |
|---|---|---|
test/xflr5-xfl.test.js |
40 | Reader of .xfl projects: byte layout of fixtures_v662.xfl; the fin flags; the old formats of Rascal110.xfl; the reserved blocks of XFLR5 6.10.01 to 6.10.04; UltraStick25e_v662_stripped.xfl against UltraStick25e.xml; projects from test/xflr5-writer.js (analyses with control gains and result points, plane results, null strings, bodies, repeated airfoil names, flaps, sanitized positions); refused files (flow5, .wpa, JSON, size, counts, odd string lengths); damage (cut at every record boundary and at every byte, damage after the planes); windows of any size; German messages |
test/xflr5-xml.test.js |
47 | Reader of XML files: fixtures in millimetres, inches and metres; the fin flags; wing files; units; syntax (byte order mark, text in 16-bit Unicode Transformation Format (UTF-16), comments, character data (CDATA) sections, entities, case, padded numbers, exponents); missing and garbled numbers; wing slots by <Type> and by order; refused files; limits; linear time on long and hostile input; German messages |
test/xflr5-map.test.js |
74 | Mapping: y and z from developed span and dihedral, twist, position, the tilt angle stored as Part tilt (the turned build against XFLR5's construction); section planes of an import (mitred, tilted, fold and stretch fallback, an airfoil switch in one plane, the airfoil frame along a rolled plane with XFLR5's panel angles); clean-up (sections at one y, chords below 1 mm, limits); surfaces of a plane; the second wing and the single, symmetric and double fin against a transcription of XFLR5's corner construction, a fin below its origin, the fins of the test files; airfoil sources, picks, uploads and flaps; airfoil frame against the numbers of Fixture A and B, also for NACA sections of the current project (generated and checked points, hand-edited metadata); report lines for current-project airfoils of an XFLR5 import, an upload or the library, and for library airfoils with an inclined chord line; a file airfoil whose trailing edge crosses its own end; an airfoil switch and a chord step at one y built with Smooth; project, JSON round trip and report; 10,000 airfoil names in linear time; German |
test/part.test.js |
7 of 18 | Version 1 and 2 files: their part placement keys dropped; upgrade of a folded tilt of project format version 2: the same part with Vertical section planes (twist pivot 0.25 and 0.5, pointed tip, Straight panels) and the shift of the MAC position, the notes with Mitred section planes, the fold kept with guide curves or a twist beyond ±360° |
test/airfoil.test.js |
1 of 79 | leadingNacaCode |
test/xflr5-writer.js writes big-endian XFLR5 project files from options with default values, written from the description of the format in src/import/xfl.js. Numbers that the reader skips are written as recognizable non-zero values, so a reader that skips too many or too few bytes misreads what follows. writeProject(options) returns { bytes, marks }; marks lists the offset of every record for the truncation tests. The plane option spare: 'index' writes the reserved blocks of the plane and its wings as XFLR5 6.10.01 to 6.10.04 do.
Browser tests: e2e/xflr5.spec.js, 10 tests, 20 runs:
- XML plane file: the stabilizer with a NACA airfoil and an uploaded
.datfile, then Undo. -
.xflproject with two planes: plane choice, a plane without stabilizer, airfoil notes kept after a reload; then a project fromtest/xflr5-writer.jswith 300 airfoils: another surface checked in slices, with Import off and the table of the previous wing emptied. - Cancel keeps the design and adds no undo step.
- A damaged
.xflproject and an XML file with another root element give a message and keep the design. -
Import waits for an uploaded
.datfile that is still being read. - The file chooser of Open accepts XFLR5 files.
- Open recognizes XFLR5 files whose name lost its extension.
- The Airfoils upload refuses XFLR5 files, also an
.xflproject above 20 MB. - The import dialog fits a 360 px wide phone (
mobileonly). - The dialog in German (locale
de-DE).
Files in test/fixtures/flow5/, written by local builds of flow5 from Wingdesigner's own inputs (planes, wings, airfoil choices); test/fixtures/flow5/SOURCE.md lists them with their content. The drivers that link the flow5 libraries (GPL-3.0) and the build changes for Ubuntu 24.04 are not part of the repository, so the repository cannot regenerate these files. The files contain no flow5 code.
| File | Bytes | Written by |
|---|---|---|
basic.fl5 |
8,648 | flow5 7.57: main wing, elevator, one-sided fin |
full.fl5 |
116,772 | flow5 7.57: 3 planes (6 wings and 3 bodies; a triangle mesh; one wing), an airfoil analysis with saved results, a flapped airfoil |
v756.fl5 |
85,968 | flow5 7.56: a sections body with its section points |
basic-plane.xml, basic-wing.xml, full-plane.xml, full-plane-files.xml
|
3,094 to 31,093 | flow5 7.57: plane and wing XML export in metres and millimetres, airfoils by name and by .dat file |
NACA 2412.dat, NACA 0009.dat, Flapped 2410.dat
|
2,782 to 2,785 | flow5 7.57: the airfoil files of the XML export |
mesh-nodes.json |
132,240 | flow5 7.57: the analysis mesh nodes of 10 wings (8 two-sided, 2 fins), the reference of the geometry test |
Unit tests (Vitest, Node.js):
| File | Tests | Content |
|---|---|---|
test/flow5-fl5.test.js |
9 | Reader of .fl5 projects: the files of flow5 7.57 and 7.56; every body kind, airfoil analyses and results, mesh planes, the record formats of flow5 7.53 and older layouts from test/fl5-writer.js; the sections body with and without its section points; refusals; windows of 1,000 bytes |
test/flow5-xml.test.js |
9 | Reader of flow5 XML: plane files in millimetres and metres, .dat file references, wing files, Type and booleans, units, several planes, refusals |
test/flow5-map.test.js |
8 | Mapping: the wing list and the one-sided wing turned about z, a further wing, a fin as the left half of the part, roll and tilt as part placement, a rolled two-sided wing with the left half turned (settings.leftHalf "turned"), flow5 airfoils and their flaps, .dat files matched by name; the right half of 8 wings, the left half of both rolled two-sided wings and both fins within 0.15 mm of flow5's analysis mesh |
test/fl5-writer.js writes .fl5 projects from the same description of the format, with the format number of each record selectable, so that the layouts of flow5 versions without a file at hand are tested.
Browser tests: e2e/flow5.spec.js, 3 tests, 6 runs:
-
.fl5project: plane choice, the wing list, the rolled elevator imported as part roll and tilt with the left half turned (project format version 4), its airfoil from the file, Undo. - flow5 plane XML with
.datfile references: Upload .dat files… takes both files, each row finds its file by name. -
.fl5project: the fin imported with part roll 90° about the wing origin.
Setup: npm ci. Requires Node.js 24 or later (.nvmrc: 24).
Browser tests and screenshots also need Chromium: npx playwright install chromium (Linux system libraries: --with-deps), or PW_CHROMIUM set to a local binary.
| Command | Runs | Result |
|---|---|---|
npm run dev |
vite |
Development server at http://localhost:5173 (next free port when 5173 is in use) |
npm run build |
vite build |
Static site in dist/
|
npm run preview |
vite preview |
Serves dist/ at http://localhost:4173 (next free port when 4173 is in use) |
npm run lint |
eslint . |
Lint errors; exit code 1 on error |
npm test |
vitest run |
Unit tests test/**/*.test.js in Node.js: 660 tests in 24 files |
npm run test:watch |
vitest |
Unit tests, re-run on file change |
npm run coverage |
vitest run --coverage |
Table on the terminal, coverage/coverage-summary.json, HyperText Markup Language (HTML) report in coverage/. Covers src/**/*.js without src/ui/ and src/main.js. |
npm run coverage:readme |
node scripts/coverage-readme.mjs |
Writes the coverage table into README.md and README.de.md between <!-- coverage:start --> and <!-- coverage:end -->
|
npm run coverage:check |
node scripts/coverage-readme.mjs --check |
Exit code 1 when a README table differs from coverage/coverage-summary.json; exit code 2 when that file or a marker is missing |
npm run airfoils:check |
node scripts/check-airfoils.mjs |
Checks in Airfoil library check; exit code 1 on a problem |
npm run e2e |
npm run build && playwright test |
Browser tests in e2e/ against vite preview on port 4173 |
npm run step:cases |
node scripts/export-step-cases.mjs step-check |
15 STEP files, 15 3MF files and cases.json in step-check/
|
npm run foam:cases |
node scripts/export-foam-cases.mjs foam-check |
Profile ZIP, SVG, DXF and PDF of 13 test wings and cases.json in foam-check/
|
npm run screenshots |
node scripts/screenshots.mjs |
28 Portable Network Graphics (PNG) files: 14 in docs/wiki/images/ (English) and 14 in docs/wiki/images/de/ (German) |
npm run docs:check |
node scripts/check-docs.mjs |
Documentation check; exit code 1 on a problem |
npm run counts:check |
node scripts/check-test-counts.mjs |
Checks in Test count check; exit code 1 on a difference |
npm run i18n:check |
node scripts/check-i18n.mjs |
Checks in Translations; exit code 1 on a problem |
| Environment variable | Used by | Effect |
|---|---|---|
PW_CHROMIUM |
npm run e2e, npm run screenshots
|
Path to a local Chromium binary instead of the Playwright download |
SHOT_DIR |
e2e/smoke.spec.js |
Directory for the smoke-test screenshots <project>-wizard.png, <project>-main.png, <project>-planform.png, <project>-upload.png; <project> is desktop or mobile
|
npm run airfoils:check reads public/airfoils/index.json (array airfoils, 6 entries).
It prints each problem and exits with code 1 when at least 1 check fails.
| Check | Pass condition |
|---|---|
| Index |
airfoils is an array |
| Required fields |
id (unique), name, file, category, source.author, source.license, source.url, source.terms
|
| License |
source.license is one of public-domain, CC0-1.0, Unlicense, CC-BY-4.0, CC-BY-3.0, MIT, BSD-2-Clause, BSD-3-Clause
|
| Source host |
source.url and source.terms are Uniform Resource Locators (URLs); host is not aerodesign.de or mh-aerotools.de or a subdomain of these (terms: personal use only) |
| File |
public/airfoils/<file> exists and imports without errors |
| Notice | When at least 1 entry exists: public/airfoils/NOTICE.md exists and contains the name of every entry |
| No unlisted file | Every file in public/airfoils/ except index.json and NOTICE.md has an entry |
| NACA presets | All 17 NACA presets pass checkAirfoil
|
Setting (playwright.config.js) |
Value |
|---|---|
| Projects |
desktop: Desktop Chrome, 1280 x 720 cascading style sheets (CSS) px. mobile: Pixel 7, 412 x 839 CSS px, device scale 2.625, touch input. |
| Server |
npm run preview -- --port 4173 --strictPort; every run starts its own server (reuseExistingServer: false) |
| Timeouts | 60000 ms per test, 60000 ms for server start |
| Retries | 0 |
| Locale |
en-US for all specs (the app starts in German on a German browser, and the specs assert English texts); e2e/language.spec.js and e2e/xflr5.spec.js set de-DE in their blocks German browser and XFLR5 import in German
|
| Reporters |
list on the terminal; json to playwright-report/results.json, input of the Test count check
|
201 tests in 15 spec files, 402 runs (both projects). The test object of e2e/helpers.js fails a test on any uncaught page error or console error.
31 tests run in one project only (test.skip in the other project):
| Spec file |
desktop only |
mobile only |
|---|---|---|
e2e/mobile-layout.spec.js |
1: sections table, brand, top bar in 1 row | 17: every test in phone layout (no sideways scroll, top bar, tap targets, section cards, tip note, 360 px wide phone, dialogs in portrait and landscape) |
e2e/viewer.spec.js |
3: mouse drag rotates, right mouse button drag pans, mouse wheel zooms | 4: one-finger rotate, two-finger pan, two-finger pinch zoom, Enlarge |
e2e/planform.spec.js |
1: zoom buttons, mouse wheel, double-click fit | 2: one-finger drag of a guide point, two-finger pinch zoom |
e2e/sections.spec.js |
2: Ctrl+Z and Ctrl+Shift+Z, Ctrl+Y | 0 |
e2e/xflr5.spec.js |
0 | 1: the import dialog fits a 360 px wide phone |
No test is marked test.fail.
npm run e2e rebuilds dist/ first; before a direct npx playwright test, run npm run build.
Python: 3.12 in the ci.yml job step, 3.11.15 in a local run on 2026-09-30; other versions not tested.
pip install cadquery-ocp==8.0.1.0.0 lib3mf==2.5.0 # OpenCascade 8 Python bindings, 3MF Consortium library
npm run step:cases
python scripts/validate_step.py step-check/cases.json
python scripts/validate_3mf.py step-check/cases.jsoncases.json holds per case: STEP file, 3MF file, expected volumes, tolerance 5e-4, triangle count per mesh object.
Expected volume: volume of the half-wing mesh tessellateHalf(build, { uRefine: 4, vRefine: 8 }); every chordwise interval is split into 4, every spanwise interval into 8.
Both scripts print a JSON report.
STEP check per file (validate_step.py) |
Pass condition |
|---|---|
| Read | STEP reader returns IFSelect_RetDone
|
| Solid count | Equals the expected count: 2 when mirrored, 1 for a half wing |
| Boundary representation (B-rep) validity |
BRepCheck_Analyzer reports valid |
| Shell | Closed and consistently oriented: no free edges |
| Volume | Greater than 0; relative deviation from the expected volume at most 5e-4 (0.05 %) |
| Planar faces | At least 1; every edge of a planar face (the end caps) within 1e-6 mm of its plane, sampled at 51 points per edge. A cap off its plane can change the volume by less than 0.05 % (0.0475 % for a vertical tip cap on mitred-gull-15-5, 0.67 mm off). |
Exit code 1 when a check fails.
3MF check per file (validate_3mf.py) |
Pass condition |
|---|---|
| Read | Reader in strict mode (SetStrictModeActive), 0 warnings |
| Objects | 1 mesh object per expected shell with the expected triangle count: 2 when mirrored (export mode halves), 1 for a half wing (export mode right) |
| Topology | Every object passes IsManifoldAndOriented
|
Exit code 1 when a check fails or when cases.json holds no 3MF case.
Cases from test/step-cases.js. Base wing (sampleProject() in test/helpers.js, Vertical section planes):
| Section | y (mm) | x (mm) | z (mm) | Chord (mm) | Twist (°) | Airfoil |
|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 200 | 0 | NACA 2412 |
| 2 | 300 | 20 | 10 | 170 | -1 | NACA 2412 |
| 3 | 600 | 60 | 30 | 110 | -3 | NACA 0010 |
x and z: position of the leading edge.
| Case | Change from the base wing | Solids |
|---|---|---|
open-te-linear |
None: trailing edge as in the airfoil (open), linear blending | 2 |
closed-te-smooth |
Closed trailing edge, smooth blending | 2 |
guided-elliptic |
Nose line and end line, 4 points each; trailing-edge thickness 0.5 mm | 2 |
root-offset-half |
All sections moved by +35 mm in y (root at y = 35 mm); not mirrored | 1 |
reflex-closed |
NACA 23112 (reflexed) at sections 1 and 2, NACA 0008 at the tip; closed trailing edge | 2 |
cusped-closed |
Not the base wing: 3 sections of a synthetic airfoil with a closed, cusped trailing edge (cuspedAirfoil in test/helpers.js: zero thickness and zero wedge angle at the trailing edge, camber slope −0.7 there), chords 240, 168 and 96 mm at y 0, 600 and 960 mm, twist 0°, −2° and −3°; Straight panels
|
2 |
pointed-tip |
Pointed tip, ratio 0.002 (1/500); trailing-edge thickness 0.4 mm | 2 |
pointed-elliptic-closed |
Pointed tip, ratio 0.005 (1/200); nose line and end line meet at x = 115 mm, y = 600 mm; closed trailing edge | 2 |
symmetric-0009 |
NACA 0009 at every section | 2 |
mitred-vtail-35 |
Not the base wing: 2 sections of NACA 0009, chords 120 and 70 mm, the tip 320 mm along a 35° panel, x 40 mm; Straight panels, Mitred section planes (tip cap rolled 35°) | 2 |
mitred-vtail-35-y-up |
As mitred-vtail-35, written with the Fusion 360 fix (Y up): every point as (x, z, −y) |
2 |
mitred-gull-15-5 |
z 80.3848 mm at section 2 and 54.1382 mm at the tip (panels of 15° and −5°); Mitred section planes (rolls 0°, 5°, −5°), linear blending with 8 stations per panel | 2 |
mitred-gull-smooth |
As mitred-gull-15-5, with smooth blending: rolls blended by the cubic, stretch from the slope of the blended reference line |
2 |
mitred-switch-short-panel |
Not the base wing: NACA 2412 at y 0 and 299.5 mm (chords 200 and 180 mm), NACA 0012 at y 300 and 600 mm (chords 180 and 120 mm, tip z 52.8981 mm, twist −2°); Straight panels, Mitred section planes: the 0.5 mm panel counts as none, both switch sections roll 5.25°; the outer panel stores a panel angle of 10.5° (tip roll 10.5°) | 2 |
part-tilt-roll |
As mitred-gull-15-5, with Part tilt 8° and Part roll 12° about the pivot (50, 0, −20) mm: turned surfaces, edge curves, cap planes and the mirror of a placed part |
2 |
Python: 3.12 in the ci.yml job foam, 3.11.15 in a local run on 2026-10-01; other versions not tested.
pip install ezdxf==1.4.4 pypdf==6.19.0
npm run foam:cases
python scripts/validate_foam.py foam-check/cases.jsonscripts/export-foam-cases.mjs takes the wings of test/step-cases.js without mitred-vtail-35-y-up and part-tilt-roll (13 cases): the first differs from its case in the STEP axes only, the cores of the second lie in the frame of the part, as those of mitred-gull-15-5. Cuts: the proposal for a longest core of 300 mm. Kerf 1 mm for guided-elliptic, paper A3 for pointed-tip and Letter for mitred-gull-15-5, else no kerf and A4. cases.json holds per case the name, the 4 files, the segment count, the points per profile, the kerf, the vertex count of each profile polyline, the page count, the page size and the paper size.
Check per case (validate_foam.py) |
Pass condition |
|---|---|
| ZIP |
README.txt, segments.csv and per segment end a file in mm/ and in normalized/, nothing else; every .dat file has its name line and the expected point count; segments.csv has one row per segment |
| DXF | ezdxf recover module reads it, its audit has no error, version AC1009; layer PROFILE holds closed polylines with the expected vertex counts; without kerf, width and height of each polyline equal those of its mm/ .dat file within 0.001 mm |
| SVG | parses as XML; width and height in mm; one polygon of class profile per segment end |
pypdf reads it in strict mode; page count as computed; every page at the computed size (mm, 0.01 mm); the text of page i holds Page i of n.
|
Exit code 1 when a check fails or when cases.json holds no case. The script prints a JSON report.
| English | German |
|---|---|
README.md |
README.de.md |
Home.md (both languages) |
Home.md (both languages) |
User-Guide.md |
Benutzerhandbuch.md |
Geometry.md |
Geometrie.md |
File-Formats.md |
Dateiformate.md |
Airfoil-Sources.md |
Profilquellen.md |
Development.md |
Entwicklung.md |
Wiki pages are in docs/wiki/.
The Docs workflow mirrors docs/wiki/ into the repository wiki with rsync --delete.
On the next run, pages edited in the wiki web interface are overwritten, and pages added there are deleted.
| Repository file (English only) | Content |
|---|---|
| RECORD.md | Verified state, decisions with reasons, measurements, open items |
| CHANGELOG.md | Version history |
| docs/Handover.md | Where the work stands, next steps, working agreements with the owner, what the repository does not hold |
| docs/Flow5upgrade.md | Plan and owner decisions for mitred section planes, a rigid tilt of the whole part and the flow5 import |
npm run screenshots regenerates all images. It builds the site, serves it on port 4175 and drives Chromium with Playwright. It produces every image twice:
| Folder | Interface language | Browser locale |
|---|---|---|
docs/wiki/images/ |
English | en-US |
docs/wiki/images/de/ |
German | de-DE |
Both folders hold 14 files with the same names and the same states. The English wiki pages embed images/<name>.png, the German pages images/de/<name>.png.
The two languages run the same steps. The steps name every control by its English label. The German run maps that label to its German entry in DE (src/i18n/de/index.js) and stops when there is no text entry. Both runs stop when the app starts in another language than the locale asks for (lang attribute of the html element). The text Sample 4412 table in upload-preview.png is the first line of the generated file and reads the same in both languages.
Desktop: 1280 x 800 CSS px, device scale 1. Phone: Pixel 7, device scale 2.625. Light color scheme. The German texts are longer, so the size of some German images differs.
| File | State | Size (px), English | Size (px), German |
|---|---|---|---|
wizard.png |
Wizard, Glider preset | 960 x 784 | 960 x 784 |
main-desktop.png |
Full window after creating the Glider | 1280 x 800 | 1280 x 800 |
sections.png |
Sections tab, Glider, 3 sections | 600 x 730 | 600 x 730 |
planform.png |
Planform tab, Glider, nose line and end line on | 600 x 730 | 600 x 730 |
airfoils.png |
Airfoils tab, Glider | 600 x 730 | 600 x 730 |
upload-preview.png |
Upload preview of a synthetic X/Yo/Yu percent table with decimal commas, opened over the Glider | 640 x 646 | 640 x 710 |
export-dialog.png |
Export dialog, Glider | 640 x 548 | 640 x 594 |
settings.png |
Settings tab, Glider | 650 x 730 | 650 x 730 |
checks.png |
Checks tab, Glider | 600 x 730 | 600 x 730 |
flying-wing-control-net.png |
3D view, Swept flying wing, Show NURBS control net on | 680 x 730 | 680 x 730 |
mobile-main.png |
Phone, Sport preset | 1082 x 2202 | 1082 x 2202 |
mobile-planform.png |
Phone, Planform, Sport, end line on | 1082 x 2202 | 1082 x 2202 |
foam-dialog.png |
Foam-cutting wizard, Glider, after Split segments over the limit, window 1280 x 1400 CSS px | 960 x 1344 | 960 x 1384 |
xflr5-import.png |
Dialog Import from XFLR5 for test/fixtures/xflr5/fixtures_v662.xfl, opened with Open over the Sport preset, window 1280 x 1200 CSS px |
960 x 966 | 960 x 1062 |
npm run docs:check checks docs/wiki/*.md, README.md, README.de.md and the version sections of CHANGELOG.md:
| Check | Fails when |
|---|---|
| Page pairs | A file of the 6 English–German pairs above is missing (Home.md excluded) |
| Coverage markers |
README.md or README.de.md lacks <!-- coverage:start --> or <!-- coverage:end -->
|
| Wiki links | The target page of a wiki link (double square brackets) has no file in docs/wiki/
|
| Wiki links in tables | Pages as markdown-it 15 reads them (CommonMark with GitHub tables and HTML; fenced code, block quotes and the end of a table follow the Markdown rules): a table cell holds, outside code spans, an opening double square bracket without the closing one, because the | of a labelled wiki link ended the cell; or a header and delimiter row form no table, because a | in the header gave it more cells. In tables a wiki link holds only the page title, with spaces for the hyphens of the page name (User Guide links to User-Guide). |
| Images | An embedded image file does not exist, or its alt text is empty |
| Relative links | The target of a relative Markdown link does not exist |
| Release texts |
CHANGELOG.md is missing, or a line of it from the first ## heading on names contributor material (INTERNAL_TERMS, sourceNames and formatKeys in scripts/check-docs.mjs): the handover; the working agreements (with the owner); RECORD; an npm command (npm run, test, ci, install, i, exec, start, version, publish) or npx; a path under scripts/, test/, e2e/, src/, public/ or .github/, also after ./ or in a GitHub blob or tree link on any branch; ci.yml, docs.yml or release.yml; CI, continuous integration, GitHub Actions or a workflow run; unit, browser, end-to-end, e2e, integration, regression, smoke, snapshot, component, acceptance or automated tests, a test suite, run, case, file or count, Vitest, Playwright, a .spec.js file; test coverage (test, code, line, branch, statement or V8 before coverage, or check, table, report or marker(s) after it); the Development page ([[Development, or Development before page, wiki, , section or / Entwicklung; Entwicklung likewise); in backticks: a function call (name(), name(arguments)), a function, method or class name in camelCase or PascalCase that src/ declares (buildWing, updateLabels), or an upper-case constant of 3 or more characters that src/ declares (LIMITS, FRAME_TOLERANCE). Bare names of the project file that File Formats documents (JSON keys and first table cells, such as sectionPlanes) pass, a call such as sectionPlanes(project) does not; and so do STEP entity names such as MANIFOLD_SOLID_BREP, which src/ does not declare. Code spans pair over the whole text, as in Markdown, so a span that wraps to the next line counts. Each line is read together with the next one, so a term broken over two lines is found, also after trailing spaces and with CRLF line ends. |
Paths resolve against docs/wiki/ for wiki pages and against the repository root for the READMEs.
Not checked: language switch line, alt text language, link target language, link anchors (#…), external links (http:, https:, mailto:), contributor material in other words than the listed terms.
npm run counts:check derives every test count that README.md, README.de.md, RECORD.md, Development, Entwicklung, Geometry and Geometrie state from the suites.
Exit code 1 when a stated number differs or a statement is not found; scripts/check-test-counts.mjs holds each statement as a pattern with the number of times it occurs. It also checks the tests of one file wherever a page states them: a table row that starts with the path of a test file in backticks and a count (all tests of the file) or <n> of <m> (<m> all tests of the file), and the path followed by (<n>).
| Count | Source |
|---|---|
| Unit tests and test files |
vitest list --staticParse=false: runs the test files to collect the tests, so a test inside a loop counts once per pass. The default static parse counts it once. |
Browser tests, spec files, runs; tests of e2e/limits.spec.js per project |
playwright test --list --reporter=json; no browser, no server |
Tests that run in one project only (total and per spec file), passed and skipped runs in RECORD.md
|
JSON report of a run, given with --e2e-report <file>. test.skip decides at run time. Without a report these numbers are not checked; the passed, skipped and failed runs in RECORD.md still have to add up to the runs. With a report, a run with a failed test fails the check. |
Sentence No test is marked test.fail.
|
Present only while no test in the listing or the report expects to fail |
| STEP and 3MF validation cases: count; names in order and solids (2 when mirrored, else 1) in the case table |
stepCases() in test/step-cases.js
|
npm run counts:check # without the device-only counts
npm run e2e && npm run counts:check -- --e2e-report playwright-report/results.json # all countsContinuous integration (CI) runs it in the job test without a report and in the job e2e with the report of that job's run.
Not checked: CHANGELOG.md (it records changes, with the counts of their time).
Platform: GitHub Actions, runner ubuntu-latest for every job.
| Workflow | Trigger | Permissions | Content |
|---|---|---|---|
ci.yml |
Push to main, pull request, manual |
Workflow: contents: read; jobs build and deploy: see the next table |
Jobs in the next table. Concurrency group ci-<ref>. On main an active run is not cancelled. At most 1 later run waits; a newer run replaces the waiting one. On other refs a later run cancels the active one. |
docs.yml |
Push to main that changes docs/wiki/** or .github/workflows/docs.yml; manual |
contents: write |
Job wiki: clones <repository>.wiki.git with GITHUB_TOKEN, copies docs/wiki/ with rsync --delete, commits Update wiki from <first 7 characters of the commit hash> and pushes. No commit when nothing changed. Concurrency group wiki: an active run is not cancelled. |
release.yml |
Push of a tag v*
|
contents: write |
Job release: steps in Release
|
Every ci.yml job except deploy, and the release.yml job, check out the repository, set up Node.js from .nvmrc with npm cache and run npm ci.
The docs.yml job wiki only checks out.
ci.yml job |
Name | Steps | Permissions | Runs on |
|---|---|---|---|---|
test |
Lint, unit tests, coverage |
lint, coverage, coverage:check, airfoils:check, docs:check, counts:check, i18n:check; uploads artifact coverage
|
contents: read |
Every trigger |
step |
STEP and 3MF validation (OpenCascade, lib3mf) | Python 3.12, pip install cadquery-ocp==8.0.1.0.0 lib3mf==2.5.0, step:cases, validate_step.py, validate_3mf.py; uploads artifact step-files (STEP, 3MF, cases.json) |
contents: read |
Every trigger |
foam |
Foam-cutting files (ezdxf, pypdf) | Python 3.12, pip install ezdxf==1.4.4 pypdf==6.19.0, foam:cases, validate_foam.py; uploads artifact foam-files (ZIP, SVG, DXF, PDF, cases.json) |
contents: read |
Every trigger |
e2e |
Browser tests (Playwright) |
npx playwright install --with-deps chromium, npm run e2e (build, then all specs in e2e/, both projects), counts:check -- --e2e-report playwright-report/results.json; on failure uploads artifact playwright-results (test-results/) |
contents: read |
Every trigger |
build |
Build site |
build; on push to main also configure-pages and upload-pages-artifact with dist/
|
contents: read, pages: read
|
Every trigger |
deploy |
Deploy to GitHub Pages |
deploy-pages to environment github-pages. Concurrency group pages: an active run is not cancelled. |
pages: write, id-token: write
|
Push to main, after test, step, foam, e2e and build pass |
Artifacts coverage, step-files and foam-files are uploaded also when a step fails; playwright-results only when a step fails. All 4 are kept 14 days.
The pip cache keys are pip-ocp-<runner operating system>-8.0.1-lib3mf-2.5.0 (job step) and pip-ezdxf-<runner operating system>-1.4.4-pypdf-6.19.0 (job foam).
| Action | Version | Used in |
|---|---|---|
actions/checkout |
v7 |
ci.yml, docs.yml, release.yml
|
actions/setup-node |
v7 |
ci.yml, release.yml; Node.js version from .nvmrc, npm cache |
actions/setup-python |
v7 |
ci.yml jobs step, foam
|
actions/cache |
v6 |
ci.yml jobs step, foam (~/.cache/pip) |
actions/upload-artifact |
v7 |
ci.yml jobs test, step, foam, e2e
|
actions/configure-pages |
v6 |
ci.yml job build
|
actions/upload-pages-artifact |
v5 |
ci.yml job build
|
actions/deploy-pages |
v5 |
ci.yml job deploy
|
All listed actions run on Node.js 24 (runs.using: node24); upload-pages-artifact is a composite action on upload-artifact@v7.
CI does not rewrite the README coverage tables.
Update them with npm run coverage && npm run coverage:readme and commit both READMEs.
The wiki clone in docs.yml requires the repository wiki to exist; GitHub creates it with the first page saved in the web interface.
<version>: the version to release, e.g. 0.7.0. package.json holds 0.6.0.
-
Set the version:
npm version <version> --no-git-tag-version(updatespackage.jsonandpackage-lock.json). -
In
CHANGELOG.md, move the entries under## [Unreleased]to a heading## [<version>] - YYYY-MM-DD. The section becomes the release notes: it describes the app, its files and its user documentation (README, User Guide, Geometry, File Formats, Airfoil Sources) and leaves out the handover,RECORD.md, the working agreements, continuous integration, tests, scripts, this page and the names of the source code.npm run docs:checkreports the terms it knows (Documentation check). -
Commit and merge to
main. Wait untilci.ymlpasses. -
Tag the merge commit of the release pull request
<number>and push the tag. The tag names that commit, not the tip ofmain, which can hold later changes. The pull request page on GitHub shows the same commit.git fetch origin main c=$(git log origin/main --merges -1 --format=%H --grep='^Merge pull request #<number>[^0-9]') test -n "$c" && git show -s --format='%h %s' "$c" && git tag v<version> "$c" && git push origin v<version>
release.yml then runs:
| Step | Fails or produces |
|---|---|
| Tag check | Fails when the tag is not v + package.json version
|
npm run lint, npm test, npm run build
|
Fails on lint error, test failure or build error |
| Package |
wingdesigner-<tag>-site.zip with the content of dist/: index.html, LICENSES.txt, assets/ (script and source map), airfoils/
|
| GitHub release | Title Wingdesigner <tag>, the zip file as asset. Notes: the CHANGELOG.md section from ## [<version>] to the next ## heading; See CHANGELOG.md. when the section is missing. Then a paragraph on use: unzip, open index.html (tested in Chromium) or serve the folder with a static web server; LICENSES.txt holds the licenses. |
release.yml runs no coverage check, no airfoil library check, no documentation check, no STEP validation, no 3MF validation and no browser tests.
After release.yml has published the release, a new pull request from main enters the tag commit, the release.yml run, the date and the size of the zip file in RECORD.md, row CI, and updates the rows Version, Next release and main of docs/Handover.md.