1
0

feat: upgrade electric aircraft fidelity and flight model

This commit is contained in:
2026-08-19 00:52:35 -07:00
parent dd18c6775d
commit 79793faed2
4 changed files with 443 additions and 88 deletions
+72 -4
View File
@@ -26,7 +26,7 @@ describe("procedural electric aircraft", () => {
assert.equal(rig.root.name, "electric-aircraft");
assert.equal(rig.root.userData.forwardAxis, "-Z");
assert.equal(rig.root.userData.aircraftModel, "electric-vtail");
assert.equal(rig.fans.length, 2);
assert.equal(rig.fans.length, 6);
assert.equal(rig.ownsMaterials, true);
assert.ok(ELECTRIC_AIRCRAFT_METRICS.wingspan > ELECTRIC_AIRCRAFT_METRICS.length);
const bounds = new THREE.Box3().setFromObject(rig.root);
@@ -39,8 +39,13 @@ describe("procedural electric aircraft", () => {
const wing = rig.root.getObjectByName("electric-aircraft:wing");
assert.ok(wing instanceof THREE.Mesh);
const normals = wing.geometry.getAttribute("normal");
assert.ok(normals && Array.from({ length: normals.count }, (_, index) => normals.getY(index)).every((y) => y > 0),
"wing front faces point toward the chase/flyover camera");
assert.ok(normals && Array.from({ length: normals.count }, (_, index) => normals.getY(index)).some((y) => y > 0.5),
"wing has a readable upper airfoil surface");
assert.ok(normals && Array.from({ length: normals.count }, (_, index) => normals.getY(index)).some((y) => y < -0.5),
"wing is a closed prism that remains visible from below");
assert.ok(rig.root.getObjectByName("electric-aircraft:canopy-spine"));
assert.ok(rig.root.getObjectByName("electric-aircraft:nav-left"));
assert.ok(rig.root.getObjectByName("electric-aircraft:gear-door-left"));
disposeElectricAircraft(rig);
assert.equal(rig.root.children.length, 0);
});
@@ -56,7 +61,7 @@ describe("procedural electric aircraft", () => {
).multiplyScalar(1 / blades.length);
assert.ok(centroid.length() < 1e-12, `fan centroid ${centroid.toArray()}`);
for (const blade of blades) {
assert.ok(Math.abs(blade.position.length() - 0.3) < 1e-12);
assert.ok(Math.abs(blade.position.length() - 0.25) < 1e-12);
const radial = new THREE.Vector3(0, 1, 0).applyQuaternion(blade.quaternion);
assert.ok(radial.angleTo(blade.position.clone().normalize()) < 1e-7);
}
@@ -168,6 +173,66 @@ describe("aircraft controller", () => {
assert.equal(controller.state().envelopeContact, true);
});
it("models deterministic wind, energy use, lift telemetry, and stalls", () => {
const options = {
mode: "manual" as const,
initialSpeedMps: 20,
minimumSpeedMps: 18,
stallSpeedMps: 25,
windNorthMps: 7,
windEastMps: -4,
turbulenceMps: 2,
batteryCapacityWh: 2_000,
fixedStepSeconds: 0.05,
};
const a = new AircraftController(options);
const b = new AircraftController(options);
for (let index = 0; index < 200; index += 1) {
a.stepFixed({ throttle: 0.15, pitch: 0.7, roll: 0.2 });
b.stepFixed({ throttle: 0.15, pitch: 0.7, roll: 0.2 });
}
assert.deepEqual(a.snapshot(), b.snapshot());
assert.equal(a.state().stalled, true);
assert.ok(a.state().angleOfAttackDeg > 0);
assert.ok(a.state().verticalSpeedMps < 0, "a deep low-speed stall loses altitude");
assert.ok(a.state().energyUsedWh > 0);
assert.ok(a.state().batteryWh < 2_000);
assert.notEqual(a.state().windNorthMps, 7, "deterministic turbulence perturbs scenario wind");
assert.ok(Number.isFinite(a.state().loadFactorG));
});
it("uses deterministic terrain contact and reports a hard landing", () => {
const controller = new AircraftController({
mode: "manual",
envelope: {
minLat: 30,
maxLat: 45,
minLng: -130,
maxLng: -110,
minAltitudeM: 0,
maxAltitudeM: 6_000,
},
initialAltitudeM: 104,
initialSpeedMps: 22,
minimumSpeedMps: 18,
stallSpeedMps: 26,
terrainElevationM: () => 100,
fixedStepSeconds: 0.1,
});
let touched = false;
let hard = false;
for (let index = 0; index < 120; index += 1) {
controller.stepFixed({ throttle: 0, pitch: -1 });
touched ||= controller.state().onGround;
hard ||= controller.state().hardLanding;
}
assert.equal(touched, true);
assert.equal(hard, true);
assert.equal(controller.state().groundClearanceM, 0);
assert.equal(controller.state().altitudeM, 100);
assert.equal(controller.state().verticalSpeedMps, 0);
});
it("resets exactly, caps sleeping-tab time, and replays bit-for-bit", () => {
const options = { route: ROUTE, initialAltitudeM: 1_200, initialSpeedMps: 48 } as const;
const controller = new AircraftController(options);
@@ -218,5 +283,8 @@ describe("aircraft controller", () => {
maxAltitudeM: 100,
},
}), /envelope/);
assert.throws(() => new AircraftController({
terrainElevationM: 3 as unknown as (lat: number, lng: number) => number,
}), /terrain/);
});
});