-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFixedWingAnimation.ts
More file actions
127 lines (108 loc) · 3.83 KB
/
Copy pathFixedWingAnimation.ts
File metadata and controls
127 lines (108 loc) · 3.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) 2025-2026 Matthew Kissinger
import * as THREE from 'three';
import { getFixedWingDisplayInfo } from './FixedWingConfigs';
const TAU = Math.PI * 2;
interface PropellerState {
nodes: Array<{
node: THREE.Object3D;
axis: PropellerSpinAxis;
}>;
}
type PropellerSpinAxis = 'x' | 'y' | 'z';
/**
* Animates propellers on fixed-wing aircraft based on throttle.
* Jets (F-4 Phantom) have no propellers and are skipped.
*/
export class FixedWingAnimation {
private propellers = new Map<string, PropellerState>();
/**
* Wire propeller nodes from a loaded GLB group.
* Searches for named parts matching the config's propellerNodes list.
*/
initialize(
aircraftId: string,
configKey: string,
group: THREE.Group,
animations: THREE.AnimationClip[] = [],
): void {
const display = getFixedWingDisplayInfo(configKey);
if (!display || !display.hasPropellers) return;
const nodes: PropellerState['nodes'] = [];
const targetNames = display.propellerNodes.map(n => n.toLowerCase());
// Catalog spin axis is the source of truth for the repaint fleet (the
// upstream prop animation clips were stripped at import). Any surviving
// animation track still overrides per-node when present.
const catalogAxis: PropellerSpinAxis = display.propellerSpinAxis;
const animationAxes = inferSpinAxesFromAnimationClips(animations);
group.traverse((child) => {
const name = child.name.toLowerCase();
for (const target of targetNames) {
if (name.includes(target)) {
nodes.push({
node: child,
axis: animationAxes.get(name) ?? catalogAxis,
});
break;
}
}
});
if (nodes.length > 0) {
this.propellers.set(aircraftId, { nodes });
}
}
/**
* Spin propellers based on throttle. Called each frame.
* Each propeller spins around its own local axis (from the war-asset catalog;
* the repaint A-1/AC-47 hubs spin around local X). When `isActive` is false
* (parked unpiloted aircraft), the propeller stops entirely — no idle spin.
*/
update(aircraftId: string, throttle: number, dt: number, isActive: boolean = true): void {
const state = this.propellers.get(aircraftId);
if (!state) return;
if (!isActive) return;
// Propeller speed proportional to throttle, with minimum idle spin
const speed = (0.1 + throttle * 0.9) * 80; // radians/sec at full throttle
for (const propeller of state.nodes) {
propeller.node.rotation[propeller.axis] = (propeller.node.rotation[propeller.axis] + speed * dt) % TAU;
}
}
dispose(aircraftId: string): void {
this.propellers.delete(aircraftId);
}
disposeAll(): void {
this.propellers.clear();
}
}
function inferSpinAxesFromAnimationClips(animations: THREE.AnimationClip[]): Map<string, PropellerSpinAxis> {
const axes = new Map<string, PropellerSpinAxis>();
for (const clip of animations) {
for (const track of clip.tracks) {
if (!track.name.endsWith('.quaternion') || track.values.length < 8) {
continue;
}
const nodeName = track.name.slice(0, -'.quaternion'.length).toLowerCase();
const axis = inferQuaternionTrackAxis(track.values);
if (axis) {
axes.set(nodeName, axis);
}
}
}
return axes;
}
function inferQuaternionTrackAxis(values: ArrayLike<number>): PropellerSpinAxis | null {
let maxX = 0;
let maxY = 0;
let maxZ = 0;
for (let i = 0; i + 3 < values.length; i += 4) {
maxX = Math.max(maxX, Math.abs(values[i]));
maxY = Math.max(maxY, Math.abs(values[i + 1]));
maxZ = Math.max(maxZ, Math.abs(values[i + 2]));
}
if (maxX < 0.5 && maxY < 0.5 && maxZ < 0.5) {
return null;
}
if (maxX >= maxY && maxX >= maxZ) return 'x';
if (maxY >= maxX && maxY >= maxZ) return 'y';
return 'z';
}