<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>http://aodhán.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Aodhan</id>
	<title>Aodhán Gallagher - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="http://aodhán.com/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Aodhan"/>
	<link rel="alternate" type="text/html" href="http://aodhán.com/index.php/Special:Contributions/Aodhan"/>
	<updated>2026-09-23T13:36:23Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.46.0</generator>
	<entry>
		<id>http://aodhán.com/index.php?title=Masters_Project&amp;diff=57</id>
		<title>Masters Project</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Masters_Project&amp;diff=57"/>
		<updated>2026-09-23T10:31:11Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This project focussed on the development and deployment of an optimisation toolchain for maximising the performance of an airborne wind energy system. Airborne wind energy system have the potential to access stronger and more consistent high-altitude wind resources while requiring significantly less material than conventional wind turbines. The toolchain aims to optimise the performance of the aerofoil by maximising the lift generated to maximise the power the system can generate.&lt;br /&gt;
&lt;br /&gt;
Through this work I developed some of the following skills:&lt;br /&gt;
&lt;br /&gt;
* Simulation-driven design&lt;br /&gt;
* Computational Fluid Dynamics&lt;br /&gt;
* High performance computing and Linux&lt;br /&gt;
* Data Analysis&lt;br /&gt;
* Critical evaluatation of model assumptions&lt;br /&gt;
* Report writing and communicating technical concepts&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Airborne wind energy uses a tethered aircraft to create electricity while avoiding the large material costs of conventional wind turbines. Drag powered kites which are the focus of this study having a tethered kite with onboard generators, the kite aerofoil allows it to fly crosswind at high speeds increasing the relative wind speed at the turbines. There are several companies working on designs, most are in concept stages with a few that have built some prototypes such as Makani which are now defunct and Kitekraft.&lt;br /&gt;
&lt;br /&gt;
The understanding on the topic for many years was that the power generated by the system is proportional to the coefficient of lift cubed divided by the equivalent coefficient of drag squared,&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto \frac{C_L^3}{C_D^2}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
so by optimising the glide ratio which is the ratio of the two  coefficients, you would maximise power.&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt; E =\frac{C_L}{C_D}&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto E C_L&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recent research done by Bauer in TU Munich who was working closely with the company kitekraft showed that when the drag forces of other parts of the system, in particular the tether drag is included the glide ratio of the aerofoil is less important as previous research suggested. In the case where the tether drag is dominant the power generated is proportional to the coefficient of lift of the aerofoil squared and should be optimised for as the drag component of the aerofoil has little effect. There have been very limited studies on the optimisation of aerofoils for airborne wind energy with even fewer optimising multi element high lift aerofoils aiming to maximise the &amp;lt;math display=&amp;quot;inline&amp;quot;&amp;gt;C_L&amp;lt;/math&amp;gt; which is the focus of my project.&lt;br /&gt;
&lt;br /&gt;
This project built on work done a previous masters project and code developed by TU Munich and Kitekraft. The toolchain uses octave to run the optimisation with the CFD analysis using simplefoam in OpenFOAM. My work focused on incorporating geometric design parameterisation, so allowing the toolchain to create its own aerofoil geometry that it can modify to optimise its performance. Then to update the toolchain to allow it to be run on high performance computing  using that to optimise the performance of a multi element aerofoil for use in an airborne wind energy system.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain ===&lt;br /&gt;
The optimisation algorithm used is the covariance matrix adaptation evolution strategy (CMA-ES) implemented in Octave by Nikolaus Hansen. It creates a set of parameters to be evaluated by the objective function, it passes these parameters along with some static inputs to the objective function.&lt;br /&gt;
[[File:overviewFlowChart.png|300px|centre]]&lt;br /&gt;
&lt;br /&gt;
The objective function takes a set of input parameters from the optimiser and returns their performance. The main update to the toolchain was the creation of a new objective function that implemented geometry parameterisation. It uses Bezier Parsec parameterisation to create the airfoil geometry. This works by creating a thickness and camber curve from 2 Bezier curves each. The control points of the Bezier curves are calculated from a set of geometry related input parameters such as thickness, the inside angle of the trailing edge and the radius of the leading edge. The curves are processed to turn them into the coordinates of the airfoil and then saved as an STL file to be used in OpenFoam. The objective function runs some checks on the geometry to ensure the elements are not intersecting or the parameterisation created impossible geometry.&lt;br /&gt;
&lt;br /&gt;
==== Interactive Bezier Parsec Aerofoil Generator ====&lt;br /&gt;
&amp;lt;div id=&amp;quot;aerofoil-tool&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
The objective then runs the CFD analysis in Openfoam and returns the coefficient of lift calculated. The CFD simulation uses a tool called SnappyHexMesh that creates a mesh with automatic refinement around the aerofoils, including creating surface layers on the aerofoil to ensure the simulation correctly models the boundary layer. The model uses the SimpleFOAM soliving utility, it is a steady state, incompressible flow solver using the SIMPLE (Semi-Implicit Method for Pressure Linked Equations) algorithm. The k-ω SST turbulence model is also used with the solver.&lt;br /&gt;
[[File:Mesh.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
Through the irish centre for high end computing, access was obtained to the national supercomputer which is currently hosted on the meluxina supercomputer in Luxembourg. Some updates to the toolchain had to be made to allow it to be run on the supercomputer such as changing the utilities used to create the STL files.&lt;br /&gt;
&lt;br /&gt;
To take advantage of the 256 threads on each compute node, the toolchain was updated to allow it to run 32 instances of the objective function simultaneously. It does this by using a wrapper function to create multiple instances of the objective function for each generation of the optisation. Each instance of the objective function has 8 threads that it can use to run the CFD solver in parallel. The CFD domain is split into 8 subdomains that can be meshed and solved on their own thread.&lt;br /&gt;
&lt;br /&gt;
[[File:Parallelisation.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
=== Results ===&lt;br /&gt;
I ran multiple optimisation runs with various sets of input parameters available to the toolchain.&lt;br /&gt;
&lt;br /&gt;
The first optimisation that I ran was optimising the flap angle and position with a fixed main angle of attack. The optimiser trended towards geometry that created unsteady flow behind the flap that the solver was unable to converge on invalidating the results. This was overcome by adding penalties to the objective function for when the geometry does not converge. This allowed the optimiser to converge on this geometry with a lift coefficient of 2.2.&lt;br /&gt;
&lt;br /&gt;
[[File:FlapA.png|500px|centre]]&lt;br /&gt;
[[File:InputsFlapA.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
The main element angle of attack was added as an additional degree of freedom to the problem. The toolchain originally converged on a geometry with the flap in front of the main element and with a lower &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt;. This shows that CMAES is susceptible to selecting the wrong initial basin in its search and can struggle to switch. By increasing the optimisers population size and more careful constraints the optimiser created the geometry shown which has a much higher &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt; of 2.6. &lt;br /&gt;
&lt;br /&gt;
[[File:MainAflapAP.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
I ran the toolchain to optimise the shape of the main aerofoil and the flap angle and position. It resulted in a design with a coefficient of lift of 3.3 and a relatively high coefficient of drag of 0.16. It has arranged the elements in a similar way to the previous optimisation but it has created the very blunt trailing edge which helps develop the wake. In some optimisations I ran for the glide ratio it instead created an extremely sharp trailing edge.&lt;br /&gt;
&lt;br /&gt;
[[File:4882velocity.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
The flow at the top of the leading edge is very fast at more than 2.5 times the freestream velocity, it also has a bump in the camber line on the main element where the flow detaches from the element on the top surface. The gap is between the two elements is slightly bigger than the previous optimisation resulting in a thicker flow over the flap.&lt;br /&gt;
&lt;br /&gt;
When the parameterisation of the main element shape was included in the optimisation, it created a very blunt trailing edge by increasing the gap between the trailing edge of the thickness curve and the 𝑋 axis. This parameter is not intended as a method of creating a blunt trailing edge but to prevent zero thickness on the trailing edge which can cause issues in some applications. A better method would be to incorporate some additional parameterisation system on the trailing edge as it is clear it has a significant impact on the performance of the geometry. The current optimisation is restricted to a flat surface on the trailing edge but the optimal design may actually have a more rounded profile or complex profile which is not possible with Bezier Parsec Parameterisation.&lt;br /&gt;
&lt;br /&gt;
When the trailing edge thickness was decreased it created unsteady flow. This indicates that the thicker trailing edge may also be allowing the aerofoil to create a high coefficient of lift while avoiding creating an unsteady flow. It does this by having a sharp edge at the end of the upper surface that causes keeps the flow from detaching until it reaches this corner. The earlier detaching of flow with the thin trailing edge results in a more horizontal wake This wake then isn’t able to keep the flow from the gap between the elements pressed along the flap. This then causes that flow to separate from the flap creating the instability. This shows that the more downward wake of the thick trailing edge main element helps ensure that the flow through the gap remains attached to the flap, allowing the flap to be at a more aggressive angle while avoiding unstable flow, increasing the coefficient of lift.&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=56</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.js</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=56"/>
		<updated>2026-09-23T10:27:02Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;(function () {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    const ANGLE_MODE = &#039;degrees&#039;;&lt;br /&gt;
&lt;br /&gt;
    const PARAMS = [&lt;br /&gt;
        { key: &#039;xt&#039;,      label: &#039;xₜ — thickness join&#039;,   min: 0.05, max: 0.95, step: 0.005, value: 0.25 },&lt;br /&gt;
        { key: &#039;yt&#039;,      label: &#039;yₜ — thickness join&#039;,   min: 0.00, max: 0.15, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;xc&#039;,      label: &#039;x꜀ — camber join&#039;,      min: 0.05, max: 0.95, step: 0.005, value: 0.40 },&lt;br /&gt;
        { key: &#039;yc&#039;,      label: &#039;y꜀ — camber join&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.02 },&lt;br /&gt;
        { key: &#039;rle&#039;,     label: &#039;rₗₑ — leading-edge radius&#039;, min: 0.001, max: 0.20, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;dzte&#039;,    label: &#039;Δzₜₑ — TE thickness offset&#039;, min: -0.10, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;zte&#039;,     label: &#039;zₜₑ — TE camber&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;betate&#039;,  label: &#039;βₜₑ — TE thickness angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;gammale&#039;, label: &#039;γₗₑ — LE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 5 },&lt;br /&gt;
        { key: &#039;alphate&#039;, label: &#039;αₜₑ — TE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;b0&#039;,      label: &#039;b₀&#039;,                  min: 0.00, max: 0.30, step: 0.005, value: 0.05 },&lt;br /&gt;
        { key: &#039;b2&#039;,      label: &#039;b₂&#039;,                  min: 0.00, max: 0.60, step: 0.005, value: 0.20 },&lt;br /&gt;
        { key: &#039;b8&#039;,      label: &#039;b₈&#039;,                  min: 0.00, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;b15&#039;,     label: &#039;b₁₅&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 },&lt;br /&gt;
        { key: &#039;b17&#039;,     label: &#039;b₁₇&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 }&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    function toRadians(angle) {&lt;br /&gt;
        return ANGLE_MODE === &#039;degrees&#039; ? angle * Math.PI / 180 : angle;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function cot(angle) {&lt;br /&gt;
        return 1 / Math.tan(angle);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function bezier(points, u) {&lt;br /&gt;
        // General Bernstein-form Bezier evaluation. This exactly covers the&lt;br /&gt;
        // cubic (4-point) and quartic (5-point) curves used by the MATLAB code.&lt;br /&gt;
        const n = points.length - 1;&lt;br /&gt;
        let value = 0;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= n; i++) {&lt;br /&gt;
            let binomial = 1;&lt;br /&gt;
            for (let j = 1; j &amp;lt;= i; j++) {&lt;br /&gt;
                binomial *= (n - j + 1) / j;&lt;br /&gt;
            }&lt;br /&gt;
            value += binomial * Math.pow(1 - u, n - i) * Math.pow(u, i) * points[i];&lt;br /&gt;
        }&lt;br /&gt;
        return value;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function sampleBezier(points, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            const u = count === 1 ? 0 : i / (count - 1);&lt;br /&gt;
            values[i] = bezier(points, u);&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function linspace(start, stop, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        if (count === 1) {&lt;br /&gt;
            values[0] = start;&lt;br /&gt;
            return values;&lt;br /&gt;
        }&lt;br /&gt;
        const step = (stop - start) / (count - 1);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            values[i] = start + i * step;&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function buildGeometry(v) {&lt;br /&gt;
        const beta = toRadians(v.betate);&lt;br /&gt;
        const gamma = toRadians(v.gammale);&lt;br /&gt;
        const alpha = toRadians(v.alphate);&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CONTROL POINTS ----&lt;br /&gt;
        const xLeadEdgeThick = [0, 0, -3 * v.b8 * v.b8 / (2 * v.rle), v.xt];&lt;br /&gt;
        const yLeadEdgeThick = [0, v.b8, v.yt, v.yt];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeThick = [&lt;br /&gt;
            v.xt,&lt;br /&gt;
            (7 * v.xt + 9 * v.b8 * v.b8 / (2 * v.rle)) / 4,&lt;br /&gt;
            3 * v.xt + 15 * v.b8 * v.b8 / (4 * v.rle),&lt;br /&gt;
            v.b15,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeThick = [&lt;br /&gt;
            v.yt,&lt;br /&gt;
            v.yt,&lt;br /&gt;
            (v.yt + v.b8) / 2,&lt;br /&gt;
            v.dzte + (1 - v.b15) * Math.tan(beta),&lt;br /&gt;
            v.dzte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const xLeadEdgeCamber = [0, v.b0, v.b2, v.xc];&lt;br /&gt;
        const yLeadEdgeCamber = [0, v.b0 * Math.tan(gamma), v.yc, v.yc];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeCamber = [&lt;br /&gt;
            v.xc,&lt;br /&gt;
            (3 * v.xc - v.yc * cot(gamma)) / 2,&lt;br /&gt;
            (-8 * v.yc * cot(gamma) + 13 * v.xc) / 6,&lt;br /&gt;
            v.b17,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeCamber = [&lt;br /&gt;
            v.yc,&lt;br /&gt;
            v.yc,&lt;br /&gt;
            5 * v.yc / 6,&lt;br /&gt;
            v.zte - (1 - v.b17) * Math.tan(alpha),&lt;br /&gt;
            v.zte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CURVES ----&lt;br /&gt;
        const n = 100;&lt;br /&gt;
        const u = linspace(0, 1, n);&lt;br /&gt;
&lt;br /&gt;
        const thickLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const thickTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        const thickCurve = {&lt;br /&gt;
            x: thickLeadCurve.x.concat(thickTrailCurve.x),&lt;br /&gt;
            y: thickLeadCurve.y.concat(thickTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
        const camberCurve = {&lt;br /&gt;
            x: camberLeadCurve.x.concat(camberTrailCurve.x),&lt;br /&gt;
            y: camberLeadCurve.y.concat(camberTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // ---- GEOMETRY VALIDITY CHECK ----&lt;br /&gt;
        let negThick = false;&lt;br /&gt;
        let thickBounds = false;&lt;br /&gt;
        let camberBounds = false;&lt;br /&gt;
        let overHung = false;&lt;br /&gt;
&lt;br /&gt;
        for (let i = 0; i &amp;lt; thickCurve.x.length; i++) {&lt;br /&gt;
            if (thickCurve.y[i] &amp;lt; 0) negThick = true;&lt;br /&gt;
            if (thickCurve.x[i] &amp;lt; 0 || thickCurve.x[i] &amp;gt; 1) thickBounds = true;&lt;br /&gt;
            if (camberCurve.x[i] &amp;lt; 0 || camberCurve.x[i] &amp;gt; 1) camberBounds = true;&lt;br /&gt;
            if (i &amp;gt; 0 &amp;amp;&amp;amp; (thickCurve.x[i] &amp;lt; thickCurve.x[i - 1] || camberCurve.x[i] &amp;lt; camberCurve.x[i - 1])) {&lt;br /&gt;
                overHung = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const validGeom = !(negThick || thickBounds || camberBounds || overHung);&lt;br /&gt;
&lt;br /&gt;
        // ---- INTERPOLATION ----&lt;br /&gt;
        // MATLAB uses interp1(..., &#039;linear&#039;) after checking for overhang.&lt;br /&gt;
        // For a valid monotonic curve, sorting by x gives the same result and&lt;br /&gt;
        // also lets us keep rendering something useful while the user explores&lt;br /&gt;
        // invalid parameter combinations.&lt;br /&gt;
        const x = linspace(0, 1, n * 2);&lt;br /&gt;
        const interpThickCurve = interpolateSorted(thickCurve.x, thickCurve.y, x);&lt;br /&gt;
        const interpCamberCurve = interpolateSorted(camberCurve.x, camberCurve.y, x);&lt;br /&gt;
&lt;br /&gt;
        const upper = new Array(x.length);&lt;br /&gt;
        const lower = new Array(x.length);&lt;br /&gt;
        for (let i = 0; i &amp;lt; x.length; i++) {&lt;br /&gt;
            upper[i] = interpCamberCurve[i] + interpThickCurve[i];&lt;br /&gt;
            lower[i] = interpCamberCurve[i] - interpThickCurve[i];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            x,&lt;br /&gt;
            upper,&lt;br /&gt;
            lower,&lt;br /&gt;
            camber: interpCamberCurve,&lt;br /&gt;
            thickness: interpThickCurve,&lt;br /&gt;
            thickCurve,&lt;br /&gt;
            camberCurve,&lt;br /&gt;
            thickLeadCurve,&lt;br /&gt;
            thickTrailCurve,&lt;br /&gt;
            camberLeadCurve,&lt;br /&gt;
            camberTrailCurve,&lt;br /&gt;
            controlPoints: {&lt;br /&gt;
                thicknessLead: zipPoints(xLeadEdgeThick, yLeadEdgeThick),&lt;br /&gt;
                thicknessTrail: zipPoints(xTrailEdgeThick, yTrailEdgeThick),&lt;br /&gt;
                camberLead: zipPoints(xLeadEdgeCamber, yLeadEdgeCamber),&lt;br /&gt;
                camberTrail: zipPoints(xTrailEdgeCamber, yTrailEdgeCamber)&lt;br /&gt;
            },&lt;br /&gt;
            validGeom,&lt;br /&gt;
            checks: { negThick, thickBounds, camberBounds, overHung }&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function zipPoints(xs, ys) {&lt;br /&gt;
        return xs.map((x, i) =&amp;gt; ({ x, y: ys[i] }));&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function interpolateSorted(xValues, yValues, xQuery) {&lt;br /&gt;
        const pairs = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; xValues.length; i++) {&lt;br /&gt;
            if (Number.isFinite(xValues[i]) &amp;amp;&amp;amp; Number.isFinite(yValues[i])) {&lt;br /&gt;
                pairs.push([xValues[i], yValues[i]]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        pairs.sort((a, b) =&amp;gt; a[0] - b[0]);&lt;br /&gt;
&lt;br /&gt;
        // Remove duplicate x values, keeping the first value, as a pragmatic&lt;br /&gt;
        // browser-side equivalent for plotting.&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        for (const pair of pairs) {&lt;br /&gt;
            if (!xs.length || Math.abs(pair[0] - xs[xs.length - 1]) &amp;gt; 1e-12) {&lt;br /&gt;
                xs.push(pair[0]);&lt;br /&gt;
                ys.push(pair[1]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return xQuery.map((q) =&amp;gt; {&lt;br /&gt;
            if (!xs.length || q &amp;lt; xs[0] || q &amp;gt; xs[xs.length - 1]) return NaN;&lt;br /&gt;
            if (xs.length === 1) return ys[0];&lt;br /&gt;
&lt;br /&gt;
            let lo = 0;&lt;br /&gt;
            let hi = xs.length - 1;&lt;br /&gt;
            while (hi - lo &amp;gt; 1) {&lt;br /&gt;
                const mid = Math.floor((lo + hi) / 2);&lt;br /&gt;
                if (xs[mid] &amp;lt;= q) lo = mid;&lt;br /&gt;
                else hi = mid;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            const dx = xs[hi] - xs[lo];&lt;br /&gt;
            const t = dx === 0 ? 0 : (q - xs[lo]) / dx;&lt;br /&gt;
            return ys[lo] + t * (ys[hi] - ys[lo]);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function fmt(value) {&lt;br /&gt;
        if (!Number.isFinite(value)) return &#039;—&#039;;&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 1) return value.toFixed(3);&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 0.1) return value.toFixed(4);&lt;br /&gt;
        return value.toFixed(5);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function createSvgElement(tag, attrs) {&lt;br /&gt;
        const ns = &#039;http://www.w3.org/2000/svg&#039;;&lt;br /&gt;
        const el = document.createElementNS(ns, tag);&lt;br /&gt;
        Object.entries(attrs).forEach(([key, value]) =&amp;gt; el.setAttribute(key, String(value)));&lt;br /&gt;
        return el;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const validPoints = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; geom.x.length; i++) {&lt;br /&gt;
            if (Number.isFinite(geom.upper[i])) validPoints.push([geom.x[i], geom.upper[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.lower[i])) validPoints.push([geom.x[i], geom.lower[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.camber[i])) validPoints.push([geom.x[i], geom.camber[i]]);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (!validPoints.length) {&lt;br /&gt;
            const text = createSvgElement(&#039;text&#039;, { x: 50, y: 50, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-empty&#039; });&lt;br /&gt;
            text.textContent = &#039;Geometry cannot be plotted for these parameters&#039;;&lt;br /&gt;
            svg.appendChild(text);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        let minY = Math.min(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        let maxY = Math.max(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {&lt;br /&gt;
            minY = -0.1;&lt;br /&gt;
            maxY = 0.1;&lt;br /&gt;
        }&lt;br /&gt;
        const span = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        const margin = span * 0.12;&lt;br /&gt;
        minY -= margin;&lt;br /&gt;
        maxY += margin;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 520;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 25;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + x * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and axes.&lt;br /&gt;
        [0, 0.25, 0.5, 0.75, 1].forEach((xVal) =&amp;gt; {&lt;br /&gt;
            const x = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: x, y1: padTop, x2: x, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = xVal.toFixed(2);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const yTicks = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTicks; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTicks;&lt;br /&gt;
            const y = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: y, x2: padLeft + plotW, y2: y, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x: padLeft - 9, y: y + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: sy(0), x2: padLeft + plotW, y2: sy(0), class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: padTop, x2: padLeft, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // X axis label.&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, { x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039; });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        // Y axis label.&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromSeries(xs, ys) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; xs.length; i++) {&lt;br /&gt;
                if (!Number.isFinite(xs[i]) || !Number.isFinite(ys[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                const command = started ? &#039;L&#039; : &#039;M&#039;;&lt;br /&gt;
                d += `${command}${sx(xs[i]).toFixed(2)},${sy(ys[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const upperPath = pathFromSeries(geom.x, geom.upper);&lt;br /&gt;
        const lowerPath = pathFromSeries([...geom.x].reverse(), [...geom.lower].reverse());&lt;br /&gt;
&lt;br /&gt;
        const profile = createSvgElement(&#039;path&#039;, { d: `${upperPath} ${lowerPath}`, class: &#039;aerofoil-profile&#039; });&lt;br /&gt;
        svg.appendChild(profile);&lt;br /&gt;
&lt;br /&gt;
        const upper = createSvgElement(&#039;path&#039;, { d: upperPath, class: &#039;aerofoil-upper&#039; });&lt;br /&gt;
        const lower = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.lower), class: &#039;aerofoil-lower&#039; });&lt;br /&gt;
        const camber = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.camber), class: &#039;aerofoil-camber&#039; });&lt;br /&gt;
        svg.appendChild(upper);&lt;br /&gt;
        svg.appendChild(lower);&lt;br /&gt;
        svg.appendChild(camber);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderConstructionPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const allCurves = [&lt;br /&gt;
            geom.thickLeadCurve,&lt;br /&gt;
            geom.thickTrailCurve,&lt;br /&gt;
            geom.camberLeadCurve,&lt;br /&gt;
            geom.camberTrailCurve&lt;br /&gt;
        ];&lt;br /&gt;
        const allPoints = [&lt;br /&gt;
            ...geom.controlPoints.thicknessLead,&lt;br /&gt;
            ...geom.controlPoints.thicknessTrail,&lt;br /&gt;
            ...geom.controlPoints.camberLead,&lt;br /&gt;
            ...geom.controlPoints.camberTrail&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const finite = (v) =&amp;gt; Number.isFinite(v);&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        allCurves.forEach((curve) =&amp;gt; {&lt;br /&gt;
            curve.x.forEach((x) =&amp;gt; { if (finite(x)) xs.push(x); });&lt;br /&gt;
            curve.y.forEach((y) =&amp;gt; { if (finite(y)) ys.push(y); });&lt;br /&gt;
        });&lt;br /&gt;
        allPoints.forEach((p) =&amp;gt; {&lt;br /&gt;
            if (finite(p.x)) xs.push(p.x);&lt;br /&gt;
            if (finite(p.y)) ys.push(p.y);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        if (!xs.length || !ys.length) return;&lt;br /&gt;
&lt;br /&gt;
        let minX = Math.min(...xs);&lt;br /&gt;
        let maxX = Math.max(...xs);&lt;br /&gt;
        let minY = Math.min(...ys);&lt;br /&gt;
        let maxY = Math.max(...ys);&lt;br /&gt;
        const spanX = Math.max(maxX - minX, 0.2);&lt;br /&gt;
        const spanY = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        minX -= 0.08 * spanX;&lt;br /&gt;
        maxX += 0.08 * spanX;&lt;br /&gt;
        minY -= 0.12 * spanY;&lt;br /&gt;
        maxY += 0.12 * spanY;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 500;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 35;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + (x - minX) / (maxX - minX) * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and x/c tick labels.&lt;br /&gt;
        const xTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= xTickCount; i++) {&lt;br /&gt;
            const xVal = minX + (maxX - minX) * i / xTickCount;&lt;br /&gt;
            const px = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: px, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(xVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const yTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTickCount; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTickCount;&lt;br /&gt;
            const py = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: padLeft - 9, y: py + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (minX &amp;lt;= 0 &amp;amp;&amp;amp; maxX &amp;gt;= 0) {&lt;br /&gt;
            const px = sx(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
        if (minY &amp;lt;= 0 &amp;amp;&amp;amp; maxY &amp;gt;= 0) {&lt;br /&gt;
            const py = sy(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;&lt;br /&gt;
        });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromCurve(curve) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; curve.x.length; i++) {&lt;br /&gt;
                if (!finite(curve.x[i]) || !finite(curve.y[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                d += `${started ? &#039;L&#039; : &#039;M&#039;}${sx(curve.x[i]).toFixed(2)},${sy(curve.y[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Curves.&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickLeadCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickTrailCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberLeadCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberTrailCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // Control polygons and labelled points.&lt;br /&gt;
        function drawControlSet(points, prefix, lineClass, pointClass) {&lt;br /&gt;
            const polygonPoints = points&lt;br /&gt;
                .filter((p) =&amp;gt; finite(p.x) &amp;amp;&amp;amp; finite(p.y))&lt;br /&gt;
                .map((p) =&amp;gt; `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`)&lt;br /&gt;
                .join(&#039; &#039;);&lt;br /&gt;
            if (polygonPoints) {&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;polyline&#039;, {&lt;br /&gt;
                    points: polygonPoints,&lt;br /&gt;
                    class: lineClass,&lt;br /&gt;
                    fill: &#039;none&#039;&lt;br /&gt;
                }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            points.forEach((p, i) =&amp;gt; {&lt;br /&gt;
                if (!finite(p.x) || !finite(p.y)) return;&lt;br /&gt;
                const px = sx(p.x);&lt;br /&gt;
                const py = sy(p.y);&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;circle&#039;, {&lt;br /&gt;
                    cx: px, cy: py, r: 4, class: pointClass&lt;br /&gt;
                }));&lt;br /&gt;
&lt;br /&gt;
                const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                    x: px + 7,&lt;br /&gt;
                    y: py - 7,&lt;br /&gt;
                    class: &#039;aerofoil-control-point-label&#039;&lt;br /&gt;
                });&lt;br /&gt;
                label.textContent = `${prefix}${i}`;&lt;br /&gt;
                svg.appendChild(label);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessLead, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessTrail, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberLead, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberTrail, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Legend.&lt;br /&gt;
        const legend = createSvgElement(&#039;g&#039;, { transform: `translate(${W - 220}, 18)` });&lt;br /&gt;
        const tLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 0, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        tLegend.textContent = &#039;Thickness curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(tLegend);&lt;br /&gt;
        const cLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 20, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        cLegend.textContent = &#039;Camber curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(cLegend);&lt;br /&gt;
        svg.appendChild(legend);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function setup(root) {&lt;br /&gt;
        if (root.dataset.aerofoilInitialised === &#039;1&#039;) return;&lt;br /&gt;
        root.dataset.aerofoilInitialised = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
        const controls = document.createElement(&#039;div&#039;);&lt;br /&gt;
        controls.className = &#039;aerofoil-controls&#039;;&lt;br /&gt;
&lt;br /&gt;
        const values = {};&lt;br /&gt;
        const inputs = {};&lt;br /&gt;
        const valueLabels = {};&lt;br /&gt;
&lt;br /&gt;
        PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
            const row = document.createElement(&#039;div&#039;);&lt;br /&gt;
            row.className = &#039;aerofoil-control&#039;;&lt;br /&gt;
&lt;br /&gt;
            const label = document.createElement(&#039;label&#039;);&lt;br /&gt;
            label.className = &#039;aerofoil-control-label&#039;;&lt;br /&gt;
            label.htmlFor = `aerofoil-${p.key}`;&lt;br /&gt;
            label.textContent = p.label;&lt;br /&gt;
&lt;br /&gt;
            const range = document.createElement(&#039;input&#039;);&lt;br /&gt;
            range.type = &#039;range&#039;;&lt;br /&gt;
            range.id = `aerofoil-${p.key}`;&lt;br /&gt;
            range.min = p.min;&lt;br /&gt;
            range.max = p.max;&lt;br /&gt;
            range.step = p.step;&lt;br /&gt;
            range.value = p.value;&lt;br /&gt;
&lt;br /&gt;
            const number = document.createElement(&#039;input&#039;);&lt;br /&gt;
            number.type = &#039;number&#039;;&lt;br /&gt;
            number.min = p.min;&lt;br /&gt;
            number.max = p.max;&lt;br /&gt;
            number.step = p.step;&lt;br /&gt;
            number.value = p.value;&lt;br /&gt;
            number.className = &#039;aerofoil-number&#039;;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
            const setValue = (raw) =&amp;gt; {&lt;br /&gt;
                let value = Number(raw);&lt;br /&gt;
                if (!Number.isFinite(value)) value = p.value;&lt;br /&gt;
                value = Math.min(p.max, Math.max(p.min, value));&lt;br /&gt;
                range.value = value;&lt;br /&gt;
                number.value = value;&lt;br /&gt;
                values[p.key] = value;&lt;br /&gt;
&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            range.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(range.value));&lt;br /&gt;
            number.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(number.value));&lt;br /&gt;
&lt;br /&gt;
            setValue(p.value);&lt;br /&gt;
            inputs[p.key] = range;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
            row.appendChild(label);&lt;br /&gt;
            row.appendChild(range);&lt;br /&gt;
            row.appendChild(number);&lt;br /&gt;
            controls.appendChild(row);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const actions = document.createElement(&#039;div&#039;);&lt;br /&gt;
        actions.className = &#039;aerofoil-actions&#039;;&lt;br /&gt;
&lt;br /&gt;
        const reset = document.createElement(&#039;button&#039;);&lt;br /&gt;
        reset.type = &#039;button&#039;;&lt;br /&gt;
        reset.className = &#039;mw-ui-button&#039;;&lt;br /&gt;
        reset.textContent = &#039;Reset&#039;;&lt;br /&gt;
        reset.addEventListener(&#039;click&#039;, () =&amp;gt; {&lt;br /&gt;
            PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
                inputs[p.key].value = p.value;&lt;br /&gt;
                inputs[p.key].dispatchEvent(new Event(&#039;input&#039;, { bubbles: true }));&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        actions.appendChild(reset);&lt;br /&gt;
&lt;br /&gt;
        const plotWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        plotWrap.className = &#039;aerofoil-plot-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const svg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 520&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Interactive aerofoil plot&#039;&lt;br /&gt;
        });&lt;br /&gt;
        plotWrap.appendChild(svg);&lt;br /&gt;
&lt;br /&gt;
        const constructionTitle = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionTitle.className = &#039;aerofoil-subplot-title&#039;;&lt;br /&gt;
        constructionTitle.textContent = &#039;Bezier thickness and camber construction&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionWrap.className = &#039;aerofoil-plot-wrap aerofoil-construction-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionSvg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 500&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Bezier thickness and camber curves with control points&#039;&lt;br /&gt;
        });&lt;br /&gt;
        constructionWrap.appendChild(constructionSvg);&lt;br /&gt;
&lt;br /&gt;
        const status = document.createElement(&#039;div&#039;);&lt;br /&gt;
        status.className = &#039;aerofoil-status&#039;;&lt;br /&gt;
&lt;br /&gt;
        const checks = document.createElement(&#039;div&#039;);&lt;br /&gt;
        checks.className = &#039;aerofoil-checks&#039;;&lt;br /&gt;
&lt;br /&gt;
        const unitNote = document.createElement(&#039;div&#039;);&lt;br /&gt;
        unitNote.className = &#039;aerofoil-note&#039;;&lt;br /&gt;
        unitNote.textContent = `Angles are entered in ${ANGLE_MODE}; calculations follow the MATLAB tan/cot convention.`;&lt;br /&gt;
&lt;br /&gt;
        root.appendChild(controls);&lt;br /&gt;
        root.appendChild(actions);&lt;br /&gt;
        root.appendChild(plotWrap);&lt;br /&gt;
        root.appendChild(constructionTitle);&lt;br /&gt;
        root.appendChild(constructionWrap);&lt;br /&gt;
        root.appendChild(status);&lt;br /&gt;
        root.appendChild(checks);&lt;br /&gt;
        root.appendChild(unitNote);&lt;br /&gt;
&lt;br /&gt;
        const update = () =&amp;gt; {&lt;br /&gt;
            try {&lt;br /&gt;
                const geom = buildGeometry(values);&lt;br /&gt;
                renderPlot(svg, geom);&lt;br /&gt;
                renderConstructionPlot(constructionSvg, geom);&lt;br /&gt;
&lt;br /&gt;
                status.className = `aerofoil-status ${geom.validGeom ? &#039;is-valid&#039; : &#039;is-invalid&#039;}`;&lt;br /&gt;
                status.textContent = geom.validGeom ? &#039;Geometry valid&#039; : &#039;Geometry invalid — see checks below.&#039;;&lt;br /&gt;
&lt;br /&gt;
                checks.innerHTML = &#039;&#039;;&lt;br /&gt;
                const messages = [&lt;br /&gt;
                    [&#039;Negative thickness&#039;, geom.checks.negThick],&lt;br /&gt;
                    [&#039;Thickness x outside [0, 1]&#039;, geom.checks.thickBounds],&lt;br /&gt;
                    [&#039;Camber x outside [0, 1]&#039;, geom.checks.camberBounds],&lt;br /&gt;
                    [&#039;Overhang / decreasing x&#039;, geom.checks.overHung]&lt;br /&gt;
                ];&lt;br /&gt;
                messages.forEach(([name, failed]) =&amp;gt; {&lt;br /&gt;
                    const item = document.createElement(&#039;span&#039;);&lt;br /&gt;
                    item.className = failed ? &#039;fail&#039; : &#039;pass&#039;;&lt;br /&gt;
                    item.textContent = `${failed ? &#039;✗&#039; : &#039;✓&#039;} ${name}`;&lt;br /&gt;
                    checks.appendChild(item);&lt;br /&gt;
                });&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                status.className = &#039;aerofoil-status is-invalid&#039;;&lt;br /&gt;
                status.textContent = `Geometry error: ${error.message}`;&lt;br /&gt;
                console.error(&#039;Aerofoil Designer:&#039;, error);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        Object.keys(inputs).forEach((key) =&amp;gt; {&lt;br /&gt;
            inputs[key].addEventListener(&#039;input&#039;, update);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        update();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function decimalPlaces(step) {&lt;br /&gt;
        const text = String(step);&lt;br /&gt;
        if (!text.includes(&#039;.&#039;)) return 0;&lt;br /&gt;
        return text.split(&#039;.&#039;)[1].length;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        document.querySelectorAll(&#039;#aerofoil-tool&#039;).forEach(setup);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (window.mw &amp;amp;&amp;amp; mw.hook) {&lt;br /&gt;
        mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
        init();&lt;br /&gt;
    } else {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, init);&lt;br /&gt;
    }&lt;br /&gt;
}());&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=55</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.css</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=55"/>
		<updated>2026-09-23T10:26:20Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Interactive Bezier aerofoil designer */&lt;br /&gt;
#aerofoil-tool {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 1rem 0;&lt;br /&gt;
    font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-controls {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));&lt;br /&gt;
    gap: 0.55rem 1rem;&lt;br /&gt;
    margin-bottom: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: 150px minmax(120px, 1fr) 84px 68px;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    gap: 0.55rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-label {&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control input[type=&#039;range&#039;] {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-number {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
.aerofoil-actions {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    margin: 0.6rem 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
    border: 1px solid #a2a9b1;&lt;br /&gt;
    background: #fff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap svg {&lt;br /&gt;
    display: block;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-grid {&lt;br /&gt;
    stroke: #eaecf0;&lt;br /&gt;
    stroke-width: 1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-axis {&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-profile {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #202122;&lt;br /&gt;
    stroke-width: 1.8;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-upper,&lt;br /&gt;
.aerofoil-lower {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.4;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.3;&lt;br /&gt;
    stroke-dasharray: 6 5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-tick,&lt;br /&gt;
.aerofoil-axis-label {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 13px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-empty {&lt;br /&gt;
    fill: #a2a9b1;&lt;br /&gt;
    font-size: 20px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    padding: 0.6rem 0.8rem;&lt;br /&gt;
    border-left: 4px solid;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-valid {&lt;br /&gt;
    border-left-color: #14866d;&lt;br /&gt;
    background: #f1fff8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-invalid {&lt;br /&gt;
    border-left-color: #b32424;&lt;br /&gt;
    background: #fff4f4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    gap: 0.45rem 1rem;&lt;br /&gt;
    margin-top: 0.6rem;&lt;br /&gt;
    font-size: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .pass {&lt;br /&gt;
    color: #14866d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .fail {&lt;br /&gt;
    color: #b32424;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-note {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    color: #72777d;&lt;br /&gt;
    font-size: 0.85rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 700px) {&lt;br /&gt;
    .aerofoil-controls {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-control {&lt;br /&gt;
        grid-template-columns: 120px 1fr 78px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-value {&lt;br /&gt;
        display: none;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-subplot-title {&lt;br /&gt;
    margin: 0.8rem 0 0.35rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    font-size: 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-construction-wrap {&lt;br /&gt;
    margin-top: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-curve {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-curve {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 2.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-control {&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 1.1;&lt;br /&gt;
    stroke-dasharray: 4 4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-control {&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 1.1;&lt;br /&gt;
    stroke-dasharray: 4 4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-point {&lt;br /&gt;
    fill: #fff;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-point {&lt;br /&gt;
    fill: #fff;&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-point-label {&lt;br /&gt;
    fill: #202122;&lt;br /&gt;
    font-size: 11px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
    paint-order: stroke;&lt;br /&gt;
    stroke: #fff;&lt;br /&gt;
    stroke-width: 3px;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-legend-text {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 12px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=54</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.js</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=54"/>
		<updated>2026-09-23T10:26:10Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;(function () {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    const ANGLE_MODE = &#039;degrees&#039;;&lt;br /&gt;
&lt;br /&gt;
    const PARAMS = [&lt;br /&gt;
        { key: &#039;xt&#039;,      label: &#039;xₜ — thickness join&#039;,   min: 0.05, max: 0.95, step: 0.005, value: 0.25 },&lt;br /&gt;
        { key: &#039;yt&#039;,      label: &#039;yₜ — thickness join&#039;,   min: 0.00, max: 0.15, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;xc&#039;,      label: &#039;x꜀ — camber join&#039;,      min: 0.05, max: 0.95, step: 0.005, value: 0.40 },&lt;br /&gt;
        { key: &#039;yc&#039;,      label: &#039;y꜀ — camber join&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.02 },&lt;br /&gt;
        { key: &#039;rle&#039;,     label: &#039;rₗₑ — leading-edge radius&#039;, min: 0.001, max: 0.20, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;dzte&#039;,    label: &#039;Δzₜₑ — TE thickness offset&#039;, min: -0.10, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;zte&#039;,     label: &#039;zₜₑ — TE camber&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;betate&#039;,  label: &#039;βₜₑ — TE thickness angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;gammale&#039;, label: &#039;γₗₑ — LE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 5 },&lt;br /&gt;
        { key: &#039;alphate&#039;, label: &#039;αₜₑ — TE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;b0&#039;,      label: &#039;b₀&#039;,                  min: 0.00, max: 0.30, step: 0.005, value: 0.05 },&lt;br /&gt;
        { key: &#039;b2&#039;,      label: &#039;b₂&#039;,                  min: 0.00, max: 0.60, step: 0.005, value: 0.20 },&lt;br /&gt;
        { key: &#039;b8&#039;,      label: &#039;b₈&#039;,                  min: 0.00, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;b15&#039;,     label: &#039;b₁₅&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 },&lt;br /&gt;
        { key: &#039;b17&#039;,     label: &#039;b₁₇&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 }&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    function toRadians(angle) {&lt;br /&gt;
        return ANGLE_MODE === &#039;degrees&#039; ? angle * Math.PI / 180 : angle;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function cot(angle) {&lt;br /&gt;
        return 1 / Math.tan(angle);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function bezier(points, u) {&lt;br /&gt;
        // General Bernstein-form Bezier evaluation. This exactly covers the&lt;br /&gt;
        // cubic (4-point) and quartic (5-point) curves used by the MATLAB code.&lt;br /&gt;
        const n = points.length - 1;&lt;br /&gt;
        let value = 0;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= n; i++) {&lt;br /&gt;
            let binomial = 1;&lt;br /&gt;
            for (let j = 1; j &amp;lt;= i; j++) {&lt;br /&gt;
                binomial *= (n - j + 1) / j;&lt;br /&gt;
            }&lt;br /&gt;
            value += binomial * Math.pow(1 - u, n - i) * Math.pow(u, i) * points[i];&lt;br /&gt;
        }&lt;br /&gt;
        return value;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function sampleBezier(points, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            const u = count === 1 ? 0 : i / (count - 1);&lt;br /&gt;
            values[i] = bezier(points, u);&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function linspace(start, stop, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        if (count === 1) {&lt;br /&gt;
            values[0] = start;&lt;br /&gt;
            return values;&lt;br /&gt;
        }&lt;br /&gt;
        const step = (stop - start) / (count - 1);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            values[i] = start + i * step;&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function buildGeometry(v) {&lt;br /&gt;
        const beta = toRadians(v.betate);&lt;br /&gt;
        const gamma = toRadians(v.gammale);&lt;br /&gt;
        const alpha = toRadians(v.alphate);&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CONTROL POINTS ----&lt;br /&gt;
        const xLeadEdgeThick = [0, 0, -3 * v.b8 * v.b8 / (2 * v.rle), v.xt];&lt;br /&gt;
        const yLeadEdgeThick = [0, v.b8, v.yt, v.yt];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeThick = [&lt;br /&gt;
            v.xt,&lt;br /&gt;
            (7 * v.xt + 9 * v.b8 * v.b8 / (2 * v.rle)) / 4,&lt;br /&gt;
            3 * v.xt + 15 * v.b8 * v.b8 / (4 * v.rle),&lt;br /&gt;
            v.b15,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeThick = [&lt;br /&gt;
            v.yt,&lt;br /&gt;
            v.yt,&lt;br /&gt;
            (v.yt + v.b8) / 2,&lt;br /&gt;
            v.dzte + (1 - v.b15) * Math.tan(beta),&lt;br /&gt;
            v.dzte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const xLeadEdgeCamber = [0, v.b0, v.b2, v.xc];&lt;br /&gt;
        const yLeadEdgeCamber = [0, v.b0 * Math.tan(gamma), v.yc, v.yc];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeCamber = [&lt;br /&gt;
            v.xc,&lt;br /&gt;
            (3 * v.xc - v.yc * cot(gamma)) / 2,&lt;br /&gt;
            (-8 * v.yc * cot(gamma) + 13 * v.xc) / 6,&lt;br /&gt;
            v.b17,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeCamber = [&lt;br /&gt;
            v.yc,&lt;br /&gt;
            v.yc,&lt;br /&gt;
            5 * v.yc / 6,&lt;br /&gt;
            v.zte - (1 - v.b17) * Math.tan(alpha),&lt;br /&gt;
            v.zte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CURVES ----&lt;br /&gt;
        const n = 100;&lt;br /&gt;
        const u = linspace(0, 1, n);&lt;br /&gt;
&lt;br /&gt;
        const thickLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const thickTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        const thickCurve = {&lt;br /&gt;
            x: thickLeadCurve.x.concat(thickTrailCurve.x),&lt;br /&gt;
            y: thickLeadCurve.y.concat(thickTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
        const camberCurve = {&lt;br /&gt;
            x: camberLeadCurve.x.concat(camberTrailCurve.x),&lt;br /&gt;
            y: camberLeadCurve.y.concat(camberTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // ---- GEOMETRY VALIDITY CHECK ----&lt;br /&gt;
        let negThick = false;&lt;br /&gt;
        let thickBounds = false;&lt;br /&gt;
        let camberBounds = false;&lt;br /&gt;
        let overHung = false;&lt;br /&gt;
&lt;br /&gt;
        for (let i = 0; i &amp;lt; thickCurve.x.length; i++) {&lt;br /&gt;
            if (thickCurve.y[i] &amp;lt; 0) negThick = true;&lt;br /&gt;
            if (thickCurve.x[i] &amp;lt; 0 || thickCurve.x[i] &amp;gt; 1) thickBounds = true;&lt;br /&gt;
            if (camberCurve.x[i] &amp;lt; 0 || camberCurve.x[i] &amp;gt; 1) camberBounds = true;&lt;br /&gt;
            if (i &amp;gt; 0 &amp;amp;&amp;amp; (thickCurve.x[i] &amp;lt; thickCurve.x[i - 1] || camberCurve.x[i] &amp;lt; camberCurve.x[i - 1])) {&lt;br /&gt;
                overHung = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const validGeom = !(negThick || thickBounds || camberBounds || overHung);&lt;br /&gt;
&lt;br /&gt;
        // ---- INTERPOLATION ----&lt;br /&gt;
        // MATLAB uses interp1(..., &#039;linear&#039;) after checking for overhang.&lt;br /&gt;
        // For a valid monotonic curve, sorting by x gives the same result and&lt;br /&gt;
        // also lets us keep rendering something useful while the user explores&lt;br /&gt;
        // invalid parameter combinations.&lt;br /&gt;
        const x = linspace(0, 1, n * 2);&lt;br /&gt;
        const interpThickCurve = interpolateSorted(thickCurve.x, thickCurve.y, x);&lt;br /&gt;
        const interpCamberCurve = interpolateSorted(camberCurve.x, camberCurve.y, x);&lt;br /&gt;
&lt;br /&gt;
        const upper = new Array(x.length);&lt;br /&gt;
        const lower = new Array(x.length);&lt;br /&gt;
        for (let i = 0; i &amp;lt; x.length; i++) {&lt;br /&gt;
            upper[i] = interpCamberCurve[i] + interpThickCurve[i];&lt;br /&gt;
            lower[i] = interpCamberCurve[i] - interpThickCurve[i];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            x,&lt;br /&gt;
            upper,&lt;br /&gt;
            lower,&lt;br /&gt;
            camber: interpCamberCurve,&lt;br /&gt;
            thickness: interpThickCurve,&lt;br /&gt;
            thickCurve,&lt;br /&gt;
            camberCurve,&lt;br /&gt;
            thickLeadCurve,&lt;br /&gt;
            thickTrailCurve,&lt;br /&gt;
            camberLeadCurve,&lt;br /&gt;
            camberTrailCurve,&lt;br /&gt;
            controlPoints: {&lt;br /&gt;
                thicknessLead: zipPoints(xLeadEdgeThick, yLeadEdgeThick),&lt;br /&gt;
                thicknessTrail: zipPoints(xTrailEdgeThick, yTrailEdgeThick),&lt;br /&gt;
                camberLead: zipPoints(xLeadEdgeCamber, yLeadEdgeCamber),&lt;br /&gt;
                camberTrail: zipPoints(xTrailEdgeCamber, yTrailEdgeCamber)&lt;br /&gt;
            },&lt;br /&gt;
            validGeom,&lt;br /&gt;
            checks: { negThick, thickBounds, camberBounds, overHung }&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function zipPoints(xs, ys) {&lt;br /&gt;
        return xs.map((x, i) =&amp;gt; ({ x, y: ys[i] }));&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function interpolateSorted(xValues, yValues, xQuery) {&lt;br /&gt;
        const pairs = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; xValues.length; i++) {&lt;br /&gt;
            if (Number.isFinite(xValues[i]) &amp;amp;&amp;amp; Number.isFinite(yValues[i])) {&lt;br /&gt;
                pairs.push([xValues[i], yValues[i]]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        pairs.sort((a, b) =&amp;gt; a[0] - b[0]);&lt;br /&gt;
&lt;br /&gt;
        // Remove duplicate x values, keeping the first value, as a pragmatic&lt;br /&gt;
        // browser-side equivalent for plotting.&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        for (const pair of pairs) {&lt;br /&gt;
            if (!xs.length || Math.abs(pair[0] - xs[xs.length - 1]) &amp;gt; 1e-12) {&lt;br /&gt;
                xs.push(pair[0]);&lt;br /&gt;
                ys.push(pair[1]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return xQuery.map((q) =&amp;gt; {&lt;br /&gt;
            if (!xs.length || q &amp;lt; xs[0] || q &amp;gt; xs[xs.length - 1]) return NaN;&lt;br /&gt;
            if (xs.length === 1) return ys[0];&lt;br /&gt;
&lt;br /&gt;
            let lo = 0;&lt;br /&gt;
            let hi = xs.length - 1;&lt;br /&gt;
            while (hi - lo &amp;gt; 1) {&lt;br /&gt;
                const mid = Math.floor((lo + hi) / 2);&lt;br /&gt;
                if (xs[mid] &amp;lt;= q) lo = mid;&lt;br /&gt;
                else hi = mid;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            const dx = xs[hi] - xs[lo];&lt;br /&gt;
            const t = dx === 0 ? 0 : (q - xs[lo]) / dx;&lt;br /&gt;
            return ys[lo] + t * (ys[hi] - ys[lo]);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function fmt(value) {&lt;br /&gt;
        if (!Number.isFinite(value)) return &#039;—&#039;;&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 1) return value.toFixed(3);&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 0.1) return value.toFixed(4);&lt;br /&gt;
        return value.toFixed(5);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function createSvgElement(tag, attrs) {&lt;br /&gt;
        const ns = &#039;http://www.w3.org/2000/svg&#039;;&lt;br /&gt;
        const el = document.createElementNS(ns, tag);&lt;br /&gt;
        Object.entries(attrs).forEach(([key, value]) =&amp;gt; el.setAttribute(key, String(value)));&lt;br /&gt;
        return el;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const validPoints = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; geom.x.length; i++) {&lt;br /&gt;
            if (Number.isFinite(geom.upper[i])) validPoints.push([geom.x[i], geom.upper[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.lower[i])) validPoints.push([geom.x[i], geom.lower[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.camber[i])) validPoints.push([geom.x[i], geom.camber[i]]);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (!validPoints.length) {&lt;br /&gt;
            const text = createSvgElement(&#039;text&#039;, { x: 50, y: 50, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-empty&#039; });&lt;br /&gt;
            text.textContent = &#039;Geometry cannot be plotted for these parameters&#039;;&lt;br /&gt;
            svg.appendChild(text);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        let minY = Math.min(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        let maxY = Math.max(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {&lt;br /&gt;
            minY = -0.1;&lt;br /&gt;
            maxY = 0.1;&lt;br /&gt;
        }&lt;br /&gt;
        const span = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        const margin = span * 0.12;&lt;br /&gt;
        minY -= margin;&lt;br /&gt;
        maxY += margin;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 520;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 25;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + x * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and axes.&lt;br /&gt;
        [0, 0.25, 0.5, 0.75, 1].forEach((xVal) =&amp;gt; {&lt;br /&gt;
            const x = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: x, y1: padTop, x2: x, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = xVal.toFixed(2);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const yTicks = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTicks; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTicks;&lt;br /&gt;
            const y = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: y, x2: padLeft + plotW, y2: y, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x: padLeft - 9, y: y + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: sy(0), x2: padLeft + plotW, y2: sy(0), class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: padTop, x2: padLeft, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // X axis label.&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, { x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039; });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        // Y axis label.&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromSeries(xs, ys) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; xs.length; i++) {&lt;br /&gt;
                if (!Number.isFinite(xs[i]) || !Number.isFinite(ys[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                const command = started ? &#039;L&#039; : &#039;M&#039;;&lt;br /&gt;
                d += `${command}${sx(xs[i]).toFixed(2)},${sy(ys[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const upperPath = pathFromSeries(geom.x, geom.upper);&lt;br /&gt;
        const lowerPath = pathFromSeries([...geom.x].reverse(), [...geom.lower].reverse());&lt;br /&gt;
&lt;br /&gt;
        const profile = createSvgElement(&#039;path&#039;, { d: `${upperPath} ${lowerPath}`, class: &#039;aerofoil-profile&#039; });&lt;br /&gt;
        svg.appendChild(profile);&lt;br /&gt;
&lt;br /&gt;
        const upper = createSvgElement(&#039;path&#039;, { d: upperPath, class: &#039;aerofoil-upper&#039; });&lt;br /&gt;
        const lower = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.lower), class: &#039;aerofoil-lower&#039; });&lt;br /&gt;
        const camber = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.camber), class: &#039;aerofoil-camber&#039; });&lt;br /&gt;
        svg.appendChild(upper);&lt;br /&gt;
        svg.appendChild(lower);&lt;br /&gt;
        svg.appendChild(camber);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderConstructionPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const allCurves = [&lt;br /&gt;
            geom.thickLeadCurve,&lt;br /&gt;
            geom.thickTrailCurve,&lt;br /&gt;
            geom.camberLeadCurve,&lt;br /&gt;
            geom.camberTrailCurve&lt;br /&gt;
        ];&lt;br /&gt;
        const allPoints = [&lt;br /&gt;
            ...geom.controlPoints.thicknessLead,&lt;br /&gt;
            ...geom.controlPoints.thicknessTrail,&lt;br /&gt;
            ...geom.controlPoints.camberLead,&lt;br /&gt;
            ...geom.controlPoints.camberTrail&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const finite = (v) =&amp;gt; Number.isFinite(v);&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        allCurves.forEach((curve) =&amp;gt; {&lt;br /&gt;
            curve.x.forEach((x) =&amp;gt; { if (finite(x)) xs.push(x); });&lt;br /&gt;
            curve.y.forEach((y) =&amp;gt; { if (finite(y)) ys.push(y); });&lt;br /&gt;
        });&lt;br /&gt;
        allPoints.forEach((p) =&amp;gt; {&lt;br /&gt;
            if (finite(p.x)) xs.push(p.x);&lt;br /&gt;
            if (finite(p.y)) ys.push(p.y);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        if (!xs.length || !ys.length) return;&lt;br /&gt;
&lt;br /&gt;
        let minX = Math.min(...xs);&lt;br /&gt;
        let maxX = Math.max(...xs);&lt;br /&gt;
        let minY = Math.min(...ys);&lt;br /&gt;
        let maxY = Math.max(...ys);&lt;br /&gt;
        const spanX = Math.max(maxX - minX, 0.2);&lt;br /&gt;
        const spanY = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        minX -= 0.08 * spanX;&lt;br /&gt;
        maxX += 0.08 * spanX;&lt;br /&gt;
        minY -= 0.12 * spanY;&lt;br /&gt;
        maxY += 0.12 * spanY;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 500;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 35;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + (x - minX) / (maxX - minX) * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and x/c tick labels.&lt;br /&gt;
        const xTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= xTickCount; i++) {&lt;br /&gt;
            const xVal = minX + (maxX - minX) * i / xTickCount;&lt;br /&gt;
            const px = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: px, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(xVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const yTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTickCount; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTickCount;&lt;br /&gt;
            const py = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: padLeft - 9, y: py + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (minX &amp;lt;= 0 &amp;amp;&amp;amp; maxX &amp;gt;= 0) {&lt;br /&gt;
            const px = sx(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
        if (minY &amp;lt;= 0 &amp;amp;&amp;amp; maxY &amp;gt;= 0) {&lt;br /&gt;
            const py = sy(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;&lt;br /&gt;
        });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromCurve(curve) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; curve.x.length; i++) {&lt;br /&gt;
                if (!finite(curve.x[i]) || !finite(curve.y[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                d += `${started ? &#039;L&#039; : &#039;M&#039;}${sx(curve.x[i]).toFixed(2)},${sy(curve.y[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Curves.&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickLeadCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickTrailCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberLeadCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberTrailCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // Control polygons and labelled points.&lt;br /&gt;
        function drawControlSet(points, prefix, lineClass, pointClass) {&lt;br /&gt;
            const polygonPoints = points&lt;br /&gt;
                .filter((p) =&amp;gt; finite(p.x) &amp;amp;&amp;amp; finite(p.y))&lt;br /&gt;
                .map((p) =&amp;gt; `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`)&lt;br /&gt;
                .join(&#039; &#039;);&lt;br /&gt;
            if (polygonPoints) {&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;polyline&#039;, {&lt;br /&gt;
                    points: polygonPoints,&lt;br /&gt;
                    class: lineClass,&lt;br /&gt;
                    fill: &#039;none&#039;&lt;br /&gt;
                }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            points.forEach((p, i) =&amp;gt; {&lt;br /&gt;
                if (!finite(p.x) || !finite(p.y)) return;&lt;br /&gt;
                const px = sx(p.x);&lt;br /&gt;
                const py = sy(p.y);&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;circle&#039;, {&lt;br /&gt;
                    cx: px, cy: py, r: 4, class: pointClass&lt;br /&gt;
                }));&lt;br /&gt;
&lt;br /&gt;
                const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                    x: px + 7,&lt;br /&gt;
                    y: py - 7,&lt;br /&gt;
                    class: &#039;aerofoil-control-point-label&#039;&lt;br /&gt;
                });&lt;br /&gt;
                label.textContent = `${prefix}${i}`;&lt;br /&gt;
                svg.appendChild(label);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessLead, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessTrail, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberLead, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberTrail, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Legend.&lt;br /&gt;
        const legend = createSvgElement(&#039;g&#039;, { transform: `translate(${W - 220}, 18)` });&lt;br /&gt;
        const tLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 0, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        tLegend.textContent = &#039;Thickness curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(tLegend);&lt;br /&gt;
        const cLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 20, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        cLegend.textContent = &#039;Camber curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(cLegend);&lt;br /&gt;
        svg.appendChild(legend);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function setup(root) {&lt;br /&gt;
        if (root.dataset.aerofoilInitialised === &#039;1&#039;) return;&lt;br /&gt;
        root.dataset.aerofoilInitialised = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
        const controls = document.createElement(&#039;div&#039;);&lt;br /&gt;
        controls.className = &#039;aerofoil-controls&#039;;&lt;br /&gt;
&lt;br /&gt;
        const values = {};&lt;br /&gt;
        const inputs = {};&lt;br /&gt;
        const valueLabels = {};&lt;br /&gt;
&lt;br /&gt;
        PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
            const row = document.createElement(&#039;div&#039;);&lt;br /&gt;
            row.className = &#039;aerofoil-control&#039;;&lt;br /&gt;
&lt;br /&gt;
            const label = document.createElement(&#039;label&#039;);&lt;br /&gt;
            label.className = &#039;aerofoil-control-label&#039;;&lt;br /&gt;
            label.htmlFor = `aerofoil-${p.key}`;&lt;br /&gt;
            label.textContent = p.label;&lt;br /&gt;
&lt;br /&gt;
            const range = document.createElement(&#039;input&#039;);&lt;br /&gt;
            range.type = &#039;range&#039;;&lt;br /&gt;
            range.id = `aerofoil-${p.key}`;&lt;br /&gt;
            range.min = p.min;&lt;br /&gt;
            range.max = p.max;&lt;br /&gt;
            range.step = p.step;&lt;br /&gt;
            range.value = p.value;&lt;br /&gt;
&lt;br /&gt;
            const number = document.createElement(&#039;input&#039;);&lt;br /&gt;
            number.type = &#039;number&#039;;&lt;br /&gt;
            number.min = p.min;&lt;br /&gt;
            number.max = p.max;&lt;br /&gt;
            number.step = p.step;&lt;br /&gt;
            number.value = p.value;&lt;br /&gt;
            number.className = &#039;aerofoil-number&#039;;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
            const setValue = (raw) =&amp;gt; {&lt;br /&gt;
                let value = Number(raw);&lt;br /&gt;
                if (!Number.isFinite(value)) value = p.value;&lt;br /&gt;
                value = Math.min(p.max, Math.max(p.min, value));&lt;br /&gt;
                range.value = value;&lt;br /&gt;
                number.value = value;&lt;br /&gt;
                values[p.key] = value;&lt;br /&gt;
                valueSpan.textContent = value.toFixed(Math.max(0, decimalPlaces(p.step)));&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            range.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(range.value));&lt;br /&gt;
            number.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(number.value));&lt;br /&gt;
&lt;br /&gt;
            setValue(p.value);&lt;br /&gt;
            inputs[p.key] = range;&lt;br /&gt;
            valueLabels[p.key] = valueSpan;&lt;br /&gt;
&lt;br /&gt;
            row.appendChild(label);&lt;br /&gt;
            row.appendChild(range);&lt;br /&gt;
            row.appendChild(number);&lt;br /&gt;
            controls.appendChild(row);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const actions = document.createElement(&#039;div&#039;);&lt;br /&gt;
        actions.className = &#039;aerofoil-actions&#039;;&lt;br /&gt;
&lt;br /&gt;
        const reset = document.createElement(&#039;button&#039;);&lt;br /&gt;
        reset.type = &#039;button&#039;;&lt;br /&gt;
        reset.className = &#039;mw-ui-button&#039;;&lt;br /&gt;
        reset.textContent = &#039;Reset&#039;;&lt;br /&gt;
        reset.addEventListener(&#039;click&#039;, () =&amp;gt; {&lt;br /&gt;
            PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
                inputs[p.key].value = p.value;&lt;br /&gt;
                inputs[p.key].dispatchEvent(new Event(&#039;input&#039;, { bubbles: true }));&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        actions.appendChild(reset);&lt;br /&gt;
&lt;br /&gt;
        const plotWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        plotWrap.className = &#039;aerofoil-plot-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const svg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 520&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Interactive aerofoil plot&#039;&lt;br /&gt;
        });&lt;br /&gt;
        plotWrap.appendChild(svg);&lt;br /&gt;
&lt;br /&gt;
        const constructionTitle = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionTitle.className = &#039;aerofoil-subplot-title&#039;;&lt;br /&gt;
        constructionTitle.textContent = &#039;Bezier thickness and camber construction&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionWrap.className = &#039;aerofoil-plot-wrap aerofoil-construction-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionSvg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 500&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Bezier thickness and camber curves with control points&#039;&lt;br /&gt;
        });&lt;br /&gt;
        constructionWrap.appendChild(constructionSvg);&lt;br /&gt;
&lt;br /&gt;
        const status = document.createElement(&#039;div&#039;);&lt;br /&gt;
        status.className = &#039;aerofoil-status&#039;;&lt;br /&gt;
&lt;br /&gt;
        const checks = document.createElement(&#039;div&#039;);&lt;br /&gt;
        checks.className = &#039;aerofoil-checks&#039;;&lt;br /&gt;
&lt;br /&gt;
        const unitNote = document.createElement(&#039;div&#039;);&lt;br /&gt;
        unitNote.className = &#039;aerofoil-note&#039;;&lt;br /&gt;
        unitNote.textContent = `Angles are entered in ${ANGLE_MODE}; calculations follow the MATLAB tan/cot convention.`;&lt;br /&gt;
&lt;br /&gt;
        root.appendChild(controls);&lt;br /&gt;
        root.appendChild(actions);&lt;br /&gt;
        root.appendChild(plotWrap);&lt;br /&gt;
        root.appendChild(constructionTitle);&lt;br /&gt;
        root.appendChild(constructionWrap);&lt;br /&gt;
        root.appendChild(status);&lt;br /&gt;
        root.appendChild(checks);&lt;br /&gt;
        root.appendChild(unitNote);&lt;br /&gt;
&lt;br /&gt;
        const update = () =&amp;gt; {&lt;br /&gt;
            try {&lt;br /&gt;
                const geom = buildGeometry(values);&lt;br /&gt;
                renderPlot(svg, geom);&lt;br /&gt;
                renderConstructionPlot(constructionSvg, geom);&lt;br /&gt;
&lt;br /&gt;
                status.className = `aerofoil-status ${geom.validGeom ? &#039;is-valid&#039; : &#039;is-invalid&#039;}`;&lt;br /&gt;
                status.textContent = geom.validGeom ? &#039;Geometry valid&#039; : &#039;Geometry invalid — see checks below.&#039;;&lt;br /&gt;
&lt;br /&gt;
                checks.innerHTML = &#039;&#039;;&lt;br /&gt;
                const messages = [&lt;br /&gt;
                    [&#039;Negative thickness&#039;, geom.checks.negThick],&lt;br /&gt;
                    [&#039;Thickness x outside [0, 1]&#039;, geom.checks.thickBounds],&lt;br /&gt;
                    [&#039;Camber x outside [0, 1]&#039;, geom.checks.camberBounds],&lt;br /&gt;
                    [&#039;Overhang / decreasing x&#039;, geom.checks.overHung]&lt;br /&gt;
                ];&lt;br /&gt;
                messages.forEach(([name, failed]) =&amp;gt; {&lt;br /&gt;
                    const item = document.createElement(&#039;span&#039;);&lt;br /&gt;
                    item.className = failed ? &#039;fail&#039; : &#039;pass&#039;;&lt;br /&gt;
                    item.textContent = `${failed ? &#039;✗&#039; : &#039;✓&#039;} ${name}`;&lt;br /&gt;
                    checks.appendChild(item);&lt;br /&gt;
                });&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                status.className = &#039;aerofoil-status is-invalid&#039;;&lt;br /&gt;
                status.textContent = `Geometry error: ${error.message}`;&lt;br /&gt;
                console.error(&#039;Aerofoil Designer:&#039;, error);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        Object.keys(inputs).forEach((key) =&amp;gt; {&lt;br /&gt;
            inputs[key].addEventListener(&#039;input&#039;, update);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        update();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function decimalPlaces(step) {&lt;br /&gt;
        const text = String(step);&lt;br /&gt;
        if (!text.includes(&#039;.&#039;)) return 0;&lt;br /&gt;
        return text.split(&#039;.&#039;)[1].length;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        document.querySelectorAll(&#039;#aerofoil-tool&#039;).forEach(setup);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (window.mw &amp;amp;&amp;amp; mw.hook) {&lt;br /&gt;
        mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
        init();&lt;br /&gt;
    } else {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, init);&lt;br /&gt;
    }&lt;br /&gt;
}());&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=53</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.css</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=53"/>
		<updated>2026-09-23T10:24:28Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Interactive Bezier aerofoil designer */&lt;br /&gt;
#aerofoil-tool {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 1rem 0;&lt;br /&gt;
    font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-controls {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));&lt;br /&gt;
    gap: 0.55rem 1rem;&lt;br /&gt;
    margin-bottom: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: 150px minmax(120px, 1fr) 84px 68px;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    gap: 0.55rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-label {&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control input[type=&#039;range&#039;] {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-number {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-value {&lt;br /&gt;
    displace: none;&lt;br /&gt;
    text-align: right;&lt;br /&gt;
    font-variant-numeric: tabular-nums;&lt;br /&gt;
    color: #54595d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-actions {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    margin: 0.6rem 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
    border: 1px solid #a2a9b1;&lt;br /&gt;
    background: #fff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap svg {&lt;br /&gt;
    display: block;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-grid {&lt;br /&gt;
    stroke: #eaecf0;&lt;br /&gt;
    stroke-width: 1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-axis {&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-profile {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #202122;&lt;br /&gt;
    stroke-width: 1.8;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-upper,&lt;br /&gt;
.aerofoil-lower {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.4;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.3;&lt;br /&gt;
    stroke-dasharray: 6 5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-tick,&lt;br /&gt;
.aerofoil-axis-label {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 13px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-empty {&lt;br /&gt;
    fill: #a2a9b1;&lt;br /&gt;
    font-size: 20px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    padding: 0.6rem 0.8rem;&lt;br /&gt;
    border-left: 4px solid;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-valid {&lt;br /&gt;
    border-left-color: #14866d;&lt;br /&gt;
    background: #f1fff8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-invalid {&lt;br /&gt;
    border-left-color: #b32424;&lt;br /&gt;
    background: #fff4f4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    gap: 0.45rem 1rem;&lt;br /&gt;
    margin-top: 0.6rem;&lt;br /&gt;
    font-size: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .pass {&lt;br /&gt;
    color: #14866d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .fail {&lt;br /&gt;
    color: #b32424;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-note {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    color: #72777d;&lt;br /&gt;
    font-size: 0.85rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 700px) {&lt;br /&gt;
    .aerofoil-controls {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-control {&lt;br /&gt;
        grid-template-columns: 120px 1fr 78px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-value {&lt;br /&gt;
        display: none;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-subplot-title {&lt;br /&gt;
    margin: 0.8rem 0 0.35rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    font-size: 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-construction-wrap {&lt;br /&gt;
    margin-top: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-curve {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-curve {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 2.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-control {&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 1.1;&lt;br /&gt;
    stroke-dasharray: 4 4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-control {&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 1.1;&lt;br /&gt;
    stroke-dasharray: 4 4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-point {&lt;br /&gt;
    fill: #fff;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-point {&lt;br /&gt;
    fill: #fff;&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-point-label {&lt;br /&gt;
    fill: #202122;&lt;br /&gt;
    font-size: 11px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
    paint-order: stroke;&lt;br /&gt;
    stroke: #fff;&lt;br /&gt;
    stroke-width: 3px;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-legend-text {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 12px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=52</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.css</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=52"/>
		<updated>2026-09-23T10:20:26Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Interactive Bezier aerofoil designer */&lt;br /&gt;
#aerofoil-tool {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 1rem 0;&lt;br /&gt;
    font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-controls {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));&lt;br /&gt;
    gap: 0.55rem 1rem;&lt;br /&gt;
    margin-bottom: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: 150px minmax(120px, 1fr) 84px 68px;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    gap: 0.55rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-label {&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control input[type=&#039;range&#039;] {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-number {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-value {&lt;br /&gt;
    text-align: right;&lt;br /&gt;
    font-variant-numeric: tabular-nums;&lt;br /&gt;
    color: #54595d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-actions {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    margin: 0.6rem 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
    border: 1px solid #a2a9b1;&lt;br /&gt;
    background: #fff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap svg {&lt;br /&gt;
    display: block;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-grid {&lt;br /&gt;
    stroke: #eaecf0;&lt;br /&gt;
    stroke-width: 1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-axis {&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-profile {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #202122;&lt;br /&gt;
    stroke-width: 1.8;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-upper,&lt;br /&gt;
.aerofoil-lower {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.4;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.3;&lt;br /&gt;
    stroke-dasharray: 6 5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-tick,&lt;br /&gt;
.aerofoil-axis-label {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 13px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-empty {&lt;br /&gt;
    fill: #a2a9b1;&lt;br /&gt;
    font-size: 20px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    padding: 0.6rem 0.8rem;&lt;br /&gt;
    border-left: 4px solid;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-valid {&lt;br /&gt;
    border-left-color: #14866d;&lt;br /&gt;
    background: #f1fff8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-invalid {&lt;br /&gt;
    border-left-color: #b32424;&lt;br /&gt;
    background: #fff4f4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    gap: 0.45rem 1rem;&lt;br /&gt;
    margin-top: 0.6rem;&lt;br /&gt;
    font-size: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .pass {&lt;br /&gt;
    color: #14866d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .fail {&lt;br /&gt;
    color: #b32424;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-note {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    color: #72777d;&lt;br /&gt;
    font-size: 0.85rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 700px) {&lt;br /&gt;
    .aerofoil-controls {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-control {&lt;br /&gt;
        grid-template-columns: 120px 1fr 78px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-value {&lt;br /&gt;
        display: none;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-subplot-title {&lt;br /&gt;
    margin: 0.8rem 0 0.35rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    font-size: 1rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-construction-wrap {&lt;br /&gt;
    margin-top: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-curve {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-curve {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 2.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-control {&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 1.1;&lt;br /&gt;
    stroke-dasharray: 4 4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-control {&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 1.1;&lt;br /&gt;
    stroke-dasharray: 4 4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-thickness-point {&lt;br /&gt;
    fill: #fff;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber-point {&lt;br /&gt;
    fill: #fff;&lt;br /&gt;
    stroke: #14866d;&lt;br /&gt;
    stroke-width: 2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-point-label {&lt;br /&gt;
    fill: #202122;&lt;br /&gt;
    font-size: 11px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
    paint-order: stroke;&lt;br /&gt;
    stroke: #fff;&lt;br /&gt;
    stroke-width: 3px;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-legend-text {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 12px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=51</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.js</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=51"/>
		<updated>2026-09-23T10:20:06Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;(function () {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    const ANGLE_MODE = &#039;degrees&#039;;&lt;br /&gt;
&lt;br /&gt;
    const PARAMS = [&lt;br /&gt;
        { key: &#039;xt&#039;,      label: &#039;xₜ — thickness join&#039;,   min: 0.05, max: 0.95, step: 0.005, value: 0.25 },&lt;br /&gt;
        { key: &#039;yt&#039;,      label: &#039;yₜ — thickness join&#039;,   min: 0.00, max: 0.15, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;xc&#039;,      label: &#039;x꜀ — camber join&#039;,      min: 0.05, max: 0.95, step: 0.005, value: 0.40 },&lt;br /&gt;
        { key: &#039;yc&#039;,      label: &#039;y꜀ — camber join&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.02 },&lt;br /&gt;
        { key: &#039;rle&#039;,     label: &#039;rₗₑ — leading-edge radius&#039;, min: 0.001, max: 0.20, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;dzte&#039;,    label: &#039;Δzₜₑ — TE thickness offset&#039;, min: -0.10, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;zte&#039;,     label: &#039;zₜₑ — TE camber&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;betate&#039;,  label: &#039;βₜₑ — TE thickness angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;gammale&#039;, label: &#039;γₗₑ — LE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 5 },&lt;br /&gt;
        { key: &#039;alphate&#039;, label: &#039;αₜₑ — TE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;b0&#039;,      label: &#039;b₀&#039;,                  min: 0.00, max: 0.30, step: 0.005, value: 0.05 },&lt;br /&gt;
        { key: &#039;b2&#039;,      label: &#039;b₂&#039;,                  min: 0.00, max: 0.60, step: 0.005, value: 0.20 },&lt;br /&gt;
        { key: &#039;b8&#039;,      label: &#039;b₈&#039;,                  min: 0.00, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;b15&#039;,     label: &#039;b₁₅&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 },&lt;br /&gt;
        { key: &#039;b17&#039;,     label: &#039;b₁₇&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 }&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    function toRadians(angle) {&lt;br /&gt;
        return ANGLE_MODE === &#039;degrees&#039; ? angle * Math.PI / 180 : angle;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function cot(angle) {&lt;br /&gt;
        return 1 / Math.tan(angle);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function bezier(points, u) {&lt;br /&gt;
        // General Bernstein-form Bezier evaluation. This exactly covers the&lt;br /&gt;
        // cubic (4-point) and quartic (5-point) curves used by the MATLAB code.&lt;br /&gt;
        const n = points.length - 1;&lt;br /&gt;
        let value = 0;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= n; i++) {&lt;br /&gt;
            let binomial = 1;&lt;br /&gt;
            for (let j = 1; j &amp;lt;= i; j++) {&lt;br /&gt;
                binomial *= (n - j + 1) / j;&lt;br /&gt;
            }&lt;br /&gt;
            value += binomial * Math.pow(1 - u, n - i) * Math.pow(u, i) * points[i];&lt;br /&gt;
        }&lt;br /&gt;
        return value;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function sampleBezier(points, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            const u = count === 1 ? 0 : i / (count - 1);&lt;br /&gt;
            values[i] = bezier(points, u);&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function linspace(start, stop, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        if (count === 1) {&lt;br /&gt;
            values[0] = start;&lt;br /&gt;
            return values;&lt;br /&gt;
        }&lt;br /&gt;
        const step = (stop - start) / (count - 1);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            values[i] = start + i * step;&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function buildGeometry(v) {&lt;br /&gt;
        const beta = toRadians(v.betate);&lt;br /&gt;
        const gamma = toRadians(v.gammale);&lt;br /&gt;
        const alpha = toRadians(v.alphate);&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CONTROL POINTS ----&lt;br /&gt;
        const xLeadEdgeThick = [0, 0, -3 * v.b8 * v.b8 / (2 * v.rle), v.xt];&lt;br /&gt;
        const yLeadEdgeThick = [0, v.b8, v.yt, v.yt];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeThick = [&lt;br /&gt;
            v.xt,&lt;br /&gt;
            (7 * v.xt + 9 * v.b8 * v.b8 / (2 * v.rle)) / 4,&lt;br /&gt;
            3 * v.xt + 15 * v.b8 * v.b8 / (4 * v.rle),&lt;br /&gt;
            v.b15,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeThick = [&lt;br /&gt;
            v.yt,&lt;br /&gt;
            v.yt,&lt;br /&gt;
            (v.yt + v.b8) / 2,&lt;br /&gt;
            v.dzte + (1 - v.b15) * Math.tan(beta),&lt;br /&gt;
            v.dzte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const xLeadEdgeCamber = [0, v.b0, v.b2, v.xc];&lt;br /&gt;
        const yLeadEdgeCamber = [0, v.b0 * Math.tan(gamma), v.yc, v.yc];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeCamber = [&lt;br /&gt;
            v.xc,&lt;br /&gt;
            (3 * v.xc - v.yc * cot(gamma)) / 2,&lt;br /&gt;
            (-8 * v.yc * cot(gamma) + 13 * v.xc) / 6,&lt;br /&gt;
            v.b17,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeCamber = [&lt;br /&gt;
            v.yc,&lt;br /&gt;
            v.yc,&lt;br /&gt;
            5 * v.yc / 6,&lt;br /&gt;
            v.zte - (1 - v.b17) * Math.tan(alpha),&lt;br /&gt;
            v.zte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CURVES ----&lt;br /&gt;
        const n = 100;&lt;br /&gt;
        const u = linspace(0, 1, n);&lt;br /&gt;
&lt;br /&gt;
        const thickLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const thickTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        const thickCurve = {&lt;br /&gt;
            x: thickLeadCurve.x.concat(thickTrailCurve.x),&lt;br /&gt;
            y: thickLeadCurve.y.concat(thickTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
        const camberCurve = {&lt;br /&gt;
            x: camberLeadCurve.x.concat(camberTrailCurve.x),&lt;br /&gt;
            y: camberLeadCurve.y.concat(camberTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // ---- GEOMETRY VALIDITY CHECK ----&lt;br /&gt;
        let negThick = false;&lt;br /&gt;
        let thickBounds = false;&lt;br /&gt;
        let camberBounds = false;&lt;br /&gt;
        let overHung = false;&lt;br /&gt;
&lt;br /&gt;
        for (let i = 0; i &amp;lt; thickCurve.x.length; i++) {&lt;br /&gt;
            if (thickCurve.y[i] &amp;lt; 0) negThick = true;&lt;br /&gt;
            if (thickCurve.x[i] &amp;lt; 0 || thickCurve.x[i] &amp;gt; 1) thickBounds = true;&lt;br /&gt;
            if (camberCurve.x[i] &amp;lt; 0 || camberCurve.x[i] &amp;gt; 1) camberBounds = true;&lt;br /&gt;
            if (i &amp;gt; 0 &amp;amp;&amp;amp; (thickCurve.x[i] &amp;lt; thickCurve.x[i - 1] || camberCurve.x[i] &amp;lt; camberCurve.x[i - 1])) {&lt;br /&gt;
                overHung = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const validGeom = !(negThick || thickBounds || camberBounds || overHung);&lt;br /&gt;
&lt;br /&gt;
        // ---- INTERPOLATION ----&lt;br /&gt;
        // MATLAB uses interp1(..., &#039;linear&#039;) after checking for overhang.&lt;br /&gt;
        // For a valid monotonic curve, sorting by x gives the same result and&lt;br /&gt;
        // also lets us keep rendering something useful while the user explores&lt;br /&gt;
        // invalid parameter combinations.&lt;br /&gt;
        const x = linspace(0, 1, n * 2);&lt;br /&gt;
        const interpThickCurve = interpolateSorted(thickCurve.x, thickCurve.y, x);&lt;br /&gt;
        const interpCamberCurve = interpolateSorted(camberCurve.x, camberCurve.y, x);&lt;br /&gt;
&lt;br /&gt;
        const upper = new Array(x.length);&lt;br /&gt;
        const lower = new Array(x.length);&lt;br /&gt;
        for (let i = 0; i &amp;lt; x.length; i++) {&lt;br /&gt;
            upper[i] = interpCamberCurve[i] + interpThickCurve[i];&lt;br /&gt;
            lower[i] = interpCamberCurve[i] - interpThickCurve[i];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            x,&lt;br /&gt;
            upper,&lt;br /&gt;
            lower,&lt;br /&gt;
            camber: interpCamberCurve,&lt;br /&gt;
            thickness: interpThickCurve,&lt;br /&gt;
            thickCurve,&lt;br /&gt;
            camberCurve,&lt;br /&gt;
            thickLeadCurve,&lt;br /&gt;
            thickTrailCurve,&lt;br /&gt;
            camberLeadCurve,&lt;br /&gt;
            camberTrailCurve,&lt;br /&gt;
            controlPoints: {&lt;br /&gt;
                thicknessLead: zipPoints(xLeadEdgeThick, yLeadEdgeThick),&lt;br /&gt;
                thicknessTrail: zipPoints(xTrailEdgeThick, yTrailEdgeThick),&lt;br /&gt;
                camberLead: zipPoints(xLeadEdgeCamber, yLeadEdgeCamber),&lt;br /&gt;
                camberTrail: zipPoints(xTrailEdgeCamber, yTrailEdgeCamber)&lt;br /&gt;
            },&lt;br /&gt;
            validGeom,&lt;br /&gt;
            checks: { negThick, thickBounds, camberBounds, overHung }&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function zipPoints(xs, ys) {&lt;br /&gt;
        return xs.map((x, i) =&amp;gt; ({ x, y: ys[i] }));&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function interpolateSorted(xValues, yValues, xQuery) {&lt;br /&gt;
        const pairs = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; xValues.length; i++) {&lt;br /&gt;
            if (Number.isFinite(xValues[i]) &amp;amp;&amp;amp; Number.isFinite(yValues[i])) {&lt;br /&gt;
                pairs.push([xValues[i], yValues[i]]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        pairs.sort((a, b) =&amp;gt; a[0] - b[0]);&lt;br /&gt;
&lt;br /&gt;
        // Remove duplicate x values, keeping the first value, as a pragmatic&lt;br /&gt;
        // browser-side equivalent for plotting.&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        for (const pair of pairs) {&lt;br /&gt;
            if (!xs.length || Math.abs(pair[0] - xs[xs.length - 1]) &amp;gt; 1e-12) {&lt;br /&gt;
                xs.push(pair[0]);&lt;br /&gt;
                ys.push(pair[1]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return xQuery.map((q) =&amp;gt; {&lt;br /&gt;
            if (!xs.length || q &amp;lt; xs[0] || q &amp;gt; xs[xs.length - 1]) return NaN;&lt;br /&gt;
            if (xs.length === 1) return ys[0];&lt;br /&gt;
&lt;br /&gt;
            let lo = 0;&lt;br /&gt;
            let hi = xs.length - 1;&lt;br /&gt;
            while (hi - lo &amp;gt; 1) {&lt;br /&gt;
                const mid = Math.floor((lo + hi) / 2);&lt;br /&gt;
                if (xs[mid] &amp;lt;= q) lo = mid;&lt;br /&gt;
                else hi = mid;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            const dx = xs[hi] - xs[lo];&lt;br /&gt;
            const t = dx === 0 ? 0 : (q - xs[lo]) / dx;&lt;br /&gt;
            return ys[lo] + t * (ys[hi] - ys[lo]);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function fmt(value) {&lt;br /&gt;
        if (!Number.isFinite(value)) return &#039;—&#039;;&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 1) return value.toFixed(3);&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 0.1) return value.toFixed(4);&lt;br /&gt;
        return value.toFixed(5);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function createSvgElement(tag, attrs) {&lt;br /&gt;
        const ns = &#039;http://www.w3.org/2000/svg&#039;;&lt;br /&gt;
        const el = document.createElementNS(ns, tag);&lt;br /&gt;
        Object.entries(attrs).forEach(([key, value]) =&amp;gt; el.setAttribute(key, String(value)));&lt;br /&gt;
        return el;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const validPoints = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; geom.x.length; i++) {&lt;br /&gt;
            if (Number.isFinite(geom.upper[i])) validPoints.push([geom.x[i], geom.upper[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.lower[i])) validPoints.push([geom.x[i], geom.lower[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.camber[i])) validPoints.push([geom.x[i], geom.camber[i]]);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (!validPoints.length) {&lt;br /&gt;
            const text = createSvgElement(&#039;text&#039;, { x: 50, y: 50, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-empty&#039; });&lt;br /&gt;
            text.textContent = &#039;Geometry cannot be plotted for these parameters&#039;;&lt;br /&gt;
            svg.appendChild(text);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        let minY = Math.min(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        let maxY = Math.max(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {&lt;br /&gt;
            minY = -0.1;&lt;br /&gt;
            maxY = 0.1;&lt;br /&gt;
        }&lt;br /&gt;
        const span = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        const margin = span * 0.12;&lt;br /&gt;
        minY -= margin;&lt;br /&gt;
        maxY += margin;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 520;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 25;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + x * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and axes.&lt;br /&gt;
        [0, 0.25, 0.5, 0.75, 1].forEach((xVal) =&amp;gt; {&lt;br /&gt;
            const x = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: x, y1: padTop, x2: x, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = xVal.toFixed(2);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const yTicks = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTicks; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTicks;&lt;br /&gt;
            const y = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: y, x2: padLeft + plotW, y2: y, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x: padLeft - 9, y: y + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: sy(0), x2: padLeft + plotW, y2: sy(0), class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: padTop, x2: padLeft, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // X axis label.&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, { x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039; });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        // Y axis label.&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromSeries(xs, ys) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; xs.length; i++) {&lt;br /&gt;
                if (!Number.isFinite(xs[i]) || !Number.isFinite(ys[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                const command = started ? &#039;L&#039; : &#039;M&#039;;&lt;br /&gt;
                d += `${command}${sx(xs[i]).toFixed(2)},${sy(ys[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const upperPath = pathFromSeries(geom.x, geom.upper);&lt;br /&gt;
        const lowerPath = pathFromSeries([...geom.x].reverse(), [...geom.lower].reverse());&lt;br /&gt;
&lt;br /&gt;
        const profile = createSvgElement(&#039;path&#039;, { d: `${upperPath} ${lowerPath}`, class: &#039;aerofoil-profile&#039; });&lt;br /&gt;
        svg.appendChild(profile);&lt;br /&gt;
&lt;br /&gt;
        const upper = createSvgElement(&#039;path&#039;, { d: upperPath, class: &#039;aerofoil-upper&#039; });&lt;br /&gt;
        const lower = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.lower), class: &#039;aerofoil-lower&#039; });&lt;br /&gt;
        const camber = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.camber), class: &#039;aerofoil-camber&#039; });&lt;br /&gt;
        svg.appendChild(upper);&lt;br /&gt;
        svg.appendChild(lower);&lt;br /&gt;
        svg.appendChild(camber);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderConstructionPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const allCurves = [&lt;br /&gt;
            geom.thickLeadCurve,&lt;br /&gt;
            geom.thickTrailCurve,&lt;br /&gt;
            geom.camberLeadCurve,&lt;br /&gt;
            geom.camberTrailCurve&lt;br /&gt;
        ];&lt;br /&gt;
        const allPoints = [&lt;br /&gt;
            ...geom.controlPoints.thicknessLead,&lt;br /&gt;
            ...geom.controlPoints.thicknessTrail,&lt;br /&gt;
            ...geom.controlPoints.camberLead,&lt;br /&gt;
            ...geom.controlPoints.camberTrail&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const finite = (v) =&amp;gt; Number.isFinite(v);&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        allCurves.forEach((curve) =&amp;gt; {&lt;br /&gt;
            curve.x.forEach((x) =&amp;gt; { if (finite(x)) xs.push(x); });&lt;br /&gt;
            curve.y.forEach((y) =&amp;gt; { if (finite(y)) ys.push(y); });&lt;br /&gt;
        });&lt;br /&gt;
        allPoints.forEach((p) =&amp;gt; {&lt;br /&gt;
            if (finite(p.x)) xs.push(p.x);&lt;br /&gt;
            if (finite(p.y)) ys.push(p.y);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        if (!xs.length || !ys.length) return;&lt;br /&gt;
&lt;br /&gt;
        let minX = Math.min(...xs);&lt;br /&gt;
        let maxX = Math.max(...xs);&lt;br /&gt;
        let minY = Math.min(...ys);&lt;br /&gt;
        let maxY = Math.max(...ys);&lt;br /&gt;
        const spanX = Math.max(maxX - minX, 0.2);&lt;br /&gt;
        const spanY = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        minX -= 0.08 * spanX;&lt;br /&gt;
        maxX += 0.08 * spanX;&lt;br /&gt;
        minY -= 0.12 * spanY;&lt;br /&gt;
        maxY += 0.12 * spanY;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 500;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 35;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + (x - minX) / (maxX - minX) * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and x/c tick labels.&lt;br /&gt;
        const xTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= xTickCount; i++) {&lt;br /&gt;
            const xVal = minX + (maxX - minX) * i / xTickCount;&lt;br /&gt;
            const px = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: px, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(xVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const yTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTickCount; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTickCount;&lt;br /&gt;
            const py = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: padLeft - 9, y: py + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (minX &amp;lt;= 0 &amp;amp;&amp;amp; maxX &amp;gt;= 0) {&lt;br /&gt;
            const px = sx(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
        if (minY &amp;lt;= 0 &amp;amp;&amp;amp; maxY &amp;gt;= 0) {&lt;br /&gt;
            const py = sy(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;&lt;br /&gt;
        });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromCurve(curve) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; curve.x.length; i++) {&lt;br /&gt;
                if (!finite(curve.x[i]) || !finite(curve.y[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                d += `${started ? &#039;L&#039; : &#039;M&#039;}${sx(curve.x[i]).toFixed(2)},${sy(curve.y[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Curves.&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickLeadCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickTrailCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberLeadCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberTrailCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // Control polygons and labelled points.&lt;br /&gt;
        function drawControlSet(points, prefix, lineClass, pointClass) {&lt;br /&gt;
            const polygonPoints = points&lt;br /&gt;
                .filter((p) =&amp;gt; finite(p.x) &amp;amp;&amp;amp; finite(p.y))&lt;br /&gt;
                .map((p) =&amp;gt; `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`)&lt;br /&gt;
                .join(&#039; &#039;);&lt;br /&gt;
            if (polygonPoints) {&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;polyline&#039;, {&lt;br /&gt;
                    points: polygonPoints,&lt;br /&gt;
                    class: lineClass,&lt;br /&gt;
                    fill: &#039;none&#039;&lt;br /&gt;
                }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            points.forEach((p, i) =&amp;gt; {&lt;br /&gt;
                if (!finite(p.x) || !finite(p.y)) return;&lt;br /&gt;
                const px = sx(p.x);&lt;br /&gt;
                const py = sy(p.y);&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;circle&#039;, {&lt;br /&gt;
                    cx: px, cy: py, r: 4, class: pointClass&lt;br /&gt;
                }));&lt;br /&gt;
&lt;br /&gt;
                const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                    x: px + 7,&lt;br /&gt;
                    y: py - 7,&lt;br /&gt;
                    class: &#039;aerofoil-control-point-label&#039;&lt;br /&gt;
                });&lt;br /&gt;
                label.textContent = `${prefix}${i}`;&lt;br /&gt;
                svg.appendChild(label);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessLead, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessTrail, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberLead, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberTrail, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Legend.&lt;br /&gt;
        const legend = createSvgElement(&#039;g&#039;, { transform: `translate(${W - 220}, 18)` });&lt;br /&gt;
        const tLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 0, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        tLegend.textContent = &#039;Thickness curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(tLegend);&lt;br /&gt;
        const cLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 20, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        cLegend.textContent = &#039;Camber curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(cLegend);&lt;br /&gt;
        svg.appendChild(legend);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function setup(root) {&lt;br /&gt;
        if (root.dataset.aerofoilInitialised === &#039;1&#039;) return;&lt;br /&gt;
        root.dataset.aerofoilInitialised = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
        const controls = document.createElement(&#039;div&#039;);&lt;br /&gt;
        controls.className = &#039;aerofoil-controls&#039;;&lt;br /&gt;
&lt;br /&gt;
        const values = {};&lt;br /&gt;
        const inputs = {};&lt;br /&gt;
        const valueLabels = {};&lt;br /&gt;
&lt;br /&gt;
        PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
            const row = document.createElement(&#039;div&#039;);&lt;br /&gt;
            row.className = &#039;aerofoil-control&#039;;&lt;br /&gt;
&lt;br /&gt;
            const label = document.createElement(&#039;label&#039;);&lt;br /&gt;
            label.className = &#039;aerofoil-control-label&#039;;&lt;br /&gt;
            label.htmlFor = `aerofoil-${p.key}`;&lt;br /&gt;
            label.textContent = p.label;&lt;br /&gt;
&lt;br /&gt;
            const range = document.createElement(&#039;input&#039;);&lt;br /&gt;
            range.type = &#039;range&#039;;&lt;br /&gt;
            range.id = `aerofoil-${p.key}`;&lt;br /&gt;
            range.min = p.min;&lt;br /&gt;
            range.max = p.max;&lt;br /&gt;
            range.step = p.step;&lt;br /&gt;
            range.value = p.value;&lt;br /&gt;
&lt;br /&gt;
            const number = document.createElement(&#039;input&#039;);&lt;br /&gt;
            number.type = &#039;number&#039;;&lt;br /&gt;
            number.min = p.min;&lt;br /&gt;
            number.max = p.max;&lt;br /&gt;
            number.step = p.step;&lt;br /&gt;
            number.value = p.value;&lt;br /&gt;
            number.className = &#039;aerofoil-number&#039;;&lt;br /&gt;
&lt;br /&gt;
            const valueSpan = document.createElement(&#039;span&#039;);&lt;br /&gt;
            valueSpan.className = &#039;aerofoil-value&#039;;&lt;br /&gt;
&lt;br /&gt;
            const setValue = (raw) =&amp;gt; {&lt;br /&gt;
                let value = Number(raw);&lt;br /&gt;
                if (!Number.isFinite(value)) value = p.value;&lt;br /&gt;
                value = Math.min(p.max, Math.max(p.min, value));&lt;br /&gt;
                range.value = value;&lt;br /&gt;
                number.value = value;&lt;br /&gt;
                values[p.key] = value;&lt;br /&gt;
                valueSpan.textContent = value.toFixed(Math.max(0, decimalPlaces(p.step)));&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            range.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(range.value));&lt;br /&gt;
            number.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(number.value));&lt;br /&gt;
&lt;br /&gt;
            setValue(p.value);&lt;br /&gt;
            inputs[p.key] = range;&lt;br /&gt;
            valueLabels[p.key] = valueSpan;&lt;br /&gt;
&lt;br /&gt;
            row.appendChild(label);&lt;br /&gt;
            row.appendChild(range);&lt;br /&gt;
            row.appendChild(number);&lt;br /&gt;
            row.appendChild(valueSpan);&lt;br /&gt;
            controls.appendChild(row);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const actions = document.createElement(&#039;div&#039;);&lt;br /&gt;
        actions.className = &#039;aerofoil-actions&#039;;&lt;br /&gt;
&lt;br /&gt;
        const reset = document.createElement(&#039;button&#039;);&lt;br /&gt;
        reset.type = &#039;button&#039;;&lt;br /&gt;
        reset.className = &#039;mw-ui-button&#039;;&lt;br /&gt;
        reset.textContent = &#039;Reset&#039;;&lt;br /&gt;
        reset.addEventListener(&#039;click&#039;, () =&amp;gt; {&lt;br /&gt;
            PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
                inputs[p.key].value = p.value;&lt;br /&gt;
                inputs[p.key].dispatchEvent(new Event(&#039;input&#039;, { bubbles: true }));&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        actions.appendChild(reset);&lt;br /&gt;
&lt;br /&gt;
        const plotWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        plotWrap.className = &#039;aerofoil-plot-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const svg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 520&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Interactive aerofoil plot&#039;&lt;br /&gt;
        });&lt;br /&gt;
        plotWrap.appendChild(svg);&lt;br /&gt;
&lt;br /&gt;
        const constructionTitle = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionTitle.className = &#039;aerofoil-subplot-title&#039;;&lt;br /&gt;
        constructionTitle.textContent = &#039;Bezier thickness and camber construction&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionWrap.className = &#039;aerofoil-plot-wrap aerofoil-construction-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionSvg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 500&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Bezier thickness and camber curves with control points&#039;&lt;br /&gt;
        });&lt;br /&gt;
        constructionWrap.appendChild(constructionSvg);&lt;br /&gt;
&lt;br /&gt;
        const status = document.createElement(&#039;div&#039;);&lt;br /&gt;
        status.className = &#039;aerofoil-status&#039;;&lt;br /&gt;
&lt;br /&gt;
        const checks = document.createElement(&#039;div&#039;);&lt;br /&gt;
        checks.className = &#039;aerofoil-checks&#039;;&lt;br /&gt;
&lt;br /&gt;
        const unitNote = document.createElement(&#039;div&#039;);&lt;br /&gt;
        unitNote.className = &#039;aerofoil-note&#039;;&lt;br /&gt;
        unitNote.textContent = `Angles are entered in ${ANGLE_MODE}; calculations follow the MATLAB tan/cot convention.`;&lt;br /&gt;
&lt;br /&gt;
        root.appendChild(controls);&lt;br /&gt;
        root.appendChild(actions);&lt;br /&gt;
        root.appendChild(plotWrap);&lt;br /&gt;
        root.appendChild(constructionTitle);&lt;br /&gt;
        root.appendChild(constructionWrap);&lt;br /&gt;
        root.appendChild(status);&lt;br /&gt;
        root.appendChild(checks);&lt;br /&gt;
        root.appendChild(unitNote);&lt;br /&gt;
&lt;br /&gt;
        const update = () =&amp;gt; {&lt;br /&gt;
            try {&lt;br /&gt;
                const geom = buildGeometry(values);&lt;br /&gt;
                renderPlot(svg, geom);&lt;br /&gt;
                renderConstructionPlot(constructionSvg, geom);&lt;br /&gt;
&lt;br /&gt;
                status.className = `aerofoil-status ${geom.validGeom ? &#039;is-valid&#039; : &#039;is-invalid&#039;}`;&lt;br /&gt;
                status.textContent = geom.validGeom ? &#039;Geometry valid&#039; : &#039;Geometry invalid — see checks below.&#039;;&lt;br /&gt;
&lt;br /&gt;
                checks.innerHTML = &#039;&#039;;&lt;br /&gt;
                const messages = [&lt;br /&gt;
                    [&#039;Negative thickness&#039;, geom.checks.negThick],&lt;br /&gt;
                    [&#039;Thickness x outside [0, 1]&#039;, geom.checks.thickBounds],&lt;br /&gt;
                    [&#039;Camber x outside [0, 1]&#039;, geom.checks.camberBounds],&lt;br /&gt;
                    [&#039;Overhang / decreasing x&#039;, geom.checks.overHung]&lt;br /&gt;
                ];&lt;br /&gt;
                messages.forEach(([name, failed]) =&amp;gt; {&lt;br /&gt;
                    const item = document.createElement(&#039;span&#039;);&lt;br /&gt;
                    item.className = failed ? &#039;fail&#039; : &#039;pass&#039;;&lt;br /&gt;
                    item.textContent = `${failed ? &#039;✗&#039; : &#039;✓&#039;} ${name}`;&lt;br /&gt;
                    checks.appendChild(item);&lt;br /&gt;
                });&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                status.className = &#039;aerofoil-status is-invalid&#039;;&lt;br /&gt;
                status.textContent = `Geometry error: ${error.message}`;&lt;br /&gt;
                console.error(&#039;Aerofoil Designer:&#039;, error);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        Object.keys(inputs).forEach((key) =&amp;gt; {&lt;br /&gt;
            inputs[key].addEventListener(&#039;input&#039;, update);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        update();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function decimalPlaces(step) {&lt;br /&gt;
        const text = String(step);&lt;br /&gt;
        if (!text.includes(&#039;.&#039;)) return 0;&lt;br /&gt;
        return text.split(&#039;.&#039;)[1].length;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        document.querySelectorAll(&#039;#aerofoil-tool&#039;).forEach(setup);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (window.mw &amp;amp;&amp;amp; mw.hook) {&lt;br /&gt;
        mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
        init();&lt;br /&gt;
    } else {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, init);&lt;br /&gt;
    }&lt;br /&gt;
}());&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=50</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.js</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=50"/>
		<updated>2026-09-23T10:19:36Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;(function () {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // =====================================================================&lt;br /&gt;
    // Aerofoil Designer - Bezier/PARSEC-style geometry from MATLAB code&lt;br /&gt;
    // =====================================================================&lt;br /&gt;
    // IMPORTANT:&lt;br /&gt;
    // Your MATLAB code uses tan() and cot() directly. MATLAB&#039;s tan() expects&lt;br /&gt;
    // radians. The UI defaults to degrees for easier slider input and converts&lt;br /&gt;
    // them to radians before evaluating tan/cot. Set ANGLE_MODE to &#039;radians&#039;&lt;br /&gt;
    // if your MATLAB input vector stores angles in radians.&lt;br /&gt;
    const ANGLE_MODE = &#039;degrees&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Edit these ranges/defaults to match your actual optimisation bounds.&lt;br /&gt;
    const PARAMS = [&lt;br /&gt;
        { key: &#039;xt&#039;,      label: &#039;xₜ — thickness join&#039;,   min: 0.05, max: 0.95, step: 0.005, value: 0.25 },&lt;br /&gt;
        { key: &#039;yt&#039;,      label: &#039;yₜ — thickness join&#039;,   min: 0.00, max: 0.15, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;xc&#039;,      label: &#039;x꜀ — camber join&#039;,      min: 0.05, max: 0.95, step: 0.005, value: 0.40 },&lt;br /&gt;
        { key: &#039;yc&#039;,      label: &#039;y꜀ — camber join&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.02 },&lt;br /&gt;
        { key: &#039;rle&#039;,     label: &#039;rₗₑ — leading-edge radius&#039;, min: 0.001, max: 0.20, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;dzte&#039;,    label: &#039;Δzₜₑ — TE thickness offset&#039;, min: -0.10, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;zte&#039;,     label: &#039;zₜₑ — TE camber&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;betate&#039;,  label: &#039;βₜₑ — TE thickness angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;gammale&#039;, label: &#039;γₗₑ — LE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 5 },&lt;br /&gt;
        { key: &#039;alphate&#039;, label: &#039;αₜₑ — TE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;b0&#039;,      label: &#039;b₀&#039;,                  min: 0.00, max: 0.30, step: 0.005, value: 0.05 },&lt;br /&gt;
        { key: &#039;b2&#039;,      label: &#039;b₂&#039;,                  min: 0.00, max: 0.60, step: 0.005, value: 0.20 },&lt;br /&gt;
        { key: &#039;b8&#039;,      label: &#039;b₈&#039;,                  min: 0.00, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;b15&#039;,     label: &#039;b₁₅&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 },&lt;br /&gt;
        { key: &#039;b17&#039;,     label: &#039;b₁₇&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 }&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    function toRadians(angle) {&lt;br /&gt;
        return ANGLE_MODE === &#039;degrees&#039; ? angle * Math.PI / 180 : angle;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function cot(angle) {&lt;br /&gt;
        return 1 / Math.tan(angle);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function bezier(points, u) {&lt;br /&gt;
        // General Bernstein-form Bezier evaluation. This exactly covers the&lt;br /&gt;
        // cubic (4-point) and quartic (5-point) curves used by the MATLAB code.&lt;br /&gt;
        const n = points.length - 1;&lt;br /&gt;
        let value = 0;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= n; i++) {&lt;br /&gt;
            let binomial = 1;&lt;br /&gt;
            for (let j = 1; j &amp;lt;= i; j++) {&lt;br /&gt;
                binomial *= (n - j + 1) / j;&lt;br /&gt;
            }&lt;br /&gt;
            value += binomial * Math.pow(1 - u, n - i) * Math.pow(u, i) * points[i];&lt;br /&gt;
        }&lt;br /&gt;
        return value;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function sampleBezier(points, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            const u = count === 1 ? 0 : i / (count - 1);&lt;br /&gt;
            values[i] = bezier(points, u);&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function linspace(start, stop, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        if (count === 1) {&lt;br /&gt;
            values[0] = start;&lt;br /&gt;
            return values;&lt;br /&gt;
        }&lt;br /&gt;
        const step = (stop - start) / (count - 1);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            values[i] = start + i * step;&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function buildGeometry(v) {&lt;br /&gt;
        const beta = toRadians(v.betate);&lt;br /&gt;
        const gamma = toRadians(v.gammale);&lt;br /&gt;
        const alpha = toRadians(v.alphate);&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CONTROL POINTS ----&lt;br /&gt;
        const xLeadEdgeThick = [0, 0, -3 * v.b8 * v.b8 / (2 * v.rle), v.xt];&lt;br /&gt;
        const yLeadEdgeThick = [0, v.b8, v.yt, v.yt];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeThick = [&lt;br /&gt;
            v.xt,&lt;br /&gt;
            (7 * v.xt + 9 * v.b8 * v.b8 / (2 * v.rle)) / 4,&lt;br /&gt;
            3 * v.xt + 15 * v.b8 * v.b8 / (4 * v.rle),&lt;br /&gt;
            v.b15,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeThick = [&lt;br /&gt;
            v.yt,&lt;br /&gt;
            v.yt,&lt;br /&gt;
            (v.yt + v.b8) / 2,&lt;br /&gt;
            v.dzte + (1 - v.b15) * Math.tan(beta),&lt;br /&gt;
            v.dzte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const xLeadEdgeCamber = [0, v.b0, v.b2, v.xc];&lt;br /&gt;
        const yLeadEdgeCamber = [0, v.b0 * Math.tan(gamma), v.yc, v.yc];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeCamber = [&lt;br /&gt;
            v.xc,&lt;br /&gt;
            (3 * v.xc - v.yc * cot(gamma)) / 2,&lt;br /&gt;
            (-8 * v.yc * cot(gamma) + 13 * v.xc) / 6,&lt;br /&gt;
            v.b17,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeCamber = [&lt;br /&gt;
            v.yc,&lt;br /&gt;
            v.yc,&lt;br /&gt;
            5 * v.yc / 6,&lt;br /&gt;
            v.zte - (1 - v.b17) * Math.tan(alpha),&lt;br /&gt;
            v.zte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CURVES ----&lt;br /&gt;
        const n = 100;&lt;br /&gt;
        const u = linspace(0, 1, n);&lt;br /&gt;
&lt;br /&gt;
        const thickLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const thickTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeThick, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeThick, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberLeadCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
        const camberTrailCurve = {&lt;br /&gt;
            x: sampleBezier(xTrailEdgeCamber, n),&lt;br /&gt;
            y: sampleBezier(yTrailEdgeCamber, n)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        const thickCurve = {&lt;br /&gt;
            x: thickLeadCurve.x.concat(thickTrailCurve.x),&lt;br /&gt;
            y: thickLeadCurve.y.concat(thickTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
        const camberCurve = {&lt;br /&gt;
            x: camberLeadCurve.x.concat(camberTrailCurve.x),&lt;br /&gt;
            y: camberLeadCurve.y.concat(camberTrailCurve.y)&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // ---- GEOMETRY VALIDITY CHECK ----&lt;br /&gt;
        let negThick = false;&lt;br /&gt;
        let thickBounds = false;&lt;br /&gt;
        let camberBounds = false;&lt;br /&gt;
        let overHung = false;&lt;br /&gt;
&lt;br /&gt;
        for (let i = 0; i &amp;lt; thickCurve.x.length; i++) {&lt;br /&gt;
            if (thickCurve.y[i] &amp;lt; 0) negThick = true;&lt;br /&gt;
            if (thickCurve.x[i] &amp;lt; 0 || thickCurve.x[i] &amp;gt; 1) thickBounds = true;&lt;br /&gt;
            if (camberCurve.x[i] &amp;lt; 0 || camberCurve.x[i] &amp;gt; 1) camberBounds = true;&lt;br /&gt;
            if (i &amp;gt; 0 &amp;amp;&amp;amp; (thickCurve.x[i] &amp;lt; thickCurve.x[i - 1] || camberCurve.x[i] &amp;lt; camberCurve.x[i - 1])) {&lt;br /&gt;
                overHung = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const validGeom = !(negThick || thickBounds || camberBounds || overHung);&lt;br /&gt;
&lt;br /&gt;
        // ---- INTERPOLATION ----&lt;br /&gt;
        // MATLAB uses interp1(..., &#039;linear&#039;) after checking for overhang.&lt;br /&gt;
        // For a valid monotonic curve, sorting by x gives the same result and&lt;br /&gt;
        // also lets us keep rendering something useful while the user explores&lt;br /&gt;
        // invalid parameter combinations.&lt;br /&gt;
        const x = linspace(0, 1, n * 2);&lt;br /&gt;
        const interpThickCurve = interpolateSorted(thickCurve.x, thickCurve.y, x);&lt;br /&gt;
        const interpCamberCurve = interpolateSorted(camberCurve.x, camberCurve.y, x);&lt;br /&gt;
&lt;br /&gt;
        const upper = new Array(x.length);&lt;br /&gt;
        const lower = new Array(x.length);&lt;br /&gt;
        for (let i = 0; i &amp;lt; x.length; i++) {&lt;br /&gt;
            upper[i] = interpCamberCurve[i] + interpThickCurve[i];&lt;br /&gt;
            lower[i] = interpCamberCurve[i] - interpThickCurve[i];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            x,&lt;br /&gt;
            upper,&lt;br /&gt;
            lower,&lt;br /&gt;
            camber: interpCamberCurve,&lt;br /&gt;
            thickness: interpThickCurve,&lt;br /&gt;
            thickCurve,&lt;br /&gt;
            camberCurve,&lt;br /&gt;
            thickLeadCurve,&lt;br /&gt;
            thickTrailCurve,&lt;br /&gt;
            camberLeadCurve,&lt;br /&gt;
            camberTrailCurve,&lt;br /&gt;
            controlPoints: {&lt;br /&gt;
                thicknessLead: zipPoints(xLeadEdgeThick, yLeadEdgeThick),&lt;br /&gt;
                thicknessTrail: zipPoints(xTrailEdgeThick, yTrailEdgeThick),&lt;br /&gt;
                camberLead: zipPoints(xLeadEdgeCamber, yLeadEdgeCamber),&lt;br /&gt;
                camberTrail: zipPoints(xTrailEdgeCamber, yTrailEdgeCamber)&lt;br /&gt;
            },&lt;br /&gt;
            validGeom,&lt;br /&gt;
            checks: { negThick, thickBounds, camberBounds, overHung }&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function zipPoints(xs, ys) {&lt;br /&gt;
        return xs.map((x, i) =&amp;gt; ({ x, y: ys[i] }));&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function interpolateSorted(xValues, yValues, xQuery) {&lt;br /&gt;
        const pairs = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; xValues.length; i++) {&lt;br /&gt;
            if (Number.isFinite(xValues[i]) &amp;amp;&amp;amp; Number.isFinite(yValues[i])) {&lt;br /&gt;
                pairs.push([xValues[i], yValues[i]]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        pairs.sort((a, b) =&amp;gt; a[0] - b[0]);&lt;br /&gt;
&lt;br /&gt;
        // Remove duplicate x values, keeping the first value, as a pragmatic&lt;br /&gt;
        // browser-side equivalent for plotting.&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        for (const pair of pairs) {&lt;br /&gt;
            if (!xs.length || Math.abs(pair[0] - xs[xs.length - 1]) &amp;gt; 1e-12) {&lt;br /&gt;
                xs.push(pair[0]);&lt;br /&gt;
                ys.push(pair[1]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return xQuery.map((q) =&amp;gt; {&lt;br /&gt;
            if (!xs.length || q &amp;lt; xs[0] || q &amp;gt; xs[xs.length - 1]) return NaN;&lt;br /&gt;
            if (xs.length === 1) return ys[0];&lt;br /&gt;
&lt;br /&gt;
            let lo = 0;&lt;br /&gt;
            let hi = xs.length - 1;&lt;br /&gt;
            while (hi - lo &amp;gt; 1) {&lt;br /&gt;
                const mid = Math.floor((lo + hi) / 2);&lt;br /&gt;
                if (xs[mid] &amp;lt;= q) lo = mid;&lt;br /&gt;
                else hi = mid;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            const dx = xs[hi] - xs[lo];&lt;br /&gt;
            const t = dx === 0 ? 0 : (q - xs[lo]) / dx;&lt;br /&gt;
            return ys[lo] + t * (ys[hi] - ys[lo]);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function fmt(value) {&lt;br /&gt;
        if (!Number.isFinite(value)) return &#039;—&#039;;&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 1) return value.toFixed(3);&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 0.1) return value.toFixed(4);&lt;br /&gt;
        return value.toFixed(5);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function createSvgElement(tag, attrs) {&lt;br /&gt;
        const ns = &#039;http://www.w3.org/2000/svg&#039;;&lt;br /&gt;
        const el = document.createElementNS(ns, tag);&lt;br /&gt;
        Object.entries(attrs).forEach(([key, value]) =&amp;gt; el.setAttribute(key, String(value)));&lt;br /&gt;
        return el;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const validPoints = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; geom.x.length; i++) {&lt;br /&gt;
            if (Number.isFinite(geom.upper[i])) validPoints.push([geom.x[i], geom.upper[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.lower[i])) validPoints.push([geom.x[i], geom.lower[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.camber[i])) validPoints.push([geom.x[i], geom.camber[i]]);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (!validPoints.length) {&lt;br /&gt;
            const text = createSvgElement(&#039;text&#039;, { x: 50, y: 50, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-empty&#039; });&lt;br /&gt;
            text.textContent = &#039;Geometry cannot be plotted for these parameters&#039;;&lt;br /&gt;
            svg.appendChild(text);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        let minY = Math.min(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        let maxY = Math.max(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {&lt;br /&gt;
            minY = -0.1;&lt;br /&gt;
            maxY = 0.1;&lt;br /&gt;
        }&lt;br /&gt;
        const span = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        const margin = span * 0.12;&lt;br /&gt;
        minY -= margin;&lt;br /&gt;
        maxY += margin;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 520;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 25;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + x * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and axes.&lt;br /&gt;
        [0, 0.25, 0.5, 0.75, 1].forEach((xVal) =&amp;gt; {&lt;br /&gt;
            const x = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: x, y1: padTop, x2: x, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = xVal.toFixed(2);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const yTicks = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTicks; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTicks;&lt;br /&gt;
            const y = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: y, x2: padLeft + plotW, y2: y, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x: padLeft - 9, y: y + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: sy(0), x2: padLeft + plotW, y2: sy(0), class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: padTop, x2: padLeft, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // X axis label.&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, { x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039; });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        // Y axis label.&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromSeries(xs, ys) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; xs.length; i++) {&lt;br /&gt;
                if (!Number.isFinite(xs[i]) || !Number.isFinite(ys[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                const command = started ? &#039;L&#039; : &#039;M&#039;;&lt;br /&gt;
                d += `${command}${sx(xs[i]).toFixed(2)},${sy(ys[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const upperPath = pathFromSeries(geom.x, geom.upper);&lt;br /&gt;
        const lowerPath = pathFromSeries([...geom.x].reverse(), [...geom.lower].reverse());&lt;br /&gt;
&lt;br /&gt;
        const profile = createSvgElement(&#039;path&#039;, { d: `${upperPath} ${lowerPath}`, class: &#039;aerofoil-profile&#039; });&lt;br /&gt;
        svg.appendChild(profile);&lt;br /&gt;
&lt;br /&gt;
        const upper = createSvgElement(&#039;path&#039;, { d: upperPath, class: &#039;aerofoil-upper&#039; });&lt;br /&gt;
        const lower = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.lower), class: &#039;aerofoil-lower&#039; });&lt;br /&gt;
        const camber = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.camber), class: &#039;aerofoil-camber&#039; });&lt;br /&gt;
        svg.appendChild(upper);&lt;br /&gt;
        svg.appendChild(lower);&lt;br /&gt;
        svg.appendChild(camber);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderConstructionPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const allCurves = [&lt;br /&gt;
            geom.thickLeadCurve,&lt;br /&gt;
            geom.thickTrailCurve,&lt;br /&gt;
            geom.camberLeadCurve,&lt;br /&gt;
            geom.camberTrailCurve&lt;br /&gt;
        ];&lt;br /&gt;
        const allPoints = [&lt;br /&gt;
            ...geom.controlPoints.thicknessLead,&lt;br /&gt;
            ...geom.controlPoints.thicknessTrail,&lt;br /&gt;
            ...geom.controlPoints.camberLead,&lt;br /&gt;
            ...geom.controlPoints.camberTrail&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const finite = (v) =&amp;gt; Number.isFinite(v);&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        allCurves.forEach((curve) =&amp;gt; {&lt;br /&gt;
            curve.x.forEach((x) =&amp;gt; { if (finite(x)) xs.push(x); });&lt;br /&gt;
            curve.y.forEach((y) =&amp;gt; { if (finite(y)) ys.push(y); });&lt;br /&gt;
        });&lt;br /&gt;
        allPoints.forEach((p) =&amp;gt; {&lt;br /&gt;
            if (finite(p.x)) xs.push(p.x);&lt;br /&gt;
            if (finite(p.y)) ys.push(p.y);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        if (!xs.length || !ys.length) return;&lt;br /&gt;
&lt;br /&gt;
        let minX = Math.min(...xs);&lt;br /&gt;
        let maxX = Math.max(...xs);&lt;br /&gt;
        let minY = Math.min(...ys);&lt;br /&gt;
        let maxY = Math.max(...ys);&lt;br /&gt;
        const spanX = Math.max(maxX - minX, 0.2);&lt;br /&gt;
        const spanY = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        minX -= 0.08 * spanX;&lt;br /&gt;
        maxX += 0.08 * spanX;&lt;br /&gt;
        minY -= 0.12 * spanY;&lt;br /&gt;
        maxY += 0.12 * spanY;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 500;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 35;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + (x - minX) / (maxX - minX) * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and x/c tick labels.&lt;br /&gt;
        const xTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= xTickCount; i++) {&lt;br /&gt;
            const xVal = minX + (maxX - minX) * i / xTickCount;&lt;br /&gt;
            const px = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: px, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(xVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const yTickCount = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTickCount; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTickCount;&lt;br /&gt;
            const py = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                x: padLeft - 9, y: py + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039;&lt;br /&gt;
            });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (minX &amp;lt;= 0 &amp;amp;&amp;amp; maxX &amp;gt;= 0) {&lt;br /&gt;
            const px = sx(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: px, y1: padTop, x2: px, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
        if (minY &amp;lt;= 0 &amp;amp;&amp;amp; maxY &amp;gt;= 0) {&lt;br /&gt;
            const py = sy(0);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: py, x2: padLeft + plotW, y2: py, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
            }));&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;&lt;br /&gt;
        });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromCurve(curve) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; curve.x.length; i++) {&lt;br /&gt;
                if (!finite(curve.x[i]) || !finite(curve.y[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                d += `${started ? &#039;L&#039; : &#039;M&#039;}${sx(curve.x[i]).toFixed(2)},${sy(curve.y[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        // Curves.&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickLeadCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.thickTrailCurve), class: &#039;aerofoil-thickness-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberLeadCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;path&#039;, {&lt;br /&gt;
            d: pathFromCurve(geom.camberTrailCurve), class: &#039;aerofoil-camber-curve&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // Control polygons and labelled points.&lt;br /&gt;
        function drawControlSet(points, prefix, lineClass, pointClass) {&lt;br /&gt;
            const polygonPoints = points&lt;br /&gt;
                .filter((p) =&amp;gt; finite(p.x) &amp;amp;&amp;amp; finite(p.y))&lt;br /&gt;
                .map((p) =&amp;gt; `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`)&lt;br /&gt;
                .join(&#039; &#039;);&lt;br /&gt;
            if (polygonPoints) {&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;polyline&#039;, {&lt;br /&gt;
                    points: polygonPoints,&lt;br /&gt;
                    class: lineClass,&lt;br /&gt;
                    fill: &#039;none&#039;&lt;br /&gt;
                }));&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            points.forEach((p, i) =&amp;gt; {&lt;br /&gt;
                if (!finite(p.x) || !finite(p.y)) return;&lt;br /&gt;
                const px = sx(p.x);&lt;br /&gt;
                const py = sy(p.y);&lt;br /&gt;
                svg.appendChild(createSvgElement(&#039;circle&#039;, {&lt;br /&gt;
                    cx: px, cy: py, r: 4, class: pointClass&lt;br /&gt;
                }));&lt;br /&gt;
&lt;br /&gt;
                const label = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
                    x: px + 7,&lt;br /&gt;
                    y: py - 7,&lt;br /&gt;
                    class: &#039;aerofoil-control-point-label&#039;&lt;br /&gt;
                });&lt;br /&gt;
                label.textContent = `${prefix}${i}`;&lt;br /&gt;
                svg.appendChild(label);&lt;br /&gt;
            });&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessLead, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.thicknessTrail, &#039;T&#039;, &#039;aerofoil-thickness-control&#039;, &#039;aerofoil-thickness-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberLead, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
        drawControlSet(geom.controlPoints.camberTrail, &#039;C&#039;, &#039;aerofoil-camber-control&#039;, &#039;aerofoil-camber-point&#039;);&lt;br /&gt;
&lt;br /&gt;
        // Legend.&lt;br /&gt;
        const legend = createSvgElement(&#039;g&#039;, { transform: `translate(${W - 220}, 18)` });&lt;br /&gt;
        const tLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 0, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        tLegend.textContent = &#039;Thickness curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(tLegend);&lt;br /&gt;
        const cLegend = createSvgElement(&#039;text&#039;, { x: 0, y: 20, class: &#039;aerofoil-legend-text&#039; });&lt;br /&gt;
        cLegend.textContent = &#039;Camber curve / controls&#039;;&lt;br /&gt;
        legend.appendChild(cLegend);&lt;br /&gt;
        svg.appendChild(legend);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function setup(root) {&lt;br /&gt;
        if (root.dataset.aerofoilInitialised === &#039;1&#039;) return;&lt;br /&gt;
        root.dataset.aerofoilInitialised = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
        const controls = document.createElement(&#039;div&#039;);&lt;br /&gt;
        controls.className = &#039;aerofoil-controls&#039;;&lt;br /&gt;
&lt;br /&gt;
        const values = {};&lt;br /&gt;
        const inputs = {};&lt;br /&gt;
        const valueLabels = {};&lt;br /&gt;
&lt;br /&gt;
        PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
            const row = document.createElement(&#039;div&#039;);&lt;br /&gt;
            row.className = &#039;aerofoil-control&#039;;&lt;br /&gt;
&lt;br /&gt;
            const label = document.createElement(&#039;label&#039;);&lt;br /&gt;
            label.className = &#039;aerofoil-control-label&#039;;&lt;br /&gt;
            label.htmlFor = `aerofoil-${p.key}`;&lt;br /&gt;
            label.textContent = p.label;&lt;br /&gt;
&lt;br /&gt;
            const range = document.createElement(&#039;input&#039;);&lt;br /&gt;
            range.type = &#039;range&#039;;&lt;br /&gt;
            range.id = `aerofoil-${p.key}`;&lt;br /&gt;
            range.min = p.min;&lt;br /&gt;
            range.max = p.max;&lt;br /&gt;
            range.step = p.step;&lt;br /&gt;
            range.value = p.value;&lt;br /&gt;
&lt;br /&gt;
            const number = document.createElement(&#039;input&#039;);&lt;br /&gt;
            number.type = &#039;number&#039;;&lt;br /&gt;
            number.min = p.min;&lt;br /&gt;
            number.max = p.max;&lt;br /&gt;
            number.step = p.step;&lt;br /&gt;
            number.value = p.value;&lt;br /&gt;
            number.className = &#039;aerofoil-number&#039;;&lt;br /&gt;
&lt;br /&gt;
            const valueSpan = document.createElement(&#039;span&#039;);&lt;br /&gt;
            valueSpan.className = &#039;aerofoil-value&#039;;&lt;br /&gt;
&lt;br /&gt;
            const setValue = (raw) =&amp;gt; {&lt;br /&gt;
                let value = Number(raw);&lt;br /&gt;
                if (!Number.isFinite(value)) value = p.value;&lt;br /&gt;
                value = Math.min(p.max, Math.max(p.min, value));&lt;br /&gt;
                range.value = value;&lt;br /&gt;
                number.value = value;&lt;br /&gt;
                values[p.key] = value;&lt;br /&gt;
                valueSpan.textContent = value.toFixed(Math.max(0, decimalPlaces(p.step)));&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            range.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(range.value));&lt;br /&gt;
            number.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(number.value));&lt;br /&gt;
&lt;br /&gt;
            setValue(p.value);&lt;br /&gt;
            inputs[p.key] = range;&lt;br /&gt;
            valueLabels[p.key] = valueSpan;&lt;br /&gt;
&lt;br /&gt;
            row.appendChild(label);&lt;br /&gt;
            row.appendChild(range);&lt;br /&gt;
            row.appendChild(number);&lt;br /&gt;
            row.appendChild(valueSpan);&lt;br /&gt;
            controls.appendChild(row);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const actions = document.createElement(&#039;div&#039;);&lt;br /&gt;
        actions.className = &#039;aerofoil-actions&#039;;&lt;br /&gt;
&lt;br /&gt;
        const reset = document.createElement(&#039;button&#039;);&lt;br /&gt;
        reset.type = &#039;button&#039;;&lt;br /&gt;
        reset.className = &#039;mw-ui-button&#039;;&lt;br /&gt;
        reset.textContent = &#039;Reset&#039;;&lt;br /&gt;
        reset.addEventListener(&#039;click&#039;, () =&amp;gt; {&lt;br /&gt;
            PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
                inputs[p.key].value = p.value;&lt;br /&gt;
                inputs[p.key].dispatchEvent(new Event(&#039;input&#039;, { bubbles: true }));&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        actions.appendChild(reset);&lt;br /&gt;
&lt;br /&gt;
        const plotWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        plotWrap.className = &#039;aerofoil-plot-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const svg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 520&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Interactive aerofoil plot&#039;&lt;br /&gt;
        });&lt;br /&gt;
        plotWrap.appendChild(svg);&lt;br /&gt;
&lt;br /&gt;
        const constructionTitle = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionTitle.className = &#039;aerofoil-subplot-title&#039;;&lt;br /&gt;
        constructionTitle.textContent = &#039;Bezier thickness and camber construction&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        constructionWrap.className = &#039;aerofoil-plot-wrap aerofoil-construction-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const constructionSvg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 500&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Bezier thickness and camber curves with control points&#039;&lt;br /&gt;
        });&lt;br /&gt;
        constructionWrap.appendChild(constructionSvg);&lt;br /&gt;
&lt;br /&gt;
        const status = document.createElement(&#039;div&#039;);&lt;br /&gt;
        status.className = &#039;aerofoil-status&#039;;&lt;br /&gt;
&lt;br /&gt;
        const checks = document.createElement(&#039;div&#039;);&lt;br /&gt;
        checks.className = &#039;aerofoil-checks&#039;;&lt;br /&gt;
&lt;br /&gt;
        const unitNote = document.createElement(&#039;div&#039;);&lt;br /&gt;
        unitNote.className = &#039;aerofoil-note&#039;;&lt;br /&gt;
        unitNote.textContent = `Angles are entered in ${ANGLE_MODE}; calculations follow the MATLAB tan/cot convention.`;&lt;br /&gt;
&lt;br /&gt;
        root.appendChild(controls);&lt;br /&gt;
        root.appendChild(actions);&lt;br /&gt;
        root.appendChild(plotWrap);&lt;br /&gt;
        root.appendChild(constructionTitle);&lt;br /&gt;
        root.appendChild(constructionWrap);&lt;br /&gt;
        root.appendChild(status);&lt;br /&gt;
        root.appendChild(checks);&lt;br /&gt;
        root.appendChild(unitNote);&lt;br /&gt;
&lt;br /&gt;
        const update = () =&amp;gt; {&lt;br /&gt;
            try {&lt;br /&gt;
                const geom = buildGeometry(values);&lt;br /&gt;
                renderPlot(svg, geom);&lt;br /&gt;
                renderConstructionPlot(constructionSvg, geom);&lt;br /&gt;
&lt;br /&gt;
                status.className = `aerofoil-status ${geom.validGeom ? &#039;is-valid&#039; : &#039;is-invalid&#039;}`;&lt;br /&gt;
                status.textContent = geom.validGeom ? &#039;Geometry valid&#039; : &#039;Geometry invalid — see checks below.&#039;;&lt;br /&gt;
&lt;br /&gt;
                checks.innerHTML = &#039;&#039;;&lt;br /&gt;
                const messages = [&lt;br /&gt;
                    [&#039;Negative thickness&#039;, geom.checks.negThick],&lt;br /&gt;
                    [&#039;Thickness x outside [0, 1]&#039;, geom.checks.thickBounds],&lt;br /&gt;
                    [&#039;Camber x outside [0, 1]&#039;, geom.checks.camberBounds],&lt;br /&gt;
                    [&#039;Overhang / decreasing x&#039;, geom.checks.overHung]&lt;br /&gt;
                ];&lt;br /&gt;
                messages.forEach(([name, failed]) =&amp;gt; {&lt;br /&gt;
                    const item = document.createElement(&#039;span&#039;);&lt;br /&gt;
                    item.className = failed ? &#039;fail&#039; : &#039;pass&#039;;&lt;br /&gt;
                    item.textContent = `${failed ? &#039;✗&#039; : &#039;✓&#039;} ${name}`;&lt;br /&gt;
                    checks.appendChild(item);&lt;br /&gt;
                });&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                status.className = &#039;aerofoil-status is-invalid&#039;;&lt;br /&gt;
                status.textContent = `Geometry error: ${error.message}`;&lt;br /&gt;
                console.error(&#039;Aerofoil Designer:&#039;, error);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        Object.keys(inputs).forEach((key) =&amp;gt; {&lt;br /&gt;
            inputs[key].addEventListener(&#039;input&#039;, update);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        update();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function decimalPlaces(step) {&lt;br /&gt;
        const text = String(step);&lt;br /&gt;
        if (!text.includes(&#039;.&#039;)) return 0;&lt;br /&gt;
        return text.split(&#039;.&#039;)[1].length;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        document.querySelectorAll(&#039;#aerofoil-tool&#039;).forEach(setup);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (window.mw &amp;amp;&amp;amp; mw.hook) {&lt;br /&gt;
        mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
        init();&lt;br /&gt;
    } else {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, init);&lt;br /&gt;
    }&lt;br /&gt;
}());&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=49</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.css</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=49"/>
		<updated>2026-09-23T10:15:33Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Interactive Bezier aerofoil designer */&lt;br /&gt;
#aerofoil-tool {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 1rem 0;&lt;br /&gt;
    font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-controls {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));&lt;br /&gt;
    gap: 0.55rem 1rem;&lt;br /&gt;
    margin-bottom: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: 35px minmax(100px, 1fr) 70px;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    gap: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-label {&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control input[type=&#039;range&#039;] {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-number {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
.aerofoil-actions {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    margin: 0.6rem 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
    border: 1px solid #a2a9b1;&lt;br /&gt;
    background: #fff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap svg {&lt;br /&gt;
    display: block;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-grid {&lt;br /&gt;
    stroke: #eaecf0;&lt;br /&gt;
    stroke-width: 1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-axis {&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-profile {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #202122;&lt;br /&gt;
    stroke-width: 1.8;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-upper,&lt;br /&gt;
.aerofoil-lower {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.4;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.3;&lt;br /&gt;
    stroke-dasharray: 6 5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-tick,&lt;br /&gt;
.aerofoil-axis-label {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 13px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-empty {&lt;br /&gt;
    fill: #a2a9b1;&lt;br /&gt;
    font-size: 20px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    padding: 0.6rem 0.8rem;&lt;br /&gt;
    border-left: 4px solid;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-valid {&lt;br /&gt;
    border-left-color: #14866d;&lt;br /&gt;
    background: #f1fff8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-invalid {&lt;br /&gt;
    border-left-color: #b32424;&lt;br /&gt;
    background: #fff4f4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    gap: 0.45rem 1rem;&lt;br /&gt;
    margin-top: 0.6rem;&lt;br /&gt;
    font-size: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .pass {&lt;br /&gt;
    color: #14866d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .fail {&lt;br /&gt;
    color: #b32424;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-note {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    color: #72777d;&lt;br /&gt;
    font-size: 0.85rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 700px) {&lt;br /&gt;
    .aerofoil-controls {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-control {&lt;br /&gt;
        grid-template-columns: 120px 1fr 78px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-value {&lt;br /&gt;
        display: none;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=48</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.css</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=48"/>
		<updated>2026-09-23T10:14:46Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Interactive Bezier aerofoil designer */&lt;br /&gt;
#aerofoil-tool {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 1rem 0;&lt;br /&gt;
    font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-controls {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));&lt;br /&gt;
    gap: 0.55rem 1rem;&lt;br /&gt;
    margin-bottom: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: 35px minmax(100px, 1fr) 70px;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    gap: 0.4rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-label {&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control input[type=&#039;range&#039;] {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-number {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-value {&lt;br /&gt;
    text-align: right;&lt;br /&gt;
    font-variant-numeric: tabular-nums;&lt;br /&gt;
    color: #54595d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-actions {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    margin: 0.6rem 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
    border: 1px solid #a2a9b1;&lt;br /&gt;
    background: #fff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap svg {&lt;br /&gt;
    display: block;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-grid {&lt;br /&gt;
    stroke: #eaecf0;&lt;br /&gt;
    stroke-width: 1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-axis {&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-profile {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #202122;&lt;br /&gt;
    stroke-width: 1.8;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-upper,&lt;br /&gt;
.aerofoil-lower {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.4;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.3;&lt;br /&gt;
    stroke-dasharray: 6 5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-tick,&lt;br /&gt;
.aerofoil-axis-label {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 13px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-empty {&lt;br /&gt;
    fill: #a2a9b1;&lt;br /&gt;
    font-size: 20px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    padding: 0.6rem 0.8rem;&lt;br /&gt;
    border-left: 4px solid;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-valid {&lt;br /&gt;
    border-left-color: #14866d;&lt;br /&gt;
    background: #f1fff8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-invalid {&lt;br /&gt;
    border-left-color: #b32424;&lt;br /&gt;
    background: #fff4f4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    gap: 0.45rem 1rem;&lt;br /&gt;
    margin-top: 0.6rem;&lt;br /&gt;
    font-size: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .pass {&lt;br /&gt;
    color: #14866d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .fail {&lt;br /&gt;
    color: #b32424;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-note {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    color: #72777d;&lt;br /&gt;
    font-size: 0.85rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 700px) {&lt;br /&gt;
    .aerofoil-controls {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-control {&lt;br /&gt;
        grid-template-columns: 120px 1fr 78px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-value {&lt;br /&gt;
        display: none;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=47</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.js</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=47"/>
		<updated>2026-09-23T10:11:59Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;(function () {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // =====================================================================&lt;br /&gt;
    // Aerofoil Designer - Bezier/PARSEC-style geometry from MATLAB code&lt;br /&gt;
    // =====================================================================&lt;br /&gt;
    // IMPORTANT:&lt;br /&gt;
    // Your MATLAB code uses tan() and cot() directly. MATLAB&#039;s tan() expects&lt;br /&gt;
    // radians. The UI defaults to degrees for easier slider input and converts&lt;br /&gt;
    // them to radians before evaluating tan/cot. Set ANGLE_MODE to &#039;radians&#039;&lt;br /&gt;
    // if your MATLAB input vector stores angles in radians.&lt;br /&gt;
    const ANGLE_MODE = &#039;degrees&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Edit these ranges/defaults to match your actual optimisation bounds.&lt;br /&gt;
    const PARAMS = [&lt;br /&gt;
        { key: &#039;xt&#039;,      label: &#039;xₜ&#039;,   min: 0.05, max: 0.95, step: 0.005, value: 0.25 },&lt;br /&gt;
        { key: &#039;yt&#039;,      label: &#039;yₜ&#039;,   min: 0.05, max: 0.5, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;xc&#039;,      label: &#039;x꜀&#039;,      min: 0.05, max: 0.95, step: 0.005, value: 0.40 },&lt;br /&gt;
        { key: &#039;yc&#039;,      label: &#039;y꜀&#039;,      min: -0.10, max: 0.2, step: 0.001, value: 0.02 },&lt;br /&gt;
        { key: &#039;rle&#039;,     label: &#039;rₗₑ&#039;, min: 0.001, max: 0.20, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;dzte&#039;,    label: &#039;Δzₜₑ&#039;, min: -0.10, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;zte&#039;,     label: &#039;zₜₑ&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;betate&#039;,  label: &#039;βₜₑ&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;gammale&#039;, label: &#039;γₗₑ&#039;, min: -45, max: 45, step: 0.5, value: 5 },&lt;br /&gt;
        { key: &#039;alphate&#039;, label: &#039;αₜₑ&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;b0&#039;,      label: &#039;b₀&#039;,                  min: 0.00, max: 0.30, step: 0.005, value: 0.05 },&lt;br /&gt;
        { key: &#039;b2&#039;,      label: &#039;b₂&#039;,                  min: 0.00, max: 0.60, step: 0.005, value: 0.20 },&lt;br /&gt;
        { key: &#039;b8&#039;,      label: &#039;b₈&#039;,                  min: 0.00, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;b15&#039;,     label: &#039;b₁₅&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 },&lt;br /&gt;
        { key: &#039;b17&#039;,     label: &#039;b₁₇&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 }&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    function toRadians(angle) {&lt;br /&gt;
        return ANGLE_MODE === &#039;degrees&#039; ? angle * Math.PI / 180 : angle;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function cot(angle) {&lt;br /&gt;
        return 1 / Math.tan(angle);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function bezier(points, u) {&lt;br /&gt;
        // General Bernstein-form Bezier evaluation. This exactly covers the&lt;br /&gt;
        // cubic (4-point) and quartic (5-point) curves used by the MATLAB code.&lt;br /&gt;
        const n = points.length - 1;&lt;br /&gt;
        let value = 0;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= n; i++) {&lt;br /&gt;
            let binomial = 1;&lt;br /&gt;
            for (let j = 1; j &amp;lt;= i; j++) {&lt;br /&gt;
                binomial *= (n - j + 1) / j;&lt;br /&gt;
            }&lt;br /&gt;
            value += binomial * Math.pow(1 - u, n - i) * Math.pow(u, i) * points[i];&lt;br /&gt;
        }&lt;br /&gt;
        return value;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function sampleBezier(points, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            const u = count === 1 ? 0 : i / (count - 1);&lt;br /&gt;
            values[i] = bezier(points, u);&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function linspace(start, stop, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        if (count === 1) {&lt;br /&gt;
            values[0] = start;&lt;br /&gt;
            return values;&lt;br /&gt;
        }&lt;br /&gt;
        const step = (stop - start) / (count - 1);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            values[i] = start + i * step;&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function buildGeometry(v) {&lt;br /&gt;
        const beta = toRadians(v.betate);&lt;br /&gt;
        const gamma = toRadians(v.gammale);&lt;br /&gt;
        const alpha = toRadians(v.alphate);&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CONTROL POINTS ----&lt;br /&gt;
        const xLeadEdgeThick = [0, 0, -3 * v.b8 * v.b8 / (2 * v.rle), v.xt];&lt;br /&gt;
        const yLeadEdgeThick = [0, v.b8, v.yt, v.yt];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeThick = [&lt;br /&gt;
            v.xt,&lt;br /&gt;
            (7 * v.xt + 9 * v.b8 * v.b8 / (2 * v.rle)) / 4,&lt;br /&gt;
            3 * v.xt + 15 * v.b8 * v.b8 / (4 * v.rle),&lt;br /&gt;
            v.b15,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeThick = [&lt;br /&gt;
            v.yt,&lt;br /&gt;
            v.yt,&lt;br /&gt;
            (v.yt + v.b8) / 2,&lt;br /&gt;
            v.dzte + (1 - v.b15) * Math.tan(beta),&lt;br /&gt;
            v.dzte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const xLeadEdgeCamber = [0, v.b0, v.b2, v.xc];&lt;br /&gt;
        const yLeadEdgeCamber = [0, v.b0 * Math.tan(gamma), v.yc, v.yc];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeCamber = [&lt;br /&gt;
            v.xc,&lt;br /&gt;
            (3 * v.xc - v.yc * cot(gamma)) / 2,&lt;br /&gt;
            (-8 * v.yc * cot(gamma) + 13 * v.xc) / 6,&lt;br /&gt;
            v.b17,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeCamber = [&lt;br /&gt;
            v.yc,&lt;br /&gt;
            v.yc,&lt;br /&gt;
            5 * v.yc / 6,&lt;br /&gt;
            v.zte - (1 - v.b17) * Math.tan(alpha),&lt;br /&gt;
            v.zte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CURVES ----&lt;br /&gt;
        const n = 100;&lt;br /&gt;
        const u = linspace(0, 1, n);&lt;br /&gt;
&lt;br /&gt;
        const thickCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeThick, n).concat(sampleBezier(xTrailEdgeThick, n)),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeThick, n).concat(sampleBezier(yTrailEdgeThick, n))&lt;br /&gt;
        };&lt;br /&gt;
        const camberCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeCamber, n).concat(sampleBezier(xTrailEdgeCamber, n)),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeCamber, n).concat(sampleBezier(yTrailEdgeCamber, n))&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // ---- GEOMETRY VALIDITY CHECK ----&lt;br /&gt;
        let negThick = false;&lt;br /&gt;
        let thickBounds = false;&lt;br /&gt;
        let camberBounds = false;&lt;br /&gt;
        let overHung = false;&lt;br /&gt;
&lt;br /&gt;
        for (let i = 0; i &amp;lt; thickCurve.x.length; i++) {&lt;br /&gt;
            if (thickCurve.y[i] &amp;lt; 0) negThick = true;&lt;br /&gt;
            if (thickCurve.x[i] &amp;lt; 0 || thickCurve.x[i] &amp;gt; 1) thickBounds = true;&lt;br /&gt;
            if (camberCurve.x[i] &amp;lt; 0 || camberCurve.x[i] &amp;gt; 1) camberBounds = true;&lt;br /&gt;
            if (i &amp;gt; 0 &amp;amp;&amp;amp; (thickCurve.x[i] &amp;lt; thickCurve.x[i - 1] || camberCurve.x[i] &amp;lt; camberCurve.x[i - 1])) {&lt;br /&gt;
                overHung = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const validGeom = !(negThick || thickBounds || camberBounds || overHung);&lt;br /&gt;
&lt;br /&gt;
        // ---- INTERPOLATION ----&lt;br /&gt;
        // MATLAB uses interp1(..., &#039;linear&#039;) after checking for overhang.&lt;br /&gt;
        // For a valid monotonic curve, sorting by x gives the same result and&lt;br /&gt;
        // also lets us keep rendering something useful while the user explores&lt;br /&gt;
        // invalid parameter combinations.&lt;br /&gt;
        const x = linspace(0, 1, n * 2);&lt;br /&gt;
        const interpThickCurve = interpolateSorted(thickCurve.x, thickCurve.y, x);&lt;br /&gt;
        const interpCamberCurve = interpolateSorted(camberCurve.x, camberCurve.y, x);&lt;br /&gt;
&lt;br /&gt;
        const upper = new Array(x.length);&lt;br /&gt;
        const lower = new Array(x.length);&lt;br /&gt;
        for (let i = 0; i &amp;lt; x.length; i++) {&lt;br /&gt;
            upper[i] = interpCamberCurve[i] + interpThickCurve[i];&lt;br /&gt;
            lower[i] = interpCamberCurve[i] - interpThickCurve[i];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            x,&lt;br /&gt;
            upper,&lt;br /&gt;
            lower,&lt;br /&gt;
            camber: interpCamberCurve,&lt;br /&gt;
            thickness: interpThickCurve,&lt;br /&gt;
            thickCurve,&lt;br /&gt;
            camberCurve,&lt;br /&gt;
            validGeom,&lt;br /&gt;
            checks: { negThick, thickBounds, camberBounds, overHung }&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function interpolateSorted(xValues, yValues, xQuery) {&lt;br /&gt;
        const pairs = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; xValues.length; i++) {&lt;br /&gt;
            if (Number.isFinite(xValues[i]) &amp;amp;&amp;amp; Number.isFinite(yValues[i])) {&lt;br /&gt;
                pairs.push([xValues[i], yValues[i]]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        pairs.sort((a, b) =&amp;gt; a[0] - b[0]);&lt;br /&gt;
&lt;br /&gt;
        // Remove duplicate x values, keeping the first value, as a pragmatic&lt;br /&gt;
        // browser-side equivalent for plotting.&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        for (const pair of pairs) {&lt;br /&gt;
            if (!xs.length || Math.abs(pair[0] - xs[xs.length - 1]) &amp;gt; 1e-12) {&lt;br /&gt;
                xs.push(pair[0]);&lt;br /&gt;
                ys.push(pair[1]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return xQuery.map((q) =&amp;gt; {&lt;br /&gt;
            if (!xs.length || q &amp;lt; xs[0] || q &amp;gt; xs[xs.length - 1]) return NaN;&lt;br /&gt;
            if (xs.length === 1) return ys[0];&lt;br /&gt;
&lt;br /&gt;
            let lo = 0;&lt;br /&gt;
            let hi = xs.length - 1;&lt;br /&gt;
            while (hi - lo &amp;gt; 1) {&lt;br /&gt;
                const mid = Math.floor((lo + hi) / 2);&lt;br /&gt;
                if (xs[mid] &amp;lt;= q) lo = mid;&lt;br /&gt;
                else hi = mid;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            const dx = xs[hi] - xs[lo];&lt;br /&gt;
            const t = dx === 0 ? 0 : (q - xs[lo]) / dx;&lt;br /&gt;
            return ys[lo] + t * (ys[hi] - ys[lo]);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function fmt(value) {&lt;br /&gt;
        if (!Number.isFinite(value)) return &#039;—&#039;;&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 1) return value.toFixed(3);&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 0.1) return value.toFixed(4);&lt;br /&gt;
        return value.toFixed(5);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function createSvgElement(tag, attrs) {&lt;br /&gt;
        const ns = &#039;http://www.w3.org/2000/svg&#039;;&lt;br /&gt;
        const el = document.createElementNS(ns, tag);&lt;br /&gt;
        Object.entries(attrs).forEach(([key, value]) =&amp;gt; el.setAttribute(key, String(value)));&lt;br /&gt;
        return el;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const validPoints = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; geom.x.length; i++) {&lt;br /&gt;
            if (Number.isFinite(geom.upper[i])) validPoints.push([geom.x[i], geom.upper[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.lower[i])) validPoints.push([geom.x[i], geom.lower[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.camber[i])) validPoints.push([geom.x[i], geom.camber[i]]);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (!validPoints.length) {&lt;br /&gt;
            const text = createSvgElement(&#039;text&#039;, { x: 50, y: 50, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-empty&#039; });&lt;br /&gt;
            text.textContent = &#039;Geometry cannot be plotted for these parameters&#039;;&lt;br /&gt;
            svg.appendChild(text);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        let minY = Math.min(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        let maxY = Math.max(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {&lt;br /&gt;
            minY = -0.1;&lt;br /&gt;
            maxY = 0.1;&lt;br /&gt;
        }&lt;br /&gt;
        const span = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        const margin = span * 0.12;&lt;br /&gt;
        minY -= margin;&lt;br /&gt;
        maxY += margin;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 520;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 25;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + x * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and axes.&lt;br /&gt;
        [0, 0.25, 0.5, 0.75, 1].forEach((xVal) =&amp;gt; {&lt;br /&gt;
            const x = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: x, y1: padTop, x2: x, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = xVal.toFixed(2);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const yTicks = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTicks; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTicks;&lt;br /&gt;
            const y = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: y, x2: padLeft + plotW, y2: y, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x: padLeft - 9, y: y + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: sy(0), x2: padLeft + plotW, y2: sy(0), class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: padTop, x2: padLeft, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // X axis label.&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, { x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039; });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        // Y axis label.&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromSeries(xs, ys) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; xs.length; i++) {&lt;br /&gt;
                if (!Number.isFinite(xs[i]) || !Number.isFinite(ys[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                const command = started ? &#039;L&#039; : &#039;M&#039;;&lt;br /&gt;
                d += `${command}${sx(xs[i]).toFixed(2)},${sy(ys[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const upperPath = pathFromSeries(geom.x, geom.upper);&lt;br /&gt;
        const lowerPath = pathFromSeries([...geom.x].reverse(), [...geom.lower].reverse());&lt;br /&gt;
&lt;br /&gt;
        const profile = createSvgElement(&#039;path&#039;, { d: `${upperPath} ${lowerPath}`, class: &#039;aerofoil-profile&#039; });&lt;br /&gt;
        svg.appendChild(profile);&lt;br /&gt;
&lt;br /&gt;
        const upper = createSvgElement(&#039;path&#039;, { d: upperPath, class: &#039;aerofoil-upper&#039; });&lt;br /&gt;
        const lower = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.lower), class: &#039;aerofoil-lower&#039; });&lt;br /&gt;
        const camber = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.camber), class: &#039;aerofoil-camber&#039; });&lt;br /&gt;
        svg.appendChild(upper);&lt;br /&gt;
        svg.appendChild(lower);&lt;br /&gt;
        svg.appendChild(camber);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function setup(root) {&lt;br /&gt;
        if (root.dataset.aerofoilInitialised === &#039;1&#039;) return;&lt;br /&gt;
        root.dataset.aerofoilInitialised = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
        const controls = document.createElement(&#039;div&#039;);&lt;br /&gt;
        controls.className = &#039;aerofoil-controls&#039;;&lt;br /&gt;
&lt;br /&gt;
        const values = {};&lt;br /&gt;
        const inputs = {};&lt;br /&gt;
        const valueLabels = {};&lt;br /&gt;
&lt;br /&gt;
        PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
            const row = document.createElement(&#039;div&#039;);&lt;br /&gt;
            row.className = &#039;aerofoil-control&#039;;&lt;br /&gt;
&lt;br /&gt;
            const label = document.createElement(&#039;label&#039;);&lt;br /&gt;
            label.className = &#039;aerofoil-control-label&#039;;&lt;br /&gt;
            label.htmlFor = `aerofoil-${p.key}`;&lt;br /&gt;
            label.textContent = p.label;&lt;br /&gt;
&lt;br /&gt;
            const range = document.createElement(&#039;input&#039;);&lt;br /&gt;
            range.type = &#039;range&#039;;&lt;br /&gt;
            range.id = `aerofoil-${p.key}`;&lt;br /&gt;
            range.min = p.min;&lt;br /&gt;
            range.max = p.max;&lt;br /&gt;
            range.step = p.step;&lt;br /&gt;
            range.value = p.value;&lt;br /&gt;
&lt;br /&gt;
            const number = document.createElement(&#039;input&#039;);&lt;br /&gt;
            number.type = &#039;number&#039;;&lt;br /&gt;
            number.min = p.min;&lt;br /&gt;
            number.max = p.max;&lt;br /&gt;
            number.step = p.step;&lt;br /&gt;
            number.value = p.value;&lt;br /&gt;
            number.className = &#039;aerofoil-number&#039;;&lt;br /&gt;
&lt;br /&gt;
            const valueSpan = document.createElement(&#039;span&#039;);&lt;br /&gt;
            valueSpan.className = &#039;aerofoil-value&#039;;&lt;br /&gt;
&lt;br /&gt;
            const setValue = (raw) =&amp;gt; {&lt;br /&gt;
                let value = Number(raw);&lt;br /&gt;
                if (!Number.isFinite(value)) value = p.value;&lt;br /&gt;
                value = Math.min(p.max, Math.max(p.min, value));&lt;br /&gt;
                range.value = value;&lt;br /&gt;
                number.value = value;&lt;br /&gt;
                values[p.key] = value;&lt;br /&gt;
                valueSpan.textContent = value.toFixed(Math.max(0, decimalPlaces(p.step)));&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            range.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(range.value));&lt;br /&gt;
            number.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(number.value));&lt;br /&gt;
&lt;br /&gt;
            setValue(p.value);&lt;br /&gt;
            inputs[p.key] = range;&lt;br /&gt;
            valueLabels[p.key] = valueSpan;&lt;br /&gt;
&lt;br /&gt;
            row.appendChild(label);&lt;br /&gt;
            row.appendChild(range);&lt;br /&gt;
            row.appendChild(number);&lt;br /&gt;
            row.appendChild(valueSpan);&lt;br /&gt;
            controls.appendChild(row);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const actions = document.createElement(&#039;div&#039;);&lt;br /&gt;
        actions.className = &#039;aerofoil-actions&#039;;&lt;br /&gt;
&lt;br /&gt;
        const reset = document.createElement(&#039;button&#039;);&lt;br /&gt;
        reset.type = &#039;button&#039;;&lt;br /&gt;
        reset.className = &#039;mw-ui-button&#039;;&lt;br /&gt;
        reset.textContent = &#039;Reset&#039;;&lt;br /&gt;
        reset.addEventListener(&#039;click&#039;, () =&amp;gt; {&lt;br /&gt;
            PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
                inputs[p.key].value = p.value;&lt;br /&gt;
                inputs[p.key].dispatchEvent(new Event(&#039;input&#039;, { bubbles: true }));&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        actions.appendChild(reset);&lt;br /&gt;
&lt;br /&gt;
        const plotWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        plotWrap.className = &#039;aerofoil-plot-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const svg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 520&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Interactive aerofoil plot&#039;&lt;br /&gt;
        });&lt;br /&gt;
        plotWrap.appendChild(svg);&lt;br /&gt;
&lt;br /&gt;
        const status = document.createElement(&#039;div&#039;);&lt;br /&gt;
        status.className = &#039;aerofoil-status&#039;;&lt;br /&gt;
&lt;br /&gt;
        const checks = document.createElement(&#039;div&#039;);&lt;br /&gt;
        checks.className = &#039;aerofoil-checks&#039;;&lt;br /&gt;
&lt;br /&gt;
        const unitNote = document.createElement(&#039;div&#039;);&lt;br /&gt;
        unitNote.className = &#039;aerofoil-note&#039;;&lt;br /&gt;
        unitNote.textContent = `Angles are entered in ${ANGLE_MODE}; calculations follow the MATLAB tan/cot convention.`;&lt;br /&gt;
&lt;br /&gt;
        root.appendChild(controls);&lt;br /&gt;
        root.appendChild(actions);&lt;br /&gt;
        root.appendChild(plotWrap);&lt;br /&gt;
        root.appendChild(status);&lt;br /&gt;
        root.appendChild(checks);&lt;br /&gt;
        root.appendChild(unitNote);&lt;br /&gt;
&lt;br /&gt;
        const update = () =&amp;gt; {&lt;br /&gt;
            try {&lt;br /&gt;
                const geom = buildGeometry(values);&lt;br /&gt;
                renderPlot(svg, geom);&lt;br /&gt;
&lt;br /&gt;
                status.className = `aerofoil-status ${geom.validGeom ? &#039;is-valid&#039; : &#039;is-invalid&#039;}`;&lt;br /&gt;
                status.textContent = geom.validGeom ? &#039;Geometry valid&#039; : &#039;Geometry invalid — see checks below.&#039;;&lt;br /&gt;
&lt;br /&gt;
                checks.innerHTML = &#039;&#039;;&lt;br /&gt;
                const messages = [&lt;br /&gt;
                    [&#039;Negative thickness&#039;, geom.checks.negThick],&lt;br /&gt;
                    [&#039;Thickness x outside [0, 1]&#039;, geom.checks.thickBounds],&lt;br /&gt;
                    [&#039;Camber x outside [0, 1]&#039;, geom.checks.camberBounds],&lt;br /&gt;
                    [&#039;Overhang / decreasing x&#039;, geom.checks.overHung]&lt;br /&gt;
                ];&lt;br /&gt;
                messages.forEach(([name, failed]) =&amp;gt; {&lt;br /&gt;
                    const item = document.createElement(&#039;span&#039;);&lt;br /&gt;
                    item.className = failed ? &#039;fail&#039; : &#039;pass&#039;;&lt;br /&gt;
                    item.textContent = `${failed ? &#039;✗&#039; : &#039;✓&#039;} ${name}`;&lt;br /&gt;
                    checks.appendChild(item);&lt;br /&gt;
                });&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                status.className = &#039;aerofoil-status is-invalid&#039;;&lt;br /&gt;
                status.textContent = `Geometry error: ${error.message}`;&lt;br /&gt;
                console.error(&#039;Aerofoil Designer:&#039;, error);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        Object.keys(inputs).forEach((key) =&amp;gt; {&lt;br /&gt;
            inputs[key].addEventListener(&#039;input&#039;, update);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        update();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function decimalPlaces(step) {&lt;br /&gt;
        const text = String(step);&lt;br /&gt;
        if (!text.includes(&#039;.&#039;)) return 0;&lt;br /&gt;
        return text.split(&#039;.&#039;)[1].length;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        document.querySelectorAll(&#039;#aerofoil-tool&#039;).forEach(setup);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (window.mw &amp;amp;&amp;amp; mw.hook) {&lt;br /&gt;
        mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
        init();&lt;br /&gt;
    } else {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, init);&lt;br /&gt;
    }&lt;br /&gt;
}());&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Aerofoil_Designer&amp;diff=46</id>
		<title>Aerofoil Designer</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Aerofoil_Designer&amp;diff=46"/>
		<updated>2026-09-23T10:05:47Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created page with &amp;quot;&amp;lt;div id=&amp;quot;aerofoil-tool&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;div id=&amp;quot;aerofoil-tool&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadgets-definition&amp;diff=45</id>
		<title>MediaWiki:Gadgets-definition</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadgets-definition&amp;diff=45"/>
		<updated>2026-09-23T09:58:05Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* AerofoilDesigner[ResourceLoader|default]|AerofoilDesigner.js|AerofoilDesigner.css&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=44</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.css</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=44"/>
		<updated>2026-09-23T09:48:11Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;/* Interactive Bezier aerofoil designer */&lt;br /&gt;
#aerofoil-tool {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 1rem 0;&lt;br /&gt;
    font-size: 0.95rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-controls {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));&lt;br /&gt;
    gap: 0.55rem 1rem;&lt;br /&gt;
    margin-bottom: 0.8rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: 150px minmax(120px, 1fr) 84px 68px;&lt;br /&gt;
    align-items: center;&lt;br /&gt;
    gap: 0.55rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control-label {&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-control input[type=&#039;range&#039;] {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-number {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-value {&lt;br /&gt;
    text-align: right;&lt;br /&gt;
    font-variant-numeric: tabular-nums;&lt;br /&gt;
    color: #54595d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-actions {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    justify-content: flex-end;&lt;br /&gt;
    margin: 0.6rem 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap {&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    overflow: hidden;&lt;br /&gt;
    border: 1px solid #a2a9b1;&lt;br /&gt;
    background: #fff;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-plot-wrap svg {&lt;br /&gt;
    display: block;&lt;br /&gt;
    width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-grid {&lt;br /&gt;
    stroke: #eaecf0;&lt;br /&gt;
    stroke-width: 1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-axis {&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.2;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-profile {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #202122;&lt;br /&gt;
    stroke-width: 1.8;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-upper,&lt;br /&gt;
.aerofoil-lower {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #36c;&lt;br /&gt;
    stroke-width: 2.4;&lt;br /&gt;
    stroke-linejoin: round;&lt;br /&gt;
    stroke-linecap: round;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-camber {&lt;br /&gt;
    fill: none;&lt;br /&gt;
    stroke: #72777d;&lt;br /&gt;
    stroke-width: 1.3;&lt;br /&gt;
    stroke-dasharray: 6 5;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-tick,&lt;br /&gt;
.aerofoil-axis-label {&lt;br /&gt;
    fill: #54595d;&lt;br /&gt;
    font-size: 13px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-empty {&lt;br /&gt;
    fill: #a2a9b1;&lt;br /&gt;
    font-size: 20px;&lt;br /&gt;
    font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    padding: 0.6rem 0.8rem;&lt;br /&gt;
    border-left: 4px solid;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-valid {&lt;br /&gt;
    border-left-color: #14866d;&lt;br /&gt;
    background: #f1fff8;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-status.is-invalid {&lt;br /&gt;
    border-left-color: #b32424;&lt;br /&gt;
    background: #fff4f4;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
    gap: 0.45rem 1rem;&lt;br /&gt;
    margin-top: 0.6rem;&lt;br /&gt;
    font-size: 0.9rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .pass {&lt;br /&gt;
    color: #14866d;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-checks .fail {&lt;br /&gt;
    color: #b32424;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.aerofoil-note {&lt;br /&gt;
    margin-top: 0.7rem;&lt;br /&gt;
    color: #72777d;&lt;br /&gt;
    font-size: 0.85rem;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 700px) {&lt;br /&gt;
    .aerofoil-controls {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-control {&lt;br /&gt;
        grid-template-columns: 120px 1fr 78px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    .aerofoil-value {&lt;br /&gt;
        display: none;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=43</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.js</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=43"/>
		<updated>2026-09-23T09:47:41Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;(function () {&lt;br /&gt;
    &#039;use strict&#039;;&lt;br /&gt;
&lt;br /&gt;
    // =====================================================================&lt;br /&gt;
    // Aerofoil Designer - Bezier/PARSEC-style geometry from MATLAB code&lt;br /&gt;
    // =====================================================================&lt;br /&gt;
    // IMPORTANT:&lt;br /&gt;
    // Your MATLAB code uses tan() and cot() directly. MATLAB&#039;s tan() expects&lt;br /&gt;
    // radians. The UI defaults to degrees for easier slider input and converts&lt;br /&gt;
    // them to radians before evaluating tan/cot. Set ANGLE_MODE to &#039;radians&#039;&lt;br /&gt;
    // if your MATLAB input vector stores angles in radians.&lt;br /&gt;
    const ANGLE_MODE = &#039;degrees&#039;;&lt;br /&gt;
&lt;br /&gt;
    // Edit these ranges/defaults to match your actual optimisation bounds.&lt;br /&gt;
    const PARAMS = [&lt;br /&gt;
        { key: &#039;xt&#039;,      label: &#039;xₜ — thickness join&#039;,   min: 0.05, max: 0.95, step: 0.005, value: 0.25 },&lt;br /&gt;
        { key: &#039;yt&#039;,      label: &#039;yₜ — thickness join&#039;,   min: 0.00, max: 0.15, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;xc&#039;,      label: &#039;x꜀ — camber join&#039;,      min: 0.05, max: 0.95, step: 0.005, value: 0.40 },&lt;br /&gt;
        { key: &#039;yc&#039;,      label: &#039;y꜀ — camber join&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.02 },&lt;br /&gt;
        { key: &#039;rle&#039;,     label: &#039;rₗₑ — leading-edge radius&#039;, min: 0.001, max: 0.20, step: 0.001, value: 0.03 },&lt;br /&gt;
        { key: &#039;dzte&#039;,    label: &#039;Δzₜₑ — TE thickness offset&#039;, min: -0.10, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;zte&#039;,     label: &#039;zₜₑ — TE camber&#039;,      min: -0.10, max: 0.15, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;betate&#039;,  label: &#039;βₜₑ — TE thickness angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;gammale&#039;, label: &#039;γₗₑ — LE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 5 },&lt;br /&gt;
        { key: &#039;alphate&#039;, label: &#039;αₜₑ — TE camber angle&#039;, min: -45, max: 45, step: 0.5, value: 0 },&lt;br /&gt;
        { key: &#039;b0&#039;,      label: &#039;b₀&#039;,                  min: 0.00, max: 0.30, step: 0.005, value: 0.05 },&lt;br /&gt;
        { key: &#039;b2&#039;,      label: &#039;b₂&#039;,                  min: 0.00, max: 0.60, step: 0.005, value: 0.20 },&lt;br /&gt;
        { key: &#039;b8&#039;,      label: &#039;b₈&#039;,                  min: 0.00, max: 0.10, step: 0.001, value: 0.00 },&lt;br /&gt;
        { key: &#039;b15&#039;,     label: &#039;b₁₅&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 },&lt;br /&gt;
        { key: &#039;b17&#039;,     label: &#039;b₁₇&#039;,                 min: 0.00, max: 1.00, step: 0.005, value: 0.85 }&lt;br /&gt;
    ];&lt;br /&gt;
&lt;br /&gt;
    function toRadians(angle) {&lt;br /&gt;
        return ANGLE_MODE === &#039;degrees&#039; ? angle * Math.PI / 180 : angle;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function cot(angle) {&lt;br /&gt;
        return 1 / Math.tan(angle);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function bezier(points, u) {&lt;br /&gt;
        // General Bernstein-form Bezier evaluation. This exactly covers the&lt;br /&gt;
        // cubic (4-point) and quartic (5-point) curves used by the MATLAB code.&lt;br /&gt;
        const n = points.length - 1;&lt;br /&gt;
        let value = 0;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= n; i++) {&lt;br /&gt;
            let binomial = 1;&lt;br /&gt;
            for (let j = 1; j &amp;lt;= i; j++) {&lt;br /&gt;
                binomial *= (n - j + 1) / j;&lt;br /&gt;
            }&lt;br /&gt;
            value += binomial * Math.pow(1 - u, n - i) * Math.pow(u, i) * points[i];&lt;br /&gt;
        }&lt;br /&gt;
        return value;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function sampleBezier(points, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            const u = count === 1 ? 0 : i / (count - 1);&lt;br /&gt;
            values[i] = bezier(points, u);&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function linspace(start, stop, count) {&lt;br /&gt;
        const values = new Array(count);&lt;br /&gt;
        if (count === 1) {&lt;br /&gt;
            values[0] = start;&lt;br /&gt;
            return values;&lt;br /&gt;
        }&lt;br /&gt;
        const step = (stop - start) / (count - 1);&lt;br /&gt;
        for (let i = 0; i &amp;lt; count; i++) {&lt;br /&gt;
            values[i] = start + i * step;&lt;br /&gt;
        }&lt;br /&gt;
        return values;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function buildGeometry(v) {&lt;br /&gt;
        const beta = toRadians(v.betate);&lt;br /&gt;
        const gamma = toRadians(v.gammale);&lt;br /&gt;
        const alpha = toRadians(v.alphate);&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CONTROL POINTS ----&lt;br /&gt;
        const xLeadEdgeThick = [0, 0, -3 * v.b8 * v.b8 / (2 * v.rle), v.xt];&lt;br /&gt;
        const yLeadEdgeThick = [0, v.b8, v.yt, v.yt];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeThick = [&lt;br /&gt;
            v.xt,&lt;br /&gt;
            (7 * v.xt + 9 * v.b8 * v.b8 / (2 * v.rle)) / 4,&lt;br /&gt;
            3 * v.xt + 15 * v.b8 * v.b8 / (4 * v.rle),&lt;br /&gt;
            v.b15,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeThick = [&lt;br /&gt;
            v.yt,&lt;br /&gt;
            v.yt,&lt;br /&gt;
            (v.yt + v.b8) / 2,&lt;br /&gt;
            v.dzte + (1 - v.b15) * Math.tan(beta),&lt;br /&gt;
            v.dzte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        const xLeadEdgeCamber = [0, v.b0, v.b2, v.xc];&lt;br /&gt;
        const yLeadEdgeCamber = [0, v.b0 * Math.tan(gamma), v.yc, v.yc];&lt;br /&gt;
&lt;br /&gt;
        const xTrailEdgeCamber = [&lt;br /&gt;
            v.xc,&lt;br /&gt;
            (3 * v.xc - v.yc * cot(gamma)) / 2,&lt;br /&gt;
            (-8 * v.yc * cot(gamma) + 13 * v.xc) / 6,&lt;br /&gt;
            v.b17,&lt;br /&gt;
            1&lt;br /&gt;
        ];&lt;br /&gt;
        const yTrailEdgeCamber = [&lt;br /&gt;
            v.yc,&lt;br /&gt;
            v.yc,&lt;br /&gt;
            5 * v.yc / 6,&lt;br /&gt;
            v.zte - (1 - v.b17) * Math.tan(alpha),&lt;br /&gt;
            v.zte&lt;br /&gt;
        ];&lt;br /&gt;
&lt;br /&gt;
        // ---- BEZIER CURVES ----&lt;br /&gt;
        const n = 100;&lt;br /&gt;
        const u = linspace(0, 1, n);&lt;br /&gt;
&lt;br /&gt;
        const thickCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeThick, n).concat(sampleBezier(xTrailEdgeThick, n)),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeThick, n).concat(sampleBezier(yTrailEdgeThick, n))&lt;br /&gt;
        };&lt;br /&gt;
        const camberCurve = {&lt;br /&gt;
            x: sampleBezier(xLeadEdgeCamber, n).concat(sampleBezier(xTrailEdgeCamber, n)),&lt;br /&gt;
            y: sampleBezier(yLeadEdgeCamber, n).concat(sampleBezier(yTrailEdgeCamber, n))&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        // ---- GEOMETRY VALIDITY CHECK ----&lt;br /&gt;
        let negThick = false;&lt;br /&gt;
        let thickBounds = false;&lt;br /&gt;
        let camberBounds = false;&lt;br /&gt;
        let overHung = false;&lt;br /&gt;
&lt;br /&gt;
        for (let i = 0; i &amp;lt; thickCurve.x.length; i++) {&lt;br /&gt;
            if (thickCurve.y[i] &amp;lt; 0) negThick = true;&lt;br /&gt;
            if (thickCurve.x[i] &amp;lt; 0 || thickCurve.x[i] &amp;gt; 1) thickBounds = true;&lt;br /&gt;
            if (camberCurve.x[i] &amp;lt; 0 || camberCurve.x[i] &amp;gt; 1) camberBounds = true;&lt;br /&gt;
            if (i &amp;gt; 0 &amp;amp;&amp;amp; (thickCurve.x[i] &amp;lt; thickCurve.x[i - 1] || camberCurve.x[i] &amp;lt; camberCurve.x[i - 1])) {&lt;br /&gt;
                overHung = true;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const validGeom = !(negThick || thickBounds || camberBounds || overHung);&lt;br /&gt;
&lt;br /&gt;
        // ---- INTERPOLATION ----&lt;br /&gt;
        // MATLAB uses interp1(..., &#039;linear&#039;) after checking for overhang.&lt;br /&gt;
        // For a valid monotonic curve, sorting by x gives the same result and&lt;br /&gt;
        // also lets us keep rendering something useful while the user explores&lt;br /&gt;
        // invalid parameter combinations.&lt;br /&gt;
        const x = linspace(0, 1, n * 2);&lt;br /&gt;
        const interpThickCurve = interpolateSorted(thickCurve.x, thickCurve.y, x);&lt;br /&gt;
        const interpCamberCurve = interpolateSorted(camberCurve.x, camberCurve.y, x);&lt;br /&gt;
&lt;br /&gt;
        const upper = new Array(x.length);&lt;br /&gt;
        const lower = new Array(x.length);&lt;br /&gt;
        for (let i = 0; i &amp;lt; x.length; i++) {&lt;br /&gt;
            upper[i] = interpCamberCurve[i] + interpThickCurve[i];&lt;br /&gt;
            lower[i] = interpCamberCurve[i] - interpThickCurve[i];&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return {&lt;br /&gt;
            x,&lt;br /&gt;
            upper,&lt;br /&gt;
            lower,&lt;br /&gt;
            camber: interpCamberCurve,&lt;br /&gt;
            thickness: interpThickCurve,&lt;br /&gt;
            thickCurve,&lt;br /&gt;
            camberCurve,&lt;br /&gt;
            validGeom,&lt;br /&gt;
            checks: { negThick, thickBounds, camberBounds, overHung }&lt;br /&gt;
        };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function interpolateSorted(xValues, yValues, xQuery) {&lt;br /&gt;
        const pairs = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; xValues.length; i++) {&lt;br /&gt;
            if (Number.isFinite(xValues[i]) &amp;amp;&amp;amp; Number.isFinite(yValues[i])) {&lt;br /&gt;
                pairs.push([xValues[i], yValues[i]]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        pairs.sort((a, b) =&amp;gt; a[0] - b[0]);&lt;br /&gt;
&lt;br /&gt;
        // Remove duplicate x values, keeping the first value, as a pragmatic&lt;br /&gt;
        // browser-side equivalent for plotting.&lt;br /&gt;
        const xs = [];&lt;br /&gt;
        const ys = [];&lt;br /&gt;
        for (const pair of pairs) {&lt;br /&gt;
            if (!xs.length || Math.abs(pair[0] - xs[xs.length - 1]) &amp;gt; 1e-12) {&lt;br /&gt;
                xs.push(pair[0]);&lt;br /&gt;
                ys.push(pair[1]);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        return xQuery.map((q) =&amp;gt; {&lt;br /&gt;
            if (!xs.length || q &amp;lt; xs[0] || q &amp;gt; xs[xs.length - 1]) return NaN;&lt;br /&gt;
            if (xs.length === 1) return ys[0];&lt;br /&gt;
&lt;br /&gt;
            let lo = 0;&lt;br /&gt;
            let hi = xs.length - 1;&lt;br /&gt;
            while (hi - lo &amp;gt; 1) {&lt;br /&gt;
                const mid = Math.floor((lo + hi) / 2);&lt;br /&gt;
                if (xs[mid] &amp;lt;= q) lo = mid;&lt;br /&gt;
                else hi = mid;&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
            const dx = xs[hi] - xs[lo];&lt;br /&gt;
            const t = dx === 0 ? 0 : (q - xs[lo]) / dx;&lt;br /&gt;
            return ys[lo] + t * (ys[hi] - ys[lo]);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function fmt(value) {&lt;br /&gt;
        if (!Number.isFinite(value)) return &#039;—&#039;;&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 1) return value.toFixed(3);&lt;br /&gt;
        if (Math.abs(value) &amp;gt;= 0.1) return value.toFixed(4);&lt;br /&gt;
        return value.toFixed(5);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function createSvgElement(tag, attrs) {&lt;br /&gt;
        const ns = &#039;http://www.w3.org/2000/svg&#039;;&lt;br /&gt;
        const el = document.createElementNS(ns, tag);&lt;br /&gt;
        Object.entries(attrs).forEach(([key, value]) =&amp;gt; el.setAttribute(key, String(value)));&lt;br /&gt;
        return el;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function renderPlot(svg, geom) {&lt;br /&gt;
        while (svg.firstChild) svg.removeChild(svg.firstChild);&lt;br /&gt;
&lt;br /&gt;
        const validPoints = [];&lt;br /&gt;
        for (let i = 0; i &amp;lt; geom.x.length; i++) {&lt;br /&gt;
            if (Number.isFinite(geom.upper[i])) validPoints.push([geom.x[i], geom.upper[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.lower[i])) validPoints.push([geom.x[i], geom.lower[i]]);&lt;br /&gt;
            if (Number.isFinite(geom.camber[i])) validPoints.push([geom.x[i], geom.camber[i]]);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        if (!validPoints.length) {&lt;br /&gt;
            const text = createSvgElement(&#039;text&#039;, { x: 50, y: 50, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-empty&#039; });&lt;br /&gt;
            text.textContent = &#039;Geometry cannot be plotted for these parameters&#039;;&lt;br /&gt;
            svg.appendChild(text);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        let minY = Math.min(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        let maxY = Math.max(...validPoints.map((p) =&amp;gt; p[1]));&lt;br /&gt;
        if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {&lt;br /&gt;
            minY = -0.1;&lt;br /&gt;
            maxY = 0.1;&lt;br /&gt;
        }&lt;br /&gt;
        const span = Math.max(maxY - minY, 0.04);&lt;br /&gt;
        const margin = span * 0.12;&lt;br /&gt;
        minY -= margin;&lt;br /&gt;
        maxY += margin;&lt;br /&gt;
&lt;br /&gt;
        const W = 900;&lt;br /&gt;
        const H = 520;&lt;br /&gt;
        const padLeft = 70;&lt;br /&gt;
        const padRight = 25;&lt;br /&gt;
        const padTop = 25;&lt;br /&gt;
        const padBottom = 55;&lt;br /&gt;
        const plotW = W - padLeft - padRight;&lt;br /&gt;
        const plotH = H - padTop - padBottom;&lt;br /&gt;
&lt;br /&gt;
        const sx = (x) =&amp;gt; padLeft + x * plotW;&lt;br /&gt;
        const sy = (y) =&amp;gt; padTop + (maxY - y) / (maxY - minY) * plotH;&lt;br /&gt;
&lt;br /&gt;
        // Grid and axes.&lt;br /&gt;
        [0, 0.25, 0.5, 0.75, 1].forEach((xVal) =&amp;gt; {&lt;br /&gt;
            const x = sx(xVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: x, y1: padTop, x2: x, y2: padTop + plotH, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x, y: H - 22, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = xVal.toFixed(2);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const yTicks = 5;&lt;br /&gt;
        for (let i = 0; i &amp;lt;= yTicks; i++) {&lt;br /&gt;
            const yVal = minY + (maxY - minY) * i / yTicks;&lt;br /&gt;
            const y = sy(yVal);&lt;br /&gt;
            svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
                x1: padLeft, y1: y, x2: padLeft + plotW, y2: y, class: &#039;aerofoil-grid&#039;&lt;br /&gt;
            }));&lt;br /&gt;
            const label = createSvgElement(&#039;text&#039;, { x: padLeft - 9, y: y + 4, &#039;text-anchor&#039;: &#039;end&#039;, class: &#039;aerofoil-tick&#039; });&lt;br /&gt;
            label.textContent = fmt(yVal);&lt;br /&gt;
            svg.appendChild(label);&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: sy(0), x2: padLeft + plotW, y2: sy(0), class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
        svg.appendChild(createSvgElement(&#039;line&#039;, {&lt;br /&gt;
            x1: padLeft, y1: padTop, x2: padLeft, y2: padTop + plotH, class: &#039;aerofoil-axis&#039;&lt;br /&gt;
        }));&lt;br /&gt;
&lt;br /&gt;
        // X axis label.&lt;br /&gt;
        const xLabel = createSvgElement(&#039;text&#039;, { x: padLeft + plotW / 2, y: H - 4, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039; });&lt;br /&gt;
        xLabel.textContent = &#039;x/c&#039;;&lt;br /&gt;
        svg.appendChild(xLabel);&lt;br /&gt;
&lt;br /&gt;
        // Y axis label.&lt;br /&gt;
        const yLabel = createSvgElement(&#039;text&#039;, {&lt;br /&gt;
            x: 16, y: padTop + plotH / 2, &#039;text-anchor&#039;: &#039;middle&#039;, class: &#039;aerofoil-axis-label&#039;,&lt;br /&gt;
            transform: `rotate(-90 16 ${padTop + plotH / 2})`&lt;br /&gt;
        });&lt;br /&gt;
        yLabel.textContent = &#039;y/c&#039;;&lt;br /&gt;
        svg.appendChild(yLabel);&lt;br /&gt;
&lt;br /&gt;
        function pathFromSeries(xs, ys) {&lt;br /&gt;
            let d = &#039;&#039;;&lt;br /&gt;
            let started = false;&lt;br /&gt;
            for (let i = 0; i &amp;lt; xs.length; i++) {&lt;br /&gt;
                if (!Number.isFinite(xs[i]) || !Number.isFinite(ys[i])) {&lt;br /&gt;
                    started = false;&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                const command = started ? &#039;L&#039; : &#039;M&#039;;&lt;br /&gt;
                d += `${command}${sx(xs[i]).toFixed(2)},${sy(ys[i]).toFixed(2)} `;&lt;br /&gt;
                started = true;&lt;br /&gt;
            }&lt;br /&gt;
            return d.trim();&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        const upperPath = pathFromSeries(geom.x, geom.upper);&lt;br /&gt;
        const lowerPath = pathFromSeries([...geom.x].reverse(), [...geom.lower].reverse());&lt;br /&gt;
&lt;br /&gt;
        const profile = createSvgElement(&#039;path&#039;, { d: `${upperPath} ${lowerPath}`, class: &#039;aerofoil-profile&#039; });&lt;br /&gt;
        svg.appendChild(profile);&lt;br /&gt;
&lt;br /&gt;
        const upper = createSvgElement(&#039;path&#039;, { d: upperPath, class: &#039;aerofoil-upper&#039; });&lt;br /&gt;
        const lower = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.lower), class: &#039;aerofoil-lower&#039; });&lt;br /&gt;
        const camber = createSvgElement(&#039;path&#039;, { d: pathFromSeries(geom.x, geom.camber), class: &#039;aerofoil-camber&#039; });&lt;br /&gt;
        svg.appendChild(upper);&lt;br /&gt;
        svg.appendChild(lower);&lt;br /&gt;
        svg.appendChild(camber);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function setup(root) {&lt;br /&gt;
        if (root.dataset.aerofoilInitialised === &#039;1&#039;) return;&lt;br /&gt;
        root.dataset.aerofoilInitialised = &#039;1&#039;;&lt;br /&gt;
&lt;br /&gt;
        const controls = document.createElement(&#039;div&#039;);&lt;br /&gt;
        controls.className = &#039;aerofoil-controls&#039;;&lt;br /&gt;
&lt;br /&gt;
        const values = {};&lt;br /&gt;
        const inputs = {};&lt;br /&gt;
        const valueLabels = {};&lt;br /&gt;
&lt;br /&gt;
        PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
            const row = document.createElement(&#039;div&#039;);&lt;br /&gt;
            row.className = &#039;aerofoil-control&#039;;&lt;br /&gt;
&lt;br /&gt;
            const label = document.createElement(&#039;label&#039;);&lt;br /&gt;
            label.className = &#039;aerofoil-control-label&#039;;&lt;br /&gt;
            label.htmlFor = `aerofoil-${p.key}`;&lt;br /&gt;
            label.textContent = p.label;&lt;br /&gt;
&lt;br /&gt;
            const range = document.createElement(&#039;input&#039;);&lt;br /&gt;
            range.type = &#039;range&#039;;&lt;br /&gt;
            range.id = `aerofoil-${p.key}`;&lt;br /&gt;
            range.min = p.min;&lt;br /&gt;
            range.max = p.max;&lt;br /&gt;
            range.step = p.step;&lt;br /&gt;
            range.value = p.value;&lt;br /&gt;
&lt;br /&gt;
            const number = document.createElement(&#039;input&#039;);&lt;br /&gt;
            number.type = &#039;number&#039;;&lt;br /&gt;
            number.min = p.min;&lt;br /&gt;
            number.max = p.max;&lt;br /&gt;
            number.step = p.step;&lt;br /&gt;
            number.value = p.value;&lt;br /&gt;
            number.className = &#039;aerofoil-number&#039;;&lt;br /&gt;
&lt;br /&gt;
            const valueSpan = document.createElement(&#039;span&#039;);&lt;br /&gt;
            valueSpan.className = &#039;aerofoil-value&#039;;&lt;br /&gt;
&lt;br /&gt;
            const setValue = (raw) =&amp;gt; {&lt;br /&gt;
                let value = Number(raw);&lt;br /&gt;
                if (!Number.isFinite(value)) value = p.value;&lt;br /&gt;
                value = Math.min(p.max, Math.max(p.min, value));&lt;br /&gt;
                range.value = value;&lt;br /&gt;
                number.value = value;&lt;br /&gt;
                values[p.key] = value;&lt;br /&gt;
                valueSpan.textContent = value.toFixed(Math.max(0, decimalPlaces(p.step)));&lt;br /&gt;
            };&lt;br /&gt;
&lt;br /&gt;
            range.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(range.value));&lt;br /&gt;
            number.addEventListener(&#039;input&#039;, () =&amp;gt; setValue(number.value));&lt;br /&gt;
&lt;br /&gt;
            setValue(p.value);&lt;br /&gt;
            inputs[p.key] = range;&lt;br /&gt;
            valueLabels[p.key] = valueSpan;&lt;br /&gt;
&lt;br /&gt;
            row.appendChild(label);&lt;br /&gt;
            row.appendChild(range);&lt;br /&gt;
            row.appendChild(number);&lt;br /&gt;
            row.appendChild(valueSpan);&lt;br /&gt;
            controls.appendChild(row);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        const actions = document.createElement(&#039;div&#039;);&lt;br /&gt;
        actions.className = &#039;aerofoil-actions&#039;;&lt;br /&gt;
&lt;br /&gt;
        const reset = document.createElement(&#039;button&#039;);&lt;br /&gt;
        reset.type = &#039;button&#039;;&lt;br /&gt;
        reset.className = &#039;mw-ui-button&#039;;&lt;br /&gt;
        reset.textContent = &#039;Reset&#039;;&lt;br /&gt;
        reset.addEventListener(&#039;click&#039;, () =&amp;gt; {&lt;br /&gt;
            PARAMS.forEach((p) =&amp;gt; {&lt;br /&gt;
                inputs[p.key].value = p.value;&lt;br /&gt;
                inputs[p.key].dispatchEvent(new Event(&#039;input&#039;, { bubbles: true }));&lt;br /&gt;
            });&lt;br /&gt;
        });&lt;br /&gt;
        actions.appendChild(reset);&lt;br /&gt;
&lt;br /&gt;
        const plotWrap = document.createElement(&#039;div&#039;);&lt;br /&gt;
        plotWrap.className = &#039;aerofoil-plot-wrap&#039;;&lt;br /&gt;
&lt;br /&gt;
        const svg = createSvgElement(&#039;svg&#039;, {&lt;br /&gt;
            viewBox: &#039;0 0 900 520&#039;,&lt;br /&gt;
            role: &#039;img&#039;,&lt;br /&gt;
            &#039;aria-label&#039;: &#039;Interactive aerofoil plot&#039;&lt;br /&gt;
        });&lt;br /&gt;
        plotWrap.appendChild(svg);&lt;br /&gt;
&lt;br /&gt;
        const status = document.createElement(&#039;div&#039;);&lt;br /&gt;
        status.className = &#039;aerofoil-status&#039;;&lt;br /&gt;
&lt;br /&gt;
        const checks = document.createElement(&#039;div&#039;);&lt;br /&gt;
        checks.className = &#039;aerofoil-checks&#039;;&lt;br /&gt;
&lt;br /&gt;
        const unitNote = document.createElement(&#039;div&#039;);&lt;br /&gt;
        unitNote.className = &#039;aerofoil-note&#039;;&lt;br /&gt;
        unitNote.textContent = `Angles are entered in ${ANGLE_MODE}; calculations follow the MATLAB tan/cot convention.`;&lt;br /&gt;
&lt;br /&gt;
        root.appendChild(controls);&lt;br /&gt;
        root.appendChild(actions);&lt;br /&gt;
        root.appendChild(plotWrap);&lt;br /&gt;
        root.appendChild(status);&lt;br /&gt;
        root.appendChild(checks);&lt;br /&gt;
        root.appendChild(unitNote);&lt;br /&gt;
&lt;br /&gt;
        const update = () =&amp;gt; {&lt;br /&gt;
            try {&lt;br /&gt;
                const geom = buildGeometry(values);&lt;br /&gt;
                renderPlot(svg, geom);&lt;br /&gt;
&lt;br /&gt;
                status.className = `aerofoil-status ${geom.validGeom ? &#039;is-valid&#039; : &#039;is-invalid&#039;}`;&lt;br /&gt;
                status.textContent = geom.validGeom ? &#039;Geometry valid&#039; : &#039;Geometry invalid — see checks below.&#039;;&lt;br /&gt;
&lt;br /&gt;
                checks.innerHTML = &#039;&#039;;&lt;br /&gt;
                const messages = [&lt;br /&gt;
                    [&#039;Negative thickness&#039;, geom.checks.negThick],&lt;br /&gt;
                    [&#039;Thickness x outside [0, 1]&#039;, geom.checks.thickBounds],&lt;br /&gt;
                    [&#039;Camber x outside [0, 1]&#039;, geom.checks.camberBounds],&lt;br /&gt;
                    [&#039;Overhang / decreasing x&#039;, geom.checks.overHung]&lt;br /&gt;
                ];&lt;br /&gt;
                messages.forEach(([name, failed]) =&amp;gt; {&lt;br /&gt;
                    const item = document.createElement(&#039;span&#039;);&lt;br /&gt;
                    item.className = failed ? &#039;fail&#039; : &#039;pass&#039;;&lt;br /&gt;
                    item.textContent = `${failed ? &#039;✗&#039; : &#039;✓&#039;} ${name}`;&lt;br /&gt;
                    checks.appendChild(item);&lt;br /&gt;
                });&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                status.className = &#039;aerofoil-status is-invalid&#039;;&lt;br /&gt;
                status.textContent = `Geometry error: ${error.message}`;&lt;br /&gt;
                console.error(&#039;Aerofoil Designer:&#039;, error);&lt;br /&gt;
            }&lt;br /&gt;
        };&lt;br /&gt;
&lt;br /&gt;
        Object.keys(inputs).forEach((key) =&amp;gt; {&lt;br /&gt;
            inputs[key].addEventListener(&#039;input&#039;, update);&lt;br /&gt;
        });&lt;br /&gt;
&lt;br /&gt;
        update();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function decimalPlaces(step) {&lt;br /&gt;
        const text = String(step);&lt;br /&gt;
        if (!text.includes(&#039;.&#039;)) return 0;&lt;br /&gt;
        return text.split(&#039;.&#039;)[1].length;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    function init() {&lt;br /&gt;
        document.querySelectorAll(&#039;#aerofoil-tool&#039;).forEach(setup);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if (window.mw &amp;amp;&amp;amp; mw.hook) {&lt;br /&gt;
        mw.hook(&#039;wikipage.content&#039;).add(init);&lt;br /&gt;
        init();&lt;br /&gt;
    } else {&lt;br /&gt;
        document.addEventListener(&#039;DOMContentLoaded&#039;, init);&lt;br /&gt;
    }&lt;br /&gt;
}());&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Masters_Project&amp;diff=42</id>
		<title>Masters Project</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Masters_Project&amp;diff=42"/>
		<updated>2026-09-23T09:44:54Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This project focussed on the development and deployment of an optimisation toolchain for maximising the performance of an airborne wind energy system. Airborne wind energy system have the potential to access stronger and more consistent high-altitude wind resources while requiring significantly less material than conventional wind turbines. The toolchain aims to optimise the performance of the aerofoil by maximising the lift generated to maximise the power the system can generate.&lt;br /&gt;
&lt;br /&gt;
Through this work I developed some of the following skills:&lt;br /&gt;
&lt;br /&gt;
* Simulation-driven design&lt;br /&gt;
* Computational Fluid Dynamics&lt;br /&gt;
* High performance computing and Linux&lt;br /&gt;
* Data Analysis&lt;br /&gt;
* Critical evaluatation of model assumptions&lt;br /&gt;
* Report writing and communicating technical concepts&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Airborne wind energy uses a tethered aircraft to create electricity while avoiding the large material costs of conventional wind turbines. Drag powered kites which are the focus of this study having a tethered kite with onboard generators, the kite aerofoil allows it to fly crosswind at high speeds increasing the relative wind speed at the turbines. There are several companies working on designs, most are in concept stages with a few that have built some prototypes such as Makani which are now defunct and Kitekraft.&lt;br /&gt;
&lt;br /&gt;
The understanding on the topic for many years was that the power generated by the system is proportional to the coefficient of lift cubed divided by the equivalent coefficient of drag squared,&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto \frac{C_L^3}{C_D^2}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
so by optimising the glide ratio which is the ratio of the two  coefficients, you would maximise power.&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt; E =\frac{C_L}{C_D}&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto E C_L&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recent research done by Bauer in TU Munich who was working closely with the company kitekraft showed that when the drag forces of other parts of the system, in particular the tether drag is included the glide ratio of the aerofoil is less important as previous research suggested. In the case where the tether drag is dominant the power generated is proportional to the coefficient of lift of the aerofoil squared and should be optimised for as the drag component of the aerofoil has little effect. There have been very limited studies on the optimisation of aerofoils for airborne wind energy with even fewer optimising multi element high lift aerofoils aiming to maximise the &amp;lt;math display=&amp;quot;inline&amp;quot;&amp;gt;C_L&amp;lt;/math&amp;gt; which is the focus of my project.&lt;br /&gt;
&lt;br /&gt;
This project built on work done a previous masters project and code developed by TU Munich and Kitekraft. The toolchain uses octave to run the optimisation with the CFD analysis using simplefoam in OpenFOAM. My work focused on incorporating geometric design parameterisation, so allowing the toolchain to create its own aerofoil geometry that it can modify to optimise its performance. Then to update the toolchain to allow it to be run on high performance computing  using that to optimise the performance of a multi element aerofoil for use in an airborne wind energy system.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain ===&lt;br /&gt;
The optimisation algorithm used is the covariance matrix adaptation evolution strategy (CMA-ES) implemented in Octave by Nikolaus Hansen. It creates a set of parameters to be evaluated by the objective function, it passes these parameters along with some static inputs to the objective function.&lt;br /&gt;
[[File:overviewFlowChart.png|300px|centre]]&lt;br /&gt;
[[File:bezier.jpg|thumb|Bezier Parsec Geometry Paremeterisation system used]]&lt;br /&gt;
The objective function takes a set of input parameters from the optimiser and returns their performance. The main update to the toolchain was the creation of a new objective function that implemented geometry parameterisation. It uses Bezier Parsec parameterisation to create the airfoil geometry. This works by creating a thickness and camber curve from 2 Bezier curves each. The control points of the Bezier curves are calculated from a set of geometry related input parameters such as thickness, the inside angle of the trailing edge and the radius of the leading edge. The curves are processed to turn them into the coordinates of the airfoil and then saved as an STL file to be used in OpenFoam. The objective function runs some checks on the geometry to ensure the elements are not intersecting or the parameterisation created impossible geometry.&lt;br /&gt;
&amp;lt;div id=&amp;quot;aerofoil-tool&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
The objective then runs the CFD analysis in Openfoam and returns the coefficient of lift calculated. The CFD simulation uses a tool called SnappyHexMesh that creates a mesh with automatic refinement around the aerofoils, including creating surface layers on the aerofoil to ensure the simulation correctly models the boundary layer. The model uses the SimpleFOAM soliving utility, it is a steady state, incompressible flow solver using the SIMPLE (Semi-Implicit Method for Pressure Linked Equations) algorithm. The k-ω SST turbulence model is also used with the solver.&lt;br /&gt;
[[File:Mesh.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
Through the irish centre for high end computing, access was obtained to the national supercomputer which is currently hosted on the meluxina supercomputer in Luxembourg. Some updates to the toolchain had to be made to allow it to be run on the supercomputer such as changing the utilities used to create the STL files.&lt;br /&gt;
&lt;br /&gt;
To take advantage of the 256 threads on each compute node, the toolchain was updated to allow it to run 32 instances of the objective function simultaneously. It does this by using a wrapper function to create multiple instances of the objective function for each generation of the optisation. Each instance of the objective function has 8 threads that it can use to run the CFD solver in parallel. The CFD domain is split into 8 subdomains that can be meshed and solved on their own thread.&lt;br /&gt;
&lt;br /&gt;
[[File:Parallelisation.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
=== Results ===&lt;br /&gt;
I ran multiple optimisation runs with various sets of input parameters available to the toolchain.&lt;br /&gt;
&lt;br /&gt;
The first optimisation that I ran was optimising the flap angle and position with a fixed main angle of attack. The optimiser trended towards geometry that created unsteady flow behind the flap that the solver was unable to converge on invalidating the results. This was overcome by adding penalties to the objective function for when the geometry does not converge. This allowed the optimiser to converge on this geometry with a lift coefficient of 2.2.&lt;br /&gt;
&lt;br /&gt;
[[File:FlapA.png|500px|centre]]&lt;br /&gt;
[[File:InputsFlapA.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
The main element angle of attack was added as an additional degree of freedom to the problem. The toolchain originally converged on a geometry with the flap in front of the main element and with a lower &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt;. This shows that CMAES is susceptible to selecting the wrong initial basin in its search and can struggle to switch. By increasing the optimisers population size and more careful constraints the optimiser created the geometry shown which has a much higher &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt; of 2.6. &lt;br /&gt;
&lt;br /&gt;
[[File:MainAflapAP.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
I ran the toolchain to optimise the shape of the main aerofoil and the flap angle and position. It resulted in a design with a coefficient of lift of 3.3 and a relatively high coefficient of drag of 0.16. It has arranged the elements in a similar way to the previous optimisation but it has created the very blunt trailing edge which helps develop the wake. In some optimisations I ran for the glide ratio it instead created an extremely sharp trailing edge.&lt;br /&gt;
&lt;br /&gt;
[[File:4882velocity.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
The flow at the top of the leading edge is very fast at more than 2.5 times the freestream velocity, it also has a bump in the camber line on the main element where the flow detaches from the element on the top surface. The gap is between the two elements is slightly bigger than the previous optimisation resulting in a thicker flow over the flap.&lt;br /&gt;
&lt;br /&gt;
When the parameterisation of the main element shape was included in the optimisation, it created a very blunt trailing edge by increasing the gap between the trailing edge of the thickness curve and the 𝑋 axis. This parameter is not intended as a method of creating a blunt trailing edge but to prevent zero thickness on the trailing edge which can cause issues in some applications. A better method would be to incorporate some additional parameterisation system on the trailing edge as it is clear it has a significant impact on the performance of the geometry. The current optimisation is restricted to a flat surface on the trailing edge but the optimal design may actually have a more rounded profile or complex profile which is not possible with Bezier Parsec Parameterisation.&lt;br /&gt;
&lt;br /&gt;
When the trailing edge thickness was decreased it created unsteady flow. This indicates that the thicker trailing edge may also be allowing the aerofoil to create a high coefficient of lift while avoiding creating an unsteady flow. It does this by having a sharp edge at the end of the upper surface that causes keeps the flow from detaching until it reaches this corner. The earlier detaching of flow with the thin trailing edge results in a more horizontal wake This wake then isn’t able to keep the flow from the gap between the elements pressed along the flap. This then causes that flow to separate from the flap creating the instability. This shows that the more downward wake of the thick trailing edge main element helps ensure that the flow through the gap remains attached to the flap, allowing the flap to be at a more aggressive angle while avoiding unstable flow, increasing the coefficient of lift.&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadgets-definition&amp;diff=41</id>
		<title>MediaWiki:Gadgets-definition</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadgets-definition&amp;diff=41"/>
		<updated>2026-09-23T09:44:21Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created page with &amp;quot;* AerofoilDesigner [ default ] | AerofoilDesigner.js | AerofoilDesigner.css&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* AerofoilDesigner [ default ] | AerofoilDesigner.js | AerofoilDesigner.css&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=40</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.css</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.css&amp;diff=40"/>
		<updated>2026-09-23T09:44:06Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created blank page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=39</id>
		<title>MediaWiki:Gadget-AerofoilDesigner.js</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Gadget-AerofoilDesigner.js&amp;diff=39"/>
		<updated>2026-09-23T09:43:21Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created blank page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=KISC&amp;diff=38</id>
		<title>KISC</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=KISC&amp;diff=38"/>
		<updated>2026-09-22T18:01:55Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created page with &amp;quot;Kandersteg International Scout Centre is the only world scout centre. Located in the village of Kandersteg in the Swiss alps it has more than 12,000 scouts from all around the world stay every Summer. The staff are all volunteers from usually over 40 different countries so it is a truly international working environment.  === Snow and Ice Guide === &amp;#039;&amp;#039;Summer 2026&amp;#039;&amp;#039;  As a Snow and Ice Guide I brought scouts up to the Bluemisalp where we would stay in an alpine hut and then...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Kandersteg International Scout Centre is the only world scout centre. Located in the village of Kandersteg in the Swiss alps it has more than 12,000 scouts from all around the world stay every Summer. The staff are all volunteers from usually over 40 different countries so it is a truly international working environment.&lt;br /&gt;
&lt;br /&gt;
=== Snow and Ice Guide ===&lt;br /&gt;
&#039;&#039;Summer 2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
As a Snow and Ice Guide I brought scouts up to the Bluemisalp where we would stay in an alpine hut and then go on a glacier on the Bluemisalp Gletcher. Alongside this I ran other hikes, climbing activities and everything in between.&lt;br /&gt;
&lt;br /&gt;
=== Reception Assistant ===&lt;br /&gt;
&#039;&#039;Summer 2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
As a reception assistant I worked in the front office with the centre helping guests with their enquiries and making bookings. This required a very keen attention to detail as bookings could be for hundreds of people, with meals, different dietary requirements, different activities etc.&lt;br /&gt;
&lt;br /&gt;
During my time as reception assistant I used the centre&#039;s booking software to get the data on all the bookings from the last 10 years. There was a large drop off in the number of different countries there were bookings from. We attributed this to closing all bookings for the summer a few months in advance. This meant that larger groups that would come more regularly typically from the UK, Ireland, Portugal, Spain etc would have made their bookings before the cutoff. However smaller groups of Scouts travelling around who are often from a wider variety of countries would typically not have made plans that far in advance and we were turning them away. This resulted in much less diversity in the bookings we were receiving. As a result we changed the booking system to ensure we would still have space to accomodate these smaller later booking groups.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Guide ===&lt;br /&gt;
&#039;&#039;Summer 2023&#039;&#039;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=37</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=37"/>
		<updated>2026-09-22T17:42:53Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the [[Maamturks Challenge]] which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
[[File:DorsalArete.jpeg|thumb|Me climbing Dorsal Arete in Scotland]]&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team. The Club grew a lot during my time as captain both in membership and the activities that we organised. We organised two international trips, one to wales and a winter trip to Scotland for the first time in more than 10 years. &lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
[[File:Donegal.jpg|thumb|Setting up outdoor climbs in Donegal]]&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Other volunteering in the club ==&lt;br /&gt;
=== Hike Leader ===&lt;br /&gt;
[[File:Hiking.jpg|thumb|Leading a hike on Mweelrea in Mayo]]&lt;br /&gt;
As a hike leader I regularly led a group of other people on the weekly club hikes. The hikes can be in very challenging terrain and weather and we have to be able to navigate, manage the group and make sure everyone is still having a good time. I am currently working towards my Mountain Leader Award which is the official qualification to guide hiking professionally in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Instructor ===&lt;br /&gt;
&lt;br /&gt;
As a Climbing Instructor I taught lessons or did assessments multiple evenings every week teaching the basic skills of climbing as well as more advanced lessons. I have my Rock Climbing Instructor award which is the professional quailification to guide indoor or outdoor rock climbing in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Webmaster ===&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=36</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=36"/>
		<updated>2026-09-22T17:42:27Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
[[File:DorsalArete.jpeg|thumb|Me climbing Dorsal Arete in Scotland]]&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team. The Club grew a lot during my time as captain both in membership and the activities that we organised. We organised two international trips, one to wales and a winter trip to Scotland for the first time in more than 10 years. &lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
[[File:Donegal.jpg|thumb|Setting up outdoor climbs in Donegal]]&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Other volunteering in the club ==&lt;br /&gt;
=== Hike Leader ===&lt;br /&gt;
[[File:Hiking.jpg|thumb|Leading a hike on Mweelrea in Mayo]]&lt;br /&gt;
As a hike leader I regularly led a group of other people on the weekly club hikes. The hikes can be in very challenging terrain and weather and we have to be able to navigate, manage the group and make sure everyone is still having a good time. I am currently working towards my Mountain Leader Award which is the official qualification to guide hiking professionally in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Instructor ===&lt;br /&gt;
&lt;br /&gt;
As a Climbing Instructor I taught lessons or did assessments multiple evenings every week teaching the basic skills of climbing as well as more advanced lessons. I have my Rock Climbing Instructor award which is the professional quailification to guide indoor or outdoor rock climbing in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Webmaster ===&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:Donegal.jpg&amp;diff=35</id>
		<title>File:Donegal.jpg</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:Donegal.jpg&amp;diff=35"/>
		<updated>2026-09-22T17:40:04Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=34</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=34"/>
		<updated>2026-09-22T17:39:09Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
[[File:DorsalArete.jpeg|thumb|Me climbing Dorsal Arete in Scotland]]&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team. The Club grew a lot during my time as captain both in membership and the activities that we organised. We organised two international trips, one to wales and a winter trip to Scotland for the first time in more than 10 years. &lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Other volunteering in the club ==&lt;br /&gt;
=== Hike Leader ===&lt;br /&gt;
[[File:Hiking.jpg|thumb]]&lt;br /&gt;
As a hike leader I regularly led a group of other people on the weekly club hikes. The hikes can be in very challenging terrain and weather and we have to be able to navigate, manage the group and make sure everyone is still having a good time. I am currently working towards my Mountain Leader Award which is the official qualification to guide hiking professionally in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Instructor ===&lt;br /&gt;
&lt;br /&gt;
As a Climbing Instructor I taught lessons or did assessments multiple evenings every week teaching the basic skills of climbing as well as more advanced lessons. I have my Rock Climbing Instructor award which is the professional quailification to guide indoor or outdoor rock climbing in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Webmaster ===&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:DorsalArete.jpeg&amp;diff=33</id>
		<title>File:DorsalArete.jpeg</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:DorsalArete.jpeg&amp;diff=33"/>
		<updated>2026-09-22T17:38:10Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=32</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=32"/>
		<updated>2026-09-22T17:36:55Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team. The Club grew a lot during my time as captain both in membership and the activities that we organised. We organised two international trips, one to wales and a winter trip to Scotland for the first time in more than 10 years. &lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Other volunteering in the club ==&lt;br /&gt;
=== Hike Leader ===&lt;br /&gt;
[[File:Hiking.jpg|thumb]]&lt;br /&gt;
As a hike leader I regularly led a group of other people on the weekly club hikes. The hikes can be in very challenging terrain and weather and we have to be able to navigate, manage the group and make sure everyone is still having a good time. I am currently working towards my Mountain Leader Award which is the official qualification to guide hiking professionally in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Instructor ===&lt;br /&gt;
&lt;br /&gt;
As a Climbing Instructor I taught lessons or did assessments multiple evenings every week teaching the basic skills of climbing as well as more advanced lessons. I have my Rock Climbing Instructor award which is the professional quailification to guide indoor or outdoor rock climbing in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Webmaster ===&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:Hiking.jpg&amp;diff=31</id>
		<title>File:Hiking.jpg</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:Hiking.jpg&amp;diff=31"/>
		<updated>2026-09-22T17:33:15Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=30</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=30"/>
		<updated>2026-09-22T17:33:03Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team. The Club grew a lot during my time as captain both in membership and the activities that we organised. We organised two international trips, one to wales and a winter trip to Scotland for the first time in more than 10 years. &lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Other volunteering in the club ==&lt;br /&gt;
=== Hike Leader ===&lt;br /&gt;
&lt;br /&gt;
As a hike leader I regularly led a group of other people on the weekly club hikes. The hikes can be in very challenging terrain and weather and we have to be able to navigate, manage the group and make sure everyone is still having a good time. I am currently working towards my Mountain Leader Award which is the official qualification to guide hiking professionally in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Instructor ===&lt;br /&gt;
&lt;br /&gt;
As a Climbing Instructor I taught lessons or did assessments multiple evenings every week teaching the basic skills of climbing as well as more advanced lessons. I have my Rock Climbing Instructor award which is the professional quailification to guide indoor or outdoor rock climbing in the UK and Ireland.&lt;br /&gt;
&lt;br /&gt;
=== Webmaster ===&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Maamturks_Challenge&amp;diff=29</id>
		<title>Maamturks Challenge</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Maamturks_Challenge&amp;diff=29"/>
		<updated>2026-09-22T15:03:34Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;First ran in 1975, the Maamturks Challenge has been a focal point of hillwalking in Ireland for over 50 years. 250 Participants are required to complete a full transverse of the famous Maamturks mountain range, a notoriously barren and rock terrain. With ridges, cliffs and numerous valleys, participants of the walk are generally some of the most experienced hillwalkers Ireland has to offer.&lt;br /&gt;
&lt;br /&gt;
The Challenge is unique in that it is organised by a student mountaineering club, as opposed to the same committee every year. &lt;br /&gt;
&lt;br /&gt;
I have been on the Subcommittee for organising the challenge for 4 years, one of which I was the volunteer coordinator and two of which I was in charge of the subcommittee&lt;br /&gt;
There are a lot of logistics in organising the challenge from &lt;br /&gt;
* Organising and purchasing the necessary equipment for each of the 8 checkpoints on the mountains along with the start, registration, control and 3 finishes&lt;br /&gt;
* Booking the hotel, pub, community centre and hostel needed for the finish, registration, control and volunteer accomodation&lt;br /&gt;
* Managing the online registration and the participant admin&lt;br /&gt;
* Organising T-shirts for participants and volunteers&lt;br /&gt;
* Recruiting 45 volunteers and coordinating where and when they need to be places, and how to get them there.&lt;br /&gt;
* Responding to any enquiries from participants&lt;br /&gt;
* Making risk assessments, event management plans, and working with the sports unit and university insurance to reduce liability&lt;br /&gt;
* Preparing a budget for the event and keeping track of the event&#039;s accounts&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
I think this has taught and really put to the test so many skills such as &lt;br /&gt;
* Project Management&lt;br /&gt;
* Managing a team of volunteers&lt;br /&gt;
* Event management&lt;br /&gt;
* Financial management&lt;br /&gt;
* Problem solving&lt;br /&gt;
* Critical decision making - I&#039;ve had to make the difficult decision to cancel the event twice due to bad weather&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Maamturks_Challenge&amp;diff=28</id>
		<title>Maamturks Challenge</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Maamturks_Challenge&amp;diff=28"/>
		<updated>2026-09-22T11:02:34Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created page with &amp;quot;First ran in 1975, the Maamturks Challenge has been a focal point of hillwalking in Ireland for over 50 years. 250 Participants are required to complete a full transverse of the famous Maamturks mountain range, a notoriously barren and rock terrain. With ridges, cliffs and numerous valleys, participants of the walk are generally some of the most experienced hillwalkers Ireland has to offer.  The Challenge is unique in that it is organised by a student mountaineering club...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;First ran in 1975, the Maamturks Challenge has been a focal point of hillwalking in Ireland for over 50 years. 250 Participants are required to complete a full transverse of the famous Maamturks mountain range, a notoriously barren and rock terrain. With ridges, cliffs and numerous valleys, participants of the walk are generally some of the most experienced hillwalkers Ireland has to offer.&lt;br /&gt;
&lt;br /&gt;
The Challenge is unique in that it is organised by a student mountaineering club, as opposed to the same committee every year. &lt;br /&gt;
&lt;br /&gt;
I have been on the Subcommittee for organising the challenge for 4 years, one of which I was the volunteer coordinator and two of which I was in charge of the subcommittee&lt;br /&gt;
There are a lot of logistics in organising the challenge from &lt;br /&gt;
* Organising and purchasing the necessary equipment for each of the 8 checkpoints on the mountains along with the start, registration, control and 3 finishes&lt;br /&gt;
* Booking the hotel, pub, community centre and hostel needed for the finish, registration, control and volunteer accomodation&lt;br /&gt;
* Managing the online registration and the participant admin&lt;br /&gt;
* Organising T-shirts for participants and volunteers&lt;br /&gt;
* Recruiting 45 volunteers and coordinating where and when they need to be places, and how to get them there.&lt;br /&gt;
* Responding to any enquiries from participants&lt;br /&gt;
* Making risk assessments, event management plans, and working with the sports unit and university insurance to reduce liability&lt;br /&gt;
* Preparing a budget for the event and keeping track of the event&#039;s accounts&lt;br /&gt;
And so many more small jobs that all add up!&lt;br /&gt;
&lt;br /&gt;
I think this has taught and really put to the test so many skills such as &lt;br /&gt;
* Project Management&lt;br /&gt;
* Managing a team of volunteers&lt;br /&gt;
* Event management&lt;br /&gt;
* Financial management&lt;br /&gt;
* Problem solving&lt;br /&gt;
* Critical decision making - I&#039;ve had to make the difficult decision to cancel the event twice due to bad weather&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=27</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=27"/>
		<updated>2026-09-22T10:48:59Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team. The Club grew a lot during my time as captain both in membership and the activities that we organised. We organised two international trips, one to wales and a winter trip to Scotland for the first time in more than 10 years. &lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Webmaster ==&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=26</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=26"/>
		<updated>2026-09-22T10:39:37Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. &lt;br /&gt;
&lt;br /&gt;
I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team.&lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Webmaster ==&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:InputsFlapA.png&amp;diff=25</id>
		<title>File:InputsFlapA.png</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:InputsFlapA.png&amp;diff=25"/>
		<updated>2026-09-21T15:08:37Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Aodhan uploaded a new version of File:InputsFlapA.png&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Masters_Project&amp;diff=24</id>
		<title>Masters Project</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Masters_Project&amp;diff=24"/>
		<updated>2026-09-21T15:06:29Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This project focussed on the development and deployment of an optimisation toolchain for maximising the performance of an airborne wind energy system. Airborne wind energy system have the potential to access stronger and more consistent high-altitude wind resources while requiring significantly less material than conventional wind turbines. The toolchain aims to optimise the performance of the aerofoil by maximising the lift generated to maximise the power the system can generate.&lt;br /&gt;
&lt;br /&gt;
Through this work I developed some of the following skills:&lt;br /&gt;
&lt;br /&gt;
* Simulation-driven design&lt;br /&gt;
* Computational Fluid Dynamics&lt;br /&gt;
* High performance computing and Linux&lt;br /&gt;
* Data Analysis&lt;br /&gt;
* Critical evaluatation of model assumptions&lt;br /&gt;
* Report writing and communicating technical concepts&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Airborne wind energy uses a tethered aircraft to create electricity while avoiding the large material costs of conventional wind turbines. Drag powered kites which are the focus of this study having a tethered kite with onboard generators, the kite aerofoil allows it to fly crosswind at high speeds increasing the relative wind speed at the turbines. There are several companies working on designs, most are in concept stages with a few that have built some prototypes such as Makani which are now defunct and Kitekraft.&lt;br /&gt;
&lt;br /&gt;
The understanding on the topic for many years was that the power generated by the system is proportional to the coefficient of lift cubed divided by the equivalent coefficient of drag squared,&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto \frac{C_L^3}{C_D^2}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
so by optimising the glide ratio which is the ratio of the two  coefficients, you would maximise power.&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt; E =\frac{C_L}{C_D}&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto E C_L&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recent research done by Bauer in TU Munich who was working closely with the company kitekraft showed that when the drag forces of other parts of the system, in particular the tether drag is included the glide ratio of the aerofoil is less important as previous research suggested. In the case where the tether drag is dominant the power generated is proportional to the coefficient of lift of the aerofoil squared and should be optimised for as the drag component of the aerofoil has little effect. There have been very limited studies on the optimisation of aerofoils for airborne wind energy with even fewer optimising multi element high lift aerofoils aiming to maximise the &amp;lt;math display=&amp;quot;inline&amp;quot;&amp;gt;C_L&amp;lt;/math&amp;gt; which is the focus of my project.&lt;br /&gt;
&lt;br /&gt;
This project built on work done a previous masters project and code developed by TU Munich and Kitekraft. The toolchain uses octave to run the optimisation with the CFD analysis using simplefoam in OpenFOAM. My work focused on incorporating geometric design parameterisation, so allowing the toolchain to create its own aerofoil geometry that it can modify to optimise its performance. Then to update the toolchain to allow it to be run on high performance computing  using that to optimise the performance of a multi element aerofoil for use in an airborne wind energy system.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain ===&lt;br /&gt;
The optimisation algorithm used is the covariance matrix adaptation evolution strategy (CMA-ES) implemented in Octave by Nikolaus Hansen. It creates a set of parameters to be evaluated by the objective function, it passes these parameters along with some static inputs to the objective function.&lt;br /&gt;
[[File:overviewFlowChart.png|300px|centre]]&lt;br /&gt;
[[File:bezier.jpg|thumb|Bezier Parsec Geometry Paremeterisation system used]]&lt;br /&gt;
The objective function takes a set of input parameters from the optimiser and returns their performance. The main update to the toolchain was the creation of a new objective function that implemented geometry parameterisation. It uses Bezier Parsec parameterisation to create the airfoil geometry. This works by creating a thickness and camber curve from 2 Bezier curves each. The control points of the Bezier curves are calculated from a set of geometry related input parameters such as thickness, the inside angle of the trailing edge and the radius of the leading edge. The curves are processed to turn them into the coordinates of the airfoil and then saved as an STL file to be used in OpenFoam. The objective function runs some checks on the geometry to ensure the elements are not intersecting or the parameterisation created impossible geometry.&lt;br /&gt;
The objective then runs the CFD analysis in Openfoam and returns the coefficient of lift calculated. The CFD simulation uses a tool called SnappyHexMesh that creates a mesh with automatic refinement around the aerofoils, including creating surface layers on the aerofoil to ensure the simulation correctly models the boundary layer. The model uses the SimpleFOAM soliving utility, it is a steady state, incompressible flow solver using the SIMPLE (Semi-Implicit Method for Pressure Linked Equations) algorithm. The k-ω SST turbulence model is also used with the solver.&lt;br /&gt;
[[File:Mesh.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
Through the irish centre for high end computing, access was obtained to the national supercomputer which is currently hosted on the meluxina supercomputer in Luxembourg. Some updates to the toolchain had to be made to allow it to be run on the supercomputer such as changing the utilities used to create the STL files.&lt;br /&gt;
&lt;br /&gt;
To take advantage of the 256 threads on each compute node, the toolchain was updated to allow it to run 32 instances of the objective function simultaneously. It does this by using a wrapper function to create multiple instances of the objective function for each generation of the optisation. Each instance of the objective function has 8 threads that it can use to run the CFD solver in parallel. The CFD domain is split into 8 subdomains that can be meshed and solved on their own thread.&lt;br /&gt;
&lt;br /&gt;
[[File:Parallelisation.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
=== Results ===&lt;br /&gt;
I ran multiple optimisation runs with various sets of input parameters available to the toolchain.&lt;br /&gt;
&lt;br /&gt;
The first optimisation that I ran was optimising the flap angle and position with a fixed main angle of attack. The optimiser trended towards geometry that created unsteady flow behind the flap that the solver was unable to converge on invalidating the results. This was overcome by adding penalties to the objective function for when the geometry does not converge. This allowed the optimiser to converge on this geometry with a lift coefficient of 2.2.&lt;br /&gt;
&lt;br /&gt;
[[File:FlapA.png|500px|centre]]&lt;br /&gt;
[[File:InputsFlapA.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
The main element angle of attack was added as an additional degree of freedom to the problem. The toolchain originally converged on a geometry with the flap in front of the main element and with a lower &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt;. This shows that CMAES is susceptible to selecting the wrong initial basin in its search and can struggle to switch. By increasing the optimisers population size and more careful constraints the optimiser created the geometry shown which has a much higher &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt; of 2.6. &lt;br /&gt;
&lt;br /&gt;
[[File:MainAflapAP.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
I ran the toolchain to optimise the shape of the main aerofoil and the flap angle and position. It resulted in a design with a coefficient of lift of 3.3 and a relatively high coefficient of drag of 0.16. It has arranged the elements in a similar way to the previous optimisation but it has created the very blunt trailing edge which helps develop the wake. In some optimisations I ran for the glide ratio it instead created an extremely sharp trailing edge.&lt;br /&gt;
&lt;br /&gt;
[[File:4882velocity.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
The flow at the top of the leading edge is very fast at more than 2.5 times the freestream velocity, it also has a bump in the camber line on the main element where the flow detaches from the element on the top surface. The gap is between the two elements is slightly bigger than the previous optimisation resulting in a thicker flow over the flap.&lt;br /&gt;
&lt;br /&gt;
When the parameterisation of the main element shape was included in the optimisation, it created a very blunt trailing edge by increasing the gap between the trailing edge of the thickness curve and the 𝑋 axis. This parameter is not intended as a method of creating a blunt trailing edge but to prevent zero thickness on the trailing edge which can cause issues in some applications. A better method would be to incorporate some additional parameterisation system on the trailing edge as it is clear it has a significant impact on the performance of the geometry. The current optimisation is restricted to a flat surface on the trailing edge but the optimal design may actually have a more rounded profile or complex profile which is not possible with Bezier Parsec Parameterisation.&lt;br /&gt;
&lt;br /&gt;
When the trailing edge thickness was decreased it created unsteady flow. This indicates that the thicker trailing edge may also be allowing the aerofoil to create a high coefficient of lift while avoiding creating an unsteady flow. It does this by having a sharp edge at the end of the upper surface that causes keeps the flow from detaching until it reaches this corner. The earlier detaching of flow with the thin trailing edge results in a more horizontal wake This wake then isn’t able to keep the flow from the gap between the elements pressed along the flap. This then causes that flow to separate from the flap creating the instability. This shows that the more downward wake of the thick trailing edge main element helps ensure that the flow through the gap remains attached to the flap, allowing the flap to be at a more aggressive angle while avoiding unstable flow, increasing the coefficient of lift.&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:MainAflapAP.png&amp;diff=23</id>
		<title>File:MainAflapAP.png</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:MainAflapAP.png&amp;diff=23"/>
		<updated>2026-09-21T15:03:41Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:InputsFlapA.png&amp;diff=22</id>
		<title>File:InputsFlapA.png</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:InputsFlapA.png&amp;diff=22"/>
		<updated>2026-09-21T15:03:34Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:FlapA.png&amp;diff=21</id>
		<title>File:FlapA.png</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:FlapA.png&amp;diff=21"/>
		<updated>2026-09-21T15:03:24Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Masters_Project&amp;diff=20</id>
		<title>Masters Project</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Masters_Project&amp;diff=20"/>
		<updated>2026-09-21T14:16:37Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This project focussed on the development and deployment of an optimisation toolchain for maximising the performance of an airborne wind energy system. Airborne wind energy system have the potential to access stronger and more consistent high-altitude wind resources while requiring significantly less material than conventional wind turbines. The toolchain aims to optimise the performance of the aerofoil by maximising the lift generated to maximise the power the system can generate.&lt;br /&gt;
&lt;br /&gt;
Through this work I developed some of the following skills:&lt;br /&gt;
&lt;br /&gt;
* Simulation-driven design&lt;br /&gt;
* Computational Fluid Dynamics&lt;br /&gt;
* High performance computing and Linux&lt;br /&gt;
* Data Analysis&lt;br /&gt;
* Critical evaluatation of model assumptions&lt;br /&gt;
* Report writing and communicating technical concepts&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Airborne wind energy uses a tethered aircraft to create electricity while avoiding the large material costs of conventional wind turbines. Drag powered kites which are the focus of this study having a tethered kite with onboard generators, the kite aerofoil allows it to fly crosswind at high speeds increasing the relative wind speed at the turbines. There are several companies working on designs, most are in concept stages with a few that have built some prototypes such as Makani which are now defunct and Kitekraft.&lt;br /&gt;
&lt;br /&gt;
The understanding on the topic for many years was that the power generated by the system is proportional to the coefficient of lift cubed divided by the equivalent coefficient of drag squared,&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto \frac{C_L^3}{C_D^2}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
so by optimising the glide ratio which is the ratio of the two  coefficients, you would maximise power.&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt; E =\frac{C_L}{C_D}&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto E C_L&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recent research done by Bauer in TU Munich who was working closely with the company kitekraft showed that when the drag forces of other parts of the system, in particular the tether drag is included the glide ratio of the aerofoil is less important as previous research suggested. In the case where the tether drag is dominant the power generated is proportional to the coefficient of lift of the aerofoil squared and should be optimised for as the drag component of the aerofoil has little effect. There have been very limited studies on the optimisation of aerofoils for airborne wind energy with even fewer optimising multi element high lift aerofoils aiming to maximise the &amp;lt;math display=&amp;quot;inline&amp;quot;&amp;gt;C_L&amp;lt;/math&amp;gt; which is the focus of my project.&lt;br /&gt;
&lt;br /&gt;
This project built on work done a previous masters project and code developed by TU Munich and Kitekraft. The toolchain uses octave to run the optimisation with the CFD analysis using simplefoam in OpenFOAM. My work focused on incorporating geometric design parameterisation, so allowing the toolchain to create its own aerofoil geometry that it can modify to optimise its performance. Then to update the toolchain to allow it to be run on high performance computing  using that to optimise the performance of a multi element aerofoil for use in an airborne wind energy system.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain ===&lt;br /&gt;
The optimisation algorithm used is the covariance matrix adaptation evolution strategy (CMA-ES) implemented in Octave by Nikolaus Hansen. It creates a set of parameters to be evaluated by the objective function, it passes these parameters along with some static inputs to the objective function.&lt;br /&gt;
[[File:overviewFlowChart.png|300px|centre]]&lt;br /&gt;
[[File:bezier.jpg|thumb|Bezier Parsec Geometry Paremeterisation system used]]&lt;br /&gt;
The objective function takes a set of input parameters from the optimiser and returns their performance. The main update to the toolchain was the creation of a new objective function that implemented geometry parameterisation. It uses Bezier Parsec parameterisation to create the airfoil geometry. This works by creating a thickness and camber curve from 2 Bezier curves each. The control points of the Bezier curves are calculated from a set of geometry related input parameters such as thickness, the inside angle of the trailing edge and the radius of the leading edge. The curves are processed to turn them into the coordinates of the airfoil and then saved as an STL file to be used in OpenFoam. The objective function runs some checks on the geometry to ensure the elements are not intersecting or the parameterisation created impossible geometry.&lt;br /&gt;
The objective then runs the CFD analysis in Openfoam and returns the coefficient of lift calculated. The CFD simulation uses a tool called SnappyHexMesh that creates a mesh with automatic refinement around the aerofoils, including creating surface layers on the aerofoil to ensure the simulation correctly models the boundary layer. The model uses the SimpleFOAM soliving utility, it is a steady state, incompressible flow solver using the SIMPLE (Semi-Implicit Method for Pressure Linked Equations) algorithm. The k-ω SST turbulence model is also used with the solver.&lt;br /&gt;
[[File:Mesh.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
Through the irish centre for high end computing, access was obtained to the national supercomputer which is currently hosted on the meluxina supercomputer in Luxembourg. Some updates to the toolchain had to be made to allow it to be run on the supercomputer such as changing the utilities used to create the STL files.&lt;br /&gt;
&lt;br /&gt;
To take advantage of the 256 threads on each compute node, the toolchain was updated to allow it to run 32 instances of the objective function simultaneously. It does this by using a wrapper function to create multiple instances of the objective function for each generation of the optisation. Each instance of the objective function has 8 threads that it can use to run the CFD solver in parallel. The CFD domain is split into 8 subdomains that can be meshed and solved on their own thread.&lt;br /&gt;
&lt;br /&gt;
[[File:Parallelisation.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
=== Results ===&lt;br /&gt;
I ran multiple optimisation runs with various sets of input parameters available to the toolchain.&lt;br /&gt;
&lt;br /&gt;
The first optimisation that I ran was optimising the flap angle and position with a fixed main angle of attack. The optimiser trended towards geometry that created unsteady flow behind the flap that the solver was unable to converge on invalidating the results. This was overcome by adding penalties to the objective function for when the geometry does not converge. This allowed the optimiser to converge on this geometry with a lift coefficient of 2.2.&lt;br /&gt;
&lt;br /&gt;
The main element angle of attack was added as an additional degree of freedom to the problem. The toolchain originally converged on a geometry with the flap in front of the main element and with a lower &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt;. This shows that CMAES is susceptible to selecting the wrong initial basin in its search and can struggle to switch. By increasing the optimisers population size and more careful constraints the optimiser created the geometry shown which has a much higher &amp;lt;math&amp;gt;C_L&amp;lt;/math&amp;gt; of 2.6. &lt;br /&gt;
&lt;br /&gt;
I ran the toolchain to optimise the shape of the main aerofoil and the flap angle and position. It resulted in a design with a coefficient of lift of 3.3 and a relatively high coefficient of drag of 0.16. It has arranged the elements in a similar way to the previous optimisation but it has created the very blunt trailing edge which helps develop the wake. In some optimisations I ran for the glide ratio it instead created an extremely sharp trailing edge&lt;br /&gt;
&lt;br /&gt;
The flow at the top of the leading edge is very fast at more than 2.5 times the freestream velocity, it also has a bump in the camber line on the main element where the flow detaches from the element on the top surface. The gap is between the two elements is slightly bigger than the previous optimisation resulting in a thicker flow over the flap.&lt;br /&gt;
&lt;br /&gt;
When the parameterisation of the main element shape was included in the optimisation, it created a very blunt trailing edge by increasing the gap between the trailing edge of the thickness curve and the 𝑋 axis. This parameter is not intended as a method of creating a blunt trailing edge but to prevent zero thickness on the trailing edge which can cause issues in some applications. A better method would be to incorporate some additional parameterisation system on the trailing edge as it is clear it has a significant impact on the performance of the geometry. The current optimisation is restricted to a flat surface on the trailing edge but the optimal design may actually have a more rounded profile or complex profile which is not possible with Bezier Parsec Parameterisation.&lt;br /&gt;
&lt;br /&gt;
When the trailing edge thickness was decreased it created unsteady flow. This indicates that the thicker trailing edge may also be allowing the aerofoil to create a high coefficient of lift while avoiding creating an unsteady flow. It does this by having a sharp edge at the end of the upper surface that causes keeps the flow from detaching until it reaches this corner. The earlier detaching of flow with the thin trailing edge results in a more horizontal wake This wake then isn’t able to keep the flow from the gap between the elements pressed along the flap. This then causes that flow to separate from the flap creating the instability. This shows that the more downward wake of the thick trailing edge main element helps ensure that the flow through the gap remains attached to the flap, allowing the flap to be at a more aggressive angle while avoiding unstable flow, increasing the coefficient of lift.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:4882velocity.png|thumb|Velocity flow over optimised aerofoil]]&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Masters_Project&amp;diff=19</id>
		<title>Masters Project</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Masters_Project&amp;diff=19"/>
		<updated>2026-09-21T10:08:21Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This project focussed on the development and deployment of an optimisation toolchain for maximising the performance of an airborne wind energy system. Airborne wind energy system have the potential to access stronger and more consistent high-altitude wind resources while requiring significantly less material than conventional wind turbines. The toolchain aims to optimise the performance of the aerofoil by maximising the lift generated to maximise the power the system can generate.&lt;br /&gt;
&lt;br /&gt;
Through this work I developed some of the following skills:&lt;br /&gt;
&lt;br /&gt;
* Simulation-driven design&lt;br /&gt;
* Computational Fluid Dynamics&lt;br /&gt;
* High performance computing and Linux&lt;br /&gt;
* Data Analysis&lt;br /&gt;
* Critical evaluatation of model assumptions&lt;br /&gt;
* Report writing and communicating technical concepts&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Airborne wind energy uses a tethered aircraft to create electricity while avoiding the large material costs of conventional wind turbines. Drag powered kites which are the focus of this study having a tethered kite with onboard generators, the kite aerofoil allows it to fly crosswind at high speeds increasing the relative wind speed at the turbines. There are several companies working on designs, most are in concept stages with a few that have built some prototypes such as Makani which are now defunct and Kitekraft.&lt;br /&gt;
&lt;br /&gt;
The understanding on the topic for many years was that the power generated by the system is proportional to the coefficient of lift cubed divided by the equivalent coefficient of drag squared,&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto \frac{C_L^3}{C_D^2}&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
so by optimising the glide ratio which is the ratio of the two  coefficients, you would maximise power.&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt; E =\frac{C_L}{C_D}&amp;lt;/math&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;math display=&amp;quot;block&amp;quot;&amp;gt;P \propto E C_L&amp;lt;/math&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Recent research done by Bauer in TU Munich who was working closely with the company kitekraft showed that when the drag forces of other parts of the system, in particular the tether drag is included the glide ratio of the aerofoil is less important as previous research suggested. In the case where the tether drag is dominant the power generated is proportional to the coefficient of lift of the aerofoil squared and should be optimised for as the drag component of the aerofoil has little effect. There have been very limited studies on the optimisation of aerofoils for airborne wind energy with even fewer optimising multi element high lift aerofoils aiming to maximise the &amp;lt;math display=&amp;quot;inline&amp;quot;&amp;gt;C_L&amp;lt;/math&amp;gt; which is the focus of my project.&lt;br /&gt;
&lt;br /&gt;
This project built on work done a previous masters project and code developed by TU Munich and Kitekraft. The toolchain uses octave to run the optimisation with the CFD analysis using simplefoam in OpenFOAM. My work focused on incorporating geometric design parameterisation, so allowing the toolchain to create its own aerofoil geometry that it can modify to optimise its performance. Then to update the toolchain to allow it to be run on high performance computing  using that to optimise the performance of a multi element aerofoil for use in an airborne wind energy system.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain ===&lt;br /&gt;
The optimisation algorithm used is the covariance matrix adaptation evolution strategy (CMA-ES) implemented in Octave by Nikolaus Hansen. It creates a set of parameters to be evaluated by the objective function, it passes these parameters along with some static inputs to the objective function.&lt;br /&gt;
[[File:overviewFlowChart.png|300px|centre]]&lt;br /&gt;
[[File:bezier.jpg|thumb|Bezier Parsec Geometry Paremeterisation system used]]&lt;br /&gt;
The objective function takes a set of input parameters from the optimiser and returns their performance. The main update to the toolchain was the creation of a new objective function that implemented geometry parameterisation. It uses Bezier Parsec parameterisation to create the airfoil geometry. This works by creating a thickness and camber curve from 2 Bezier curves each. The control points of the Bezier curves are calculated from a set of geometry related input parameters such as thickness, the inside angle of the trailing edge and the radius of the leading edge. The curves are processed to turn them into the coordinates of the airfoil and then saved as an STL file to be used in OpenFoam. The objective function runs some checks on the geometry to ensure the elements are not intersecting or the parameterisation created impossible geometry.&lt;br /&gt;
The objective then runs the CFD analysis in Openfoam and returns the coefficient of lift calculated. The CFD simulation uses a tool called SnappyHexMesh that creates a mesh with automatic refinement around the aerofoils, including creating surface layers on the aerofoil to ensure the simulation correctly models the boundary layer. The model uses the SimpleFOAM soliving utility, it is a steady state, incompressible flow solver using the SIMPLE (Semi-Implicit Method for Pressure Linked Equations) algorithm. The k-ω SST turbulence model is also used with the solver.&lt;br /&gt;
[[File:Mesh.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
Through the irish centre for high end computing, access was obtained to the national supercomputer which is currently hosted on the meluxina supercomputer in Luxembourg. Some updates to the toolchain had to be made to allow it to be run on the supercomputer such as changing the utilities used to create the STL files.&lt;br /&gt;
&lt;br /&gt;
To take advantage of the 256 threads on each compute node, the toolchain was updated to allow it to run 32 instances of the objective function simultaneously. It does this by using a wrapper function to create multiple instances of the objective function for each generation of the optisation. Each instance of the objective function has 8 threads that it can use to run the CFD solver in parallel. The CFD domain is split into 8 subdomains that can be meshed and solved on their own thread.&lt;br /&gt;
&lt;br /&gt;
[[File:Parallelisation.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
=== Results ===&lt;br /&gt;
[[File:4882velocity.png|thumb|Velocity flow over optimised aerofoil]]&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:Mesh.png&amp;diff=18</id>
		<title>File:Mesh.png</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:Mesh.png&amp;diff=18"/>
		<updated>2026-09-21T10:07:30Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Sidebar&amp;diff=17</id>
		<title>MediaWiki:Sidebar</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Sidebar&amp;diff=17"/>
		<updated>2026-09-21T08:58:14Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
* navigation&lt;br /&gt;
** mainpage|mainpage-description&lt;br /&gt;
** Masters Project|Masters Project&lt;br /&gt;
** Mountaineering Club|Mountaineering Club&lt;br /&gt;
** recentchanges-url|recentchanges&lt;br /&gt;
** randompage-url|randompage&lt;br /&gt;
** specialpages-url|specialpages&lt;br /&gt;
* SEARCH&lt;br /&gt;
* TOOLBOX&lt;br /&gt;
* LANGUAGES&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=MediaWiki:Sidebar&amp;diff=16</id>
		<title>MediaWiki:Sidebar</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=MediaWiki:Sidebar&amp;diff=16"/>
		<updated>2026-09-21T08:57:20Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created page with &amp;quot; * navigation ** mainpage|mainpage-description ** Masters Project|Masters Project ** recentchanges-url|recentchanges ** randompage-url|randompage ** specialpages-url|specialpages * SEARCH * TOOLBOX * LANGUAGES&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
* navigation&lt;br /&gt;
** mainpage|mainpage-description&lt;br /&gt;
** Masters Project|Masters Project&lt;br /&gt;
** recentchanges-url|recentchanges&lt;br /&gt;
** randompage-url|randompage&lt;br /&gt;
** specialpages-url|specialpages&lt;br /&gt;
* SEARCH&lt;br /&gt;
* TOOLBOX&lt;br /&gt;
* LANGUAGES&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Masters_Project&amp;diff=15</id>
		<title>Masters Project</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Masters_Project&amp;diff=15"/>
		<updated>2026-09-18T21:50:22Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This project focussed on the development and deployment of an optimisation toolchain for maximising the performance of an airborne wind energy system. Airborne wind energy system have the potential to access stronger and more consistent high-altitude wind resources while requiring significantly less material than conventional wind turbines. The toolchain aims to optimise the performance of the aerofoil by maximising the lift generated to maximise the power the system can generate.&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Airborne wind energy uses a tethered aircraft to create electricity while avoiding the large material costs of conventional wind turbines. Drag powered kites which are the focus of this study having a tethered kite with onboard generators, the kite aerofoil allows it to fly crosswind at high speeds increasing the relative wind speed at the turbines. There are several companies working on designs, most are in concept stages with a few that have built some prototypes such as Makani which are now defunct and Kitekraft.&lt;br /&gt;
&lt;br /&gt;
The understanding on the topic for many years was that the power generated by the system is proportional to the coefficient of lift cubed divided by the equivalent coefficient of drag squared, so by optimising the glide ratio which is the ratio of the two  coefficients, you would maximise power. Recent research done by Bauer in TU Munich who was working closely with the company kitekraft showed that when the drag forces of other parts of the system, in particular the tether drag is included the previous research is not always valid. In the case where the tether drag is dominant the power generated is proportional to the coefficient of lift squared and should be optimised for. There have been very limited studies on the optimisation of airfoils for airborne wind energy with even fewer optimising multi element high lift aerofoils aiming to maximise the CL which is the focus of my project.&lt;br /&gt;
&lt;br /&gt;
This project built on work done a previous masters project and code developed by TU Munich and Kitekraft. The toolchain uses octave to run the optimisation with the CFD analysis using simplefoam in OpenFOAM. My work focused on incorporating geometric design parameterisation, so allowing the toolchain to create its own aerofoil geometry that it can modify to optimise its performance. Then to update the toolchain to allow it to be run on high performance computing  using that to optimise the performance of a multi element aerofoil for use in an airborne wind energy system.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain Updates ===&lt;br /&gt;
[[File:bezier.jpg|thumb|Bezier Parsec Geometry Paremeterisation system used]]&lt;br /&gt;
The main update to the toolchain was the creation of a new objective function that implemented geometry parameterisation. It uses Bezier Parsec parameterisation to create the airfoil geometry. This works by creating a thickness and camber curve from 2 Bezier curves each. The control points of the Bezier curves are calculated from a set of geometry related input parameters such as thickness, the inside angle of the trailing edge and the radius of the leading edge. The curves are processed to turn them into the coordinates of the airfoil and then saved as an STL file to be used in OpenFoam. The objective function runs some checks on the geometry to ensure the elements are not intersecting or the parameterisation created impossible geometry. Then it runs the CFD analysis in Openfoam and returns the coefficient of lift calculated.&lt;br /&gt;
&lt;br /&gt;
[[File:overviewFlowChart.png|300px|centre]]&lt;br /&gt;
&lt;br /&gt;
Through the irish centre for high end computing, access was obtained to the national supercomputer which is currently hosted on the meluxina supercomputer in Luxembourg. Some updates to the toolchain had to be made to allow it to be run on the supercomputer such as changing the utilities used to create the STL files. To take advantage of the 256 threads on each compute node, the toolchain was updated to allow it to run 32 instances of the objective function simultaneously. It does this by using a wrapper function to create multiple instances of the objective function for each generation of the optisation. Each instance of the objective function has 8 threads that it can use to run the CFD solver in parallel.&lt;br /&gt;
&lt;br /&gt;
[[File:Parallelisation.png|500px|centre]]&lt;br /&gt;
&lt;br /&gt;
=== Results ===&lt;br /&gt;
[[File:4882velocity.png|thumb|Velocity flow over optimised aerofoil]]&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:Bezier.jpg&amp;diff=14</id>
		<title>File:Bezier.jpg</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:Bezier.jpg&amp;diff=14"/>
		<updated>2026-09-18T21:44:05Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:OverviewFlowChart.png&amp;diff=13</id>
		<title>File:OverviewFlowChart.png</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:OverviewFlowChart.png&amp;diff=13"/>
		<updated>2026-09-18T21:42:18Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=File:Parallelisation.png&amp;diff=12</id>
		<title>File:Parallelisation.png</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=File:Parallelisation.png&amp;diff=12"/>
		<updated>2026-09-18T21:41:16Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Masters_Project&amp;diff=11</id>
		<title>Masters Project</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Masters_Project&amp;diff=11"/>
		<updated>2026-09-18T21:36:57Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;This project focussed on the development and deployment of an optimisation toolchain for maximising the performance of an airborne wind energy system. Airborne wind energy system have the potential to access stronger and more consistent high-altitude wind resources while requiring significantly less material than conventional wind turbines. The toolchain aims to optimise the performance of the aerofoil by maximising the lift generated to maximise the power the system can generate.&lt;br /&gt;
&lt;br /&gt;
=== Background ===&lt;br /&gt;
Airborne wind energy uses a tethered aircraft to create electricity while avoiding the large material costs of conventional wind turbines. Drag powered kites which are the focus of this study having a tethered kite with onboard generators, the kite aerofoil allows it to fly crosswind at high speeds increasing the relative wind speed at the turbines. There are several companies working on designs, most are in concept stages with a few that have built some prototypes such as Makani which are now defunct and Kitekraft.&lt;br /&gt;
&lt;br /&gt;
The understanding on the topic for many years was that the power generated by the system is proportional to the coefficient of lift cubed divided by the equivalent coefficient of drag squared, so by optimising the glide ratio which is the ratio of the two  coefficients, you would maximise power. Recent research done by Bauer in TU Munich who was working closely with the company kitekraft showed that when the drag forces of other parts of the system, in particular the tether drag is included the previous research is not always valid. In the case where the tether drag is dominant the power generated is proportional to the coefficient of lift squared and should be optimised for. There have been very limited studies on the optimisation of airfoils for airborne wind energy with even fewer optimising multi element high lift aerofoils aiming to maximise the CL which is the focus of my project.&lt;br /&gt;
&lt;br /&gt;
This project built on work done a previous masters project and code developed by TU Munich and Kitekraft. The toolchain uses octave to run the optimisation with the CFD analysis using simplefoam in OpenFOAM. My work focused on incorporating geometric design parameterisation, so allowing the toolchain to create its own aerofoil geometry that it can modify to optimise its performance. Then to update the toolchain to allow it to be run on high performance computing  using that to optimise the performance of a multi element aerofoil for use in an airborne wind energy system.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain Updates ===&lt;br /&gt;
The main update to the toolchain was the creation of a new objective function that implemented geometry parameterisation. It uses Bezier parsec parameterisation to create the airfoil geometry. This works by creating a thickness and camber curve from 2 Bezier curves each. The control points of the Bezier curves are calculated from a set of geometry related input parameters such as thickness, the inside angle of the trailing edge and the radius of the leading edge. The curves are processed to turn them into the coordinates of the airfoil and then saved as an STL file to be used in OpenFoam. The objective function runs some checks on the geometry to ensure the elements are not intersecting or the parameterisation created impossible geometry. Then it runs the CFD analysis in Openfoam and returns the coefficient of lift calculated &lt;br /&gt;
&lt;br /&gt;
Through the irish centre for high end computing, access was obtained to the national supercomputer which is currently hosted on the meluxina supercomputer in Luxembourg. Some updates to the toolchain had to be made to allow it to be run on the supercomputer such as changing the utilities used to create the STL files. To take advantage of the 256 threads on each compute node, the toolchain was updated to allow it to run 32 instances of the objective function simultaneously. It does this by using a wrapper function to create multiple instances of the objective function for each generation of the optisation. Each instance of the objective function has 8 threads that it can use to run the CFD solver in parallel.&lt;br /&gt;
&lt;br /&gt;
=== Results ===&lt;br /&gt;
[[File:4882velocity.png|thumb|Velocity flow over optimised aerofoil]]&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Main_Page&amp;diff=10</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Main_Page&amp;diff=10"/>
		<updated>2026-09-18T21:30:42Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Introduction ==&lt;br /&gt;
Hello, my name is Aodhán, this wiki is a digital portfolio of my CV so you can find out more about anything you are interested in!&lt;br /&gt;
&lt;br /&gt;
You can contact me at [mailto:aodhantgallagher@gmail.com aodhantgallagher@gmail.com]&lt;br /&gt;
== CV ==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Aodhán Gallagher&#039;&#039;&#039; is a mechanical engineering postgraduate at the University of Galway, specialising in computational fluid dynamics and design optimisation, with industry experience in medical device manufacturing and a background in outdoor leadership and international scouting.&lt;br /&gt;
&lt;br /&gt;
__TOC__&lt;br /&gt;
&lt;br /&gt;
== Academic ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
! style=&amp;quot;width:14%;&amp;quot; | Years&lt;br /&gt;
! style=&amp;quot;width:46%;&amp;quot; | Qualification&lt;br /&gt;
! Result&lt;br /&gt;
|-&lt;br /&gt;
| 2025 – 2026&lt;br /&gt;
| &#039;&#039;&#039;M.E. Mechanical Engineering&#039;&#039;&#039;, University of Galway&lt;br /&gt;
| &#039;&#039;First Class Honours&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| 2021 – 2025&lt;br /&gt;
| &#039;&#039;&#039;B.E. Mechanical Engineering&#039;&#039;&#039;, University of Galway&lt;br /&gt;
| &#039;&#039;First Class Honours&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| 2015 – 2021&lt;br /&gt;
| &#039;&#039;&#039;Castletroy College&#039;&#039;&#039; — Leaving Certificate&lt;br /&gt;
| &#039;&#039;613 / 625 points&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== [[Masters Project|Master&#039;s individual project]] ===&lt;br /&gt;
&lt;br /&gt;
;Aerofoil optimisation for high-lift airborne wind energy wings&lt;br /&gt;
* Incorporate geometric design parameterisation to enable optimisation of the aerofoil.&lt;br /&gt;
* Use CFD to characterise the performance of the aerofoil and integrate performance with an optimisation algorithm.&lt;br /&gt;
* Deploy the optimisation framework on a high-performance computing system.&lt;br /&gt;
&lt;br /&gt;
=== Modules ===&lt;br /&gt;
&lt;br /&gt;
; M.E. modules&lt;br /&gt;
: Digital Control Systems &amp;amp;bull; Composite Materials &amp;amp;bull; Advanced Energy Systems Engineering &amp;amp;bull; Research Methods &amp;amp;bull; Financial Management &amp;amp;bull; Engineering Acoustics &amp;amp;bull; Advanced Mechanics of Materials &amp;amp;bull; Turbomachines and Advanced Fluid Dynamics &amp;amp;bull; Advanced Manufacturing&lt;br /&gt;
&lt;br /&gt;
; B.E. final-year modules&lt;br /&gt;
: Advanced Mechanical Analysis and Design &amp;amp;bull; Energy Conversion &amp;amp;bull; Materials II &amp;amp;bull; Finite Element Methods in Engineering II &amp;amp;bull; Computational Fluid Dynamics &amp;amp;bull; Biomechanics &amp;amp;bull; Project Management for Engineers &amp;amp;bull; Professional Skills &amp;amp;bull; Professional Experience Programme&lt;br /&gt;
&lt;br /&gt;
== Employment experience ==&lt;br /&gt;
&lt;br /&gt;
=== Manufacturing Engineer Intern, Medtronic ===&lt;br /&gt;
&#039;&#039;January – August 2025&#039;&#039;&lt;br /&gt;
* Designed in CAD, tested and implemented new fixturing for manufacturing lines.&lt;br /&gt;
* Created CAD drawings for an electrical safety test system.&lt;br /&gt;
* Worked on developing new manufacturing lines implementing Lean principles.&lt;br /&gt;
&lt;br /&gt;
=== Reception Assistant, World Scout Centre (KISC) ===&lt;br /&gt;
&#039;&#039;Summer 2024&#039;&#039;&lt;br /&gt;
* Managed large, complex bookings with a high attention to detail.&lt;br /&gt;
* Gathered booking statistics for review and applied critical thinking to improve the centre&#039;s processes.&lt;br /&gt;
* Consistently delivered a high level of guest service, ensuring a positive experience for all visitors.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Guide, World Scout Centre (KISC) ===&lt;br /&gt;
&#039;&#039;Summer 2023&#039;&#039;&lt;br /&gt;
* Led and instructed groups of scouts from all over the world climbing in the Swiss Alps.&lt;br /&gt;
* Worked in an international environment with staff and guests from all over the world.&lt;br /&gt;
* Developed skills such as group management, decision making and communication.&lt;br /&gt;
&lt;br /&gt;
== Other experience ==&lt;br /&gt;
&lt;br /&gt;
=== University club and society committees ===&lt;br /&gt;
&#039;&#039;2021 – present&#039;&#039;&lt;br /&gt;
* Captain of the [[Mountaineering Club|mountaineering club]]: led the committee and was responsible for running the largest club in the university.&lt;br /&gt;
* Organised events including international trips, training courses and weekly activities.&lt;br /&gt;
* Created operating procedures for climbing and carried out risk assessments to ensure events are safe.&lt;br /&gt;
* Maintained equipment and secured over &#039;&#039;&#039;€15,000&#039;&#039;&#039; of equipment funding for the Rover Society and Mountaineering Club.&lt;br /&gt;
* Led the team organising the &#039;&#039;&#039;Maamturks Challenge Walk&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== World Scouting European Region Youth Empowerment Team ===&lt;br /&gt;
&#039;&#039;2022 – 2024&#039;&#039;&lt;br /&gt;
* Promoted youth empowerment and engagement within the European Scout Region.&lt;br /&gt;
* Wrote a regional action plan for youth empowerment in the European Scout Region.&lt;br /&gt;
&lt;br /&gt;
== Skills ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
! style=&amp;quot;width:18%;&amp;quot; | Skill&lt;br /&gt;
! Evidence&lt;br /&gt;
|-&lt;br /&gt;
! style=&amp;quot;text-align:left;&amp;quot; | Teamwork&lt;br /&gt;
| Group projects in university; member of club and society committees; worked with people from all around the world at an international scout centre.&lt;br /&gt;
|-&lt;br /&gt;
! style=&amp;quot;text-align:left;&amp;quot; | Problem-solving&lt;br /&gt;
| Experience diagnosing and solving problems when designing and working on projects; quick problem-solving when leading climbing groups, handling situations effectively but safely.&lt;br /&gt;
|-&lt;br /&gt;
! style=&amp;quot;text-align:left;&amp;quot; | Project management&lt;br /&gt;
| Led the organisation of the Maamturks Challenge, coordinating logistics, volunteers and participant management to deliver a safe and successful large-scale mountain hiking event.&lt;br /&gt;
|-&lt;br /&gt;
! style=&amp;quot;text-align:left;&amp;quot; | Leadership&lt;br /&gt;
| Led committees through university clubs and societies and led event-organising teams; delegates tasks and ensures team members have the skills and resources to complete them, with a focus on upskilling so people progress.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Achievements ==&lt;br /&gt;
&lt;br /&gt;
* University of Galway &#039;&#039;&#039;Postgraduate Excellence Scholarship&#039;&#039;&#039;.&lt;br /&gt;
* University Scholarship in first and second year for placing in the top 6% of the class.&lt;br /&gt;
* One of three Limerick Leaving Certificate students shortlisted for the &#039;&#039;&#039;Naughton Scholarship&#039;&#039;&#039; in 2021.&lt;br /&gt;
* Competed in the Limerick Maths Olympiad, Irish Maths Olympiad and Irish Chemistry Olympiad.&lt;br /&gt;
* Attended the World Scout Youth Forum and World Scout Conference as a member of the Ireland delegation.&lt;br /&gt;
&lt;br /&gt;
== Interests and hobbies ==&lt;br /&gt;
&lt;br /&gt;
; Mountaineering&lt;br /&gt;
: A lifelong hiker who started climbing at university; has hiked and climbed in Wales, Switzerland, Austria and all over Ireland.&lt;br /&gt;
&lt;br /&gt;
; Scouting&lt;br /&gt;
: Active in scouts at local, national and international level, including the Venture Programme and Youth Empowerment teams.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
! style=&amp;quot;width:26%;&amp;quot; | Name&lt;br /&gt;
! style=&amp;quot;width:38%;&amp;quot; | Position&lt;br /&gt;
! Contact&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Dr. Nathan Quinlan&#039;&#039;&#039;&lt;br /&gt;
| Senior Lecturer, School of Engineering, University of Galway&lt;br /&gt;
| [mailto:nathan.quinlan@universityofgalway.ie nathan.quinlan@universityofgalway.ie]&amp;lt;br /&amp;gt;+353 91 492726&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Javier Paniagua Petisco&#039;&#039;&#039;&lt;br /&gt;
| Director, World Scout Centre (KISC)&lt;br /&gt;
| [mailto:director@kisc.ch director@kisc.ch]&amp;lt;br /&amp;gt;+41 79 263 84 49&amp;lt;br /&amp;gt;&#039;&#039;Letter available upon request&#039;&#039;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
[[Category:Curricula vitae]]&lt;br /&gt;
[[Category:Mechanical engineering]]&lt;br /&gt;
[[Category:University of Galway]]&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=9</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=9"/>
		<updated>2026-09-18T21:28:02Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
The Captain is responsible for ensuring the whole club runs smoothly, leading the rest of the committee and as a liason with the sports unit. &lt;br /&gt;
&lt;br /&gt;
I learnt a lot about managing a team in this role and how to create an environment that is fun and welcoming for everyone to work together as a team.&lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Webmaster ==&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
	<entry>
		<id>http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=8</id>
		<title>Mountaineering Club</title>
		<link rel="alternate" type="text/html" href="http://aodhán.com/index.php?title=Mountaineering_Club&amp;diff=8"/>
		<updated>2026-09-18T21:21:18Z</updated>

		<summary type="html">&lt;p&gt;Aodhan: Created page with &amp;quot;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter tri...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The University of Galway Mountaineering Club is one of the oldest and the largest club in the University. Founded in 1970 it has more than 400 paid members every year. The club organises weekly hikes in Connemara and operates the climbing wall in the Kingfisher Gym on the University Campus. The club organises several trips every year typically one main trip a semester to somewhere in Ireland or to Wales. With additional trips being organised some years such as Winter trips to Scotland and smaller trips around Ireland.&lt;br /&gt;
&lt;br /&gt;
The club also organises the Maamturks Challenge which is one of the most popular hillwalking events in the country. This is organised by a subcommittee which I have also been on and led several times.&lt;br /&gt;
&lt;br /&gt;
== Committee Roles ==&lt;br /&gt;
&lt;br /&gt;
=== Captain ===&lt;br /&gt;
&#039;&#039;2025-2026&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Equipment Officer ===&lt;br /&gt;
&#039;&#039;2024-2025&#039;&#039;&lt;br /&gt;
The Equipment Officer inspects, maintains, replaces and procures all the club&#039;s equipment. This is especially important for climbing equipment which is critical to safety. As equipment officer I set up a new selfhosted database that allows equipment to be signed out to people with it automatically sending email notifications to the borrower. It allows the history of the equipment to be tracked such as when it was bought, who used it and when and the equipment&#039;s current condition.&lt;br /&gt;
&lt;br /&gt;
I also acquired funding from the Sports Unit for €5k to be invested in the club&#039;s equipment to replace gear that was past its reccomended lifespan and buy new equipment to allow the club to organise more activities and increase safety.&lt;br /&gt;
&lt;br /&gt;
=== Climbing Vice Captain ===&lt;br /&gt;
&#039;&#039;2023-2024&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The Climbing VC is responsible for all climbing in the club. This primarily involves managing the climbing wall which includes &lt;br /&gt;
* Organising training of new members&lt;br /&gt;
* Training and assessing new climbing instructors&lt;br /&gt;
* Organising taking down the holds and setting new routes twice a year&lt;br /&gt;
* Maintenance of the Climbing wall&lt;br /&gt;
&lt;br /&gt;
The other part of the job is organising outdoor climbing trips for the club which involves&lt;br /&gt;
* Organising training courses for members to upskill&lt;br /&gt;
* Planning outdoor climbing trips &lt;br /&gt;
* Responsible for safety and the organisation of outdoor climbing on trips&lt;br /&gt;
&lt;br /&gt;
=== Secretary ===&lt;br /&gt;
&#039;&#039;2022-2023&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
In this role I was responsible for all email correspondence and moved our document storage to shared drives to ensure all committee members could access documents instead of storing them in personal drives.&lt;br /&gt;
&lt;br /&gt;
I also took minutes for the weekly committee meeting as well as assisting other committee members with their responsibilities.&lt;br /&gt;
&lt;br /&gt;
== Webmaster ==&lt;br /&gt;
&lt;br /&gt;
As an additional voluntary role I have been part of the team responsible for maintaining and developing the clubs digital presence. We have a number of websites/web services that are all self hosted:&lt;br /&gt;
* Club main website&lt;br /&gt;
* Webstore where we sell tickets to events&lt;br /&gt;
* Club Wiki similar to this &lt;br /&gt;
* Club email system&lt;br /&gt;
* Database of climbing assessments&lt;br /&gt;
* Equipment database as previously mentioned&lt;/div&gt;</summary>
		<author><name>Aodhan</name></author>
	</entry>
</feed>