feat: complete pre-deployment simulator implementation
Implements comprehensive pre-deployment simulator for WiFi CSI-based positioning that allows users to predict detection quality before purchasing hardware. Components: - Virtual space definition with rooms, walls, and material properties - Virtual nodes with configurable roles (TX/RX/TX_RX/PASSIVE/IDLE) - Synthetic walkers (random walk, path-following, node-to-node) - GDOP overlay for coverage quality visualization - Signal propagation model with path loss and wall attenuation - Fresnel zone accumulation for blob detection - REST API endpoints for simulator control - Dashboard integration with space/node/walker management - Coverage optimization recommendations The simulator produces realistic synthetic CSI data matching real-world conditions by using the same propagation models and localization algorithms as the live system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a70e3e433d
commit
cdd9c64800
7 changed files with 379 additions and 46 deletions
|
|
@ -1483,6 +1483,20 @@
|
|||
#room-editor-btn:hover { background: rgba(255,255,255,0.12); color: #ccc; }
|
||||
#room-editor-btn.active { background: rgba(79,195,247,0.2); color: #4fc3f7; border-color: #4fc3f7; }
|
||||
|
||||
/* Simulator button */
|
||||
#simulator-btn {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
#simulator-btn:hover { background: rgba(255,255,255,0.12); color: #ccc; }
|
||||
#simulator-btn.active { background: rgba(156,39,176,0.2); color: #ab47bc; border-color: #ab47bc; }
|
||||
|
||||
/* GDOP toggle */
|
||||
#gdop-toggle-btn {
|
||||
background: rgba(255,255,255,0.06);
|
||||
|
|
@ -1704,6 +1718,14 @@
|
|||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
#gdop-coverage-score {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #22c65e;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#gdop-gradient {
|
||||
width: 140px;
|
||||
height: 12px;
|
||||
|
|
@ -2543,6 +2565,7 @@
|
|||
<button id="fresnel-toggle-btn" onclick="toggleFresnelZones()">Fresnel</button>
|
||||
<button id="room-editor-btn" onclick="Placement && Placement.toggleRoomEditor()">Room</button>
|
||||
<button id="floorplan-btn" onclick="FloorPlanSetup.togglePanel()">Floor plan</button>
|
||||
<button id="simulator-btn" onclick="Simulate && Simulate.togglePanel()">Simulator</button>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>FPS: <strong id="fps-counter">0</strong></span>
|
||||
|
|
@ -2710,9 +2733,190 @@
|
|||
<button id="room-apply-btn" onclick="Placement && Placement.applyRoomFromEditor()">Apply</button>
|
||||
</div>
|
||||
|
||||
<!-- Pre-Deployment Simulator Panel -->
|
||||
<div id="simulator-panel" class="simulator-panel" style="display:none;">
|
||||
<div class="simulator-header">
|
||||
<h2>Pre-Deployment Simulator</h2>
|
||||
<button id="sim-close-btn" class="sim-close-btn" aria-label="Close simulator">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="simulator-content">
|
||||
<!-- Space Configuration -->
|
||||
<section class="sim-section">
|
||||
<h3>Space Configuration</h3>
|
||||
<div class="sim-field-group">
|
||||
<div class="sim-field">
|
||||
<label>Width <span class="unit">(m)</span></label>
|
||||
<input type="number" id="sim-space-width" value="10" min="1" max="100" step="0.5">
|
||||
</div>
|
||||
<div class="sim-field">
|
||||
<label>Depth <span class="unit">(m)</span></label>
|
||||
<input type="number" id="sim-space-depth" value="10" min="1" max="100" step="0.5">
|
||||
</div>
|
||||
<div class="sim-field">
|
||||
<label>Height <span class="unit">(m)</span></label>
|
||||
<input type="number" id="sim-space-height" value="2.5" min="1" max="50" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
<button id="sim-apply-space" class="sim-btn sim-btn-primary">Apply Space</button>
|
||||
</section>
|
||||
|
||||
<!-- Tools -->
|
||||
<section class="sim-section">
|
||||
<h3>Tools</h3>
|
||||
<div class="sim-tools">
|
||||
<button class="sim-tool-btn active" data-tool="select" title="Select">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path>
|
||||
</svg>
|
||||
Select
|
||||
</button>
|
||||
<button class="sim-tool-btn" data-tool="node" title="Add Virtual Node">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M12 1v6m0 6v6"></path>
|
||||
<path d="M1 12h6m6 0h6"></path>
|
||||
</svg>
|
||||
Node
|
||||
</button>
|
||||
<button class="sim-tool-btn" data-tool="walker" title="Add Walker">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="5" r="1"></circle>
|
||||
<path d="M4 20l4-5 3 3 5-7 4 4v5H4z"></path>
|
||||
</svg>
|
||||
Walker
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Virtual Nodes -->
|
||||
<section class="sim-section">
|
||||
<h3>Virtual Nodes <span id="sim-node-count" class="sim-count">(0)</span></h3>
|
||||
<div class="sim-actions">
|
||||
<button id="sim-add-node" class="sim-btn sim-btn-sm">+ Add Node</button>
|
||||
<button id="sim-clear-nodes" class="sim-btn sim-btn-sm sim-btn-danger">Clear All</button>
|
||||
</div>
|
||||
<div id="sim-nodes-container" class="sim-items-list">
|
||||
<div class="sim-empty-state">No virtual nodes. Click "Add Node" or use the Node tool.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Walkers -->
|
||||
<section class="sim-section">
|
||||
<h3>Synthetic Walkers <span id="sim-walker-count" class="sim-count">(0)</span></h3>
|
||||
<div class="sim-field">
|
||||
<label>Walker Type</label>
|
||||
<select id="sim-walker-type">
|
||||
<option value="random">Random Walk</option>
|
||||
<option value="path">Path Following</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sim-actions">
|
||||
<button id="sim-add-walker" class="sim-btn sim-btn-sm">+ Add Walker</button>
|
||||
<button id="sim-clear-walkers" class="sim-btn sim-btn-sm sim-btn-danger">Clear All</button>
|
||||
</div>
|
||||
<div id="sim-walkers-container" class="sim-items-list">
|
||||
<div class="sim-empty-state">No walkers. Add walkers to simulate movement.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- GDOP Analysis -->
|
||||
<section class="sim-section">
|
||||
<h3>GDOP Coverage Analysis</h3>
|
||||
<div class="sim-actions">
|
||||
<button id="sim-show-gdop" class="sim-btn sim-btn-sm">Show GDOP</button>
|
||||
<button id="sim-update-gdop" class="sim-btn sim-btn-sm">Update</button>
|
||||
</div>
|
||||
<div id="sim-gdop-results" class="sim-gdop-results" style="display:none;">
|
||||
<div class="sim-gdop-stats">
|
||||
<div class="sim-stat">
|
||||
<span class="sim-stat-label">Coverage</span>
|
||||
<span id="sim-gdop-coverage" class="sim-stat-value">--%</span>
|
||||
</div>
|
||||
<div class="sim-stat">
|
||||
<span class="sim-stat-label">Mean GDOP</span>
|
||||
<span id="sim-gdop-mean" class="sim-stat-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sim-gdop-legend" class="sim-gdop-legend">
|
||||
<div class="gdop-legend-item" data-quality="excellent">
|
||||
<div class="gdop-legend-color" style="background:#22c65e;"></div>
|
||||
<span class="gdop-legend-label">Excellent (<2)</span>
|
||||
</div>
|
||||
<div class="gdop-legend-item" data-quality="good">
|
||||
<div class="gdop-legend-color" style="background:#ffc107;"></div>
|
||||
<span class="gdop-legend-label">Good (2-4)</span>
|
||||
</div>
|
||||
<div class="gdop-legend-item" data-quality="fair">
|
||||
<div class="gdop-legend-color" style="background:#ff9200;"></div>
|
||||
<span class="gdop-legend-label">Fair (4-8)</span>
|
||||
</div>
|
||||
<div class="gdop-legend-item" data-quality="poor">
|
||||
<div class="gdop-legend-color" style="background:#dc3545;"></div>
|
||||
<span class="gdop-legend-label">Poor (>8)</span>
|
||||
</div>
|
||||
<div class="gdop-legend-item" data-quality="none">
|
||||
<div class="gdop-legend-color" style="background:#505050;"></div>
|
||||
<span class="gdop-legend-label">No Coverage</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Simulation Controls -->
|
||||
<section class="sim-section">
|
||||
<h3>Simulation</h3>
|
||||
<div class="sim-sim-controls">
|
||||
<button id="sim-start-btn" class="sim-btn sim-btn-primary">Start Simulation</button>
|
||||
<button id="sim-pause-btn" class="sim-btn" disabled>Pause</button>
|
||||
<button id="sim-stop-btn" class="sim-btn sim-btn-danger" disabled>Stop</button>
|
||||
</div>
|
||||
<div class="sim-progress">
|
||||
<span class="sim-progress-label">Time: <span id="sim-time">0:00</span></span>
|
||||
<div class="sim-progress-bar">
|
||||
<div id="sim-progress-fill" class="sim-progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Results -->
|
||||
<section id="sim-results-section" class="sim-section" style="display:none;">
|
||||
<h3>Results</h3>
|
||||
<div class="sim-results">
|
||||
<div class="sim-result-item">
|
||||
<span class="sim-result-label">Expected Accuracy</span>
|
||||
<span id="sim-result-accuracy" class="sim-result-value">--</span>
|
||||
</div>
|
||||
<div class="sim-result-item">
|
||||
<span class="sim-result-label">Coverage</span>
|
||||
<span id="sim-result-coverage" class="sim-result-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Recommendations -->
|
||||
<section id="sim-recommendations-section" class="sim-section" style="display:none;">
|
||||
<h3>Recommendations</h3>
|
||||
<div id="sim-recommendations" class="sim-recommendations"></div>
|
||||
</section>
|
||||
|
||||
<!-- Shopping List -->
|
||||
<section id="sim-shopping-section" class="sim-section" style="display:none;">
|
||||
<h3>Hardware Shopping List</h3>
|
||||
<div id="sim-shopping-list" class="sim-shopping-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GDOP legend -->
|
||||
<div id="gdop-legend">
|
||||
<h4>GDOP Coverage</h4>
|
||||
<div id="gdop-coverage-score">Coverage: --%</div>
|
||||
<canvas id="gdop-gradient" width="140" height="12"></canvas>
|
||||
<div id="gdop-labels">
|
||||
<span>Excellent</span>
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ const Placement = (function () {
|
|||
|
||||
// ── GDOP overlay ─────────────────────────────────────────────────────────
|
||||
|
||||
// Cache for backend GDOP data
|
||||
var _gdopCache = null;
|
||||
var _gdopPending = false;
|
||||
|
||||
function updateGDOPOverlay() {
|
||||
var nodes = getNodePositions();
|
||||
|
||||
|
|
@ -118,32 +122,70 @@ const Placement = (function () {
|
|||
return;
|
||||
}
|
||||
|
||||
// Fetch GDOP data from backend API (uses angular diversity algorithm)
|
||||
if (!_gdopPending) {
|
||||
_gdopPending = true;
|
||||
fetch('/api/simulator/gdop/heatmap')
|
||||
.then(function (resp) { return resp.json(); })
|
||||
.then(function (data) {
|
||||
_gdopCache = data;
|
||||
renderGDOPFromCache();
|
||||
_gdopPending = false;
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error('[Placement] GDOP fetch failed:', e);
|
||||
_gdopPending = false;
|
||||
});
|
||||
} else if (_gdopCache) {
|
||||
renderGDOPFromCache();
|
||||
}
|
||||
}
|
||||
|
||||
function renderGDOPFromCache() {
|
||||
if (!_gdopCache || !_roomConfig) return;
|
||||
|
||||
var data = _gdopCache;
|
||||
var w = _roomConfig.width;
|
||||
var d = _roomConfig.depth;
|
||||
var ox = _roomConfig.origin_x || 0;
|
||||
var oz = _roomConfig.origin_z || 0;
|
||||
|
||||
// Get grid dimensions from backend response
|
||||
var gridWidth = data.grid_dimensions ? data.grid_dimensions[0] : GDOP_RES;
|
||||
var gridDepth = data.grid_dimensions ? data.grid_dimensions[1] : GDOP_RES;
|
||||
var gdopValues = data.gdop_map || [];
|
||||
var colors = data.colors || [];
|
||||
|
||||
// Create or reuse DataTexture
|
||||
if (!_gdopTexture) {
|
||||
var data = new Uint8Array(GDOP_RES * GDOP_RES * 4);
|
||||
_gdopTexture = new THREE.DataTexture(data, GDOP_RES, GDOP_RES, THREE.RGBAFormat);
|
||||
var texData = new Uint8Array(gridWidth * gridDepth * 4);
|
||||
_gdopTexture = new THREE.DataTexture(texData, gridWidth, gridDepth, THREE.RGBAFormat);
|
||||
_gdopTexture.magFilter = THREE.LinearFilter;
|
||||
_gdopTexture.minFilter = THREE.LinearFilter;
|
||||
}
|
||||
|
||||
var data = _gdopTexture.image.data;
|
||||
var texData = _gdopTexture.image.data;
|
||||
|
||||
for (var iz = 0; iz < GDOP_RES; iz++) {
|
||||
var pz = oz + (iz / (GDOP_RES - 1)) * d;
|
||||
for (var ix = 0; ix < GDOP_RES; ix++) {
|
||||
var px = ox + (ix / (GDOP_RES - 1)) * w;
|
||||
var hdop = computeHDOP(px, pz, nodes);
|
||||
var c = gdopToColor(hdop);
|
||||
var idx = (iz * GDOP_RES + ix) * 4;
|
||||
data[idx] = c[0];
|
||||
data[idx + 1] = c[1];
|
||||
data[idx + 2] = c[2];
|
||||
data[idx + 3] = c[3];
|
||||
// Use backend-provided colors if available, otherwise compute from GDOP values
|
||||
if (colors.length > 0) {
|
||||
// Backend provides RGB colors
|
||||
for (var i = 0; i < colors.length && i < gridWidth * gridDepth; i++) {
|
||||
var idx = i * 4;
|
||||
texData[idx] = colors[i][0];
|
||||
texData[idx + 1] = colors[i][1];
|
||||
texData[idx + 2] = colors[i][2];
|
||||
texData[idx + 3] = 150; // Alpha
|
||||
}
|
||||
} else if (gdopValues.length > 0) {
|
||||
// Fallback: compute colors from GDOP values
|
||||
for (var i = 0; i < gdopValues.length && i < gridWidth * gridDepth; i++) {
|
||||
var gdop = gdopValues[i];
|
||||
var c = gdopToColor(gdop);
|
||||
var idx = i * 4;
|
||||
texData[idx] = c[0];
|
||||
texData[idx + 1] = c[1];
|
||||
texData[idx + 2] = c[2];
|
||||
texData[idx + 3] = c[3];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,6 +210,20 @@ const Placement = (function () {
|
|||
}
|
||||
|
||||
_gdopMesh.visible = true;
|
||||
|
||||
// Update coverage score display if available
|
||||
updateCoverageScore(data.coverage_score || data.coverage_percent);
|
||||
}
|
||||
|
||||
function updateCoverageScore(score) {
|
||||
var scoreEl = document.getElementById('gdop-coverage-score');
|
||||
if (scoreEl) {
|
||||
var pct = typeof score === 'number' ? score : 0;
|
||||
scoreEl.textContent = 'Coverage: ' + pct.toFixed(1) + '%';
|
||||
|
||||
// Color-code the score
|
||||
scoreEl.style.color = pct >= 70 ? '#22c65e' : pct >= 50 ? '#ffc107' : '#dc3545';
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildGDOPIfDirty() {
|
||||
|
|
@ -211,6 +267,7 @@ const Placement = (function () {
|
|||
obj.position.z = Math.max(oz, Math.min(oz + _roomConfig.depth, obj.position.z));
|
||||
|
||||
_gdopDirty = true;
|
||||
_gdopCache = null; // Clear cache to force refresh with new node positions
|
||||
|
||||
// Update link lines to follow dragged node
|
||||
if (Viz3D.rebuildLinkLines) Viz3D.rebuildLinkLines();
|
||||
|
|
|
|||
|
|
@ -443,12 +443,14 @@
|
|||
const node = {
|
||||
id: id,
|
||||
name: 'Node ' + (state.nodes.length + 1),
|
||||
type: 'virtual',
|
||||
position: {
|
||||
x: state.space.width / 2,
|
||||
y: 1.0,
|
||||
z: state.space.depth / 2,
|
||||
},
|
||||
role: 'tx_rx',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
state.nodes.push(node);
|
||||
|
|
@ -537,9 +539,14 @@
|
|||
function addWalker() {
|
||||
const type = elements.walkerType.value;
|
||||
const id = 'walker_' + Date.now();
|
||||
|
||||
// Map frontend type to backend WalkerType
|
||||
const backendType = type === 'path' ? 'path_follow' :
|
||||
type === 'zone' ? 'node_to_node' : 'random_walk';
|
||||
|
||||
const walker = {
|
||||
id: id,
|
||||
type: type,
|
||||
type: backendType,
|
||||
position: {
|
||||
x: state.space.width / 2,
|
||||
y: 1.0,
|
||||
|
|
@ -550,19 +557,19 @@
|
|||
y: 0,
|
||||
z: (Math.random() - 0.5) * CONFIG.walkerSpeed,
|
||||
},
|
||||
speed: CONFIG.walkerSpeed,
|
||||
height: 1.7,
|
||||
};
|
||||
|
||||
if (type === 'path') {
|
||||
// Create default path
|
||||
walker.path = [
|
||||
{ x: 2, y: 1, z: 2 },
|
||||
{ x: state.space.width - 2, y: 1, z: 2 },
|
||||
{ x: state.space.width - 2, y: 1, z: state.space.depth - 2 },
|
||||
{ x: 2, y: 1, z: state.space.depth - 2 },
|
||||
{ x: 2, y: 1.0, z: 2 },
|
||||
{ x: state.space.width - 2, y: 1.0, z: 2 },
|
||||
{ x: state.space.width - 2, y: 1.0, z: state.space.depth - 2 },
|
||||
{ x: 2, y: 1.0, z: state.space.depth - 2 },
|
||||
];
|
||||
walker.path_index = 0;
|
||||
} else if (type === 'zone') {
|
||||
walker.target_zones = [];
|
||||
}
|
||||
|
||||
state.walkers.push(walker);
|
||||
|
|
@ -636,6 +643,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Panel Toggle
|
||||
// ============================================
|
||||
function togglePanel() {
|
||||
const panel = document.getElementById('simulator-panel');
|
||||
const btn = document.getElementById('simulator-btn');
|
||||
if (!panel || !btn) return;
|
||||
|
||||
const isVisible = panel.style.display !== 'none';
|
||||
panel.style.display = isVisible ? 'none' : 'block';
|
||||
btn.classList.toggle('active', !isVisible);
|
||||
|
||||
console.log('[Simulate] Panel', isVisible ? 'hidden' : 'shown');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GDOP Visualization
|
||||
// ============================================
|
||||
|
|
|
|||
|
|
@ -2142,15 +2142,7 @@ const Viz3D = (function () {
|
|||
* Fetch GDOP heatmap data from API.
|
||||
*/
|
||||
function fetchGDOPData() {
|
||||
fetch('/api/simulator/gdop/compute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
cell_size: 0.2,
|
||||
max_zone: 3,
|
||||
threshold: 0.02
|
||||
})
|
||||
})
|
||||
fetch('/api/simulator/gdop/heatmap')
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
_gdopData = data;
|
||||
|
|
@ -2166,21 +2158,39 @@ const Viz3D = (function () {
|
|||
* @param {Object} data - GDOP computation results
|
||||
*/
|
||||
function updateGDOPOverlay(data) {
|
||||
if (!data || !data.gdop_heatmap) {
|
||||
if (!data || !data.gdop_map) {
|
||||
console.warn('[Viz3D] No GDOP heatmap data in response');
|
||||
return;
|
||||
}
|
||||
|
||||
var heatmap = data.gdop_heatmap;
|
||||
var width = heatmap.width;
|
||||
var depth = heatmap.depth;
|
||||
var cellSize = heatmap.cell_size;
|
||||
var originX = heatmap.origin_x;
|
||||
var originY = heatmap.origin_y;
|
||||
var gridDimensions = data.grid_dimensions || [0, 0, 0];
|
||||
var width = gridDimensions[0];
|
||||
var depth = gridDimensions[1];
|
||||
var cellSize = 0.2; // Default cell size (not provided by this endpoint)
|
||||
|
||||
// Create texture from GDOP data
|
||||
var gdopValues = new Float32Array(heatmap.gdop_values);
|
||||
var colors = new Uint8Array(heatmap.colors.flat());
|
||||
var gdopValues = new Float32Array(data.gdop_map);
|
||||
|
||||
// Generate colors from GDOP values using GDOPColorMap
|
||||
var colors = new Uint8Array(width * depth * 3);
|
||||
for (var i = 0; i < gdopValues.length; i++) {
|
||||
var gdop = gdopValues[i];
|
||||
var color;
|
||||
if (gdop >= 9999) {
|
||||
color = { r: 80, g: 80, b: 80 }; // None - gray
|
||||
} else if (gdop < 2.0) {
|
||||
color = { r: 34, g: 197, b: 94 }; // Excellent - green
|
||||
} else if (gdop < 4.0) {
|
||||
color = { r: 255, g: 193, b: 7 }; // Good - yellow
|
||||
} else if (gdop < 8.0) {
|
||||
color = { r: 255, g: 146, b: 0 }; // Fair - orange
|
||||
} else {
|
||||
color = { r: 220, g: 53, b: 69 }; // Poor - red
|
||||
}
|
||||
colors[i * 3] = color.r;
|
||||
colors[i * 3 + 1] = color.g;
|
||||
colors[i * 3 + 2] = color.b;
|
||||
}
|
||||
|
||||
// Create data texture
|
||||
if (_gdopTexture) {
|
||||
|
|
@ -2206,9 +2216,9 @@ const Viz3D = (function () {
|
|||
_gdopMesh = new THREE.Mesh(geo, mat);
|
||||
_gdopMesh.rotation.x = -Math.PI / 2;
|
||||
_gdopMesh.position.set(
|
||||
originX + (width * cellSize) / 2,
|
||||
(width * cellSize) / 2,
|
||||
0.01, // Slightly above floor
|
||||
originY + (depth * cellSize) / 2
|
||||
(depth * cellSize) / 2
|
||||
);
|
||||
_scene.add(_gdopMesh);
|
||||
_gdopMesh.visible = _gdopOverlayVisible;
|
||||
|
|
@ -2220,17 +2230,17 @@ const Viz3D = (function () {
|
|||
depth * cellSize
|
||||
);
|
||||
_gdopMesh.position.set(
|
||||
originX + (width * cellSize) / 2,
|
||||
(width * cellSize) / 2,
|
||||
0.01,
|
||||
originY + (depth * cellSize) / 2
|
||||
(depth * cellSize) / 2
|
||||
);
|
||||
_gdopMesh.material.map = _gdopTexture;
|
||||
}
|
||||
|
||||
// Update or create legend
|
||||
updateGDOPLegend(data.coverage_score);
|
||||
updateGDOPLegend(data.coverage_percent || data.coverage_score);
|
||||
|
||||
console.log('[Viz3D] GDOP overlay updated:', data.coverage_score.toFixed(1) + '% coverage');
|
||||
console.log('[Viz3D] GDOP overlay updated:', (data.coverage_percent || data.coverage_score || 0).toFixed(1) + '% coverage');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/spaxel/mothership/internal/simulator"
|
||||
|
|
@ -38,6 +40,7 @@ func (h *SimulatorHandler) RegisterRoutes(r chi.Router) {
|
|||
r.Route("/simulator", func(r chi.Router) {
|
||||
r.Get("/", h.GetState)
|
||||
r.Post("/reset", h.Reset)
|
||||
r.Post("/session", h.CreateSession)
|
||||
|
||||
// Space management
|
||||
r.Route("/space", func(r chi.Router) {
|
||||
|
|
@ -108,6 +111,37 @@ func (h *SimulatorHandler) Reset(w http.ResponseWriter, r *http.Request) {
|
|||
respondJSON(w, http.StatusOK, map[string]string{"status": "reset"})
|
||||
}
|
||||
|
||||
// CreateSession creates a new simulator session
|
||||
func (h *SimulatorHandler) CreateSession(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Space *simulator.Space `json:"space"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
// Use default space if request body is empty/invalid
|
||||
req.Space = simulator.DefaultSpace()
|
||||
}
|
||||
|
||||
if req.Space == nil {
|
||||
req.Space = simulator.DefaultSpace()
|
||||
}
|
||||
|
||||
// Update the handler's space if provided
|
||||
if req.Space != nil {
|
||||
h.mu.Lock()
|
||||
h.space = req.Space
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// Generate session ID
|
||||
sessionID := fmt.Sprintf("sim_%d", time.Now().UnixNano())
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"session_id": sessionID,
|
||||
"space": h.space,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSpace returns the current space definition
|
||||
func (h *SimulatorHandler) GetSpace(w http.ResponseWriter, r *http.Request) {
|
||||
h.mu.RLock()
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ func (b *FleetRegistryBridge) OptimizeCoverage(space *Space) (*CoverageOptimizat
|
|||
weakAreas := make([]Point, 0)
|
||||
for row := range results {
|
||||
for col := range results[row] {
|
||||
if results[row][col] > 4 {
|
||||
if results[row][col].GDOP > 4 {
|
||||
// Calculate position from grid indices
|
||||
x := minX + float64(col)*config.CellSize
|
||||
y := minY + float64(row)*config.CellSize
|
||||
|
|
|
|||
|
|
@ -244,6 +244,12 @@ func (s *Space) TotalVolume() float64 {
|
|||
return v
|
||||
}
|
||||
|
||||
// Dimensions returns the overall width, depth, and height of the space
|
||||
func (s *Space) Dimensions() (width, depth, height float64) {
|
||||
minX, minY, minZ, maxX, maxY, maxZ := s.Bounds()
|
||||
return maxX - minX, maxY - minY, maxZ - minZ
|
||||
}
|
||||
|
||||
// GetWalls returns all wall segments from all rooms plus standalone walls
|
||||
func (s *Space) GetWalls() []WallSegment {
|
||||
walls := make([]WallSegment, 0, len(s.Walls))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue