Skip to content

Commit 1960a01

Browse files
WomB0ComB0claude
andcommitted
feat: add keyboard shortcuts, wind indicator, battery stats — Phase 1 complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 736f77f commit 1960a01

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

src/ResQ.Viz.Web/wwwroot/css/viz.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,12 @@ select {
157157
color: #8b949e;
158158
margin-bottom: 2px;
159159
}
160+
161+
#wind-indicator {
162+
position: absolute;
163+
top: 10px;
164+
left: 10px;
165+
text-align: center;
166+
}
167+
#wind-canvas { display: block; }
168+
#wind-label { font-size: 10px; color: #8b949e; margin-top: 2px; }

src/ResQ.Viz.Web/wwwroot/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ <h3>Fault Injection</h3>
9494
</div>
9595
</section>
9696
</div>
97+
<div id="wind-indicator">
98+
<canvas id="wind-canvas" width="60" height="60"></canvas>
99+
<div id="wind-label">N</div>
100+
</div>
97101
<div id="stats">
102+
<span id="avg-battery">Bat: --</span> |
98103
<span id="fps">FPS: --</span> |
99104
<span id="drone-count">Drones: 0</span> |
100105
<span id="sim-time">T: 0.0s</span>

src/ResQ.Viz.Web/wwwroot/js/app.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,37 @@ import { DroneManager } from './drones.js';
77
import { EffectsManager } from './effects.js';
88
import { ControlPanel } from './controls.js';
99

10+
function drawWindArrow(degrees) {
11+
const canvas = document.getElementById('wind-canvas');
12+
if (!canvas) return;
13+
const ctx = canvas.getContext('2d');
14+
const cx = 30, cy = 30, r = 22;
15+
ctx.clearRect(0, 0, 60, 60);
16+
17+
// Circle
18+
ctx.strokeStyle = 'rgba(139, 148, 158, 0.5)';
19+
ctx.lineWidth = 1;
20+
ctx.beginPath();
21+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
22+
ctx.stroke();
23+
24+
// Arrow
25+
const rad = (degrees - 90) * Math.PI / 180;
26+
ctx.strokeStyle = '#58a6ff';
27+
ctx.lineWidth = 2;
28+
ctx.beginPath();
29+
ctx.moveTo(cx, cy);
30+
ctx.lineTo(cx + Math.cos(rad) * r * 0.8, cy + Math.sin(rad) * r * 0.8);
31+
ctx.stroke();
32+
33+
// N marker
34+
ctx.fillStyle = '#8b949e';
35+
ctx.font = '8px sans-serif';
36+
ctx.textAlign = 'center';
37+
ctx.fillText('N', cx, cy - r - 4);
38+
}
39+
drawWindArrow(0); // Initial draw
40+
1041
const container = document.getElementById('scene-container');
1142
const statusEl = document.getElementById('connection-status');
1243
const fpsEl = document.getElementById('fps');
@@ -30,11 +61,18 @@ const connection = new signalR.HubConnectionBuilder()
3061
.build();
3162

3263
connection.on('ReceiveFrame', (frame) => {
33-
droneManager.update(frame.drones ?? []);
64+
const drones = frame.drones ?? [];
65+
droneManager.update(drones);
3466
effectsManager.update(frame);
35-
controlPanel.updateDroneList(frame.drones ?? []);
67+
controlPanel.updateDroneList(drones);
3668
droneCountEl.textContent = `Drones: ${droneManager.count}`;
3769
simTimeEl.textContent = `T: ${frame.time?.toFixed(1) ?? '0.0'}s`;
70+
71+
const avgBattery = drones.length > 0
72+
? (drones.reduce((s, d) => s + (d.battery ?? 100), 0) / drones.length).toFixed(0)
73+
: '--';
74+
const battEl = document.getElementById('avg-battery');
75+
if (battEl) battEl.textContent = `Bat: ${avgBattery}%`;
3876
});
3977

4078
connection.onreconnecting(() => {

src/ResQ.Viz.Web/wwwroot/js/controls.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export class ControlPanel {
55
constructor() {
66
this._bindButtons();
77
this._bindSliders();
8+
this._bindKeyboard();
89
}
910

1011
// Call each time a new frame arrives to keep drone select lists current
@@ -35,6 +36,33 @@ export class ControlPanel {
3536
if (ids.includes(current)) sel.value = current;
3637
}
3738

39+
_bindKeyboard() {
40+
document.addEventListener('keydown', async (e) => {
41+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
42+
switch (e.code) {
43+
case 'Space':
44+
e.preventDefault();
45+
await this._post('/api/sim/stop');
46+
break;
47+
case 'KeyR':
48+
await this._post('/api/sim/reset');
49+
break;
50+
case 'Digit1':
51+
await this._post('/api/sim/scenario/single');
52+
break;
53+
case 'Digit2':
54+
await this._post('/api/sim/scenario/swarm-5');
55+
break;
56+
case 'Digit3':
57+
await this._post('/api/sim/scenario/swarm-20');
58+
break;
59+
case 'Digit4':
60+
await this._post('/api/sim/scenario/sar');
61+
break;
62+
}
63+
});
64+
}
65+
3866
_bindButtons() {
3967
this._on('btn-start', () => this._post('/api/sim/start'));
4068
this._on('btn-stop', () => this._post('/api/sim/stop'));

0 commit comments

Comments
 (0)