feat: upgrade electric aircraft fidelity and flight model
This commit is contained in:
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user