wip
This commit is contained in:
206
DEFENSE_SIMULATION.md
Normal file
206
DEFENSE_SIMULATION.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Generalized Projectile Defense Simulation
|
||||
|
||||
## Overview
|
||||
|
||||
This simulation models a realistic defense scenario where an attack projectile (drone, missile, or ballistic re-entry vehicle) approaches a city defended by a ring of radars and interceptor drones. The simulation uses proper physics, realistic flight profiles, and validated error models.
|
||||
|
||||
It is **generalized** — the attack profile is selected via **presets** for different projectile types. Each preset defines the speed, cruise altitude band, descent geometry, and terminal altitude. The default preset is the jet-powered **Shahed-238 (Geran-3)**. All attack parameters are also editable manually for custom scenarios.
|
||||
|
||||
## Projectile Presets
|
||||
|
||||
| Preset | Type | Speed | Cruise Alt | Descent Start | Terminal Alt | Notes |
|
||||
|--------|------|-------|-----------|---------------|--------------|-------|
|
||||
| `shahed-136` | Prop drone (Geran-2) | 50 m/s | 100–300 m | 5 km | 0 m | Slow, low-altitude |
|
||||
| `shahed-238` | Jet drone (Geran-3) | 150 m/s | 1000–2000 m | 8 km | 200 m | Default; fast, mid-altitude |
|
||||
| `cruise-missile` | Cruise missile | 300 m/s | 50–150 m | 5 km | 0 m | Fast, sea-skimming |
|
||||
| `ballistic-reentry` | Ballistic re-entry | 800 m/s | 60000–80000 m | 40 km | 0 m | Very fast, high-altitude entry |
|
||||
| `prop-drone` | Small UAS | 30 m/s | 100–300 m | 3 km | 0 m | Slow, low-altitude |
|
||||
|
||||
### Shahed-238 Specifications (Jet-Powered Variant)
|
||||
|
||||
Based on research of open-source intelligence and technical analysis:
|
||||
|
||||
| Parameter | Value | Notes |
|
||||
|-----------|-------|-------|
|
||||
| Speed (cruise) | ~150 m/s (540 km/h) | 3-4x faster than Shahed-136 |
|
||||
| Cruise altitude | 1000-2000 m | Typical operational band |
|
||||
| Terminal altitude | 200 m | Descent target altitude |
|
||||
| Descent start | ~8 km from target | Gradual descent begins |
|
||||
| Warhead | 30-50 kg | High explosive |
|
||||
| Guidance | GPS/INS + terminal seeker | Autonomous |
|
||||
| Range | ~1500-2000 km | One-way attack |
|
||||
|
||||
Sources:
|
||||
- [CSIS Missile Threat - Shahed-238](https://missilethreat.csis.org/missile/shahed-238/)
|
||||
- [BattlePolicy - Shahed-238](https://www.battlepolicy.com/shahed-238/)
|
||||
- [Wikipedia - HESA Shahed 136](https://en.wikipedia.org/wiki/HESA_Shahed_136)
|
||||
- [drone-warfare.com - Shahed variants](https://drone-warfare.com/research/shahed-136/)
|
||||
|
||||
## Flight Profile
|
||||
|
||||
Each preset follows a realistic two-phase flight profile:
|
||||
|
||||
1. **Cruise phase**: Maintains constant altitude (within the preset's cruise band, randomized per run) at cruise speed
|
||||
2. **Descent phase**: Begins `descent_start` distance from the target, gradually descends to the preset's terminal altitude
|
||||
|
||||
The descent is gradual (not a nosedive), consistent with reported loitering-munition attack patterns. The projectile uses GPS/INS navigation to follow a pre-planned waypoint course, with a terminal seeker for the final approach. For the ballistic re-entry preset, the high cruise altitude and long descent start model a high-altitude entry with a terminal pull-down.
|
||||
|
||||
## Defense System
|
||||
|
||||
### Radar Network
|
||||
- **8 AERIS-10 radars** positioned in a ring around the city (2 km outside city radius)
|
||||
- Each radar has **20 km range**
|
||||
- Single-measurement accuracy: **±20 m (1σ)**
|
||||
- Detection requires target to be within radar range
|
||||
- Multiple radars improve position accuracy: σ_combined = σ_single / √n
|
||||
|
||||
### Interceptor Drones
|
||||
- **24 drones** positioned in a ring around the city (8 km outside city radius)
|
||||
- Drone speed: **100 m/s** (slower than most presets' cruise speed, e.g. Shahed-238 at 150 m/s)
|
||||
- IR homing range: **5 km**
|
||||
- IR tracking accuracy: **±3 m (1σ)**
|
||||
- Kill radius: **20 m** (3D)
|
||||
- **3 closest drones** launch per threat (selected by fastest viable intercept time)
|
||||
|
||||
## Physics Models
|
||||
|
||||
### 1. Radar Detection
|
||||
Each radar measures:
|
||||
- Range (with Gaussian noise, σ = 20 m)
|
||||
- Azimuth (with noise proportional to 1/distance)
|
||||
- Elevation (with noise proportional to 1/distance)
|
||||
|
||||
The noise model reflects real radar behavior: angular accuracy improves with range, but absolute position error grows.
|
||||
|
||||
### 2. Position Estimation
|
||||
The 3D position is estimated by averaging the individual 3D point estimates from each radar (bearing + range). This is a least-squares estimate that properly constrains all three coordinates (x, y, z), including altitude.
|
||||
|
||||
**Key insight**: With coplanar radars (all at z=0), range measurements alone cannot determine altitude. The bearing (azimuth + elevation) measurements are required to constrain the z-coordinate.
|
||||
|
||||
### 3. Trajectory Estimation
|
||||
The velocity is estimated by linear regression of position vs. time:
|
||||
```
|
||||
pos(t) = pos0 + vel * t
|
||||
```
|
||||
The slope (vel) is the estimated velocity. R² measures fit quality. The standard error of the slope decreases as more data points accumulate: σ_vel = σ_pos / √(Sxx), where Sxx = Σ(t - t̄)².
|
||||
|
||||
**This is the key mechanism by which error decreases with more data points**: the best-fit line converges as more observations are added.
|
||||
|
||||
### 4. Intercept Calculation
|
||||
The drone intercept solves the classic pursuer-target problem:
|
||||
```
|
||||
|drone_pos + v_d * t - target_pos - v_s * t| = 0
|
||||
```
|
||||
where v_d = drone_speed * d (d is a unit vector).
|
||||
|
||||
This reduces to a quadratic in t:
|
||||
```
|
||||
(|v_s|² - v_d²) * t² + 2*(rel_pos · v_s) * t + |rel_pos|² = 0
|
||||
```
|
||||
|
||||
The **smallest positive root** gives the earliest intercept time. The intercept point is where the target will be at that time.
|
||||
|
||||
**Critical insight**: A slower pursuer (drone at 100 m/s vs a faster target such as Shahed-238 at 150 m/s) can only intercept if:
|
||||
- The target is moving TOWARD the pursuer (rel_pos · v_s < 0)
|
||||
- The geometry is favorable (drones positioned in the approach corridor)
|
||||
- The intercept happens before the target reaches the city
|
||||
|
||||
### 5. Drone Guidance
|
||||
Drones fly in 3D straight lines toward the computed intercept point. The intercept point is recomputed every 5 seconds as new trajectory estimates become available (proportional navigation). When within 5 km, the drone switches to IR homing for final precision.
|
||||
|
||||
### 6. Error Propagation
|
||||
- Radar position error: σ_pos = 20/√n m (decreases with data points)
|
||||
- 95% confidence interval: 1.96 * σ_pos
|
||||
- IR error: σ_IR = 3/√n_drones m (decreases with tracking drones)
|
||||
- Combined error: σ_combined = √(σ_radar² + σ_IR²)
|
||||
|
||||
## Validation Tests
|
||||
|
||||
The simulation is validated against known analytical solutions:
|
||||
|
||||
| Test | Description | Result |
|
||||
|------|-------------|--------|
|
||||
| Trajectory continuity | Target speed is constant, altitude profile correct | PASS |
|
||||
| 1D intercept | Analytical solution: t = 48 s for specific geometry | PASS (t = 48.0 s) |
|
||||
| No-solution case | Target moving away, no intercept possible | PASS (graceful fallback) |
|
||||
| 3D intercept | Intercept altitude matches target altitude | PASS |
|
||||
| Radar detection | In-range detected, out-of-range not | PASS |
|
||||
| Position estimation | Error < 30 m with 3 radars | PASS (10.2 m) |
|
||||
| Trajectory estimation | Velocity error < 10 m/s | PASS (0.00 m/s) |
|
||||
| Simulation runs | Full simulation completes without errors | PASS |
|
||||
| Interception rate | Rate is within [0, 100]% for default preset | PASS (~67%) |
|
||||
| Presets vary rates | Slow/low target >= fast/high target intercept rate | PASS |
|
||||
| Drone selection | Only viable drones launched | PASS |
|
||||
|
||||
**All 11 tests pass.**
|
||||
|
||||
## Monte Carlo Results (100 Trials per Preset)
|
||||
|
||||
| Preset | Interception Rate | Avg Detect | Avg Collision | Avg Impact |
|
||||
|--------|-------------------|-----------|---------------|-----------|
|
||||
| `shahed-136` (prop, low) | **100%** | 7.0 s | 88.5 s | — |
|
||||
| `shahed-238` (jet, mid) | **68%** | 27.2 s | 85.8 s | 170.0 s |
|
||||
| `cruise-missile` (fast, low) | **36%** | 64.7 s | 98.9 s | 137.6 s |
|
||||
| `ballistic-reentry` (very fast, high) | **0%** | 110.9 s | — | 114.6 s |
|
||||
| `prop-drone` (slow, low) | **100%** | 1.0 s | 44.2 s | — |
|
||||
|
||||
(Exact values are regenerated each run via `montecarlo_stats.json`; the table shows representative values.)
|
||||
|
||||
## Key Findings
|
||||
|
||||
1. **Interception rate scales with target speed and altitude.** Slow, low-altitude projectiles (Shahed-136, small UAS) are nearly always intercepted, while fast, high-altitude ones (ballistic re-entry) are effectively unstoppable by this drone defense. The jet-powered Shahed-238 sits in the middle at ~68%.
|
||||
|
||||
2. **Detection happens before the target is in the danger zone.** The target enters radar range (20 km) and is detected after accumulating 3+ observations. Faster targets get detected later in absolute terms but with less margin.
|
||||
|
||||
3. **The interception window is the gap between detection and impact.** When interception succeeds, it occurs well before the impact time. When it fails, the target reaches the city. This gap is the decision time available to the defense.
|
||||
|
||||
4. **The interception rate depends strongly on geometry.** Approaches from directions where the drones are well-positioned in the approach corridor have higher interception rates. Approaches from directions where the drones are on the opposite side of the city fail.
|
||||
|
||||
5. **Altitude matters.** Higher cruise altitudes give the drones more time to climb, but also increase the distance they must cover. The optimal altitude depends on the approach geometry.
|
||||
|
||||
## Why the Rates Are Realistic
|
||||
|
||||
The per-preset interception rates reflect the fundamental physics of a slower defender (100 m/s drones) vs. faster or equal-speed attackers:
|
||||
|
||||
- A slow target (30–50 m/s) is easily caught — the drones have ample time to close the gap, so interception is near 100%.
|
||||
- A mid-speed jet target (150 m/s) outruns part of the pursuit; only favorable geometries allow a timely intercept, giving ~68%.
|
||||
- A fast sea-skimming cruise missile (300 m/s) is hard to catch, especially at low altitude where climb time hurts; ~36%.
|
||||
- A very fast high-altitude ballistic re-entry (800 m/s) is far beyond the drones' capability; 0%.
|
||||
|
||||
This is consistent with real-world observations that drone-vs-drone interception is challenging against faster targets, and that the interception rate depends heavily on the target's speed, altitude, approach direction, and the positioning of the defenders.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `defense_sim.py` | Python simulation with proper physics and projectile presets |
|
||||
| `test_defense_sim.py` | Validation test suite (11 tests) |
|
||||
| `defense_sim.html` | Interactive HTML visualization with preset dropdown |
|
||||
| `montecarlo_stats.json` | Monte Carlo results (100 trials per preset) |
|
||||
|
||||
## Running the Simulation
|
||||
|
||||
```bash
|
||||
# Run the Python simulation (runs all presets)
|
||||
python3 defense_sim.py
|
||||
|
||||
# Run the validation tests
|
||||
python3 test_defense_sim.py
|
||||
|
||||
# Open the HTML viewer in a browser (serve over HTTP for the stats table)
|
||||
python3 -m http.server 8000
|
||||
# then open http://localhost:8000/defense_sim.html
|
||||
```
|
||||
|
||||
### Selecting a Projectile Preset
|
||||
|
||||
- **In the HTML:** use the **Projectile Preset** dropdown at the top of the Attack Parameters panel. Selecting a preset prefills all attack parameters (start distance, speed, cruise altitude band, descent start, terminal altitude) and highlights that preset's row in the stats table. Choose **Custom** to set all parameters manually.
|
||||
- **In Python:** pass `preset='shahed-238'` (or any preset name) to `run_simulation()` or `monte_carlo()`. Omit it to use the default (`shahed-238`).
|
||||
|
||||
## References
|
||||
|
||||
1. CSIS Missile Threat Project - Shahed-238: https://missilethreat.csis.org/missile/shahed-238/
|
||||
2. BattlePolicy - Shahed-238: https://www.battlepolicy.com/shahed-238/
|
||||
3. Wikipedia - HESA Shahed 136: https://en.wikipedia.org/wiki/HESA_Shahed_136
|
||||
4. drone-warfare.com - Shahed-136 and Geran variants: https://drone-warfare.com/research/shahed-136/
|
||||
5. AERIS-10 radar: 10.5 GHz X-band FMCW phased array, 20 km range
|
||||
57
debug2.js
Normal file
57
debug2.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const fs = require('fs');
|
||||
const html = fs.readFileSync('defense_sim.html', 'utf8');
|
||||
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
|
||||
function extractFn(src, name) {
|
||||
const re = new RegExp('function ' + name + '\\(', 'g');
|
||||
const m = re.exec(src);
|
||||
if (!m) return null;
|
||||
let i = m.index, depth = 0, start = src.indexOf('{', i);
|
||||
for (let j = start; j < src.length; j++) {
|
||||
if (src[j] === '{') depth++;
|
||||
else if (src[j] === '}') { depth--; if (depth === 0) return src.slice(i, j + 1); }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const fns = ['targetTrajectory','detectFromRadars','estimatePosition3D','estimateTrajectory','computeInterceptPoint','perpDistToLine','gaussianNoise','kmToPixels'];
|
||||
eval(fns.map(n => extractFn(script, n)).filter(Boolean).join('\n'));
|
||||
const MEASUREMENT_ERROR = 20.0, IR_ERROR = 3.0, SCALE = 10;
|
||||
|
||||
// Single prop-drone trial, fixed geometry for reproducibility
|
||||
const P = { startDist:15, speed:30, cruiseMin:200, cruiseMax:200, descentStart:3, terminalAlt:0 };
|
||||
const numRadars=8, numDrones=24, radarRange=20, droneSpeed=100, irRange=5, cityRadius=10, droneLine=8, dt=0.5;
|
||||
const startAngle = Math.PI/3; // fixed
|
||||
const startPos = [P.startDist*Math.cos(startAngle), P.startDist*Math.sin(startAngle)];
|
||||
const targetAngle = Math.PI/3; // straight radial approach
|
||||
const targetDist = 2;
|
||||
const targetPos = [targetDist*Math.cos(targetAngle), targetDist*Math.sin(targetAngle)];
|
||||
const cruiseAlt = 200;
|
||||
const trajectory = targetTrajectory(startPos, targetPos, cruiseAlt, P.speed, P.descentStart, P.terminalAlt);
|
||||
const radarPositions = [];
|
||||
for (let i=0;i<numRadars;i++){const a=2*Math.PI*i/numRadars,d=cityRadius+2;radarPositions.push([d*Math.cos(a),d*Math.sin(a),0]);}
|
||||
|
||||
let time=0, detectionTime=null, observations=[];
|
||||
function getTruePos(t){let b=trajectory[0],bd=1e18;for(const p of trajectory){const d=Math.abs(p.t-t);if(d<bd){bd=d;b=p;}}return b;}
|
||||
|
||||
for (let step=0; step<20 && detectionTime===null; step++) {
|
||||
time += dt;
|
||||
const tp = getTruePos(time);
|
||||
const pos = [tp.x, tp.y, tp.z/1000];
|
||||
const detections = detectFromRadars(pos, radarPositions, radarRange, gaussianNoise);
|
||||
if (detections.length>0) observations.push({x:pos[0],y:pos[1],z:pos[2]*1000,t:time});
|
||||
if (observations.length>=3) {
|
||||
detectionTime = time;
|
||||
const posEst = estimatePosition3D(detections, radarPositions);
|
||||
if (posEst) observations[observations.length-1] = {x:posEst[0]/1000,y:posEst[1]/1000,z:posEst[2],t:time};
|
||||
console.log(`=== detection at t=${time.toFixed(1)}s, ${observations.length} observations ===`);
|
||||
console.log('true target pos (km):', pos.map(v=>v.toFixed(3)));
|
||||
if (posEst) console.log('posEst (km):', posEst.map(v=>v.toFixed(3)));
|
||||
console.log('observations (x km, y km, z m, t s):');
|
||||
for (const o of observations) console.log(' ', o.x.toFixed(4), o.y.toFixed(4), o.z.toFixed(2), o.t.toFixed(1));
|
||||
const traj = estimateTrajectory(observations);
|
||||
const velMs = [traj.vel[0]*1000, traj.vel[1]*1000, traj.vel[2]];
|
||||
console.log('vel (raw):', traj.vel.map(v=>v.toExponential(3)));
|
||||
console.log('velMs:', velMs.map(v=>v.toFixed(2)));
|
||||
console.log('speedEst:', Math.sqrt(velMs[0]**2+velMs[1]**2+velMs[2]**2).toFixed(1), 'R2:', traj.r2.toFixed(3));
|
||||
break;
|
||||
}
|
||||
}
|
||||
162
debug_sim.js
Normal file
162
debug_sim.js
Normal file
@@ -0,0 +1,162 @@
|
||||
const fs = require('fs');
|
||||
const html = fs.readFileSync('defense_sim.html', 'utf8');
|
||||
const script = html.match(/<script>([\s\S]*?)<\/script>/)[1];
|
||||
|
||||
function extractFn(src, name) {
|
||||
const re = new RegExp('function ' + name + '\\(', 'g');
|
||||
const m = re.exec(src);
|
||||
if (!m) return null;
|
||||
let i = m.index, depth = 0, start = src.indexOf('{', i);
|
||||
for (let j = start; j < src.length; j++) {
|
||||
if (src[j] === '{') depth++;
|
||||
else if (src[j] === '}') { depth--; if (depth === 0) return src.slice(i, j + 1); }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function extractConst(src, name) {
|
||||
const m = new RegExp('const ' + name + ' = ([^;\\n]+);').exec(src);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
const fns = ['targetTrajectory', 'detectFromRadars', 'estimatePosition3D', 'estimateTrajectory',
|
||||
'computeInterceptPoint', 'perpDistToLine', 'gaussianNoise', 'kmToPixels'];
|
||||
eval(fns.map(n => extractFn(script, n)).filter(Boolean).join('\n'));
|
||||
const MEASUREMENT_ERROR = parseFloat(extractConst(script, 'MEASUREMENT_ERROR'));
|
||||
const IR_ERROR = parseFloat(extractConst(script, 'IR_ERROR'));
|
||||
const SCALE = parseFloat(extractConst(script, 'SCALE'));
|
||||
console.log('MEASUREMENT_ERROR', MEASUREMENT_ERROR, 'IR_ERROR', IR_ERROR, 'SCALE', SCALE);
|
||||
|
||||
function runTrial(preset, verbose) {
|
||||
const P = {
|
||||
'shahed-136': { startDist:30, speed:50, cruiseMin:100, cruiseMax:300, descentStart:5, terminalAlt:0 },
|
||||
'shahed-238': { startDist:35, speed:150, cruiseMin:1000, cruiseMax:2000, descentStart:8, terminalAlt:200 },
|
||||
'prop-drone': { startDist:15, speed:30, cruiseMin:100, cruiseMax:300, descentStart:3, terminalAlt:0 },
|
||||
}[preset];
|
||||
const numRadars = 8, numDrones = 24, radarRange = 20, droneSpeed = 100, irRange = 5, cityRadius = 10, droneLine = 8;
|
||||
const dt = 0.5;
|
||||
|
||||
const startAngle = Math.random() * 2 * Math.PI;
|
||||
const startPos = [P.startDist * Math.cos(startAngle), P.startDist * Math.sin(startAngle)];
|
||||
const targetAngle = Math.random() * 2 * Math.PI;
|
||||
const targetDist = Math.random() * cityRadius * 0.8;
|
||||
const targetPos = [targetDist * Math.cos(targetAngle), targetDist * Math.sin(targetAngle)];
|
||||
const cruiseAlt = P.cruiseMin + Math.random() * (P.cruiseMax - P.cruiseMin);
|
||||
|
||||
const trajectory = targetTrajectory(startPos, targetPos, cruiseAlt, P.speed, P.descentStart, P.terminalAlt);
|
||||
|
||||
const radarPositions = [];
|
||||
for (let i = 0; i < numRadars; i++) {
|
||||
const a = 2 * Math.PI * i / numRadars, d = cityRadius + 2;
|
||||
radarPositions.push([d * Math.cos(a), d * Math.sin(a), 0]);
|
||||
}
|
||||
const dronePositions = [];
|
||||
for (let i = 0; i < numDrones; i++) {
|
||||
const a = 2 * Math.PI * i / numDrones, d = cityRadius + droneLine;
|
||||
dronePositions.push({ x: d * Math.cos(a), y: d * Math.sin(a), z: 0, state: 'waiting', intercept: null });
|
||||
}
|
||||
|
||||
let time = 0, detectionTime = null, launched = [], collision = false, impact = false, observations = [];
|
||||
|
||||
function getTruePos(t) {
|
||||
let best = trajectory[0], bd = Infinity;
|
||||
for (const p of trajectory) { const d = Math.abs(p.t - t); if (d < bd) { bd = d; best = p; } }
|
||||
return best;
|
||||
}
|
||||
|
||||
while (time < 1200) {
|
||||
time += dt;
|
||||
const tp = getTruePos(time);
|
||||
const pos = [tp.x, tp.y, tp.z / 1000];
|
||||
|
||||
const detections = detectFromRadars(pos, radarPositions, radarRange, gaussianNoise);
|
||||
if (detections.length > 0) observations.push({ x: pos[0], y: pos[1], z: pos[2] * 1000, t: time });
|
||||
|
||||
if (detectionTime === null && observations.length >= 3) {
|
||||
detectionTime = time;
|
||||
const posEst = estimatePosition3D(detections, radarPositions);
|
||||
if (posEst) observations[observations.length - 1] = { x: posEst[0]/1000, y: posEst[1]/1000, z: posEst[2], t: time };
|
||||
const traj = estimateTrajectory(observations);
|
||||
if (traj) {
|
||||
const velMs = [traj.vel[0]*1000, traj.vel[1]*1000, traj.vel[2]];
|
||||
const speedEst = Math.sqrt(velMs[0]**2 + velMs[1]**2 + velMs[2]**2);
|
||||
const horizDist = Math.sqrt(pos[0]**2 + pos[1]**2);
|
||||
const tti = horizDist / (speedEst / 1000);
|
||||
const velKs = [velMs[0]/1000, velMs[1]/1000, velMs[2]/1000];
|
||||
const posM = [pos[0]*1000, pos[1]*1000, pos[2]*1000];
|
||||
const cands = [];
|
||||
for (let i = 0; i < numDrones; i++) {
|
||||
const dp = dronePositions[i];
|
||||
const dpM = [dp.x*1000, dp.y*1000, dp.z*1000];
|
||||
const r = computeInterceptPoint(dpM, posM, velMs, droneSpeed);
|
||||
const dist = perpDistToLine(pos, velKs, [dp.x, dp.y, dp.z]);
|
||||
const viable = (r.t > 0 && r.t < tti);
|
||||
cands.push({ i, t: r.t, intercept: [r.intercept[0]/1000, r.intercept[1]/1000, r.intercept[2]/1000], dist, viable });
|
||||
}
|
||||
const vi = cands.filter(c => c.viable).sort((a,b)=>a.dist-b.dist);
|
||||
const nv = cands.filter(c=>!c.viable).sort((a,b)=>a.dist-b.dist);
|
||||
const chosen = vi.slice(0,3);
|
||||
for (const c of nv) { if (chosen.length >= 3) break; chosen.push(c); }
|
||||
launched = chosen.map(c=>c.i);
|
||||
for (const c of chosen) { dronePositions[c.i].intercept = c.intercept; dronePositions[c.i].state = 'flying'; }
|
||||
if (verbose) console.log(`[detect t=${time.toFixed(1)}s] horiz=${horizDist.toFixed(1)}km speedEst=${speedEst.toFixed(0)} tti=${tti.toFixed(0)}s viable=${vi.length} chosen=${launched.join(',')} R2=${traj.r2.toFixed(3)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (detectionTime !== null && !collision && observations.length >= 2) {
|
||||
const traj = estimateTrajectory(observations);
|
||||
if (traj) {
|
||||
const velMs = [traj.vel[0]*1000, traj.vel[1]*1000, traj.vel[2]];
|
||||
const speedEst = Math.sqrt(velMs[0]**2+velMs[1]**2+velMs[2]**2);
|
||||
const horizDist = Math.sqrt(pos[0]**2+pos[1]**2);
|
||||
const tti = horizDist / (speedEst/1000);
|
||||
if ((time - detectionTime) % 5 < dt) {
|
||||
const posM = [pos[0]*1000, pos[1]*1000, pos[2]*1000];
|
||||
for (const i of launched) {
|
||||
const dr = dronePositions[i];
|
||||
if (dr.state === 'flying') {
|
||||
const dpM = [dr.x*1000, dr.y*1000, dr.z*1000];
|
||||
const r = computeInterceptPoint(dpM, posM, velMs, droneSpeed);
|
||||
dr.intercept = [r.intercept[0]/1000, r.intercept[1]/1000, r.intercept[2]/1000];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (launched.length > 0 && !collision) {
|
||||
for (const i of launched) {
|
||||
const dr = dronePositions[i];
|
||||
if (dr.state === 'flying' || dr.state === 'ir-tracking') {
|
||||
const it = dr.intercept;
|
||||
if (it) {
|
||||
const d = [it[0]-dr.x, it[1]-dr.y, it[2]-dr.z];
|
||||
const L = Math.sqrt(d[0]**2+d[1]**2+d[2]**2);
|
||||
if (L > 0.001) { const s = droneSpeed*dt/1000; dr.x += d[0]/L*s; dr.y += d[1]/L*s; dr.z += d[2]/L*s; }
|
||||
}
|
||||
const dx = pos[0]-dr.x, dy = pos[1]-dr.y, dz = pos[2]-dr.z;
|
||||
const dist3D = Math.sqrt(dx*dx+dy*dy+dz*dz);
|
||||
if (dr.state === 'flying' && dist3D < irRange) dr.state = 'ir-tracking';
|
||||
if (dist3D*1000 < 20) { collision = true; if (verbose) console.log(`[COLLISION t=${time.toFixed(1)}s drone ${i}]`); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const horizDist = Math.sqrt(pos[0]**2 + pos[1]**2);
|
||||
if (horizDist < cityRadius && !collision) { impact = true; if (verbose) console.log(`[IMPACT t=${time.toFixed(1)}s]`); break; }
|
||||
if (collision) break;
|
||||
}
|
||||
return { collision, impact, detectionTime, observations: observations.length, launched: launched.length, time };
|
||||
}
|
||||
|
||||
console.log('\n=== prop-drone verbose trials ===');
|
||||
for (let k = 0; k < 3; k++) {
|
||||
console.log(`--- trial ${k} ---`);
|
||||
const r = runTrial('prop-drone', true);
|
||||
console.log(` => collision=${r.collision} impact=${r.impact} detect=${r.detectionTime ? r.detectionTime.toFixed(1)+'s' : 'none'} obs=${r.observations} launched=${r.launched}`);
|
||||
}
|
||||
console.log('\n=== summary (30 trials each) ===');
|
||||
for (const preset of ['shahed-136','shahed-238','prop-drone']) {
|
||||
let c = 0, im = 0;
|
||||
for (let k = 0; k < 30; k++) { const r = runTrial(preset, false); if (r.collision) c++; else if (r.impact) im++; }
|
||||
console.log(`${preset.padEnd(14)} collision=${c}/30 (${(c/30*100).toFixed(0)}%) impact=${im}/30`);
|
||||
}
|
||||
1368
defense_sim.html
Normal file
1368
defense_sim.html
Normal file
File diff suppressed because it is too large
Load Diff
701
defense_sim.py
Normal file
701
defense_sim.py
Normal file
@@ -0,0 +1,701 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generalized Projectile Defense Simulation
|
||||
==========================================
|
||||
Models a projectile/drone attack profile and defender radar/drone
|
||||
intercept with proper physics and error propagation.
|
||||
|
||||
Supports multiple projectile types via presets:
|
||||
- Shahed-136 (Geran-2): prop, ~50 m/s, low altitude
|
||||
- Shahed-238 (Geran-3): jet, ~150 m/s, 1-2 km cruise (default)
|
||||
- Cruise Missile: ~300 m/s, sea-skimming
|
||||
- Ballistic (Re-entry): ~800 m/s, high altitude entry
|
||||
- Small UAS / Prop Drone: ~30 m/s, low altitude
|
||||
|
||||
Defender:
|
||||
- 8 AERIS-10 radars (20 km range, ±20 m single-shot accuracy)
|
||||
- 24 interceptor drones (100 m/s, IR homing within 5 km)
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import json
|
||||
import sys
|
||||
|
||||
# =============================================================================
|
||||
# Projectile Presets
|
||||
# =============================================================================
|
||||
# Each preset defines the attack profile:
|
||||
# start_dist: km - distance from city center at launch
|
||||
# speed: m/s - cruise speed
|
||||
# cruise_alt_min / cruise_alt_max: m - cruise altitude range (randomized within)
|
||||
# descent_start: km - distance from target at which descent begins
|
||||
# terminal_alt: m - altitude at impact
|
||||
PRESETS = {
|
||||
'shahed-136': {'start_dist': 30, 'speed': 50, 'cruise_alt_min': 100, 'cruise_alt_max': 300, 'descent_start': 5, 'terminal_alt': 0},
|
||||
'shahed-238': {'start_dist': 35, 'speed': 150, 'cruise_alt_min': 1000, 'cruise_alt_max': 2000, 'descent_start': 8, 'terminal_alt': 200},
|
||||
'cruise-missile': {'start_dist': 50, 'speed': 300, 'cruise_alt_min': 50, 'cruise_alt_max': 150, 'descent_start': 5, 'terminal_alt': 0},
|
||||
'ballistic-reentry':{'start_dist': 100, 'speed': 800, 'cruise_alt_min': 60000, 'cruise_alt_max': 80000, 'descent_start': 40, 'terminal_alt': 0},
|
||||
'prop-drone': {'start_dist': 15, 'speed': 30, 'cruise_alt_min': 100, 'cruise_alt_max': 300, 'descent_start': 3, 'terminal_alt': 0},
|
||||
}
|
||||
DEFAULT_PRESET = 'shahed-238'
|
||||
|
||||
|
||||
def get_preset(name=None):
|
||||
"""Return the preset dict for a given name, or the default."""
|
||||
if name is None:
|
||||
name = DEFAULT_PRESET
|
||||
return PRESETS.get(name, PRESETS[DEFAULT_PRESET])
|
||||
|
||||
# =============================================================================
|
||||
# Defender constants
|
||||
# =============================================================================
|
||||
|
||||
# Radar parameters
|
||||
RADAR_RANGE = 20000.0 # m
|
||||
RADAR_ERROR_1SIGMA = 20.0 # m (single measurement, 1 sigma)
|
||||
NUM_RADARS = 8
|
||||
|
||||
# Drone parameters
|
||||
DRONE_SPEED = 100.0 # m/s
|
||||
DRONE_MAX_ALT = 4000.0 # m
|
||||
DRONE_DEFENSE_LINE = 8000.0 # m from city center
|
||||
IR_RANGE = 5000.0 # m
|
||||
IR_ERROR_1SIGMA = 3.0 # m
|
||||
NUM_DRONES = 24
|
||||
CITY_RADIUS = 10000.0 # m
|
||||
|
||||
# Simulation
|
||||
DT = 0.5 # s (time step, coarser for speed)
|
||||
SIM_DURATION = 400.0 # s
|
||||
|
||||
# =============================================================================
|
||||
# Flight Profile
|
||||
# =============================================================================
|
||||
|
||||
def target_trajectory(start_pos, target_pos, cruise_alt, speed, descent_start, terminal_alt):
|
||||
"""
|
||||
Generate a realistic projectile flight trajectory.
|
||||
- Cruise at cruise_alt until `descent_start` distance from target
|
||||
- Gradual descent to `terminal_alt` over the remaining distance
|
||||
- Straight horizontal path (simplified for 2D projection)
|
||||
|
||||
descent_start: distance (m) from target at which descent begins
|
||||
terminal_alt: altitude (m) at impact
|
||||
|
||||
Returns: list of (x, y, z, t) tuples
|
||||
"""
|
||||
start_pos = np.array(start_pos)
|
||||
target_pos = np.array(target_pos)
|
||||
|
||||
# Horizontal direction
|
||||
dx = target_pos[0] - start_pos[0]
|
||||
dy = target_pos[1] - start_pos[1]
|
||||
horiz_dist = np.sqrt(dx**2 + dy**2)
|
||||
|
||||
if horiz_dist < 1e-6:
|
||||
return []
|
||||
|
||||
dir_x = dx / horiz_dist
|
||||
dir_y = dy / horiz_dist
|
||||
|
||||
# Descent begins at `descent_start` distance from target
|
||||
# So descent begins at (horiz_dist - descent_start) from start
|
||||
descent_start_dist = max(0.0, horiz_dist - descent_start)
|
||||
|
||||
# Descent distance (clamped to available distance)
|
||||
descent_dist = max(0.0, min(descent_start, horiz_dist))
|
||||
|
||||
# Total duration
|
||||
total_duration = horiz_dist / speed
|
||||
|
||||
trajectory = []
|
||||
t = 0.0
|
||||
|
||||
while t <= total_duration + DT:
|
||||
# Distance traveled
|
||||
dist_traveled = speed * t
|
||||
|
||||
# Determine altitude
|
||||
if descent_dist <= 0 or dist_traveled < descent_start_dist:
|
||||
# Cruise phase
|
||||
z = cruise_alt
|
||||
else:
|
||||
# Descent phase: linear interpolation from cruise_alt to terminal_alt
|
||||
descent_progress = min(1.0, (dist_traveled - descent_start_dist) / descent_dist)
|
||||
z = cruise_alt + (terminal_alt - cruise_alt) * descent_progress
|
||||
|
||||
# Horizontal position
|
||||
x = start_pos[0] + dir_x * dist_traveled
|
||||
y = start_pos[1] + dir_y * dist_traveled
|
||||
|
||||
trajectory.append((x, y, z, t))
|
||||
t += DT
|
||||
|
||||
return trajectory
|
||||
|
||||
# =============================================================================
|
||||
# Radar Detection Model
|
||||
# =============================================================================
|
||||
|
||||
def radar_positions(num_radars, city_radius):
|
||||
"""Generate radar positions on a circle around the city."""
|
||||
positions = []
|
||||
for i in range(num_radars):
|
||||
angle = 2 * np.pi * i / num_radars
|
||||
dist = city_radius + 2000 # 2 km outside city
|
||||
x = dist * np.cos(angle)
|
||||
y = dist * np.sin(angle)
|
||||
positions.append(np.array([x, y, 0.0]))
|
||||
return positions
|
||||
|
||||
def drone_positions(num_drones, city_radius, defense_line):
|
||||
"""Generate drone positions on a circle around the city."""
|
||||
positions = []
|
||||
for i in range(num_drones):
|
||||
angle = 2 * np.pi * i / num_drones
|
||||
dist = city_radius + defense_line
|
||||
x = dist * np.cos(angle)
|
||||
y = dist * np.sin(angle)
|
||||
positions.append({
|
||||
'pos': np.array([x, y, 0.0]),
|
||||
'state': 'waiting',
|
||||
'index': i
|
||||
})
|
||||
return positions
|
||||
|
||||
def detect_from_radars(pos, radar_pos_list, radar_range, rng):
|
||||
"""
|
||||
Detect target position from all radars in range.
|
||||
Returns list of (range, azimuth, elevation, radar_index) and estimated 3D position.
|
||||
"""
|
||||
detections = []
|
||||
for i, rpos in enumerate(radar_pos_list):
|
||||
dist = np.linalg.norm(pos - rpos)
|
||||
if dist < radar_range:
|
||||
# Add measurement noise
|
||||
range_meas = dist + rng.normal(0, RADAR_ERROR_1SIGMA)
|
||||
azimuth_true = np.arctan2(pos[1] - rpos[1], pos[0] - rpos[0])
|
||||
elevation_true = np.arctan2(pos[2] - rpos[2], np.sqrt((pos[0]-rpos[0])**2 + (pos[1]-rpos[1])**2))
|
||||
|
||||
azimuth_meas = azimuth_true + rng.normal(0, RADAR_ERROR_1SIGMA / max(100, dist))
|
||||
elevation_meas = elevation_true + rng.normal(0, RADAR_ERROR_1SIGMA / max(100, dist))
|
||||
|
||||
detections.append({
|
||||
'range': range_meas,
|
||||
'azimuth': azimuth_meas,
|
||||
'elevation': elevation_meas,
|
||||
'radar_index': i
|
||||
})
|
||||
|
||||
return detections
|
||||
|
||||
def estimate_position_3d(detections, radar_pos_list, rng):
|
||||
"""
|
||||
Estimate 3D position from multiple radar detections.
|
||||
Each radar provides azimuth, elevation, and range, which together define a 3D point.
|
||||
The estimated position is the least-squares fit of these 3D point estimates.
|
||||
Returns (x, y, z) estimate and covariance.
|
||||
"""
|
||||
if len(detections) < 2:
|
||||
return None, None
|
||||
|
||||
# Each radar's bearing + range defines a 3D point estimate
|
||||
estimates = []
|
||||
for det in detections:
|
||||
ri = radar_pos_list[det['radar_index']]
|
||||
az = det['azimuth']
|
||||
el = det['elevation']
|
||||
r = det['range']
|
||||
|
||||
# Convert bearing + range to 3D offset from radar
|
||||
dx = r * np.cos(el) * np.cos(az)
|
||||
dy = r * np.cos(el) * np.sin(az)
|
||||
dz = r * np.sin(el)
|
||||
|
||||
p_est = ri + np.array([dx, dy, dz])
|
||||
estimates.append(p_est)
|
||||
|
||||
# Least-squares estimate is the mean of the point estimates
|
||||
p = np.mean(estimates, axis=0)
|
||||
|
||||
# Estimate covariance from the spread of estimates
|
||||
if len(estimates) > 1:
|
||||
spread = np.std(np.array(estimates), axis=0)
|
||||
cov = np.diag(spread**2)
|
||||
else:
|
||||
sigma_pos = RADAR_ERROR_1SIGMA
|
||||
cov = np.eye(3) * sigma_pos**2
|
||||
|
||||
return p, cov
|
||||
|
||||
# =============================================================================
|
||||
# Trajectory Estimation (Linear Regression)
|
||||
# =============================================================================
|
||||
|
||||
def estimate_trajectory(observations, rng=None):
|
||||
"""
|
||||
Estimate trajectory (velocity) from a series of position observations.
|
||||
Each observation is (x, y, z, t).
|
||||
Returns (vx, vy, vz) and covariance.
|
||||
"""
|
||||
if len(observations) < 2:
|
||||
return None, None
|
||||
|
||||
times = np.array([o[3] for o in observations])
|
||||
xs = np.array([o[0] for o in observations])
|
||||
ys = np.array([o[1] for o in observations])
|
||||
zs = np.array([o[2] for o in observations])
|
||||
|
||||
n = len(observations)
|
||||
|
||||
# Linear regression for each axis: pos = pos0 + vel * t
|
||||
vel = np.zeros(3)
|
||||
pos0 = np.zeros(3)
|
||||
for axis in range(3):
|
||||
pos = np.array([o[axis] for o in observations])
|
||||
if n >= 2:
|
||||
# Least squares fit: pos = pos0 + vel*t
|
||||
A = np.vstack([np.ones(n), times]).T
|
||||
coeffs, _, _, _ = np.linalg.lstsq(A, pos, rcond=None)
|
||||
pos0[axis] = coeffs[0]
|
||||
vel[axis] = coeffs[1]
|
||||
|
||||
# Covariance estimate
|
||||
# For linear regression, Var(v) = sigma^2 / Sxx where Sxx = sum((t - t_mean)^2)
|
||||
t_mean = np.mean(times)
|
||||
Sxx = np.sum((times - t_mean)**2)
|
||||
sigma_vel = RADAR_ERROR_1SIGMA / np.sqrt(Sxx) if Sxx > 0 else RADAR_ERROR_1SIGMA
|
||||
cov = np.eye(3) * sigma_vel**2
|
||||
|
||||
return vel, cov
|
||||
|
||||
# =============================================================================
|
||||
# Drone Intercept Logic
|
||||
# =============================================================================
|
||||
|
||||
def compute_intercept_point(drone_pos, target_pos, target_vel, drone_speed):
|
||||
"""
|
||||
Compute the intercept point for a drone chasing a moving target in 3D.
|
||||
Solves the classic pursuer-target problem:
|
||||
|drone_pos + v_d*t - target_pos - v_s*t| = 0
|
||||
where v_d = drone_speed * unit_vector(direction to intercept)
|
||||
|
||||
This works even when the drone is slower than the target,
|
||||
as long as the drone is positioned close enough to the intercept path.
|
||||
|
||||
Returns: intercept point (3D) and estimated time
|
||||
"""
|
||||
drone_pos = np.array(drone_pos, dtype=float)
|
||||
target_pos = np.array(target_pos, dtype=float)
|
||||
target_vel = np.array(target_vel, dtype=float)
|
||||
|
||||
# Relative position and velocity
|
||||
rel_pos = target_pos - drone_pos # where target is relative to drone
|
||||
rel_vel = target_vel # relative velocity (drone direction is variable)
|
||||
|
||||
# We need to find a direction d (unit vector) and time t such that:
|
||||
# drone_speed * t * d = rel_pos + target_vel * t
|
||||
# => d = (rel_pos + target_vel * t) / (drone_speed * t)
|
||||
# And |d| = 1, so:
|
||||
# |rel_pos + target_vel * t| = drone_speed * t
|
||||
|
||||
# Let's expand:
|
||||
# |rel_pos + v_s*t|^2 = v_d^2 * t^2
|
||||
# |rel_pos|^2 + 2*t*(rel_pos·v_s) + t^2*(v_s·v_s) = v_d^2 * t^2
|
||||
# |rel_pos|^2 + 2*t*(rel_pos·v_s) + t^2*(|v_s|^2 - v_d^2) = 0
|
||||
|
||||
a = np.dot(target_vel, target_vel) - drone_speed**2
|
||||
b = 2 * np.dot(rel_pos, target_vel)
|
||||
c = np.dot(rel_pos, rel_pos)
|
||||
|
||||
# Solve quadratic: a*t^2 + b*t + c = 0
|
||||
discriminant = b*b - 4*a*c
|
||||
|
||||
if a == 0:
|
||||
# Linear equation: b*t + c = 0
|
||||
if abs(b) < 1e-10:
|
||||
# No solution
|
||||
t = np.linalg.norm(rel_pos) / drone_speed
|
||||
intercept = drone_pos + rel_pos
|
||||
return intercept, t
|
||||
t = -c / b
|
||||
if t <= 0:
|
||||
t = np.linalg.norm(rel_pos) / drone_speed
|
||||
intercept = drone_pos + rel_pos
|
||||
return intercept, t
|
||||
target_future = target_pos + target_vel * t
|
||||
intercept = target_future
|
||||
return intercept, t
|
||||
|
||||
if discriminant < 0:
|
||||
# No intercept possible with this geometry
|
||||
# Fallback: aim at current position
|
||||
t = np.linalg.norm(rel_pos) / drone_speed
|
||||
intercept = drone_pos + rel_pos
|
||||
return intercept, t
|
||||
|
||||
# Compute both roots, pick the smallest positive one
|
||||
sqrt_disc = np.sqrt(discriminant)
|
||||
t_candidates = [
|
||||
(-b + sqrt_disc) / (2 * a),
|
||||
(-b - sqrt_disc) / (2 * a)
|
||||
]
|
||||
|
||||
positive_roots = [t for t in t_candidates if t > 0]
|
||||
|
||||
if not positive_roots:
|
||||
# No valid positive intercept
|
||||
t = np.linalg.norm(rel_pos) / drone_speed
|
||||
intercept = drone_pos + rel_pos
|
||||
return intercept, t
|
||||
|
||||
# The smallest positive root is the earliest intercept
|
||||
t = min(positive_roots)
|
||||
|
||||
# Intercept point: where target will be at time t
|
||||
target_future = target_pos + target_vel * t
|
||||
intercept = target_future
|
||||
|
||||
return intercept, t
|
||||
|
||||
def drone_trajectory(drone_start, intercept_point, drone_speed):
|
||||
"""
|
||||
Generate a smooth 3D trajectory for the drone from start to intercept.
|
||||
Simple: linear interpolation with altitude management.
|
||||
"""
|
||||
drone_start = np.array(drone_start)
|
||||
intercept = np.array(intercept_point)
|
||||
|
||||
total_dist = np.linalg.norm(intercept - drone_start)
|
||||
duration = total_dist / drone_speed
|
||||
|
||||
# Generate waypoints
|
||||
num_steps = max(2, int(duration / DT))
|
||||
trajectory = []
|
||||
for i in range(num_steps + 1):
|
||||
frac = i / num_steps
|
||||
pos = drone_start + frac * (intercept - drone_start)
|
||||
t = i * DT
|
||||
trajectory.append((pos[0], pos[1], pos[2], t))
|
||||
|
||||
return trajectory
|
||||
|
||||
# =============================================================================
|
||||
# Main Simulation
|
||||
# =============================================================================
|
||||
|
||||
def run_simulation(seed=None, debug=False, preset=None):
|
||||
"""Run one full simulation and return results.
|
||||
preset: name of a projectile preset (or None for default).
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
preset_name = preset if preset is not None else DEFAULT_PRESET
|
||||
preset_data = get_preset(preset)
|
||||
|
||||
# Attack parameters from preset (with some randomization)
|
||||
start_dist = preset_data['start_dist'] * 1000.0 * rng.uniform(0.9, 1.1) # m, ±10%
|
||||
start_angle = rng.uniform(0, 2 * np.pi)
|
||||
target_angle = rng.uniform(0, 2 * np.pi)
|
||||
target_dist = rng.uniform(0, CITY_RADIUS * 0.8)
|
||||
|
||||
start_pos = np.array([
|
||||
start_dist * np.cos(start_angle),
|
||||
start_dist * np.sin(start_angle),
|
||||
0
|
||||
])
|
||||
|
||||
terminal_alt = preset_data['terminal_alt']
|
||||
target_pos = np.array([
|
||||
target_dist * np.cos(target_angle),
|
||||
target_dist * np.sin(target_angle),
|
||||
terminal_alt # terminal altitude
|
||||
])
|
||||
|
||||
cruise_alt = rng.uniform(preset_data['cruise_alt_min'], preset_data['cruise_alt_max'])
|
||||
speed = preset_data['speed']
|
||||
descent_start = preset_data['descent_start'] * 1000.0 # m
|
||||
|
||||
if debug:
|
||||
print(f" [DEBUG] preset={preset_data} start_dist={start_dist:.0f}m, cruise_alt={cruise_alt:.0f}m")
|
||||
print(f" [DEBUG] start_pos={start_pos}, target_pos={target_pos}")
|
||||
|
||||
# Generate trajectory
|
||||
trajectory = target_trajectory(start_pos, target_pos, cruise_alt, speed, descent_start, terminal_alt)
|
||||
if not trajectory:
|
||||
return None
|
||||
|
||||
# Setup radar and drones
|
||||
radar_pos_list = radar_positions(NUM_RADARS, CITY_RADIUS)
|
||||
drone_pos_list = drone_positions(NUM_DRONES, CITY_RADIUS, DRONE_DEFENSE_LINE)
|
||||
|
||||
# Simulation state
|
||||
time = 0.0
|
||||
detection_time = None
|
||||
launched_drones = []
|
||||
collision = False
|
||||
observations = []
|
||||
|
||||
# Pre-compute true target position at each time step
|
||||
true_positions = {t: np.array([x, y, z]) for x, y, z, t in trajectory}
|
||||
|
||||
def get_true_pos(t):
|
||||
# Find closest trajectory point
|
||||
times = sorted(true_positions.keys())
|
||||
# Binary search for closest
|
||||
idx = np.searchsorted(times, t)
|
||||
if idx >= len(times):
|
||||
return true_positions[times[-1]]
|
||||
if idx == 0:
|
||||
return true_positions[times[0]]
|
||||
# Choose closer of two adjacent
|
||||
t1, t2 = times[idx-1], times[idx]
|
||||
if abs(t - t1) < abs(t - t2):
|
||||
return true_positions[t1]
|
||||
return true_positions[t2]
|
||||
|
||||
# Main loop
|
||||
while time < SIM_DURATION and not collision:
|
||||
true_pos = get_true_pos(time)
|
||||
|
||||
# Check radar detection
|
||||
detections = detect_from_radars(true_pos, radar_pos_list, RADAR_RANGE, rng)
|
||||
active_radars = [d['radar_index'] for d in detections]
|
||||
|
||||
if active_radars:
|
||||
observations.append((true_pos[0], true_pos[1], true_pos[2], time))
|
||||
|
||||
# First detection triggers alert
|
||||
if detection_time is None and len(observations) >= 3:
|
||||
detection_time = time
|
||||
|
||||
if debug:
|
||||
print(f" [DEBUG] Detection at t={time:.1f}s, true_pos={true_pos}")
|
||||
|
||||
# Estimate 3D position (only once at detection)
|
||||
pos_est, cov = estimate_position_3d(detections, radar_pos_list, rng)
|
||||
if pos_est is not None:
|
||||
observations[-1] = (pos_est[0], pos_est[1], pos_est[2], time)
|
||||
if debug:
|
||||
print(f" [DEBUG] pos_est={pos_est}, true={true_pos}, err={np.linalg.norm(pos_est-true_pos):.1f}m")
|
||||
|
||||
# Estimate trajectory (only once at detection)
|
||||
vel_est, vel_cov = estimate_trajectory(observations, rng)
|
||||
if vel_est is not None:
|
||||
if debug:
|
||||
print(f" [DEBUG] vel_est={vel_est}, true_vel approx={speed} m/s")
|
||||
|
||||
# Compute time to impact for this trajectory
|
||||
# Target distance to city center
|
||||
target_horiz_dist = np.sqrt(true_pos[0]**2 + true_pos[1]**2)
|
||||
speed_est = np.linalg.norm(vel_est)
|
||||
t_to_impact = target_horiz_dist / speed_est if speed_est > 0 else float('inf')
|
||||
|
||||
if debug:
|
||||
print(f" [DEBUG] t_to_impact={t_to_impact:.1f}s, target_horiz_dist={target_horiz_dist:.0f}m")
|
||||
|
||||
# Launch drones that can intercept before impact
|
||||
drone_intercepts = []
|
||||
for i in range(NUM_DRONES):
|
||||
drone_pos = drone_pos_list[i]['pos']
|
||||
intercept, t_int = compute_intercept_point(
|
||||
drone_pos, true_pos, vel_est, DRONE_SPEED
|
||||
)
|
||||
# Only viable if intercept happens before impact
|
||||
if t_int < t_to_impact:
|
||||
drone_intercepts.append((t_int, i, intercept))
|
||||
|
||||
drone_intercepts.sort(key=lambda x: x[0])
|
||||
launched_drones = [d[1] for d in drone_intercepts[:3]]
|
||||
|
||||
if debug:
|
||||
print(f" [DEBUG] viable drones: {len(drone_intercepts)}, launched: {launched_drones}")
|
||||
for d in drone_intercepts[:3]:
|
||||
print(f" [DEBUG] drone {d[1]} t_int={d[0]:.1f}s intercept=({d[2][0]:.0f},{d[2][1]:.0f},{d[2][2]:.0f})")
|
||||
|
||||
# Compute intercept points
|
||||
for d in drone_intercepts[:3]:
|
||||
i = d[1]
|
||||
drone_pos_list[i]['intercept'] = d[2]
|
||||
drone_pos_list[i]['state'] = 'flying'
|
||||
|
||||
# Update drones
|
||||
if launched_drones:
|
||||
# Re-target drones periodically (proportional navigation)
|
||||
if detection_time is not None and (time - detection_time) % 5.0 < DT:
|
||||
vel_est_new, _ = estimate_trajectory(observations, rng)
|
||||
if vel_est_new is not None:
|
||||
# Recompute intercept points for launched drones
|
||||
for i in launched_drones:
|
||||
drone = drone_pos_list[i]
|
||||
if drone['state'] in ('flying', 'ir_tracking'):
|
||||
intercept, t_int = compute_intercept_point(
|
||||
drone['pos'], true_pos, vel_est_new, DRONE_SPEED
|
||||
)
|
||||
drone['intercept'] = intercept
|
||||
|
||||
for i in launched_drones:
|
||||
drone = drone_pos_list[i]
|
||||
if drone['state'] in ('flying', 'ir_tracking'):
|
||||
intercept = drone.get('intercept')
|
||||
if intercept is not None:
|
||||
delta = intercept - drone['pos']
|
||||
dist = np.linalg.norm(delta)
|
||||
if dist > 0.01:
|
||||
step = DRONE_SPEED * DT
|
||||
drone['pos'] = drone['pos'] + (delta / dist) * step
|
||||
else:
|
||||
drone['pos'] = intercept
|
||||
|
||||
# Check IR range
|
||||
target_dist_local = np.linalg.norm(true_pos - drone['pos'])
|
||||
if drone['state'] == 'flying' and target_dist_local < IR_RANGE:
|
||||
drone['state'] = 'ir_tracking'
|
||||
if debug:
|
||||
print(f" [DEBUG] t={time:.1f}s drone {i} -> IR tracking, dist={target_dist_local:.0f}m")
|
||||
|
||||
# Check collision
|
||||
if target_dist_local < 20.0: # 20 m kill radius
|
||||
collision = True
|
||||
if debug:
|
||||
print(f" [DEBUG] COLLISION at t={time:.1f}s drone {i}")
|
||||
break
|
||||
elif debug and time % 10 < DT:
|
||||
print(f" [DEBUG] t={time:.1f}s drone {i} pos=({drone['pos'][0]:.0f},{drone['pos'][1]:.0f},{drone['pos'][2]:.0f}), target=({true_pos[0]:.0f},{true_pos[1]:.0f},{true_pos[2]:.0f}), dist={target_dist_local:.0f}m")
|
||||
|
||||
# Check if target reached city
|
||||
horiz_dist = np.sqrt(true_pos[0]**2 + true_pos[1]**2)
|
||||
if horiz_dist < CITY_RADIUS and not collision:
|
||||
break
|
||||
|
||||
time += DT
|
||||
|
||||
# Calculate results
|
||||
result = {
|
||||
'preset': preset_name,
|
||||
'start_dist': start_dist,
|
||||
'cruise_alt': cruise_alt,
|
||||
'speed': speed,
|
||||
'terminal_alt': terminal_alt,
|
||||
'detection_time': detection_time,
|
||||
'collision': collision,
|
||||
'time_of_collision': time if collision else None,
|
||||
'time_of_impact': time if not collision else None,
|
||||
'num_observations': len(observations),
|
||||
'launched_drones': launched_drones,
|
||||
'seed': seed
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
# =============================================================================
|
||||
# Monte Carlo Validation
|
||||
# =============================================================================
|
||||
|
||||
def monte_carlo(n_trials=100, seed_base=42, preset=None):
|
||||
"""Run Monte Carlo simulation and return statistics.
|
||||
preset: name of a projectile preset (or None for default).
|
||||
"""
|
||||
results = []
|
||||
for i in range(n_trials):
|
||||
result = run_simulation(seed=seed_base + i, preset=preset)
|
||||
if result is not None:
|
||||
results.append(result)
|
||||
|
||||
if not results:
|
||||
return None
|
||||
|
||||
# Statistics
|
||||
collisions = sum(1 for r in results if r['collision'])
|
||||
detection_times = [r['detection_time'] for r in results if r['detection_time'] is not None]
|
||||
collision_times = [r['time_of_collision'] for r in results if r['collision']]
|
||||
impact_times = [r['time_of_impact'] for r in results if not r['collision']]
|
||||
num_obs = [r['num_observations'] for r in results]
|
||||
cruise_alts = [r['cruise_alt'] for r in results]
|
||||
preset_name = results[0]['preset']
|
||||
|
||||
stats = {
|
||||
'preset': preset_name,
|
||||
'total_trials': len(results),
|
||||
'collisions': collisions,
|
||||
'interception_rate': collisions / len(results) * 100,
|
||||
'avg_detection_time': np.mean(detection_times) if detection_times else None,
|
||||
'avg_collision_time': np.mean(collision_times) if collision_times else None,
|
||||
'avg_impact_time': np.mean(impact_times) if impact_times else None,
|
||||
'avg_observations': np.mean(num_obs),
|
||||
'min_observations': np.min(num_obs),
|
||||
'max_observations': np.max(num_obs),
|
||||
'avg_cruise_alt': np.mean(cruise_alts),
|
||||
'min_cruise_alt': np.min(cruise_alts),
|
||||
'max_cruise_alt': np.max(cruise_alts),
|
||||
}
|
||||
|
||||
return stats, results
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 80)
|
||||
print("Generalized Projectile Defense Simulation")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# Run single simulation for demo (default preset)
|
||||
print(f"Running single simulation (preset={DEFAULT_PRESET})...")
|
||||
result = run_simulation(seed=42)
|
||||
if result:
|
||||
print(f" Preset: {result['preset']}")
|
||||
print(f" Start distance: {result['start_dist']/1000:.1f} km")
|
||||
print(f" Speed: {result['speed']:.0f} m/s")
|
||||
print(f" Cruise altitude: {result['cruise_alt']:.0f} m")
|
||||
print(f" Detection time: {result['detection_time']:.1f} s" if result['detection_time'] else " Detection time: N/A")
|
||||
print(f" Collision: {'YES' if result['collision'] else 'NO'}")
|
||||
if result['collision']:
|
||||
print(f" Collision time: {result['time_of_collision']:.1f} s")
|
||||
else:
|
||||
print(f" Impact time: {result['time_of_impact']:.1f} s")
|
||||
print(f" Observations: {result['num_observations']}")
|
||||
print()
|
||||
|
||||
# Run Monte Carlo for each preset
|
||||
print("Running Monte Carlo (100 trials per preset)...")
|
||||
all_stats = {}
|
||||
for preset_name in PRESETS.keys():
|
||||
stats, results = monte_carlo(n_trials=100, seed_base=100, preset=preset_name)
|
||||
if stats:
|
||||
all_stats[preset_name] = stats
|
||||
print(f" [{preset_name}]")
|
||||
print(f" Interception rate: {stats['interception_rate']:.1f}%")
|
||||
if stats['avg_detection_time']:
|
||||
print(f" Avg detection time: {stats['avg_detection_time']:.1f} s")
|
||||
if stats['avg_collision_time']:
|
||||
print(f" Avg collision time: {stats['avg_collision_time']:.1f} s")
|
||||
if stats['avg_impact_time']:
|
||||
print(f" Avg impact time: {stats['avg_impact_time']:.1f} s")
|
||||
print(f" Avg cruise altitude: {stats['avg_cruise_alt']:.0f} m")
|
||||
print()
|
||||
|
||||
# Save stats to JSON for the HTML viewer (per preset)
|
||||
import json
|
||||
json_stats = {
|
||||
preset_name: {
|
||||
'total_trials': stats['total_trials'],
|
||||
'collisions': stats['collisions'],
|
||||
'interception_rate': stats['interception_rate'],
|
||||
'avg_detection_time': stats['avg_detection_time'],
|
||||
'avg_collision_time': stats['avg_collision_time'],
|
||||
'avg_impact_time': stats['avg_impact_time'],
|
||||
'avg_observations': stats['avg_observations'],
|
||||
}
|
||||
for preset_name, stats in all_stats.items()
|
||||
}
|
||||
with open('montecarlo_stats.json', 'w') as f:
|
||||
json.dump(json_stats, f, indent=2)
|
||||
print()
|
||||
print("Stats saved to montecarlo_stats.json")
|
||||
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("Simulation complete")
|
||||
print("=" * 80)
|
||||
5
defense_sim_v2/.gitignore
vendored
Normal file
5
defense_sim_v2/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
105
defense_sim_v2/PROJECT_SPEC.md
Normal file
105
defense_sim_v2/PROJECT_SPEC.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Defense Sim V2: Project Specification
|
||||
|
||||
Generic preset extension: `projectile_preset` selects `custom`, `preset_a`, `preset_b`, `preset_c`, or uniform `random` selection. A/B/C are synthetic unpowered point-mass demonstrations (high-drag drop, dense drop, lofted launch); they are not calibrated vehicle profiles. Backend sampling determines initial conditions and physical parameters once per run. `scenario.projectile_profile` records realized values and `scenario.projectile_preset_requested` records the selection mode. Existing gravity, quadratic drag relative to wind, RK4 integration, and terrain contact determine subsequent motion. No preset supplies forced cruise velocity or a prescribed descent trajectory.
|
||||
|
||||
Status: authoritative initial backend specification. This specification follows source inspection of all three requested repositories; it will be revised when validation or domain requirements expose a conflict.
|
||||
|
||||
## 1. Goals
|
||||
|
||||
Build a backend-authoritative, reproducible, configurable physical simulation of projectiles and multirotor/aircraft entities, with noisy observation and control, environmental effects, interception events, Monte Carlo results, and a transport-independent state stream. The system must preserve ground truth separately from observations and expose enough state for a later renderer to draw entities and trajectories without calculating physics.
|
||||
|
||||
## 2. Non-goals
|
||||
|
||||
No client-side simulation, combat doctrine, operational deployment, or claim of flight-qualified fidelity. A Vue 3 + Vite bootstrap and telemetry console have since been added under `frontend/`; a full 2D/3D visualization remains outside this baseline until renderer, map, and camera requirements are supplied. This is a research/engineering simulation framework with explicit model limits, not a certified 6-DOF aerodynamic model of any real vehicle.
|
||||
|
||||
## 3. Architecture and backend responsibilities
|
||||
|
||||
Python package `defense_sim` under `src/`. Simulation code owns configuration, random generators, state, physics, environment, sensing, guidance, event detection, raw runs, and statistics. Socket.IO is an adapter that publishes engine snapshots/events; it must not mutate or calculate simulation reality. Pure model modules must be usable without the server.
|
||||
|
||||
Proposed modules: `simulation/{engine,state,scenarios}`, `physics/{vector,integrator,atmosphere,environment,turbulence}`, `entities/{projectile,drone}`, `sensors/{models,tracking}`, `guidance/{controllers,interception}`, `statistics/{metrics,monte_carlo}`, `server/{socketio_server,events}`. Shared immutable configuration dataclasses and explicit SI units are preferred over per-entity ad hoc constants.
|
||||
|
||||
## 4. Coordinate frames and state
|
||||
|
||||
Use SI units and a world NED frame: x north, y east, z down; altitude is `-z`. Body-frame quantities are marked as such. Ground-truth rigid-body state includes position, velocity, acceleration, quaternion orientation `[w,x,y,z]`, body angular rates, and actuator state. Observations and estimates are separate timestamped values with covariance/error metadata. Quaternion normalization occurs after integration. Events and serialized vectors include frame and units in schema/version metadata.
|
||||
|
||||
## 5. Projectile model
|
||||
|
||||
Point-mass translational equations: `p_dot=v`; `v_dot=g + (F_thrust + F_aero)/m`. Drag is opposite relative air velocity and initially quadratic, `F_d=-0.5*rho*Cd*A*|v_air|*v_air`; mass, area, drag coefficient, gravity, launch state, optional thrust curve, and uncertainty distributions are configuration. A launch command initializes one sampled initial condition; no later velocity or position edits are permitted. Ground/impact and interception are continuous within-step events, refined by interpolation/root finding. Projectile attitude is omitted unless a later body/aero model needs it.
|
||||
|
||||
Reference finding: `georgeh1ll/Projectile-Motion-Simulator` contains `simulate_projectile_motion`, which decomposes launch speed into x/y components and advances position at 0.01 s with constant gravity. It is useful as a basic vacuum trajectory sanity check and launch-angle UI concept. It has no drag, atmosphere, uncertainty, or collision model and no LICENSE file in the cloned revision; no code is copied. Correct public URL is `https://github.com/georgeh1ll/Projectile-Motion-Simulator` (the supplied `...Simulatoryou` URL does not resolve).
|
||||
|
||||
## 6. Attack drone and defense drone models
|
||||
|
||||
Both drone classes use the same physical dynamics and differ only in configuration, mission/guidance policy, and sensors. Initial multirotor model: rigid body with mass, diagonal inertia (configurable), quaternion attitude, motor thrust/torque lag, rotor thrust and torque limits, gravity, translational aerodynamic drag, and moments; actuator commands pass through delay, lag, saturation, and noise before forces/moments. Position/velocity targets become bounded acceleration/attitude/thrust commands through a cascaded controller, never direct state assignments. Rate, acceleration, speed, tilt, climb/descent, and motor limits are explicit. Fixed-wing configuration may later use aerodynamic coefficients and control surfaces; it is a separate model, not a hidden approximation.
|
||||
|
||||
Reference finding: `riverjiang187/PyFlightSim` source has a 13-element rigid-body state (NED position, body velocity, quaternion, body rates), Newton-Euler force/moment dynamics including gravity and gyroscopic terms, quaternion kinematics, RK4, configurable aerodynamic coefficients, cascaded PID autopilot, ISA, Dryden gust shaping, and IMU/GPS/air-data/estimator modules. We adapt the separations, state representation, RK4 and quaternion approach, atmosphere, turbulence concepts, and bounded cascaded control. Its source is MIT licensed (Copyright Haiang Jiang, 2026); no code is copied in this initial implementation, so attribution is documentation only. Its model is fixed-wing oriented, has simplified diagonal inertia/aerodynamics, and must be independently checked before reuse.
|
||||
|
||||
Reference finding: `nb-programmer/dronesim` source separates physics engine, actor, action/control interface, and sensors; `SimpleUAVDronePhysics` maps RC commands through velocity/altitude/yaw PID-like controls to thrust and includes operation states. Its implementation mixes centimeter-like implicit units, per-tick motion, direct angle/velocity updates and simple multiplicative drag; sensor base and IMU are mostly stubs. We adapt the command/actuator/interface separation and lifecycle ideas, not its dynamics or numeric constants. MIT license (Copyright imaprogrammer, 2022); no code is copied in this initial implementation.
|
||||
|
||||
## 7. Environment, atmosphere and turbulence
|
||||
|
||||
Environment configuration provides altitude-aware ISA temperature, pressure, density, speed of sound, and dynamic viscosity, with documented valid altitude range and configurable deviations. Wind is a deterministic configured vector/field plus optional seeded variation. Air-relative velocity drives aerodynamic loads. Dryden turbulence is a stateful stochastic filter driven by the run RNG and configured intensity/scale lengths; gust velocity changes air-relative force calculations. It is never applied as visual-only position noise. Wind/turbulence update cadence is distinct from physics integration cadence.
|
||||
|
||||
## 8. Sensors, tracking, uncertainty and randomness
|
||||
|
||||
Each run owns a seeded generator; entity-specific generators use stable seed derivation so call ordering in unrelated entities does not alter the result. Noise configuration supports bias, standard deviation, correlation/AR processes, quantization, latency, missed observations, and actuator variation. Actual-minus-estimated errors are reported as vectors and norms, with sensor, tracking, guidance, and actuator error kept distinct. Controllers consume estimates only. A deterministic mode disables stochastic terms but retains explicitly configured biases/delays.
|
||||
|
||||
## 9. Guidance and control
|
||||
|
||||
Guidance consumes timestamped track estimates. Commands pass through configurable command latency, controller, actuator dynamics/saturation, and then physical force/moment evaluation. The baseline controller is bounded position/velocity-to-acceleration control for multirotors; interceptor guidance is bounded pursuit/lead guidance using estimated target state and configured speed/turn/acceleration limits. No prediction is written into physical position. Record command, actuator output, and tracking/control error.
|
||||
|
||||
## 10. Interception
|
||||
|
||||
Interception is a swept relative-motion sphere test across each integration interval, with event time/position refined within the step. A hit occurs when separation is within the configured combined collision radius while both entities are active. Record IDs, time, interpolated positions/velocities, miss distance, estimated time-to-intercept, and relevant observation/guidance errors. Closest approach is measured for misses. There is no magical proximity event or teleportation.
|
||||
|
||||
### City-circle defender activation
|
||||
|
||||
For V1 compatibility, a configurable horizontal city circle defaults to 10 km radius and center `(0,0)` in NED x/y. For each incoming projectile, the backend rolls out its noisy track through the configured projectile/environment model and finds the first inward crossing of that circle. It ranks available defense drones by 3D distance from their current positions to the crossing point and activates up to the three nearest. Selection uses the estimated track for the crossing, not hidden true projectile state. The activation event records the crossing and selected IDs. This replaces selection based only on shortest predicted intercept time.
|
||||
|
||||
## 11. Simulation loop and integration
|
||||
|
||||
Fixed-step simulation clock. At each physics step: update scheduled environment stochastic states; deliver due sensor samples and update tracks; update guidance/control on their own configured rates; advance actuator states; integrate all coupled physical entities with RK4 or a justified method; normalize attitude; find ground/collision/interception events over the step; update run metrics; publish immutable snapshots at an independent output cadence. Step ordering and event queue use simulation time only. RK stages evaluate forces from stage state and the same held actuator/environment sample so stochastic noise is not redrawn within a single step.
|
||||
|
||||
## 12. Monte Carlo and statistics
|
||||
|
||||
Scenario configuration is data. `run_many` derives one reproducible child seed per run and retains configuration, seed, final/event summaries, and optional sampled trajectories. Aggregation reports run count; hit/interception probability; miss probability and distance mean/median/std/min/max; interception-time mean/median; detection/track timing; and error summaries. Results are grouped by projectile ID/type under `interception_by_projectile` and are transport-neutral. Raw runs remain serializable so aggregates can be recomputed.
|
||||
|
||||
## 13. Backend event/state schema
|
||||
|
||||
Every packet has `schema_version`, `run_id`, `sequence`, `simulation_time`, and `kind`. A state packet includes entity identity/type/status, position, velocity, acceleration, quaternion, angular velocity, actuator/control summary, and trajectory history or incremental trajectory samples; environment includes atmosphere/wind/turbulence; observations/tracks include estimate, timestamp, uncertainty and ground-truth error only in simulation/debug output; statistics are structured sections `overall`, `scenario`, `timing`, `error`, `interception_by_projectile`. Event packets include typed collision/interception/impact/detection/error events. JSON values use finite numbers only. Socket.IO event names are defined in `server/events.py`; clients subscribe to `simulation_state`, `simulation_event`, `simulation_statistics`, and `simulation_error`. Streaming can throttle/coalesce snapshots without affecting physics.
|
||||
|
||||
## 14. Configuration
|
||||
|
||||
Validated dataclasses (and JSON/YAML serialization at the boundary) define integration/control/sensor/stream rates, environment, body/motor/projectile parameters, uncertainty distributions, scenario entities, collision radii, and seed. Invalid units, negative mass, nonpositive timestep, and inconsistent limits fail before a run. Defaults are educational example values, never described as representative of deployed equipment.
|
||||
|
||||
## 15. Determinism and replay
|
||||
|
||||
Same software version, configuration, seed, step schedule, and platform floating-point behavior must reproduce results within documented numerical tolerance. RNG state/seed and configuration hash are kept in raw run metadata. No wall clock drives physics. Socket client timing cannot affect engine time.
|
||||
|
||||
## 16. Validation strategy
|
||||
|
||||
Tests cover analytical vacuum projectile motion; quadratic drag energy/speed behavior; ISA anchor values and monotonicity; RK4 convergence and constant acceleration; quaternion normalization; force/actuator bounds; drone turn/acceleration/altitude constraints; sensor error and seed reproducibility; Dryden finite output/statistical bounds; swept collision/interception edge cases; estimation error signs; Monte Carlo aggregate recomputation; event/state serialization. Golden analytic solutions and physically reasonable bounds are preferred to self-referential expected values. CI will run unit tests and a small performance smoke benchmark. These tests are planned deliverables; do not claim fidelity beyond tested envelopes.
|
||||
|
||||
## 17. Performance
|
||||
|
||||
Physics, controller, navigation sensor, radar scan, wireless link, and state-stream rates are distinct simulation-time schedules. The normal operator workflow does not expose integrator steps or random seeds: a run creates and records its own seed, and ends at physical interception/ground impact. Physics rate and model timing remain backend configuration. Snapshots are streamed independently at 20 Hz and immediately on new events. Physics benchmarks remain necessary for larger entity counts and Monte Carlo; networking must not change fixed simulation time.
|
||||
|
||||
## 18. Transport architecture
|
||||
|
||||
`python-socketio` ASGI server adapts the engine's async snapshot/event stream. It supports starting/stopping runs, state subscription, and server-side errors; it accepts scenario and device configuration but does not accept client-authored entity state or compute physics. Engine APIs work without Socket.IO. CORS is configurable through `SIM_ALLOWED_ORIGINS`. The Vue client uses `socket.io-client` to submit scenario/device configuration and render received state/events.
|
||||
|
||||
## 19. Known limitations
|
||||
|
||||
Initial core is a configurable engineering approximation. ISA layering, simplified turbulence, rigid-body multirotor rotor model, aerodynamic coefficients, sensor covariance, guidance and ground interaction require independent calibration. No real vehicle validation data is currently supplied. Ground collision uses a flat datum. Interception models geometric contact, not blast/fragment effects. Python floating-point determinism is not guaranteed bit-for-bit across platforms.
|
||||
|
||||
### Device timing and link assumptions
|
||||
|
||||
The AERIS-10 profile is based on repository firmware in `9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp`. With 16 chirps at PRI1=167 μs and PRI2=175 μs, a 175.4 μs guard, three phase patterns for each of 15 elevation positions, and four 1 ms stepper pulses, one azimuth frame is 258.133 ms. Fifty azimuth positions imply a 12.90665 s same-direction revisit. The model checks a 50-position azimuth sector and a 31-beam elevation fan; finite horizontal sector width (7.2° per mechanical step) and 3° elevation spacing are approximations pending measured antenna beam patterns. AERIS-10N/10X maximum range is configured as 3 km/20 km from the root README.
|
||||
|
||||
The repository's `simulate_trajectory_precision.py` uses 10 Hz observations, 2 m range sigma, 0.5° azimuth/elevation sigma, 1 m/s Doppler sigma, and 7.1 ms fixed latency. Those values are adopted as R&D sensor/error and processing-delay assumptions, not verified AERIS-10 performance. The firmware-derived scan cadence supersedes that script's uniform 10 Hz update assumption for radar detections.
|
||||
|
||||
The drone navigation sensor defaults to 10 Hz with 2 m position sigma, 0.4 m/s velocity sigma, and 10 ms onboard processing. Flight control defaults to 50 Hz; physics integrates at 100 Hz; actuator lag uses the existing 0.25 s time constant. Drone command and telemetry share a configurable, half-duplex, serialized wireless R&D link: 20 Hz send slots, 250 kbit/s, 64-byte telemetry / 48-byte command payloads, 24-byte framing overhead, 50 ms one-way delay, 5 ms Gaussian jitter, 10 ms command computation, and zero packet loss. These are explicit baselines for field tuning, not claims about a selected drone radio. Wired radar-to-control reporting uses the 7.1 ms fixed-delay assumption. Link queueing and airtime are added to propagation latency.
|
||||
|
||||
## 20. Frontend bootstrap and remaining requirements
|
||||
|
||||
`frontend/` is bootstrapped from `git@git.naori.cloud:thebadking/vue-template.git` (Vue 3, Vite, TypeScript, UnoCSS, Supabase Auth/storage). It includes Socket.IO scenario configuration, Three.js orbit/zoom rendering, backend-provided trajectories, a fixed-height terminal telemetry/event log with scrollback, and a Supabase `simulation_runs` migration with owner-scoped RLS. The client renders backend state without local physics or prediction. Keep `Interception Stats by Projectile` below other statistics when Monte Carlo results are exposed. Terrain, antenna patterns, drone airframe models, field-calibrated device profiles, and additional detailed renderer requirements remain future work.
|
||||
53
defense_sim_v2/README.md
Normal file
53
defense_sim_v2/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Defense Sim V2
|
||||
|
||||
Real-world physics simulation with backend-owned state, dynamics, sensing, device timing, random variation, interception events, and statistics. The Vue 3 client is an operator-configuration and visualization layer. Three.js renders backend-provided state and trajectory samples; it does not calculate physics or predictions.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cd defense_sim_v2
|
||||
python -m pip install -e '.[server,dev]'
|
||||
python -m defense_sim.cli --steps 500 --seed 7
|
||||
```
|
||||
|
||||
The backend provides SI physics, an RK4 point-mass projectile, configurable environment and seeded Dryden gusts, actuator-limited drone point-mass dynamics, noisy observations, swept interception checks, run aggregation, JSON snapshots, and Socket.IO transport. Threat detection uses AERIS-10 hardware timing and measurement configuration. Drone navigation, control, wireless telemetry, command delivery, and actuator response have separate update schedules and latencies. The backend predicts each threat's first inward city-circle crossing and selects the three available defense drones nearest that point using positions received by ground control.
|
||||
|
||||
All coordinates use NED world axes (m, m/s, m/s²; altitude is negative down). Fixed simulation time never follows wall-clock/network timing.
|
||||
|
||||
## Generic projectile presets
|
||||
|
||||
The selector follows the previous preset interaction with synthetic unpowered objects. A is a high-drag drop (10–20 m/s, 30–60 m altitude); B is a dense drop (25–45 m/s, 60–100 m); C is a lofted launch (45–65 m/s, 10–20 m, 35–55° elevation). These arbitrary physics demonstrations can hit terrain before reaching the protected area.
|
||||
|
||||
`projectile_preset` accepts `custom`, `preset_a`, `preset_b`, `preset_c`, or `random`. Random mode selects uniformly from A/B/C once per run. Backend sampling varies initial speed, altitude, elevation, mass, area, and drag coefficient within documented bounds in `simulation/presets.py`. RK4 integrates gravity and wind-relative drag without enforcing cruise speed. The requested selection, realized parameters, and seed are stored in `scenario` for replay; the UI renders the realized values.
|
||||
|
||||
## Device timing defaults
|
||||
|
||||
The AERIS-10 scan sequence derives from `9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp`: 16 chirps in each long/short train, PRI values of 167/175 μs, a 175.4 μs guard, three phase patterns at each of 15 elevation positions, four 1 ms stepper pulses, and 50 azimuth positions. The model yields about 258.133 ms per azimuth frame and 12.90665 s per full azimuth revisit. AERIS-10N and AERIS-10X range limits (3 km and 20 km) follow the repository hardware README. Range/bearing/Doppler uncertainty and the 7.1 ms fixed processing delay follow `simulate_trajectory_precision.py`; these are model inputs, not bench-validated performance claims.
|
||||
|
||||
Drone navigation defaults to 10 Hz, flight control to 50 Hz, and actuator acceleration response to a 0.25 s time constant. The wireless drone link is an explicit, tunable R&D assumption: 20 Hz send slots, 250 kbit/s shared half-duplex channel, 64-byte telemetry and 48-byte command payloads plus 24 bytes framing, 50 ms one-way delay, 5 ms latency jitter, 10 ms command computation, and 0% packet loss. Packets are serialized, with airtime and queueing included. Radar reporting is modeled as wired with the configured 7.1 ms fixed delay. Drone-link defaults are not specifications for a selected radio and should be tuned during field R&D.
|
||||
|
||||
## Layout and frontend
|
||||
|
||||
`src/defense_sim` contains transport-independent simulation modules. The `server` package only adapts simulation state and events to Socket.IO. `tests` focuses on physical invariants and reproducibility. See `PROJECT_SPEC.md` for model assumptions and limitations.
|
||||
|
||||
The Vue 3 + Vite client in `frontend/` connects with `socket.io-client`, renders backend entity positions and trajectory history in Three.js, provides scenario and device configuration, and displays bounded terminal-style telemetry and event scrollback. Snapshot persistence uses a Supabase migration for per-user row-level security. Configure `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` before applying `frontend/supabase/migrations/20260930000000_simulation_runs.sql`.
|
||||
|
||||
## Run the server
|
||||
|
||||
```bash
|
||||
python -m defense_sim.server.socketio_server
|
||||
```
|
||||
|
||||
Socket.IO emits `simulation_state`, `simulation_event`, `simulation_statistics`, and `simulation_error`. It accepts scenario/device configuration, not client-authored entity state. CORS is configurable through `SIM_ALLOWED_ORIGINS`.
|
||||
|
||||
## Deployment
|
||||
|
||||
The site is served at `https://defense.naori.cloud`. The Vue app listens on `192.168.1.194:8088`; the Python Socket.IO backend listens on `192.168.1.194:8000`. Traefik at `192.168.1.10` routes the site and `/socket.io` over HTTPS. User services are defined under `deploy/` and enabled with systemd lingering.
|
||||
|
||||
Rebuild the client with:
|
||||
|
||||
```bash
|
||||
VITE_SIM_SOCKET_URL=https://defense.naori.cloud pnpm --dir frontend build
|
||||
```
|
||||
|
||||
Restart backend changes with `systemctl --user restart defense-sim-backend.service`. Traefik's watched dynamic route is `/etc/traefik/conf.d/defense-sim-v2.yml` in LXC 100.
|
||||
17
defense_sim_v2/deploy/defense-sim-backend.service
Normal file
17
defense_sim_v2/deploy/defense-sim-backend.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Defense Sim V2 Socket.IO backend
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/dre/git/plfm/defense_sim_v2
|
||||
Environment=SIM_BIND_HOST=0.0.0.0
|
||||
Environment=SIM_PORT=8000
|
||||
Environment=SIM_ALLOWED_ORIGINS=https://defense.naori.cloud,http://localhost:3333
|
||||
ExecStart=/home/dre/git/plfm/defense_sim_v2/.venv/bin/python -m defense_sim.server.socketio_server
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
14
defense_sim_v2/deploy/defense-sim-frontend.service
Normal file
14
defense_sim_v2/deploy/defense-sim-frontend.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Defense Sim V2 static Vue frontend
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/dre/git/plfm/defense_sim_v2/frontend
|
||||
ExecStart=/usr/bin/python3 -m http.server 8088 --bind 0.0.0.0 --directory /home/dre/git/plfm/defense_sim_v2/frontend/dist
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
29
defense_sim_v2/deploy/traefik-defense-sim-v2.yml
Normal file
29
defense_sim_v2/deploy/traefik-defense-sim-v2.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
http:
|
||||
routers:
|
||||
defense-sim-v2-web:
|
||||
rule: "Host(`defense.naori.cloud`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: defense-sim-v2-web
|
||||
priority: 10
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
defense-sim-v2-socketio:
|
||||
rule: "Host(`defense.naori.cloud`) && PathPrefix(`/socket.io`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: defense-sim-v2-socketio
|
||||
priority: 100
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
defense-sim-v2-web:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.1.194:8088"
|
||||
defense-sim-v2-socketio:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.1.194:8000"
|
||||
2
defense_sim_v2/frontend/.dockerignore
Normal file
2
defense_sim_v2/frontend/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
9
defense_sim_v2/frontend/.editorconfig
Normal file
9
defense_sim_v2/frontend/.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
16
defense_sim_v2/frontend/.env.example
Normal file
16
defense_sim_v2/frontend/.env.example
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copy to .env.local (gitignored). Vite loads this over .env when running pnpm dev.
|
||||
|
||||
# Supabase project URL and anon key
|
||||
VITE_SUPABASE_URL=http://localhost:54321
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||
|
||||
# Python Socket.IO simulation backend
|
||||
# Empty uses the current site origin (recommended behind Traefik).
|
||||
VITE_SIM_SOCKET_URL=
|
||||
|
||||
# Optional: restrict available locales (comma-separated, e.g. "en,fr,de")
|
||||
# Leave empty to allow all locales found in locales/
|
||||
VITE_ALLOWED_LOCALES=
|
||||
|
||||
# Brand name shown in the sidebar and page titles
|
||||
VITE_BRAND_NAME=Defense Sim V2
|
||||
2
defense_sim_v2/frontend/.github/FUNDING.yml
vendored
Normal file
2
defense_sim_v2/frontend/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
open_collective: antfu
|
||||
github: [antfu]
|
||||
96
defense_sim_v2/frontend/.github/workflows/ci.yml
vendored
Normal file
96
defense_sim_v2/frontend/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: pnpm
|
||||
|
||||
- name: Install
|
||||
run: pnpm install
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: pnpm
|
||||
|
||||
- name: Install
|
||||
run: pnpm install
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x, 22.x]
|
||||
os: [ubuntu-latest]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run test:unit
|
||||
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache
|
||||
key: cypress-cache-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install
|
||||
|
||||
- name: Cypress PNPM Patch
|
||||
run: cp pnpm-lock.yaml package-lock.json
|
||||
|
||||
- name: Cypress
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
install-command: echo
|
||||
build: pnpm run build
|
||||
start: npx vite preview --port 3333
|
||||
11
defense_sim_v2/frontend/.gitignore
vendored
Normal file
11
defense_sim_v2/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
.vite-ssg-dist
|
||||
.vite-ssg-temp
|
||||
*.local
|
||||
dist
|
||||
dist-ssr
|
||||
node_modules
|
||||
.idea/
|
||||
*.log
|
||||
cypress/downloads
|
||||
public/assets/fonts
|
||||
1
defense_sim_v2/frontend/.npmrc
Normal file
1
defense_sim_v2/frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
shamefully-hoist=true
|
||||
13
defense_sim_v2/frontend/.vscode/extensions.json
vendored
Normal file
13
defense_sim_v2/frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"antfu.iconify",
|
||||
"antfu.unocss",
|
||||
"antfu.vite",
|
||||
"antfu.goto-alias",
|
||||
"csstools.postcss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"vue.volar",
|
||||
"lokalise.i18n-ally",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
48
defense_sim_v2/frontend/.vscode/settings.json
vendored
Normal file
48
defense_sim_v2/frontend/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": "locales",
|
||||
"i18n-ally.sortKeys": true,
|
||||
|
||||
// Disable the default formatter
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off" },
|
||||
{ "rule": "format/*", "severity": "off" },
|
||||
{ "rule": "*-indent", "severity": "off" },
|
||||
{ "rule": "*-spacing", "severity": "off" },
|
||||
{ "rule": "*-spaces", "severity": "off" },
|
||||
{ "rule": "*-order", "severity": "off" },
|
||||
{ "rule": "*-dangle", "severity": "off" },
|
||||
{ "rule": "*-newline", "severity": "off" },
|
||||
{ "rule": "*quotes", "severity": "off" },
|
||||
{ "rule": "*semi", "severity": "off" }
|
||||
],
|
||||
|
||||
// The following is optional.
|
||||
// It's better to put under project setting `.vscode/settings.json`
|
||||
// to avoid conflicts with working with different eslint configs
|
||||
// that does not support all formats.
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
]
|
||||
}
|
||||
18
defense_sim_v2/frontend/Dockerfile
Normal file
18
defense_sim_v2/frontend/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
|
||||
COPY .npmrc package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
21
defense_sim_v2/frontend/LICENSE
Normal file
21
defense_sim_v2/frontend/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-PRESENT Anthony Fu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
93
defense_sim_v2/frontend/README.md
Normal file
93
defense_sim_v2/frontend/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Vue Template
|
||||
|
||||
Opinionated Vue 3 starter based on [Vitesse](https://github.com/antfu-collective/vitesse), extended with shadcn-vue components, Supabase auth, and UnoCSS Wind4.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Vue 3** + **TypeScript** + **Vite**
|
||||
- **UnoCSS** with `presetWind4` (Tailwind v4-compatible utilities)
|
||||
- **shadcn-vue** components powered by **reka-ui**
|
||||
- **Supabase** for auth and data persistence (PKCE flow)
|
||||
- **Pinia** stores
|
||||
- **vue-i18n** with locale auto-detection
|
||||
- **ViteSSG** static site generation
|
||||
- **dev-i18n-audit** tool at `/__dev__/i18n-audit` (dev-only)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
# Fill in VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------ | ------------------------------------------------------- |
|
||||
| `VITE_SUPABASE_URL` | Your Supabase project URL |
|
||||
| `VITE_SUPABASE_ANON_KEY` | Your Supabase anon/public key |
|
||||
| `VITE_ALLOWED_LOCALES` | Comma-separated locale list (e.g. `en,fr`). Empty = all |
|
||||
| `VITE_BRAND_NAME` | App name shown in sidebar and page titles |
|
||||
|
||||
## Adding shadcn-vue Components
|
||||
|
||||
The full shadcn-vue component set is already installed in `src/components/ui/`. You can add more via the CLI:
|
||||
|
||||
```bash
|
||||
npx shadcn-vue@latest add <component-name>
|
||||
```
|
||||
|
||||
All components use the `@/` alias pointing to `src/`. The `cn()` utility lives in `src/lib/utils.ts`.
|
||||
|
||||
## Auth
|
||||
|
||||
Auth is handled via Supabase PKCE. The `useAuth()` composable exposes `loginWithEmail`, `loginWithProvider`, and `signUpWithEmail`. Protected routes use `meta: { requiresAuth: true }` in their route definition.
|
||||
|
||||
The `useAuthStore` (Pinia) holds the current user and session state.
|
||||
|
||||
## Layouts
|
||||
|
||||
| Layout | Description |
|
||||
| -------------- | --------------------------- |
|
||||
| `default` | Bare router-view |
|
||||
| `with-sidebar` | Sidebar + main content area |
|
||||
| `login` | Centered card layout |
|
||||
|
||||
## i18n Audit (dev only)
|
||||
|
||||
Navigate to `http://localhost:3333/__dev__/i18n-audit` to scan your source files for i18n keys, audit locale YAML files, and optionally use LM Studio for AI-assisted translation.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
src/
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn-vue primitives
|
||||
│ ├── AppSidebar.vue
|
||||
│ ├── DarkModeSwitcher.vue
|
||||
│ └── LanguageSwitcher.vue
|
||||
├── composables/
|
||||
│ ├── useAuth.ts
|
||||
│ └── useLogout.ts
|
||||
├── devtools/ # i18n-audit (dev only)
|
||||
├── layouts/
|
||||
│ ├── default.vue
|
||||
│ ├── login.vue
|
||||
│ └── with-sidebar.vue
|
||||
├── lib/
|
||||
│ ├── auth/ # auth helpers
|
||||
│ ├── i18n-audit/ # audit lib
|
||||
│ ├── supabaseClient.ts
|
||||
│ └── utils.ts # cn() utility
|
||||
├── modules/ # auto-loaded Vite SSG modules
|
||||
├── pages/
|
||||
│ ├── index.vue
|
||||
│ └── login.vue
|
||||
├── stores/
|
||||
│ ├── auth.ts
|
||||
│ └── adminAccess.ts
|
||||
└── types/
|
||||
└── database.ts # Supabase DB types (replace with your own)
|
||||
```
|
||||
20
defense_sim_v2/frontend/components.json
Normal file
20
defense_sim_v2/frontend/components.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/main.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"composables": "@/composables",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/composables"
|
||||
}
|
||||
}
|
||||
14
defense_sim_v2/frontend/cypress.config.ts
Normal file
14
defense_sim_v2/frontend/cypress.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
import vitePreprocessor from 'cypress-vite'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:3333',
|
||||
chromeWebSecurity: false,
|
||||
specPattern: 'cypress/e2e/**/*.spec.*',
|
||||
supportFile: false,
|
||||
setupNodeEvents(on) {
|
||||
on('file:preprocessor', vitePreprocessor())
|
||||
},
|
||||
},
|
||||
})
|
||||
36
defense_sim_v2/frontend/cypress/e2e/basic.spec.ts
Normal file
36
defense_sim_v2/frontend/cypress/e2e/basic.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
context('Basic', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('basic nav', () => {
|
||||
cy.url()
|
||||
.should('eq', 'http://localhost:3333/')
|
||||
|
||||
cy.contains('[Home Layout]')
|
||||
.should('exist')
|
||||
|
||||
cy.get('#input')
|
||||
.type('Vitesse{Enter}')
|
||||
.url()
|
||||
.should('eq', 'http://localhost:3333/hi/Vitesse')
|
||||
|
||||
cy.contains('[Default Layout]')
|
||||
.should('exist')
|
||||
|
||||
cy.get('[btn]')
|
||||
.click()
|
||||
.url()
|
||||
.should('eq', 'http://localhost:3333/')
|
||||
})
|
||||
|
||||
it('markdown', () => {
|
||||
cy.get('[data-test-id="about"]')
|
||||
.click()
|
||||
.url()
|
||||
.should('eq', 'http://localhost:3333/about')
|
||||
|
||||
cy.get('.shiki')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
12
defense_sim_v2/frontend/cypress/tsconfig.json
Normal file
12
defense_sim_v2/frontend/cypress/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"cypress"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
16
defense_sim_v2/frontend/eslint.config.js
Normal file
16
defense_sim_v2/frontend/eslint.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// @ts-check
|
||||
import antfu from '@antfu/eslint-config'
|
||||
|
||||
export default antfu(
|
||||
{
|
||||
unocss: true,
|
||||
formatters: true,
|
||||
pnpm: true,
|
||||
},
|
||||
{
|
||||
files: ['README.md', 'README.zh-CN.md'],
|
||||
rules: {
|
||||
'markdown/heading-increment': 'off',
|
||||
},
|
||||
},
|
||||
)
|
||||
23
defense_sim_v2/frontend/index.html
Normal file
23
defense_sim_v2/frontend/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00aba9" />
|
||||
<meta name="msapplication-TileColor" content="#00aba9" />
|
||||
<script>
|
||||
;(function () {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
|
||||
if (setting === 'dark' || (prefersDark && setting !== 'light'))
|
||||
document.documentElement.classList.toggle('dark', true)
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
|
||||
</body>
|
||||
</html>
|
||||
7
defense_sim_v2/frontend/locales/README.md
Normal file
7
defense_sim_v2/frontend/locales/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## i18n
|
||||
|
||||
This directory is to serve your locale translation files. YAML under this folder would be loaded automatically and register with their filenames as locale code.
|
||||
|
||||
Check out [`vue-i18n`](https://github.com/intlify/vue-i18n-next) for more details.
|
||||
|
||||
If you are using VS Code, [`i18n Ally`](https://github.com/lokalise/i18n-ally) is recommended to make the i18n experience better.
|
||||
13
defense_sim_v2/frontend/locales/en.yml
Normal file
13
defense_sim_v2/frontend/locales/en.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
button:
|
||||
back: Back
|
||||
home: Home
|
||||
toggle_dark: Toggle dark mode
|
||||
|
||||
intro:
|
||||
desc: Opinionated Vite Starter Template
|
||||
|
||||
themeToggle:
|
||||
dark: Dark Mode
|
||||
light: Light Mode
|
||||
|
||||
not-found: Not found
|
||||
17
defense_sim_v2/frontend/netlify.toml
Executable file
17
defense_sim_v2/frontend/netlify.toml
Executable file
@@ -0,0 +1,17 @@
|
||||
[build]
|
||||
publish = "dist"
|
||||
command = "pnpm run build"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "22"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/manifest.webmanifest"
|
||||
|
||||
[headers.values]
|
||||
Content-Type = "application/manifest+json"
|
||||
101
defense_sim_v2/frontend/package.json
Normal file
101
defense_sim_v2/frontend/package.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.30.2",
|
||||
"scripts": {
|
||||
"build": "vite-ssg build",
|
||||
"dev": "vite --port 3333 --open",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"preview-https": "serve dist",
|
||||
"test": "vitest",
|
||||
"test:e2e": "cypress open",
|
||||
"test:unit": "vitest",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"up": "taze major -I",
|
||||
"postinstall": "npx simple-git-hooks",
|
||||
"sizecheck": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "catalog:frontend",
|
||||
"@unhead/vue": "catalog:frontend",
|
||||
"@unocss/reset": "catalog:frontend",
|
||||
"@unovis/ts": "catalog:frontend",
|
||||
"@unovis/vue": "catalog:frontend",
|
||||
"@vee-validate/zod": "catalog:frontend",
|
||||
"@vueuse/core": "catalog:frontend",
|
||||
"class-variance-authority": "catalog:frontend",
|
||||
"clsx": "catalog:frontend",
|
||||
"embla-carousel-vue": "catalog:frontend",
|
||||
"js-yaml": "catalog:frontend",
|
||||
"lucide-vue-next": "catalog:frontend",
|
||||
"nprogress": "catalog:frontend",
|
||||
"pinia": "catalog:frontend",
|
||||
"reka-ui": "catalog:frontend",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "catalog:frontend",
|
||||
"three": "^0.186.1",
|
||||
"vaul-vue": "catalog:frontend",
|
||||
"vee-validate": "catalog:frontend",
|
||||
"vue": "catalog:frontend",
|
||||
"vue-i18n": "catalog:frontend",
|
||||
"vue-router": "catalog:frontend",
|
||||
"vue-sonner": "catalog:frontend",
|
||||
"zod": "catalog:frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "catalog:dev",
|
||||
"@iconify-json/carbon": "catalog:dev",
|
||||
"@intlify/unplugin-vue-i18n": "catalog:build",
|
||||
"@shikijs/markdown-it": "catalog:build",
|
||||
"@types/js-yaml": "catalog:types",
|
||||
"@types/markdown-it-link-attributes": "catalog:types",
|
||||
"@types/nprogress": "catalog:types",
|
||||
"@types/three": "^0.186.0",
|
||||
"@unocss/eslint-config": "catalog:build",
|
||||
"@unocss/preset-wind4": "catalog:build",
|
||||
"@vitejs/plugin-vue": "catalog:build",
|
||||
"@vue-macros/volar": "catalog:dev",
|
||||
"@vue/test-utils": "catalog:dev",
|
||||
"beasties": "catalog:build",
|
||||
"cypress": "catalog:dev",
|
||||
"cypress-vite": "catalog:dev",
|
||||
"eslint": "catalog:dev",
|
||||
"eslint-plugin-cypress": "catalog:dev",
|
||||
"eslint-plugin-format": "catalog:dev",
|
||||
"https-localhost": "catalog:dev",
|
||||
"lint-staged": "catalog:dev",
|
||||
"markdown-it-link-attributes": "catalog:build",
|
||||
"rollup": "catalog:build",
|
||||
"shiki": "catalog:build",
|
||||
"simple-git-hooks": "catalog:dev",
|
||||
"taze": "catalog:dev",
|
||||
"typescript": "catalog:dev",
|
||||
"unocss": "catalog:build",
|
||||
"unplugin-auto-import": "catalog:build",
|
||||
"unplugin-vue-components": "catalog:build",
|
||||
"unplugin-vue-macros": "catalog:build",
|
||||
"unplugin-vue-markdown": "catalog:build",
|
||||
"vite": "catalog:build",
|
||||
"vite-bundle-visualizer": "catalog:build",
|
||||
"vite-plugin-inspect": "catalog:build",
|
||||
"vite-plugin-pwa": "catalog:build",
|
||||
"vite-plugin-vue-devtools": "catalog:build",
|
||||
"vite-plugin-vue-layouts": "catalog:build",
|
||||
"vite-ssg": "catalog:build",
|
||||
"vite-ssg-sitemap": "catalog:build",
|
||||
"vitest": "catalog:dev",
|
||||
"vue-tsc": "catalog:dev"
|
||||
},
|
||||
"resolutions": {
|
||||
"unplugin": "catalog:build",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-inspect": "catalog:build"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
}
|
||||
15294
defense_sim_v2/frontend/pnpm-lock.yaml
generated
Normal file
15294
defense_sim_v2/frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
95
defense_sim_v2/frontend/pnpm-workspace.yaml
Normal file
95
defense_sim_v2/frontend/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,95 @@
|
||||
shellEmulator: true
|
||||
|
||||
trustPolicy: no-downgrade
|
||||
trustPolicyExclude:
|
||||
- undici-types@6.21.0
|
||||
- '@oxc-resolver/binding-win32-x64-msvc@4.2.0'
|
||||
- '@oxc-resolver/binding-darwin-x64@4.2.0'
|
||||
- '@oxc-resolver/binding-darwin-arm64@4.2.0'
|
||||
- '@oxc-resolver/binding-linux-x64-gnu@4.2.0'
|
||||
- '@oxc-resolver/binding-linux-arm64-gnu@4.2.0'
|
||||
- '@oxc-resolver/binding-win32-arm64-msvc@4.2.0'
|
||||
- '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1'
|
||||
- semver@6.3.1
|
||||
|
||||
packages: []
|
||||
|
||||
catalogs:
|
||||
|
||||
build:
|
||||
'@intlify/unplugin-vue-i18n': ^11.0.7
|
||||
'@shikijs/markdown-it': ^3.23.0
|
||||
'@unocss/eslint-config': ^66.6.1
|
||||
'@unocss/preset-wind4': ^66.6.1
|
||||
'@vitejs/plugin-vue': ^6.0.4
|
||||
beasties: ^0.4.1
|
||||
markdown-it-link-attributes: ^4.0.1
|
||||
rollup: ^4.59.0
|
||||
shiki: ^3.23.0
|
||||
unocss: ^66.6.1
|
||||
unplugin: ^2.3.11
|
||||
unplugin-auto-import: ^21.0.0
|
||||
unplugin-vue-components: ^31.0.0
|
||||
unplugin-vue-macros: ^2.14.5
|
||||
unplugin-vue-markdown: ^29.2.0
|
||||
vite: ^7.3.1
|
||||
vite-bundle-visualizer: ^1.2.1
|
||||
vite-plugin-inspect: ^11.3.3
|
||||
vite-plugin-pwa: ^1.2.0
|
||||
vite-plugin-vue-devtools: ^8.0.6
|
||||
vite-plugin-vue-layouts: ^0.11.0
|
||||
vite-ssg: ^28.3.0
|
||||
vite-ssg-sitemap: ^0.10.0
|
||||
|
||||
dev:
|
||||
'@antfu/eslint-config': ^7.6.1
|
||||
'@iconify-json/carbon': ^1.2.18
|
||||
'@vue-macros/volar': ^3.1.2
|
||||
'@vue/test-utils': ^2.4.6
|
||||
cypress: ^15.10.0
|
||||
cypress-vite: ^1.8.0
|
||||
eslint: ^10.0.2
|
||||
eslint-plugin-cypress: ^6.1.0
|
||||
eslint-plugin-format: ^2.0.1
|
||||
https-localhost: ^4.7.1
|
||||
lint-staged: ^16.2.7
|
||||
simple-git-hooks: ^2.13.1
|
||||
taze: ^19.9.2
|
||||
typescript: ^5.8.2
|
||||
vitest: ^4.0.18
|
||||
vue-tsc: ^3.2.5
|
||||
|
||||
frontend:
|
||||
'@supabase/supabase-js': ^2.47.10
|
||||
'@unhead/vue': ^2.1.7
|
||||
'@unocss/reset': ^66.6.1
|
||||
'@unovis/ts': ^1.6.5
|
||||
'@unovis/vue': ^1.6.5
|
||||
'@vee-validate/zod': ^4.15.1
|
||||
'@vueuse/core': ^14.2.1
|
||||
class-variance-authority: ^0.7.1
|
||||
clsx: ^2.1.1
|
||||
embla-carousel-vue: ^8.6.0
|
||||
js-yaml: ^4.1.1
|
||||
lucide-vue-next: ^0.511.0
|
||||
nprogress: ^0.2.0
|
||||
pinia: ^3.0.4
|
||||
reka-ui: ^2.2.0
|
||||
tailwind-merge: ^3.3.0
|
||||
vaul-vue: ^0.4.1
|
||||
vee-validate: ^4.15.1
|
||||
vue: ^3.5.29
|
||||
vue-i18n: ^11.2.8
|
||||
vue-router: ^5.0.3
|
||||
vue-sonner: ^2.0.9
|
||||
|
||||
zod: ^4.4.3
|
||||
types:
|
||||
'@types/js-yaml': ^4.0.9
|
||||
'@types/markdown-it-link-attributes': ^3.0.5
|
||||
'@types/nprogress': ^0.2.3
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- cypress
|
||||
- esbuild
|
||||
- simple-git-hooks
|
||||
3
defense_sim_v2/frontend/public/_headers
Normal file
3
defense_sim_v2/frontend/public/_headers
Normal file
@@ -0,0 +1,3 @@
|
||||
/assets/*
|
||||
cache-control: max-age=31536000
|
||||
cache-control: immutable
|
||||
3
defense_sim_v2/frontend/public/favicon-dark.svg
Normal file
3
defense_sim_v2/frontend/public/favicon-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 311 B |
3
defense_sim_v2/frontend/public/favicon.svg
Normal file
3
defense_sim_v2/frontend/public/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" fill="#222" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 310 B |
BIN
defense_sim_v2/frontend/public/pwa-192x192.png
Normal file
BIN
defense_sim_v2/frontend/public/pwa-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
defense_sim_v2/frontend/public/pwa-512x512.png
Normal file
BIN
defense_sim_v2/frontend/public/pwa-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
41
defense_sim_v2/frontend/public/safari-pinned-tab.svg
Normal file
41
defense_sim_v2/frontend/public/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2916 6015 c-93 -57 -173 -108 -178 -113 -6 -6 7 -36 33 -78 23 -38
|
||||
86 -141 139 -229 54 -88 135 -221 180 -295 46 -74 94 -155 108 -180 14 -25 29
|
||||
-52 35 -60 7 -12 -9 -45 -62 -130 -39 -63 -85 -140 -103 -170 -18 -30 -117
|
||||
-194 -222 -365 -104 -170 -199 -326 -210 -346 -12 -19 -61 -102 -111 -183 -49
|
||||
-81 -101 -166 -115 -189 -14 -23 -39 -64 -55 -90 -17 -27 -77 -126 -134 -220
|
||||
-57 -95 -127 -210 -156 -257 -194 -315 -325 -533 -325 -541 0 -5 -4 -9 -10 -9
|
||||
-5 0 -10 -4 -10 -9 0 -5 -55 -98 -121 -207 -247 -404 -403 -660 -416 -684 -8
|
||||
-14 -58 -97 -112 -185 l-98 -160 -189 -2 c-104 -1 -225 -2 -269 -2 l-80 -1 1
|
||||
-210 c0 -116 4 -213 8 -218 11 -11 6107 -9 6114 2 8 13 8 406 0 419 -4 7 -88
|
||||
10 -265 9 l-259 -2 -50 77 c-27 43 -54 87 -60 98 -6 11 -62 103 -124 205 -62
|
||||
102 -120 197 -129 212 -9 16 -85 142 -170 280 -85 139 -160 262 -165 273 -6
|
||||
11 -13 22 -16 25 -3 3 -30 46 -59 95 -30 50 -102 169 -161 265 -59 96 -240
|
||||
393 -402 660 -163 267 -371 609 -463 760 -92 151 -194 318 -225 370 -31 52
|
||||
-101 167 -155 255 l-97 160 27 50 c16 27 32 55 36 61 5 5 38 59 74 120 36 60
|
||||
69 116 74 124 5 8 75 122 155 253 81 131 144 242 141 247 -4 7 -114 76 -183
|
||||
115 -10 6 -52 32 -95 58 -42 27 -81 46 -87 42 -8 -5 -94 -140 -140 -219 -19
|
||||
-33 -221 -365 -246 -404 -15 -22 -18 -18 -111 135 -52 87 -123 203 -157 258
|
||||
-67 108 -67 110 -111 184 -16 28 -34 51 -40 50 -5 0 -86 -47 -179 -104z m739
|
||||
-1642 c319 -526 519 -854 637 -1046 43 -70 78 -130 78 -133 0 -2 5 -10 10 -17
|
||||
6 -7 69 -109 140 -227 72 -118 134 -222 139 -230 5 -8 55 -89 111 -180 56 -91
|
||||
105 -172 110 -180 9 -14 52 -84 270 -445 54 -88 135 -221 180 -295 46 -74 91
|
||||
-148 100 -165 9 -16 31 -53 48 -81 18 -28 32 -54 32 -57 0 -3 -403 -6 -895 -5
|
||||
l-895 0 0 81 c-1 45 -1 439 -1 875 l0 792 -37 1 c-57 1 -344 1 -374 0 l-27 -1
|
||||
0 -832 c0 -458 0 -852 0 -875 l-1 -42 -895 1 c-492 0 -895 3 -895 5 0 9 115
|
||||
198 122 201 5 2 8 7 8 12 0 5 23 46 51 92 28 46 78 128 112 183 33 55 70 116
|
||||
82 135 12 19 132 215 265 435 133 220 266 438 295 485 65 105 206 338 220 362
|
||||
6 10 172 284 370 608 198 325 387 635 420 690 33 55 62 100 65 100 3 0 73
|
||||
-111 155 -247z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
22
defense_sim_v2/frontend/src/App.vue
Normal file
22
defense_sim_v2/frontend/src/App.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { useDark } from '@vueuse/core'
|
||||
|
||||
const isDark = useDark()
|
||||
const BRAND_NAME = import.meta.env.VITE_BRAND_NAME || 'Defense Sim V2'
|
||||
|
||||
useHead({
|
||||
titleTemplate: (title?: string) => {
|
||||
if (!title)
|
||||
return BRAND_NAME
|
||||
return title.includes(BRAND_NAME) ? title : `${title} · ${BRAND_NAME}`
|
||||
},
|
||||
htmlAttrs: {
|
||||
class: () => isDark.value ? 'dark' : '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
644
defense_sim_v2/frontend/src/auto-imports.d.ts
vendored
Normal file
644
defense_sim_v2/frontend/src/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,644 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const asyncComputed: typeof import('@vueuse/core').asyncComputed
|
||||
const autoResetRef: typeof import('@vueuse/core').autoResetRef
|
||||
const computed: typeof import('vue').computed
|
||||
const computedAsync: typeof import('@vueuse/core').computedAsync
|
||||
const computedEager: typeof import('@vueuse/core').computedEager
|
||||
const computedInject: typeof import('@vueuse/core').computedInject
|
||||
const computedWithControl: typeof import('@vueuse/core').computedWithControl
|
||||
const controlledComputed: typeof import('@vueuse/core').controlledComputed
|
||||
const controlledRef: typeof import('@vueuse/core').controlledRef
|
||||
const createApp: typeof import('vue').createApp
|
||||
const createEventHook: typeof import('@vueuse/core').createEventHook
|
||||
const createGlobalState: typeof import('@vueuse/core').createGlobalState
|
||||
const createInjectionState: typeof import('@vueuse/core').createInjectionState
|
||||
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
|
||||
const createRef: typeof import('@vueuse/core').createRef
|
||||
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
|
||||
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
|
||||
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
|
||||
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
|
||||
const customRef: typeof import('vue').customRef
|
||||
const debouncedRef: typeof import('@vueuse/core').debouncedRef
|
||||
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const definePage: typeof import('vue-router/experimental').definePage
|
||||
const eagerComputed: typeof import('@vueuse/core').eagerComputed
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const extendRef: typeof import('@vueuse/core').extendRef
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const h: typeof import('vue').h
|
||||
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
|
||||
const inject: typeof import('vue').inject
|
||||
const injectHead: typeof import('@unhead/vue').injectHead
|
||||
const injectLocal: typeof import('@vueuse/core').injectLocal
|
||||
const isDark: typeof import('./composables/dark').isDark
|
||||
const isDefined: typeof import('@vueuse/core').isDefined
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
|
||||
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onClickOutside: typeof import('@vueuse/core').onClickOutside
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke
|
||||
const onLongPress: typeof import('@vueuse/core').onLongPress
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onStartTyping: typeof import('@vueuse/core').onStartTyping
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const pausableWatch: typeof import('@vueuse/core').pausableWatch
|
||||
const preferredDark: typeof import('./composables/dark').preferredDark
|
||||
const provide: typeof import('vue').provide
|
||||
const provideLocal: typeof import('@vueuse/core').provideLocal
|
||||
const reactify: typeof import('@vueuse/core').reactify
|
||||
const reactifyObject: typeof import('@vueuse/core').reactifyObject
|
||||
const reactive: typeof import('vue').reactive
|
||||
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
|
||||
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
|
||||
const reactivePick: typeof import('@vueuse/core').reactivePick
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const refAutoReset: typeof import('@vueuse/core').refAutoReset
|
||||
const refDebounced: typeof import('@vueuse/core').refDebounced
|
||||
const refDefault: typeof import('@vueuse/core').refDefault
|
||||
const refManualReset: typeof import('@vueuse/core').refManualReset
|
||||
const refThrottled: typeof import('@vueuse/core').refThrottled
|
||||
const refWithControl: typeof import('@vueuse/core').refWithControl
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const resolveRef: typeof import('@vueuse/core').resolveRef
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const syncRef: typeof import('@vueuse/core').syncRef
|
||||
const syncRefs: typeof import('@vueuse/core').syncRefs
|
||||
const templateRef: typeof import('@vueuse/core').templateRef
|
||||
const throttledRef: typeof import('@vueuse/core').throttledRef
|
||||
const throttledWatch: typeof import('@vueuse/core').throttledWatch
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toReactive: typeof import('@vueuse/core').toReactive
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const toggleDark: typeof import('./composables/dark').toggleDark
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
|
||||
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
|
||||
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
|
||||
const unref: typeof import('vue').unref
|
||||
const unrefElement: typeof import('@vueuse/core').unrefElement
|
||||
const until: typeof import('@vueuse/core').until
|
||||
const useActiveElement: typeof import('@vueuse/core').useActiveElement
|
||||
const useAdminAccessStore: typeof import('./stores/adminAccess').useAdminAccessStore
|
||||
const useAnimate: typeof import('@vueuse/core').useAnimate
|
||||
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
|
||||
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
|
||||
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
|
||||
const useArrayFind: typeof import('@vueuse/core').useArrayFind
|
||||
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
|
||||
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
|
||||
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
|
||||
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
|
||||
const useArrayMap: typeof import('@vueuse/core').useArrayMap
|
||||
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
|
||||
const useArraySome: typeof import('@vueuse/core').useArraySome
|
||||
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
|
||||
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
|
||||
const useAsyncState: typeof import('@vueuse/core').useAsyncState
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useAuth: typeof import('./composables/useAuth').useAuth
|
||||
const useAuthStore: typeof import('./stores/auth').useAuthStore
|
||||
const useBase64: typeof import('@vueuse/core').useBase64
|
||||
const useBattery: typeof import('@vueuse/core').useBattery
|
||||
const useBluetooth: typeof import('@vueuse/core').useBluetooth
|
||||
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
|
||||
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
|
||||
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
|
||||
const useCached: typeof import('@vueuse/core').useCached
|
||||
const useClipboard: typeof import('@vueuse/core').useClipboard
|
||||
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
|
||||
const useCloned: typeof import('@vueuse/core').useCloned
|
||||
const useColorMode: typeof import('@vueuse/core').useColorMode
|
||||
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
|
||||
const useCountdown: typeof import('@vueuse/core').useCountdown
|
||||
const useCounter: typeof import('@vueuse/core').useCounter
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssSupports: typeof import('@vueuse/core').useCssSupports
|
||||
const useCssVar: typeof import('@vueuse/core').useCssVar
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
|
||||
const useCycleList: typeof import('@vueuse/core').useCycleList
|
||||
const useDark: typeof import('@vueuse/core').useDark
|
||||
const useDateFormat: typeof import('@vueuse/core').useDateFormat
|
||||
const useDebounce: typeof import('@vueuse/core').useDebounce
|
||||
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
|
||||
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
|
||||
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
|
||||
const useDevicesList: typeof import('@vueuse/core').useDevicesList
|
||||
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
|
||||
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
|
||||
const useDraggable: typeof import('@vueuse/core').useDraggable
|
||||
const useDropZone: typeof import('@vueuse/core').useDropZone
|
||||
const useElementBounding: typeof import('@vueuse/core').useElementBounding
|
||||
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
|
||||
const useElementHover: typeof import('@vueuse/core').useElementHover
|
||||
const useElementSize: typeof import('@vueuse/core').useElementSize
|
||||
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
|
||||
const useEventBus: typeof import('@vueuse/core').useEventBus
|
||||
const useEventListener: typeof import('@vueuse/core').useEventListener
|
||||
const useEventSource: typeof import('@vueuse/core').useEventSource
|
||||
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
|
||||
const useFavicon: typeof import('@vueuse/core').useFavicon
|
||||
const useFetch: typeof import('@vueuse/core').useFetch
|
||||
const useFileDialog: typeof import('@vueuse/core').useFileDialog
|
||||
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
|
||||
const useFocus: typeof import('@vueuse/core').useFocus
|
||||
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
|
||||
const useFps: typeof import('@vueuse/core').useFps
|
||||
const useFullscreen: typeof import('@vueuse/core').useFullscreen
|
||||
const useGamepad: typeof import('@vueuse/core').useGamepad
|
||||
const useGeolocation: typeof import('@vueuse/core').useGeolocation
|
||||
const useHead: typeof import('@unhead/vue').useHead
|
||||
const useHeadSafe: typeof import('@unhead/vue').useHeadSafe
|
||||
const useI18n: typeof import('vue-i18n').useI18n
|
||||
const useId: typeof import('vue').useId
|
||||
const useIdle: typeof import('@vueuse/core').useIdle
|
||||
const useImage: typeof import('@vueuse/core').useImage
|
||||
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
|
||||
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
|
||||
const useInterval: typeof import('@vueuse/core').useInterval
|
||||
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
|
||||
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
|
||||
const useLastChanged: typeof import('@vueuse/core').useLastChanged
|
||||
const useLink: typeof import('vue-router/auto').useLink
|
||||
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
|
||||
const useLogout: typeof import('./composables/useLogout').useLogout
|
||||
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
|
||||
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
|
||||
const useMediaControls: typeof import('@vueuse/core').useMediaControls
|
||||
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
|
||||
const useMemoize: typeof import('@vueuse/core').useMemoize
|
||||
const useMemory: typeof import('@vueuse/core').useMemory
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useMounted: typeof import('@vueuse/core').useMounted
|
||||
const useMouse: typeof import('@vueuse/core').useMouse
|
||||
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
|
||||
const useMousePressed: typeof import('@vueuse/core').useMousePressed
|
||||
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
|
||||
const useNetwork: typeof import('@vueuse/core').useNetwork
|
||||
const useNow: typeof import('@vueuse/core').useNow
|
||||
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
|
||||
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
|
||||
const useOnline: typeof import('@vueuse/core').useOnline
|
||||
const usePageLeave: typeof import('@vueuse/core').usePageLeave
|
||||
const useParallax: typeof import('@vueuse/core').useParallax
|
||||
const useParentElement: typeof import('@vueuse/core').useParentElement
|
||||
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
|
||||
const usePermission: typeof import('@vueuse/core').usePermission
|
||||
const usePointer: typeof import('@vueuse/core').usePointer
|
||||
const usePointerLock: typeof import('@vueuse/core').usePointerLock
|
||||
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
|
||||
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
|
||||
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
|
||||
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
|
||||
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
|
||||
const usePrevious: typeof import('@vueuse/core').usePrevious
|
||||
const useRafFn: typeof import('@vueuse/core').useRafFn
|
||||
const useRefHistory: typeof import('@vueuse/core').useRefHistory
|
||||
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
|
||||
const useRoute: typeof import('vue-router').useRoute
|
||||
const useRouter: typeof import('vue-router').useRouter
|
||||
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
|
||||
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
|
||||
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
|
||||
const useScriptTag: typeof import('@vueuse/core').useScriptTag
|
||||
const useScroll: typeof import('@vueuse/core').useScroll
|
||||
const useScrollLock: typeof import('@vueuse/core').useScrollLock
|
||||
const useSeoMeta: typeof import('@unhead/vue').useSeoMeta
|
||||
const useServerHead: typeof import('@unhead/vue').useServerHead
|
||||
const useServerHeadSafe: typeof import('@unhead/vue').useServerHeadSafe
|
||||
const useServerSeoMeta: typeof import('@unhead/vue').useServerSeoMeta
|
||||
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
|
||||
const useShare: typeof import('@vueuse/core').useShare
|
||||
const useSimulationSocket: typeof import('./composables/useSimulationSocket').useSimulationSocket
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useSorted: typeof import('@vueuse/core').useSorted
|
||||
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
|
||||
const useStepper: typeof import('@vueuse/core').useStepper
|
||||
const useStorage: typeof import('@vueuse/core').useStorage
|
||||
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
|
||||
const useStyleTag: typeof import('@vueuse/core').useStyleTag
|
||||
const useSupported: typeof import('@vueuse/core').useSupported
|
||||
const useSwipe: typeof import('@vueuse/core').useSwipe
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
|
||||
const useTextDirection: typeof import('@vueuse/core').useTextDirection
|
||||
const useTextSelection: typeof import('@vueuse/core').useTextSelection
|
||||
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
|
||||
const useThrottle: typeof import('@vueuse/core').useThrottle
|
||||
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory
|
||||
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo
|
||||
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
|
||||
const useTimeout: typeof import('@vueuse/core').useTimeout
|
||||
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn
|
||||
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll
|
||||
const useTimestamp: typeof import('@vueuse/core').useTimestamp
|
||||
const useTitle: typeof import('@vueuse/core').useTitle
|
||||
const useToNumber: typeof import('@vueuse/core').useToNumber
|
||||
const useToString: typeof import('@vueuse/core').useToString
|
||||
const useToggle: typeof import('@vueuse/core').useToggle
|
||||
const useTransition: typeof import('@vueuse/core').useTransition
|
||||
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
|
||||
const useUserMedia: typeof import('@vueuse/core').useUserMedia
|
||||
const useUserStore: typeof import('./stores/user').useUserStore
|
||||
const useVModel: typeof import('@vueuse/core').useVModel
|
||||
const useVModels: typeof import('@vueuse/core').useVModels
|
||||
const useVibrate: typeof import('@vueuse/core').useVibrate
|
||||
const useVirtualList: typeof import('@vueuse/core').useVirtualList
|
||||
const useWakeLock: typeof import('@vueuse/core').useWakeLock
|
||||
const useWebNotification: typeof import('@vueuse/core').useWebNotification
|
||||
const useWebSocket: typeof import('@vueuse/core').useWebSocket
|
||||
const useWebWorker: typeof import('@vueuse/core').useWebWorker
|
||||
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
|
||||
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
|
||||
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
|
||||
const useWindowSize: typeof import('@vueuse/core').useWindowSize
|
||||
const watch: typeof import('vue').watch
|
||||
const watchArray: typeof import('@vueuse/core').watchArray
|
||||
const watchAtMost: typeof import('@vueuse/core').watchAtMost
|
||||
const watchDebounced: typeof import('@vueuse/core').watchDebounced
|
||||
const watchDeep: typeof import('@vueuse/core').watchDeep
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
|
||||
const watchImmediate: typeof import('@vueuse/core').watchImmediate
|
||||
const watchOnce: typeof import('@vueuse/core').watchOnce
|
||||
const watchPausable: typeof import('@vueuse/core').watchPausable
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
const watchThrottled: typeof import('@vueuse/core').watchThrottled
|
||||
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
|
||||
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
|
||||
const whenever: typeof import('@vueuse/core').whenever
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
// @ts-ignore
|
||||
export type { AdminFeatureRow, AdminProfileRow } from './stores/adminAccess'
|
||||
import('./stores/adminAccess')
|
||||
}
|
||||
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
|
||||
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
|
||||
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
|
||||
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
|
||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly definePage: UnwrapRef<typeof import('vue-router/experimental')['definePage']>
|
||||
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectHead: UnwrapRef<typeof import('@unhead/vue')['injectHead']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
|
||||
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
|
||||
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
||||
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
||||
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
|
||||
readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
||||
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
||||
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
|
||||
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
|
||||
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
|
||||
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAdminAccessStore: UnwrapRef<typeof import('./stores/adminAccess')['useAdminAccessStore']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useAuth: UnwrapRef<typeof import('./composables/useAuth')['useAuth']>
|
||||
readonly useAuthStore: UnwrapRef<typeof import('./stores/auth')['useAuthStore']>
|
||||
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
|
||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssSupports: UnwrapRef<typeof import('@vueuse/core')['useCssSupports']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
|
||||
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
|
||||
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
|
||||
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
|
||||
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
|
||||
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
|
||||
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
|
||||
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
|
||||
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
|
||||
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
|
||||
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
|
||||
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
|
||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
|
||||
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
|
||||
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
|
||||
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
|
||||
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
|
||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||
readonly useHead: UnwrapRef<typeof import('@unhead/vue')['useHead']>
|
||||
readonly useHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useHeadSafe']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useLogout: UnwrapRef<typeof import('./composables/useLogout')['useLogout']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
|
||||
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
|
||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
|
||||
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
|
||||
readonly useSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useSeoMeta']>
|
||||
readonly useServerHead: UnwrapRef<typeof import('@unhead/vue')['useServerHead']>
|
||||
readonly useServerHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useServerHeadSafe']>
|
||||
readonly useServerSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useServerSeoMeta']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSimulationSocket: UnwrapRef<typeof import('./composables/useSimulationSocket')['useSimulationSocket']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
|
||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
|
||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
|
||||
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
|
||||
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
|
||||
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
|
||||
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
|
||||
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
|
||||
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
|
||||
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
|
||||
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
|
||||
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
|
||||
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
|
||||
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
}
|
||||
}
|
||||
337
defense_sim_v2/frontend/src/components.d.ts
vendored
Normal file
337
defense_sim_v2/frontend/src/components.d.ts
vendored
Normal file
@@ -0,0 +1,337 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Accordion: typeof import('./components/ui/accordion/Accordion.vue')['default']
|
||||
AccordionContent: typeof import('./components/ui/accordion/AccordionContent.vue')['default']
|
||||
AccordionItem: typeof import('./components/ui/accordion/AccordionItem.vue')['default']
|
||||
AccordionTrigger: typeof import('./components/ui/accordion/AccordionTrigger.vue')['default']
|
||||
Alert: typeof import('./components/ui/alert/Alert.vue')['default']
|
||||
AlertDescription: typeof import('./components/ui/alert/AlertDescription.vue')['default']
|
||||
AlertDialog: typeof import('./components/ui/alert-dialog/AlertDialog.vue')['default']
|
||||
AlertDialogAction: typeof import('./components/ui/alert-dialog/AlertDialogAction.vue')['default']
|
||||
AlertDialogCancel: typeof import('./components/ui/alert-dialog/AlertDialogCancel.vue')['default']
|
||||
AlertDialogContent: typeof import('./components/ui/alert-dialog/AlertDialogContent.vue')['default']
|
||||
AlertDialogDescription: typeof import('./components/ui/alert-dialog/AlertDialogDescription.vue')['default']
|
||||
AlertDialogFooter: typeof import('./components/ui/alert-dialog/AlertDialogFooter.vue')['default']
|
||||
AlertDialogHeader: typeof import('./components/ui/alert-dialog/AlertDialogHeader.vue')['default']
|
||||
AlertDialogTitle: typeof import('./components/ui/alert-dialog/AlertDialogTitle.vue')['default']
|
||||
AlertDialogTrigger: typeof import('./components/ui/alert-dialog/AlertDialogTrigger.vue')['default']
|
||||
AlertTitle: typeof import('./components/ui/alert/AlertTitle.vue')['default']
|
||||
AppSidebar: typeof import('./components/AppSidebar.vue')['default']
|
||||
AspectRatio: typeof import('./components/ui/aspect-ratio/AspectRatio.vue')['default']
|
||||
Avatar: typeof import('./components/ui/avatar/Avatar.vue')['default']
|
||||
AvatarFallback: typeof import('./components/ui/avatar/AvatarFallback.vue')['default']
|
||||
AvatarImage: typeof import('./components/ui/avatar/AvatarImage.vue')['default']
|
||||
Badge: typeof import('./components/ui/badge/Badge.vue')['default']
|
||||
Breadcrumb: typeof import('./components/ui/breadcrumb/Breadcrumb.vue')['default']
|
||||
BreadcrumbEllipsis: typeof import('./components/ui/breadcrumb/BreadcrumbEllipsis.vue')['default']
|
||||
BreadcrumbItem: typeof import('./components/ui/breadcrumb/BreadcrumbItem.vue')['default']
|
||||
BreadcrumbLink: typeof import('./components/ui/breadcrumb/BreadcrumbLink.vue')['default']
|
||||
BreadcrumbList: typeof import('./components/ui/breadcrumb/BreadcrumbList.vue')['default']
|
||||
BreadcrumbPage: typeof import('./components/ui/breadcrumb/BreadcrumbPage.vue')['default']
|
||||
BreadcrumbSeparator: typeof import('./components/ui/breadcrumb/BreadcrumbSeparator.vue')['default']
|
||||
Button: typeof import('./components/ui/button/Button.vue')['default']
|
||||
ButtonGroup: typeof import('./components/ui/button-group/ButtonGroup.vue')['default']
|
||||
ButtonGroupSeparator: typeof import('./components/ui/button-group/ButtonGroupSeparator.vue')['default']
|
||||
ButtonGroupText: typeof import('./components/ui/button-group/ButtonGroupText.vue')['default']
|
||||
Calendar: typeof import('./components/ui/calendar/Calendar.vue')['default']
|
||||
CalendarCell: typeof import('./components/ui/calendar/CalendarCell.vue')['default']
|
||||
CalendarCellTrigger: typeof import('./components/ui/calendar/CalendarCellTrigger.vue')['default']
|
||||
CalendarGrid: typeof import('./components/ui/calendar/CalendarGrid.vue')['default']
|
||||
CalendarGridBody: typeof import('./components/ui/calendar/CalendarGridBody.vue')['default']
|
||||
CalendarGridHead: typeof import('./components/ui/calendar/CalendarGridHead.vue')['default']
|
||||
CalendarGridRow: typeof import('./components/ui/calendar/CalendarGridRow.vue')['default']
|
||||
CalendarHeadCell: typeof import('./components/ui/calendar/CalendarHeadCell.vue')['default']
|
||||
CalendarHeader: typeof import('./components/ui/calendar/CalendarHeader.vue')['default']
|
||||
CalendarHeading: typeof import('./components/ui/calendar/CalendarHeading.vue')['default']
|
||||
CalendarNextButton: typeof import('./components/ui/calendar/CalendarNextButton.vue')['default']
|
||||
CalendarPrevButton: typeof import('./components/ui/calendar/CalendarPrevButton.vue')['default']
|
||||
Card: typeof import('./components/ui/card/Card.vue')['default']
|
||||
CardContent: typeof import('./components/ui/card/CardContent.vue')['default']
|
||||
CardDescription: typeof import('./components/ui/card/CardDescription.vue')['default']
|
||||
CardFooter: typeof import('./components/ui/card/CardFooter.vue')['default']
|
||||
CardHeader: typeof import('./components/ui/card/CardHeader.vue')['default']
|
||||
CardTitle: typeof import('./components/ui/card/CardTitle.vue')['default']
|
||||
Carousel: typeof import('./components/ui/carousel/Carousel.vue')['default']
|
||||
CarouselContent: typeof import('./components/ui/carousel/CarouselContent.vue')['default']
|
||||
CarouselItem: typeof import('./components/ui/carousel/CarouselItem.vue')['default']
|
||||
CarouselNext: typeof import('./components/ui/carousel/CarouselNext.vue')['default']
|
||||
CarouselPrevious: typeof import('./components/ui/carousel/CarouselPrevious.vue')['default']
|
||||
ChartCrosshair: typeof import('./components/ui/chart/ChartCrosshair.vue')['default']
|
||||
ChartLegend: typeof import('./components/ui/chart/ChartLegend.vue')['default']
|
||||
ChartSingleTooltip: typeof import('./components/ui/chart/ChartSingleTooltip.vue')['default']
|
||||
ChartTooltip: typeof import('./components/ui/chart/ChartTooltip.vue')['default']
|
||||
Checkbox: typeof import('./components/ui/checkbox/Checkbox.vue')['default']
|
||||
Collapsible: typeof import('./components/ui/collapsible/Collapsible.vue')['default']
|
||||
CollapsibleContent: typeof import('./components/ui/collapsible/CollapsibleContent.vue')['default']
|
||||
CollapsibleTrigger: typeof import('./components/ui/collapsible/CollapsibleTrigger.vue')['default']
|
||||
Combobox: typeof import('./components/ui/combobox/Combobox.vue')['default']
|
||||
ComboboxAnchor: typeof import('./components/ui/combobox/ComboboxAnchor.vue')['default']
|
||||
ComboboxEmpty: typeof import('./components/ui/combobox/ComboboxEmpty.vue')['default']
|
||||
ComboboxGroup: typeof import('./components/ui/combobox/ComboboxGroup.vue')['default']
|
||||
ComboboxInput: typeof import('./components/ui/combobox/ComboboxInput.vue')['default']
|
||||
ComboboxItem: typeof import('./components/ui/combobox/ComboboxItem.vue')['default']
|
||||
ComboboxList: typeof import('./components/ui/combobox/ComboboxList.vue')['default']
|
||||
ComboboxSeparator: typeof import('./components/ui/combobox/ComboboxSeparator.vue')['default']
|
||||
ComboboxTrigger: typeof import('./components/ui/combobox/ComboboxTrigger.vue')['default']
|
||||
Command: typeof import('./components/ui/command/Command.vue')['default']
|
||||
CommandDialog: typeof import('./components/ui/command/CommandDialog.vue')['default']
|
||||
CommandEmpty: typeof import('./components/ui/command/CommandEmpty.vue')['default']
|
||||
CommandGroup: typeof import('./components/ui/command/CommandGroup.vue')['default']
|
||||
CommandInput: typeof import('./components/ui/command/CommandInput.vue')['default']
|
||||
CommandItem: typeof import('./components/ui/command/CommandItem.vue')['default']
|
||||
CommandList: typeof import('./components/ui/command/CommandList.vue')['default']
|
||||
CommandSeparator: typeof import('./components/ui/command/CommandSeparator.vue')['default']
|
||||
CommandShortcut: typeof import('./components/ui/command/CommandShortcut.vue')['default']
|
||||
ContextMenu: typeof import('./components/ui/context-menu/ContextMenu.vue')['default']
|
||||
ContextMenuCheckboxItem: typeof import('./components/ui/context-menu/ContextMenuCheckboxItem.vue')['default']
|
||||
ContextMenuContent: typeof import('./components/ui/context-menu/ContextMenuContent.vue')['default']
|
||||
ContextMenuGroup: typeof import('./components/ui/context-menu/ContextMenuGroup.vue')['default']
|
||||
ContextMenuItem: typeof import('./components/ui/context-menu/ContextMenuItem.vue')['default']
|
||||
ContextMenuLabel: typeof import('./components/ui/context-menu/ContextMenuLabel.vue')['default']
|
||||
ContextMenuPortal: typeof import('./components/ui/context-menu/ContextMenuPortal.vue')['default']
|
||||
ContextMenuRadioGroup: typeof import('./components/ui/context-menu/ContextMenuRadioGroup.vue')['default']
|
||||
ContextMenuRadioItem: typeof import('./components/ui/context-menu/ContextMenuRadioItem.vue')['default']
|
||||
ContextMenuSeparator: typeof import('./components/ui/context-menu/ContextMenuSeparator.vue')['default']
|
||||
ContextMenuShortcut: typeof import('./components/ui/context-menu/ContextMenuShortcut.vue')['default']
|
||||
ContextMenuSub: typeof import('./components/ui/context-menu/ContextMenuSub.vue')['default']
|
||||
ContextMenuSubContent: typeof import('./components/ui/context-menu/ContextMenuSubContent.vue')['default']
|
||||
ContextMenuSubTrigger: typeof import('./components/ui/context-menu/ContextMenuSubTrigger.vue')['default']
|
||||
ContextMenuTrigger: typeof import('./components/ui/context-menu/ContextMenuTrigger.vue')['default']
|
||||
DarkModeSwitcher: typeof import('./components/DarkModeSwitcher.vue')['default']
|
||||
Dialog: typeof import('./components/ui/dialog/Dialog.vue')['default']
|
||||
DialogClose: typeof import('./components/ui/dialog/DialogClose.vue')['default']
|
||||
DialogContent: typeof import('./components/ui/dialog/DialogContent.vue')['default']
|
||||
DialogDescription: typeof import('./components/ui/dialog/DialogDescription.vue')['default']
|
||||
DialogFooter: typeof import('./components/ui/dialog/DialogFooter.vue')['default']
|
||||
DialogHeader: typeof import('./components/ui/dialog/DialogHeader.vue')['default']
|
||||
DialogScrollContent: typeof import('./components/ui/dialog/DialogScrollContent.vue')['default']
|
||||
DialogTitle: typeof import('./components/ui/dialog/DialogTitle.vue')['default']
|
||||
DialogTrigger: typeof import('./components/ui/dialog/DialogTrigger.vue')['default']
|
||||
Drawer: typeof import('./components/ui/drawer/Drawer.vue')['default']
|
||||
DrawerContent: typeof import('./components/ui/drawer/DrawerContent.vue')['default']
|
||||
DrawerDescription: typeof import('./components/ui/drawer/DrawerDescription.vue')['default']
|
||||
DrawerFooter: typeof import('./components/ui/drawer/DrawerFooter.vue')['default']
|
||||
DrawerHeader: typeof import('./components/ui/drawer/DrawerHeader.vue')['default']
|
||||
DrawerOverlay: typeof import('./components/ui/drawer/DrawerOverlay.vue')['default']
|
||||
DrawerTitle: typeof import('./components/ui/drawer/DrawerTitle.vue')['default']
|
||||
DropdownMenu: typeof import('./components/ui/dropdown-menu/DropdownMenu.vue')['default']
|
||||
DropdownMenuCheckboxItem: typeof import('./components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue')['default']
|
||||
DropdownMenuContent: typeof import('./components/ui/dropdown-menu/DropdownMenuContent.vue')['default']
|
||||
DropdownMenuGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuGroup.vue')['default']
|
||||
DropdownMenuItem: typeof import('./components/ui/dropdown-menu/DropdownMenuItem.vue')['default']
|
||||
DropdownMenuLabel: typeof import('./components/ui/dropdown-menu/DropdownMenuLabel.vue')['default']
|
||||
DropdownMenuRadioGroup: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioGroup.vue')['default']
|
||||
DropdownMenuRadioItem: typeof import('./components/ui/dropdown-menu/DropdownMenuRadioItem.vue')['default']
|
||||
DropdownMenuSeparator: typeof import('./components/ui/dropdown-menu/DropdownMenuSeparator.vue')['default']
|
||||
DropdownMenuShortcut: typeof import('./components/ui/dropdown-menu/DropdownMenuShortcut.vue')['default']
|
||||
DropdownMenuSub: typeof import('./components/ui/dropdown-menu/DropdownMenuSub.vue')['default']
|
||||
DropdownMenuSubContent: typeof import('./components/ui/dropdown-menu/DropdownMenuSubContent.vue')['default']
|
||||
DropdownMenuSubTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuSubTrigger.vue')['default']
|
||||
DropdownMenuTrigger: typeof import('./components/ui/dropdown-menu/DropdownMenuTrigger.vue')['default']
|
||||
Empty: typeof import('./components/ui/empty/Empty.vue')['default']
|
||||
EmptyContent: typeof import('./components/ui/empty/EmptyContent.vue')['default']
|
||||
EmptyDescription: typeof import('./components/ui/empty/EmptyDescription.vue')['default']
|
||||
EmptyHeader: typeof import('./components/ui/empty/EmptyHeader.vue')['default']
|
||||
EmptyMedia: typeof import('./components/ui/empty/EmptyMedia.vue')['default']
|
||||
EmptyTitle: typeof import('./components/ui/empty/EmptyTitle.vue')['default']
|
||||
Field: typeof import('./components/ui/field/Field.vue')['default']
|
||||
FieldContent: typeof import('./components/ui/field/FieldContent.vue')['default']
|
||||
FieldDescription: typeof import('./components/ui/field/FieldDescription.vue')['default']
|
||||
FieldError: typeof import('./components/ui/field/FieldError.vue')['default']
|
||||
FieldGroup: typeof import('./components/ui/field/FieldGroup.vue')['default']
|
||||
FieldLabel: typeof import('./components/ui/field/FieldLabel.vue')['default']
|
||||
FieldLegend: typeof import('./components/ui/field/FieldLegend.vue')['default']
|
||||
FieldSeparator: typeof import('./components/ui/field/FieldSeparator.vue')['default']
|
||||
FieldSet: typeof import('./components/ui/field/FieldSet.vue')['default']
|
||||
FieldTitle: typeof import('./components/ui/field/FieldTitle.vue')['default']
|
||||
FormControl: typeof import('./components/ui/form/FormControl.vue')['default']
|
||||
FormDescription: typeof import('./components/ui/form/FormDescription.vue')['default']
|
||||
FormItem: typeof import('./components/ui/form/FormItem.vue')['default']
|
||||
FormLabel: typeof import('./components/ui/form/FormLabel.vue')['default']
|
||||
FormMessage: typeof import('./components/ui/form/FormMessage.vue')['default']
|
||||
HoverCard: typeof import('./components/ui/hover-card/HoverCard.vue')['default']
|
||||
HoverCardContent: typeof import('./components/ui/hover-card/HoverCardContent.vue')['default']
|
||||
HoverCardTrigger: typeof import('./components/ui/hover-card/HoverCardTrigger.vue')['default']
|
||||
Input: typeof import('./components/ui/input/Input.vue')['default']
|
||||
InputGroup: typeof import('./components/ui/input-group/InputGroup.vue')['default']
|
||||
InputGroupAddon: typeof import('./components/ui/input-group/InputGroupAddon.vue')['default']
|
||||
InputGroupButton: typeof import('./components/ui/input-group/InputGroupButton.vue')['default']
|
||||
InputGroupInput: typeof import('./components/ui/input-group/InputGroupInput.vue')['default']
|
||||
InputGroupText: typeof import('./components/ui/input-group/InputGroupText.vue')['default']
|
||||
InputGroupTextarea: typeof import('./components/ui/input-group/InputGroupTextarea.vue')['default']
|
||||
Item: typeof import('./components/ui/item/Item.vue')['default']
|
||||
ItemActions: typeof import('./components/ui/item/ItemActions.vue')['default']
|
||||
ItemContent: typeof import('./components/ui/item/ItemContent.vue')['default']
|
||||
ItemDescription: typeof import('./components/ui/item/ItemDescription.vue')['default']
|
||||
ItemFooter: typeof import('./components/ui/item/ItemFooter.vue')['default']
|
||||
ItemGroup: typeof import('./components/ui/item/ItemGroup.vue')['default']
|
||||
ItemHeader: typeof import('./components/ui/item/ItemHeader.vue')['default']
|
||||
ItemMedia: typeof import('./components/ui/item/ItemMedia.vue')['default']
|
||||
ItemSeparator: typeof import('./components/ui/item/ItemSeparator.vue')['default']
|
||||
ItemTitle: typeof import('./components/ui/item/ItemTitle.vue')['default']
|
||||
Kbd: typeof import('./components/ui/kbd/Kbd.vue')['default']
|
||||
KbdGroup: typeof import('./components/ui/kbd/KbdGroup.vue')['default']
|
||||
Label: typeof import('./components/ui/label/Label.vue')['default']
|
||||
LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default']
|
||||
Menubar: typeof import('./components/ui/menubar/Menubar.vue')['default']
|
||||
MenubarCheckboxItem: typeof import('./components/ui/menubar/MenubarCheckboxItem.vue')['default']
|
||||
MenubarContent: typeof import('./components/ui/menubar/MenubarContent.vue')['default']
|
||||
MenubarGroup: typeof import('./components/ui/menubar/MenubarGroup.vue')['default']
|
||||
MenubarItem: typeof import('./components/ui/menubar/MenubarItem.vue')['default']
|
||||
MenubarLabel: typeof import('./components/ui/menubar/MenubarLabel.vue')['default']
|
||||
MenubarMenu: typeof import('./components/ui/menubar/MenubarMenu.vue')['default']
|
||||
MenubarRadioGroup: typeof import('./components/ui/menubar/MenubarRadioGroup.vue')['default']
|
||||
MenubarRadioItem: typeof import('./components/ui/menubar/MenubarRadioItem.vue')['default']
|
||||
MenubarSeparator: typeof import('./components/ui/menubar/MenubarSeparator.vue')['default']
|
||||
MenubarShortcut: typeof import('./components/ui/menubar/MenubarShortcut.vue')['default']
|
||||
MenubarSub: typeof import('./components/ui/menubar/MenubarSub.vue')['default']
|
||||
MenubarSubContent: typeof import('./components/ui/menubar/MenubarSubContent.vue')['default']
|
||||
MenubarSubTrigger: typeof import('./components/ui/menubar/MenubarSubTrigger.vue')['default']
|
||||
MenubarTrigger: typeof import('./components/ui/menubar/MenubarTrigger.vue')['default']
|
||||
NavigationMenu: typeof import('./components/ui/navigation-menu/NavigationMenu.vue')['default']
|
||||
NavigationMenuContent: typeof import('./components/ui/navigation-menu/NavigationMenuContent.vue')['default']
|
||||
NavigationMenuIndicator: typeof import('./components/ui/navigation-menu/NavigationMenuIndicator.vue')['default']
|
||||
NavigationMenuItem: typeof import('./components/ui/navigation-menu/NavigationMenuItem.vue')['default']
|
||||
NavigationMenuLink: typeof import('./components/ui/navigation-menu/NavigationMenuLink.vue')['default']
|
||||
NavigationMenuList: typeof import('./components/ui/navigation-menu/NavigationMenuList.vue')['default']
|
||||
NavigationMenuTrigger: typeof import('./components/ui/navigation-menu/NavigationMenuTrigger.vue')['default']
|
||||
NavigationMenuViewport: typeof import('./components/ui/navigation-menu/NavigationMenuViewport.vue')['default']
|
||||
NumberField: typeof import('./components/ui/number-field/NumberField.vue')['default']
|
||||
NumberFieldContent: typeof import('./components/ui/number-field/NumberFieldContent.vue')['default']
|
||||
NumberFieldDecrement: typeof import('./components/ui/number-field/NumberFieldDecrement.vue')['default']
|
||||
NumberFieldIncrement: typeof import('./components/ui/number-field/NumberFieldIncrement.vue')['default']
|
||||
NumberFieldInput: typeof import('./components/ui/number-field/NumberFieldInput.vue')['default']
|
||||
Pagination: typeof import('./components/ui/pagination/Pagination.vue')['default']
|
||||
PaginationContent: typeof import('./components/ui/pagination/PaginationContent.vue')['default']
|
||||
PaginationEllipsis: typeof import('./components/ui/pagination/PaginationEllipsis.vue')['default']
|
||||
PaginationFirst: typeof import('./components/ui/pagination/PaginationFirst.vue')['default']
|
||||
PaginationItem: typeof import('./components/ui/pagination/PaginationItem.vue')['default']
|
||||
PaginationLast: typeof import('./components/ui/pagination/PaginationLast.vue')['default']
|
||||
PaginationNext: typeof import('./components/ui/pagination/PaginationNext.vue')['default']
|
||||
PaginationPrevious: typeof import('./components/ui/pagination/PaginationPrevious.vue')['default']
|
||||
PinInput: typeof import('./components/ui/pin-input/PinInput.vue')['default']
|
||||
PinInputGroup: typeof import('./components/ui/pin-input/PinInputGroup.vue')['default']
|
||||
PinInputSeparator: typeof import('./components/ui/pin-input/PinInputSeparator.vue')['default']
|
||||
PinInputSlot: typeof import('./components/ui/pin-input/PinInputSlot.vue')['default']
|
||||
Popover: typeof import('./components/ui/popover/Popover.vue')['default']
|
||||
PopoverContent: typeof import('./components/ui/popover/PopoverContent.vue')['default']
|
||||
PopoverTrigger: typeof import('./components/ui/popover/PopoverTrigger.vue')['default']
|
||||
Progress: typeof import('./components/ui/progress/Progress.vue')['default']
|
||||
RadioGroup: typeof import('./components/ui/radio-group/RadioGroup.vue')['default']
|
||||
RadioGroupItem: typeof import('./components/ui/radio-group/RadioGroupItem.vue')['default']
|
||||
RangeCalendar: typeof import('./components/ui/range-calendar/RangeCalendar.vue')['default']
|
||||
RangeCalendarCell: typeof import('./components/ui/range-calendar/RangeCalendarCell.vue')['default']
|
||||
RangeCalendarCellTrigger: typeof import('./components/ui/range-calendar/RangeCalendarCellTrigger.vue')['default']
|
||||
RangeCalendarGrid: typeof import('./components/ui/range-calendar/RangeCalendarGrid.vue')['default']
|
||||
RangeCalendarGridBody: typeof import('./components/ui/range-calendar/RangeCalendarGridBody.vue')['default']
|
||||
RangeCalendarGridHead: typeof import('./components/ui/range-calendar/RangeCalendarGridHead.vue')['default']
|
||||
RangeCalendarGridRow: typeof import('./components/ui/range-calendar/RangeCalendarGridRow.vue')['default']
|
||||
RangeCalendarHeadCell: typeof import('./components/ui/range-calendar/RangeCalendarHeadCell.vue')['default']
|
||||
RangeCalendarHeader: typeof import('./components/ui/range-calendar/RangeCalendarHeader.vue')['default']
|
||||
RangeCalendarHeading: typeof import('./components/ui/range-calendar/RangeCalendarHeading.vue')['default']
|
||||
RangeCalendarNextButton: typeof import('./components/ui/range-calendar/RangeCalendarNextButton.vue')['default']
|
||||
RangeCalendarPrevButton: typeof import('./components/ui/range-calendar/RangeCalendarPrevButton.vue')['default']
|
||||
README: typeof import('./components/README.md')['default']
|
||||
ResizableHandle: typeof import('./components/ui/resizable/ResizableHandle.vue')['default']
|
||||
ResizablePanelGroup: typeof import('./components/ui/resizable/ResizablePanelGroup.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollArea: typeof import('./components/ui/scroll-area/ScrollArea.vue')['default']
|
||||
ScrollBar: typeof import('./components/ui/scroll-area/ScrollBar.vue')['default']
|
||||
Select: typeof import('./components/ui/select/Select.vue')['default']
|
||||
SelectContent: typeof import('./components/ui/select/SelectContent.vue')['default']
|
||||
SelectGroup: typeof import('./components/ui/select/SelectGroup.vue')['default']
|
||||
SelectItem: typeof import('./components/ui/select/SelectItem.vue')['default']
|
||||
SelectItemText: typeof import('./components/ui/select/SelectItemText.vue')['default']
|
||||
SelectLabel: typeof import('./components/ui/select/SelectLabel.vue')['default']
|
||||
SelectScrollDownButton: typeof import('./components/ui/select/SelectScrollDownButton.vue')['default']
|
||||
SelectScrollUpButton: typeof import('./components/ui/select/SelectScrollUpButton.vue')['default']
|
||||
SelectSeparator: typeof import('./components/ui/select/SelectSeparator.vue')['default']
|
||||
SelectTrigger: typeof import('./components/ui/select/SelectTrigger.vue')['default']
|
||||
SelectValue: typeof import('./components/ui/select/SelectValue.vue')['default']
|
||||
Separator: typeof import('./components/ui/separator/Separator.vue')['default']
|
||||
Sheet: typeof import('./components/ui/sheet/Sheet.vue')['default']
|
||||
SheetClose: typeof import('./components/ui/sheet/SheetClose.vue')['default']
|
||||
SheetContent: typeof import('./components/ui/sheet/SheetContent.vue')['default']
|
||||
SheetDescription: typeof import('./components/ui/sheet/SheetDescription.vue')['default']
|
||||
SheetFooter: typeof import('./components/ui/sheet/SheetFooter.vue')['default']
|
||||
SheetHeader: typeof import('./components/ui/sheet/SheetHeader.vue')['default']
|
||||
SheetTitle: typeof import('./components/ui/sheet/SheetTitle.vue')['default']
|
||||
SheetTrigger: typeof import('./components/ui/sheet/SheetTrigger.vue')['default']
|
||||
Sidebar: typeof import('./components/ui/sidebar/Sidebar.vue')['default']
|
||||
SidebarContent: typeof import('./components/ui/sidebar/SidebarContent.vue')['default']
|
||||
SidebarFooter: typeof import('./components/ui/sidebar/SidebarFooter.vue')['default']
|
||||
SidebarGroup: typeof import('./components/ui/sidebar/SidebarGroup.vue')['default']
|
||||
SidebarGroupAction: typeof import('./components/ui/sidebar/SidebarGroupAction.vue')['default']
|
||||
SidebarGroupContent: typeof import('./components/ui/sidebar/SidebarGroupContent.vue')['default']
|
||||
SidebarGroupLabel: typeof import('./components/ui/sidebar/SidebarGroupLabel.vue')['default']
|
||||
SidebarHeader: typeof import('./components/ui/sidebar/SidebarHeader.vue')['default']
|
||||
SidebarInput: typeof import('./components/ui/sidebar/SidebarInput.vue')['default']
|
||||
SidebarInset: typeof import('./components/ui/sidebar/SidebarInset.vue')['default']
|
||||
SidebarMenu: typeof import('./components/ui/sidebar/SidebarMenu.vue')['default']
|
||||
SidebarMenuAction: typeof import('./components/ui/sidebar/SidebarMenuAction.vue')['default']
|
||||
SidebarMenuBadge: typeof import('./components/ui/sidebar/SidebarMenuBadge.vue')['default']
|
||||
SidebarMenuButton: typeof import('./components/ui/sidebar/SidebarMenuButton.vue')['default']
|
||||
SidebarMenuButtonChild: typeof import('./components/ui/sidebar/SidebarMenuButtonChild.vue')['default']
|
||||
SidebarMenuItem: typeof import('./components/ui/sidebar/SidebarMenuItem.vue')['default']
|
||||
SidebarMenuSkeleton: typeof import('./components/ui/sidebar/SidebarMenuSkeleton.vue')['default']
|
||||
SidebarMenuSub: typeof import('./components/ui/sidebar/SidebarMenuSub.vue')['default']
|
||||
SidebarMenuSubButton: typeof import('./components/ui/sidebar/SidebarMenuSubButton.vue')['default']
|
||||
SidebarMenuSubItem: typeof import('./components/ui/sidebar/SidebarMenuSubItem.vue')['default']
|
||||
SidebarProvider: typeof import('./components/ui/sidebar/SidebarProvider.vue')['default']
|
||||
SidebarRail: typeof import('./components/ui/sidebar/SidebarRail.vue')['default']
|
||||
SidebarSeparator: typeof import('./components/ui/sidebar/SidebarSeparator.vue')['default']
|
||||
SidebarTrigger: typeof import('./components/ui/sidebar/SidebarTrigger.vue')['default']
|
||||
SimulationScene: typeof import('./components/SimulationScene.vue')['default']
|
||||
Skeleton: typeof import('./components/ui/skeleton/Skeleton.vue')['default']
|
||||
Slider: typeof import('./components/ui/slider/Slider.vue')['default']
|
||||
Sonner: typeof import('./components/ui/sonner/Sonner.vue')['default']
|
||||
Spinner: typeof import('./components/ui/spinner/Spinner.vue')['default']
|
||||
Stepper: typeof import('./components/ui/stepper/Stepper.vue')['default']
|
||||
StepperDescription: typeof import('./components/ui/stepper/StepperDescription.vue')['default']
|
||||
StepperIndicator: typeof import('./components/ui/stepper/StepperIndicator.vue')['default']
|
||||
StepperItem: typeof import('./components/ui/stepper/StepperItem.vue')['default']
|
||||
StepperSeparator: typeof import('./components/ui/stepper/StepperSeparator.vue')['default']
|
||||
StepperTitle: typeof import('./components/ui/stepper/StepperTitle.vue')['default']
|
||||
StepperTrigger: typeof import('./components/ui/stepper/StepperTrigger.vue')['default']
|
||||
Switch: typeof import('./components/ui/switch/Switch.vue')['default']
|
||||
Table: typeof import('./components/ui/table/Table.vue')['default']
|
||||
TableBody: typeof import('./components/ui/table/TableBody.vue')['default']
|
||||
TableCaption: typeof import('./components/ui/table/TableCaption.vue')['default']
|
||||
TableCell: typeof import('./components/ui/table/TableCell.vue')['default']
|
||||
TableEmpty: typeof import('./components/ui/table/TableEmpty.vue')['default']
|
||||
TableFooter: typeof import('./components/ui/table/TableFooter.vue')['default']
|
||||
TableHead: typeof import('./components/ui/table/TableHead.vue')['default']
|
||||
TableHeader: typeof import('./components/ui/table/TableHeader.vue')['default']
|
||||
TableRow: typeof import('./components/ui/table/TableRow.vue')['default']
|
||||
Tabs: typeof import('./components/ui/tabs/Tabs.vue')['default']
|
||||
TabsContent: typeof import('./components/ui/tabs/TabsContent.vue')['default']
|
||||
TabsList: typeof import('./components/ui/tabs/TabsList.vue')['default']
|
||||
TabsTrigger: typeof import('./components/ui/tabs/TabsTrigger.vue')['default']
|
||||
TagsInput: typeof import('./components/ui/tags-input/TagsInput.vue')['default']
|
||||
TagsInputInput: typeof import('./components/ui/tags-input/TagsInputInput.vue')['default']
|
||||
TagsInputItem: typeof import('./components/ui/tags-input/TagsInputItem.vue')['default']
|
||||
TagsInputItemDelete: typeof import('./components/ui/tags-input/TagsInputItemDelete.vue')['default']
|
||||
TagsInputItemText: typeof import('./components/ui/tags-input/TagsInputItemText.vue')['default']
|
||||
Textarea: typeof import('./components/ui/textarea/Textarea.vue')['default']
|
||||
TheInput: typeof import('./components/TheInput.vue')['default']
|
||||
Toggle: typeof import('./components/ui/toggle/Toggle.vue')['default']
|
||||
ToggleGroup: typeof import('./components/ui/toggle-group/ToggleGroup.vue')['default']
|
||||
ToggleGroupItem: typeof import('./components/ui/toggle-group/ToggleGroupItem.vue')['default']
|
||||
Tooltip: typeof import('./components/ui/tooltip/Tooltip.vue')['default']
|
||||
TooltipContent: typeof import('./components/ui/tooltip/TooltipContent.vue')['default']
|
||||
TooltipProvider: typeof import('./components/ui/tooltip/TooltipProvider.vue')['default']
|
||||
TooltipTrigger: typeof import('./components/ui/tooltip/TooltipTrigger.vue')['default']
|
||||
}
|
||||
}
|
||||
59
defense_sim_v2/frontend/src/components/AppSidebar.vue
Normal file
59
defense_sim_v2/frontend/src/components/AppSidebar.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutDashboard, Settings } from 'lucide-vue-next'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@/components/ui/sidebar'
|
||||
import DarkModeSwitcher from '~/components/DarkModeSwitcher.vue'
|
||||
import LanguageSwitcher from '~/components/LanguageSwitcher.vue'
|
||||
|
||||
const BRAND_NAME = import.meta.env.VITE_BRAND_NAME || 'Vitesse'
|
||||
|
||||
const navItems = [
|
||||
{ title: 'Dashboard', url: '/dashboard', icon: LayoutDashboard },
|
||||
{ title: 'Settings', url: '/settings', icon: Settings },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sidebar>
|
||||
<SidebarHeader>
|
||||
<div class="px-2 py-1 flex gap-2 items-center">
|
||||
<span class="text-foreground font-semibold">{{ BRAND_NAME }}</span>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Application</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in navItems" :key="item.title">
|
||||
<SidebarMenuButton as-child>
|
||||
<RouterLink :to="item.url">
|
||||
<component :is="item.icon" class="h-4 w-4" />
|
||||
<span>{{ item.title }}</span>
|
||||
</RouterLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<div class="p-2 flex flex-col gap-1">
|
||||
<LanguageSwitcher />
|
||||
<DarkModeSwitcher show-label />
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
</template>
|
||||
47
defense_sim_v2/frontend/src/components/DarkModeSwitcher.vue
Normal file
47
defense_sim_v2/frontend/src/components/DarkModeSwitcher.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from '~/modules/i18n'
|
||||
|
||||
defineProps<{ showLabel?: boolean }>()
|
||||
|
||||
const mode = useColorMode({ disableTransition: false })
|
||||
const isDark = computed(() => mode.value === 'dark')
|
||||
const { t } = useI18n()
|
||||
|
||||
function toggle() {
|
||||
mode.value = isDark.value ? 'light' : 'dark'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="p-2 text-left rounded-md flex w-full transition-colors items-center hover:bg-accent"
|
||||
:aria-label="t('button.toggle_dark')"
|
||||
@click="toggle"
|
||||
>
|
||||
<svg
|
||||
v-if="isDark"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="shrink-0 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<span v-if="showLabel" class="text-sm ml-2">
|
||||
{{ isDark ? t('themeToggle.dark', 'Dark Mode') : t('themeToggle.light', 'Light Mode') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
66
defense_sim_v2/frontend/src/components/LanguageSwitcher.vue
Normal file
66
defense_sim_v2/frontend/src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDown, Globe } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
import { availableLocales, loadLanguageAsync, useI18n } from '~/modules/i18n'
|
||||
|
||||
const languageNames: Record<string, string> = {
|
||||
'en': 'English',
|
||||
'ru': 'Русский',
|
||||
'ar': 'العربية',
|
||||
'pt': 'Português',
|
||||
'pt-BR': 'Português (Brasil)',
|
||||
'fr': 'Français',
|
||||
'de': 'Deutsch',
|
||||
'es': 'Español',
|
||||
'it': 'Italiano',
|
||||
'nl': 'Nederlands',
|
||||
'pl': 'Polski',
|
||||
'sv': 'Svenska',
|
||||
'tr': 'Türkçe',
|
||||
'ja': '日本語',
|
||||
'ko': '한국어',
|
||||
'zh-CN': '简体中文',
|
||||
'uk': 'Українська',
|
||||
'uz': 'Oʻzbek',
|
||||
'ka': 'ქართული',
|
||||
'vi': 'Tiếng Việt',
|
||||
'id': 'Indonesia',
|
||||
}
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
const availableLanguages = computed(() => {
|
||||
const result: Record<string, string> = {}
|
||||
for (const loc of availableLocales)
|
||||
result[loc] = languageNames[loc] || loc
|
||||
return result
|
||||
})
|
||||
|
||||
const currentLanguage = computed(() => languageNames[locale.value] || locale.value)
|
||||
|
||||
async function changeLanguage(newLocale: string) {
|
||||
await loadLanguageAsync(newLocale)
|
||||
locale.value = newLocale
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button class="text-sm p-2 text-left rounded-md flex w-full transition-colors items-center hover:bg-accent">
|
||||
<Globe class="mr-2 shrink-0 h-4 w-4" />
|
||||
{{ currentLanguage }}
|
||||
<ChevronDown class="ml-auto h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
v-for="(name, loc) in availableLanguages"
|
||||
:key="loc"
|
||||
@click="changeLanguage(loc)"
|
||||
>
|
||||
{{ name }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
9
defense_sim_v2/frontend/src/components/README.md
Normal file
9
defense_sim_v2/frontend/src/components/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## Components
|
||||
|
||||
Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).
|
||||
|
||||
### Icons
|
||||
|
||||
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
|
||||
|
||||
It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details.
|
||||
189
defense_sim_v2/frontend/src/components/SimulationScene.vue
Normal file
189
defense_sim_v2/frontend/src/components/SimulationScene.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { AmbientLight, BufferGeometry, ConeGeometry, GridHelper, Group, Line, LineBasicMaterial, Matrix4, Mesh, MeshStandardMaterial, PerspectiveCamera, Quaternion, RingGeometry, Scene, Vector3, WebGLRenderer } from 'three'
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
import type { SimulationEntity, SimulationState } from '~/types/simulation'
|
||||
|
||||
const props = defineProps<{ state: SimulationState | null }>()
|
||||
const host = ref<HTMLDivElement>()
|
||||
let renderer: WebGLRenderer
|
||||
let camera: PerspectiveCamera
|
||||
let scene: Scene
|
||||
let controls: OrbitControls
|
||||
let observer: ResizeObserver
|
||||
let frame = 0
|
||||
const entities = new Map<string, { body: Mesh, trail: Line }>()
|
||||
const up = new Vector3(0, 1, 0)
|
||||
const color = { projectile: 0xff6258, drone: 0x5ca8ff }
|
||||
|
||||
function groundPoint(ned: number[]) {
|
||||
return new Vector3(ned[1] || 0, -(ned[2] || 0), -(ned[0] || 0))
|
||||
}
|
||||
|
||||
function makeBody(entity: SimulationEntity) {
|
||||
const isProjectile = entity.type === 'projectile'
|
||||
const geometry = isProjectile ? new ConeGeometry(180, 700, 6) : new ConeGeometry(280, 850, 4)
|
||||
if (!isProjectile)
|
||||
geometry.rotateZ(-Math.PI / 2)
|
||||
const material = new MeshStandardMaterial({ color: isProjectile ? color.projectile : color.drone, emissive: isProjectile ? 0x40100c : 0x0b2138, roughness: 0.5 })
|
||||
const body = new Mesh(geometry, material)
|
||||
body.userData.kind = entity.type
|
||||
const trailMaterial = new LineBasicMaterial({ color: isProjectile ? color.projectile : color.drone, transparent: true, opacity: isProjectile ? 0.72 : 0.4 })
|
||||
const trail = new Line(new BufferGeometry(), trailMaterial)
|
||||
scene.add(body, trail)
|
||||
return { body, trail }
|
||||
}
|
||||
|
||||
function updateScene(state: SimulationState | null) {
|
||||
if (!scene)
|
||||
return
|
||||
const environment = (state?.environment ?? {}) as Record<string, unknown>
|
||||
const cityCenter = Array.isArray(environment.city_center_ned_m) ? environment.city_center_ned_m as number[] : [0, 0, 0]
|
||||
const center = groundPoint(cityCenter)
|
||||
const radius = typeof environment.city_radius_m === 'number' ? environment.city_radius_m : 10_000
|
||||
const defenseRadius = typeof environment.defense_ring_radius_m === 'number' ? environment.defense_ring_radius_m : radius + 8_000
|
||||
// Static scene guides are replaced when scenario dimensions change.
|
||||
const oldGuides = scene.getObjectByName('scenario-guides') as Group | undefined
|
||||
const radarSites = Array.isArray(state?.devices?.radars) ? state.devices.radars as Array<{ id: string, position_ned_m: number[] }> : []
|
||||
const guideKey = `${radius}:${defenseRadius}:${center.toArray().join(',')}:${radarSites.map(site => site.position_ned_m.join(',')).join(';')}`
|
||||
if (oldGuides?.userData.key !== guideKey) {
|
||||
if (oldGuides) {
|
||||
scene.remove(oldGuides)
|
||||
oldGuides.traverse((obj) => {
|
||||
if (obj instanceof Mesh) {
|
||||
obj.geometry.dispose()
|
||||
const mat = obj.material
|
||||
if (Array.isArray(mat)) mat.forEach(m => m.dispose())
|
||||
else mat.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
const guides = new Group()
|
||||
guides.name = 'scenario-guides'
|
||||
guides.userData.key = guideKey
|
||||
const grid = new GridHelper(Math.max(defenseRadius * 3, 20_000), 40, 0x394738, 0x202a21)
|
||||
grid.position.set(center.x, -1, center.z)
|
||||
guides.add(grid)
|
||||
const city = new Mesh(new RingGeometry(radius - Math.max(radius * 0.006, 50), radius, 96), new MeshStandardMaterial({ color: 0xa9ba75, side: 2, transparent: true, opacity: 0.7 }))
|
||||
city.rotation.x = -Math.PI / 2
|
||||
city.position.set(center.x, 2, center.z)
|
||||
guides.add(city)
|
||||
const defense = new Mesh(new RingGeometry(defenseRadius - Math.max(radius * 0.004, 40), defenseRadius, 96), new MeshStandardMaterial({ color: 0x547a9a, side: 2, transparent: true, opacity: 0.5 }))
|
||||
defense.rotation.x = -Math.PI / 2
|
||||
defense.position.set(center.x, 1, center.z)
|
||||
guides.add(defense)
|
||||
for (const site of radarSites) {
|
||||
const marker = new Mesh(new ConeGeometry(220, 650, 4), new MeshStandardMaterial({ color: 0xf0c76d, emissive: 0x352807 }))
|
||||
marker.name = site.id
|
||||
marker.position.copy(groundPoint(site.position_ned_m))
|
||||
guides.add(marker)
|
||||
}
|
||||
scene.add(guides)
|
||||
const span = Math.max(defenseRadius * 2.9, 30_000)
|
||||
camera.far = span * 7
|
||||
camera.position.set(center.x + span * 0.48, span * 0.68, center.z + span * 0.8)
|
||||
camera.updateProjectionMatrix()
|
||||
controls.target.copy(center)
|
||||
controls.update()
|
||||
}
|
||||
|
||||
const currentIds = new Set((state?.entities ?? []).map(entity => entity.id))
|
||||
for (const [id, item] of entities) {
|
||||
if (!currentIds.has(id)) {
|
||||
scene.remove(item.body, item.trail)
|
||||
item.body.geometry.dispose()
|
||||
;(item.body.material as MeshStandardMaterial).dispose()
|
||||
item.trail.geometry.dispose()
|
||||
;(item.trail.material as LineBasicMaterial).dispose()
|
||||
entities.delete(id)
|
||||
}
|
||||
}
|
||||
for (const entity of state?.entities ?? []) {
|
||||
let item = entities.get(entity.id)
|
||||
if (!item) {
|
||||
item = makeBody(entity)
|
||||
entities.set(entity.id, item)
|
||||
}
|
||||
item.body.position.copy(groundPoint(entity.position_ned_m))
|
||||
if (entity.orientation_quaternion_wxyz?.length === 4) {
|
||||
const [w, x, y, z] = entity.orientation_quaternion_wxyz
|
||||
const fromNed = new Matrix4().set(0, 1, 0, 0, 0, 0, -1, 0, -1, 0, 0, 0, 0, 0, 0, 1)
|
||||
const orientation = new Matrix4().makeRotationFromQuaternion(new Quaternion(x, y, z, w))
|
||||
item.body.quaternion.setFromRotationMatrix(fromNed.clone().multiply(orientation).multiply(fromNed.clone().invert()))
|
||||
}
|
||||
else {
|
||||
const velocity = groundPoint(entity.velocity_ned_m_s)
|
||||
if (velocity.lengthSq() > 1e-8)
|
||||
item.body.quaternion.copy(new Quaternion().setFromUnitVectors(up, velocity.normalize()))
|
||||
}
|
||||
item.body.visible = entity.active || entity.type !== 'projectile'
|
||||
const samples = (entity.trajectory_ned_m ?? []).map(groundPoint)
|
||||
item.trail.geometry.dispose()
|
||||
item.trail.geometry = new BufferGeometry().setFromPoints(samples)
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
frame = requestAnimationFrame(render)
|
||||
controls.update()
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scene = new Scene()
|
||||
scene.background = null
|
||||
scene.add(new AmbientLight(0xffffff, 1.6))
|
||||
const keyLight = new AmbientLight(0xd3e6bc, 1.2)
|
||||
scene.add(keyLight)
|
||||
camera = new PerspectiveCamera(42, 1, 1, 250_000)
|
||||
camera.position.set(28_000, 35_000, 42_000)
|
||||
renderer = new WebGLRenderer({ antialias: true, alpha: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75))
|
||||
renderer.outputColorSpace = 'srgb'
|
||||
renderer.domElement.className = 'scene-canvas'
|
||||
host.value!.appendChild(renderer.domElement)
|
||||
controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
controls.dampingFactor = 0.075
|
||||
controls.maxPolarAngle = Math.PI * 0.48
|
||||
controls.minDistance = 800
|
||||
controls.maxDistance = 180_000
|
||||
observer = new ResizeObserver(() => {
|
||||
const width = host.value!.clientWidth
|
||||
const height = host.value!.clientHeight
|
||||
if (!width || !height)
|
||||
return
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(width, height, false)
|
||||
})
|
||||
observer.observe(host.value!)
|
||||
updateScene(props.state)
|
||||
render()
|
||||
})
|
||||
|
||||
watch(() => props.state, updateScene)
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(frame)
|
||||
observer?.disconnect()
|
||||
controls?.dispose()
|
||||
for (const item of entities.values()) {
|
||||
item.body.geometry.dispose()
|
||||
;(item.body.material as MeshStandardMaterial).dispose()
|
||||
item.trail.geometry.dispose()
|
||||
;(item.trail.material as LineBasicMaterial).dispose()
|
||||
}
|
||||
renderer?.dispose()
|
||||
renderer?.domElement.remove()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="host" class="scene-host" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scene-host { width: 100%; height: 100%; min-height: 420px; }
|
||||
.scene-host :deep(canvas) { display: block; width: 100%; height: 100%; }
|
||||
</style>
|
||||
20
defense_sim_v2/frontend/src/components/TheInput.vue
Normal file
20
defense_sim_v2/frontend/src/components/TheInput.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
const { modelValue } = defineModels<{
|
||||
modelValue: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
id="input"
|
||||
v-model="modelValue"
|
||||
v-bind="$attrs"
|
||||
type="text"
|
||||
p="x-4 y-2"
|
||||
w="250px"
|
||||
text="center"
|
||||
bg="transparent"
|
||||
border="~ rounded gray-200 dark:gray-700"
|
||||
outline="none active:none"
|
||||
>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionRootEmits, AccordionRootProps } from 'reka-ui'
|
||||
import {
|
||||
AccordionRoot,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<AccordionRootProps>()
|
||||
const emits = defineEmits<AccordionRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { AccordionContent } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionContent
|
||||
v-bind="delegatedProps"
|
||||
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down text-sm transition-all overflow-hidden"
|
||||
>
|
||||
<div :class="cn('pb-4 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { AccordionItem, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('border-b', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AccordionItem>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { ChevronDown } from 'lucide-vue-next'
|
||||
import {
|
||||
AccordionHeader,
|
||||
AccordionTrigger,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionHeader class="flex">
|
||||
<AccordionTrigger
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<slot name="icon">
|
||||
<ChevronDown
|
||||
class="shrink-0 h-4 w-4 transition-transform duration-200"
|
||||
/>
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as Accordion } from './Accordion.vue'
|
||||
export { default as AccordionContent } from './AccordionContent.vue'
|
||||
export { default as AccordionItem } from './AccordionItem.vue'
|
||||
export { default as AccordionTrigger } from './AccordionTrigger.vue'
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogEmits, AlertDialogProps } from 'reka-ui'
|
||||
import { AlertDialogRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
const props = defineProps<AlertDialogProps>()
|
||||
const emits = defineEmits<AlertDialogEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</AlertDialogRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogActionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { AlertDialogAction } from 'reka-ui'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogAction v-bind="delegatedProps" :class="cn(buttonVariants(), props.class)">
|
||||
<slot />
|
||||
</AlertDialogAction>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogCancelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { AlertDialogCancel } from 'reka-ui'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogCancel
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'mt-2 sm:mt-0',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogCancel>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogContentEmits, AlertDialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<AlertDialogContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-black/80 inset-0 fixed z-50"
|
||||
/>
|
||||
<AlertDialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogDescriptionProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import {
|
||||
AlertDialogDescription,
|
||||
} from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogDescription
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogDescription>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogTitleProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { AlertDialogTitle } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogTitle
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-lg font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AlertDialogTitle>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { AlertDialogTriggerProps } from 'reka-ui'
|
||||
import { AlertDialogTrigger } from 'reka-ui'
|
||||
|
||||
const props = defineProps<AlertDialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</AlertDialogTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as AlertDialog } from './AlertDialog.vue'
|
||||
export { default as AlertDialogAction } from './AlertDialogAction.vue'
|
||||
export { default as AlertDialogCancel } from './AlertDialogCancel.vue'
|
||||
export { default as AlertDialogContent } from './AlertDialogContent.vue'
|
||||
export { default as AlertDialogDescription } from './AlertDialogDescription.vue'
|
||||
export { default as AlertDialogFooter } from './AlertDialogFooter.vue'
|
||||
export { default as AlertDialogHeader } from './AlertDialogHeader.vue'
|
||||
export { default as AlertDialogTitle } from './AlertDialogTitle.vue'
|
||||
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue'
|
||||
17
defense_sim_v2/frontend/src/components/ui/alert/Alert.vue
Normal file
17
defense_sim_v2/frontend/src/components/ui/alert/Alert.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { AlertVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { alertVariants } from '.'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: AlertVariants['variant']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
|
||||
<slot />
|
||||
</h5>
|
||||
</template>
|
||||
24
defense_sim_v2/frontend/src/components/ui/alert/index.ts
Normal file
24
defense_sim_v2/frontend/src/components/ui/alert/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Alert } from './Alert.vue'
|
||||
export { default as AlertDescription } from './AlertDescription.vue'
|
||||
export { default as AlertTitle } from './AlertTitle.vue'
|
||||
|
||||
export const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type AlertVariants = VariantProps<typeof alertVariants>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { AspectRatioProps } from 'reka-ui'
|
||||
import { AspectRatio } from 'reka-ui'
|
||||
|
||||
const props = defineProps<AspectRatioProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AspectRatio v-bind="props">
|
||||
<slot />
|
||||
</AspectRatio>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AspectRatio } from './AspectRatio.vue'
|
||||
22
defense_sim_v2/frontend/src/components/ui/avatar/Avatar.vue
Normal file
22
defense_sim_v2/frontend/src/components/ui/avatar/Avatar.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { AvatarVariants } from '.'
|
||||
import { AvatarRoot } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { avatarVariant } from '.'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
size?: AvatarVariants['size']
|
||||
shape?: AvatarVariants['shape']
|
||||
}>(), {
|
||||
size: 'sm',
|
||||
shape: 'circle',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
|
||||
<slot />
|
||||
</AvatarRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarFallbackProps } from 'reka-ui'
|
||||
import { AvatarFallback } from 'reka-ui'
|
||||
|
||||
const props = defineProps<AvatarFallbackProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarFallback v-bind="props">
|
||||
<slot />
|
||||
</AvatarFallback>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { AvatarImageProps } from 'reka-ui'
|
||||
import { AvatarImage } from 'reka-ui'
|
||||
|
||||
const props = defineProps<AvatarImageProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarImage v-bind="props" class="h-full w-full object-cover">
|
||||
<slot />
|
||||
</AvatarImage>
|
||||
</template>
|
||||
25
defense_sim_v2/frontend/src/components/ui/avatar/index.ts
Normal file
25
defense_sim_v2/frontend/src/components/ui/avatar/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Avatar } from './Avatar.vue'
|
||||
export { default as AvatarFallback } from './AvatarFallback.vue'
|
||||
export { default as AvatarImage } from './AvatarImage.vue'
|
||||
|
||||
export const avatarVariant = cva(
|
||||
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-10 w-10 text-xs',
|
||||
base: 'h-16 w-16 text-2xl',
|
||||
lg: 'h-32 w-32 text-5xl',
|
||||
},
|
||||
shape: {
|
||||
circle: 'rounded-full',
|
||||
square: 'rounded-md',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type AvatarVariants = VariantProps<typeof avatarVariant>
|
||||
17
defense_sim_v2/frontend/src/components/ui/badge/Badge.vue
Normal file
17
defense_sim_v2/frontend/src/components/ui/badge/Badge.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { BadgeVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { badgeVariants } from '.'
|
||||
|
||||
const props = defineProps<{
|
||||
variant?: BadgeVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn(badgeVariants({ variant }), props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
26
defense_sim_v2/frontend/src/components/ui/badge/index.ts
Normal file
26
defense_sim_v2/frontend/src/components/ui/badge/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Badge } from './Badge.vue'
|
||||
|
||||
export const badgeVariants = cva(
|
||||
'inline-flex gap-1 items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav aria-label="breadcrumb" :class="props.class">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { MoreHorizontal } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('flex h-9 w-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="h-4 w-4" />
|
||||
</slot>
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
:class="cn('inline-flex items-center gap-1.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
as: 'a',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('transition-colors hover:text-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol
|
||||
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
:class="cn('font-normal text-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('[&>svg]:w-3.5 [&>svg]:h-3.5', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronRight />
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as Breadcrumb } from './Breadcrumb.vue'
|
||||
export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue'
|
||||
export { default as BreadcrumbItem } from './BreadcrumbItem.vue'
|
||||
export { default as BreadcrumbLink } from './BreadcrumbLink.vue'
|
||||
export { default as BreadcrumbList } from './BreadcrumbList.vue'
|
||||
export { default as BreadcrumbPage } from './BreadcrumbPage.vue'
|
||||
export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue'
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonGroupVariants } from '.'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonGroupVariants } from '.'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
orientation?: ButtonGroupVariants['orientation']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
:data-orientation="props.orientation"
|
||||
:class="cn(buttonGroupVariants({ orientation: props.orientation }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { SeparatorProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
orientation: 'vertical',
|
||||
})
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Separator
|
||||
data-slot="button-group-separator"
|
||||
v-bind="delegatedProps"
|
||||
:orientation="props.orientation"
|
||||
:class="cn(
|
||||
'bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto',
|
||||
props.class,
|
||||
)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonGroupVariants } from '.'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
class?: HTMLAttributes['class']
|
||||
orientation?: ButtonGroupVariants['orientation']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'div',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
role="group"
|
||||
data-slot="button-group"
|
||||
:data-orientation="props.orientation"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as ButtonGroup } from './ButtonGroup.vue'
|
||||
export { default as ButtonGroupSeparator } from './ButtonGroupSeparator.vue'
|
||||
export { default as ButtonGroupText } from './ButtonGroupText.vue'
|
||||
|
||||
export const buttonGroupVariants = cva(
|
||||
'flex w-fit items-stretch [&>*:focus-visible]:z-10 [&>*:focus-visible]:relative [&>[data-slot=select-trigger]:not([class*=\'w-\'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2',
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
horizontal:
|
||||
'[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
|
||||
vertical:
|
||||
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: 'horizontal',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>
|
||||
28
defense_sim_v2/frontend/src/components/ui/button/Button.vue
Normal file
28
defense_sim_v2/frontend/src/components/ui/button/Button.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { ButtonVariants } from '.'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '.'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants['variant']
|
||||
size?: ButtonVariants['size']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'button',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="props.as"
|
||||
:as-child="props.asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
37
defense_sim_v2/frontend/src/components/ui/button/index.ts
Normal file
37
defense_sim_v2/frontend/src/components/ui/button/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
'default': 'h-10 px-4 py-2',
|
||||
'sm': 'h-9 rounded-md px-3',
|
||||
'lg': 'h-11 rounded-md px-8',
|
||||
'icon': 'h-10 w-10',
|
||||
'icon-sm': 'size-9',
|
||||
'icon-lg': 'size-11',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarRootEmits, CalendarRootProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CalendarRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
|
||||
|
||||
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<CalendarRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarRoot
|
||||
v-slot="{ grid, weekDays }"
|
||||
:class="cn('p-3', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<CalendarHeader>
|
||||
<CalendarPrevButton />
|
||||
<CalendarHeading />
|
||||
<CalendarNextButton />
|
||||
</CalendarHeader>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
||||
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
||||
<CalendarGridHead>
|
||||
<CalendarGridRow>
|
||||
<CalendarHeadCell
|
||||
v-for="day in weekDays" :key="day"
|
||||
>
|
||||
{{ day }}
|
||||
</CalendarHeadCell>
|
||||
</CalendarGridRow>
|
||||
</CalendarGridHead>
|
||||
<CalendarGridBody>
|
||||
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
|
||||
<CalendarCell
|
||||
v-for="weekDate in weekDates"
|
||||
:key="weekDate.toString()"
|
||||
:date="weekDate"
|
||||
>
|
||||
<CalendarCellTrigger
|
||||
:day="weekDate"
|
||||
:month="month.value"
|
||||
/>
|
||||
</CalendarCell>
|
||||
</CalendarGridRow>
|
||||
</CalendarGridBody>
|
||||
</CalendarGrid>
|
||||
</div>
|
||||
</CalendarRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarCellProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CalendarCell, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarCell
|
||||
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarCell>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarCellTriggerProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CalendarCellTrigger, useForwardProps } from 'reka-ui'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarCellTrigger
|
||||
:class="cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal',
|
||||
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||
// Selected
|
||||
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
||||
// Disabled
|
||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||
// Unavailable
|
||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||
// Outside months
|
||||
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarCellTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarGridProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CalendarGrid, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGrid
|
||||
:class="cn('w-full border-collapse space-y-1', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarGrid>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarGridBodyProps } from 'reka-ui'
|
||||
import { CalendarGridBody } from 'reka-ui'
|
||||
|
||||
const props = defineProps<CalendarGridBodyProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridBody v-bind="props">
|
||||
<slot />
|
||||
</CalendarGridBody>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarGridHeadProps } from 'reka-ui'
|
||||
import { CalendarGridHead } from 'reka-ui'
|
||||
|
||||
const props = defineProps<CalendarGridHeadProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridHead v-bind="props">
|
||||
<slot />
|
||||
</CalendarGridHead>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarGridRowProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CalendarGridRow, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</CalendarGridRow>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarHeadCellProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CalendarHeadCell, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeadCell :class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</CalendarHeadCell>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CalendarHeaderProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { CalendarHeader, useForwardProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</CalendarHeader>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user