1
0

fix: close California play and asset acceptance gaps

This commit is contained in:
2026-08-11 22:49:37 -07:00
parent d4859b33c6
commit dd18c6775d
12 changed files with 267 additions and 126 deletions
+18 -4
View File
@@ -89,7 +89,11 @@ function wingGeometry(span: number, rootChord: number, tipChord: number): THREE.
]);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
geometry.setIndex([0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 5]);
// Counter-clockwise from above. The reverse winding points the generated
// normals down, so Three.js culls the whole wing from the chase/flyover view
// while still drawing its shadow — leaving only two apparently floating
// ailerons around the fuselage.
geometry.setIndex([0, 2, 1, 0, 3, 2, 0, 4, 3, 0, 5, 4]);
geometry.computeVertexNormals();
return geometry;
}
@@ -106,9 +110,14 @@ function addFan(root: THREE.Group, x: number, materials: ElectricAircraftMateria
hub.rotation.x = Math.PI / 2;
fan.add(hub);
for (let index = 0; index < 5; index += 1) {
const angle = index * TWO_PI / 5;
const blade = mesh(new THREE.BoxGeometry(0.07, 0.68, 0.025), materials.dark, `${fan.name}:blade-${index}`);
blade.position.y = 0.3;
blade.rotation.z = index * TWO_PI / 5;
// Place every blade on its own radial spoke. Rotating five differently
// oriented rectangles around the same off-centre point makes a lopsided
// paddle; matching centre and local +Y to this angle produces a balanced
// rotor whose group can spin continuously around the hub.
blade.position.set(-Math.sin(angle) * 0.3, Math.cos(angle) * 0.3, 0);
blade.rotation.z = angle;
fan.add(blade);
}
root.add(fan);
@@ -122,6 +131,9 @@ export function buildElectricAircraft(options: ElectricAircraftBuildOptions = {}
const materials = options.materials ?? createElectricAircraftMaterials(options.bodyColor);
const root = new THREE.Group();
root.name = "electric-aircraft";
root.userData.kind = "aircraft";
root.userData.aircraftModel = "electric-vtail";
root.userData.forwardAxis = "-Z";
const fuselage = mesh(new THREE.CapsuleGeometry(0.66, 5.9, 8, 16), materials.body, "electric-aircraft:fuselage");
fuselage.rotation.x = Math.PI / 2;
@@ -144,7 +156,9 @@ export function buildElectricAircraft(options: ElectricAircraftBuildOptions = {}
leftAileron.name = "electric-aircraft.aileron-left";
rightAileron.name = "electric-aircraft.aileron-right";
for (const [joint, x] of [[leftAileron, -4.2], [rightAileron, 4.2]] as const) {
joint.position.set(x, 0.48, 0.72);
// On the tapered wing's trailing edge. At 0.72 the outer leading corner
// sat behind the tip chord and the bright surface read as a floating bar.
joint.position.set(x, 0.48, 0.58);
joint.add(mesh(new THREE.BoxGeometry(2.15, 0.08, 0.45), materials.accent, `${joint.name}:surface`));
root.add(joint);
}
+12 -4
View File
@@ -168,15 +168,23 @@ function checkedEnvelope(value: CaliforniaFlightEnvelope | undefined): Californi
return envelope;
}
function checkedRoute(route: readonly AircraftWaypoint[] | undefined): readonly AircraftWaypoint[] {
function checkedRoute(
route: readonly AircraftWaypoint[] | undefined,
envelope: CaliforniaFlightEnvelope,
): readonly AircraftWaypoint[] {
if (!route) return [];
const ids = new Set<string>();
return route.map((point) => {
if (
typeof point.id !== "string" || point.id.length === 0 || ids.has(point.id) ||
!Number.isFinite(point.lat) || !Number.isFinite(point.lng) ||
!Number.isFinite(point.altitudeM)
) throw new RangeError("aircraft route waypoints must be finite with unique ids");
!Number.isFinite(point.altitudeM) ||
point.lat < envelope.minLat || point.lat > envelope.maxLat ||
point.lng < envelope.minLng || point.lng > envelope.maxLng ||
point.altitudeM < envelope.minAltitudeM || point.altitudeM > envelope.maxAltitudeM
) throw new RangeError(
"aircraft route waypoints must be finite, unique, and inside the flight envelope",
);
ids.add(point.id);
return { ...point };
});
@@ -200,7 +208,7 @@ function resolveOptions(value: AircraftControllerOptions): ResolvedOptions {
initialHeadingDeg: wrapDegrees(finiteOr(value.initialHeadingDeg, 320)),
initialSpeedMps: clamp(finiteOr(value.initialSpeedMps, 55), minimumSpeedMps, maximumSpeedMps),
mode: value.mode === "manual" ? "manual" : "assisted",
route: checkedRoute(value.route),
route: checkedRoute(value.route, envelope),
envelope,
minimumSpeedMps,
maximumSpeedMps,