';
}
// Close button at bottom
html += '';
content.innerHTML = html;
}
function _shortenMAC(mac) {
// Shorten MAC to last 6 characters
if (!mac) return '----';
if (mac.length <= 8) return mac;
return '...' + mac.slice(-8);
}
function _getZoneColor(zoneNumber) {
// Color gradient for Fresnel zones
var colors = [
'#22c55e', // zone 1 - green
'#84cc16', // zone 2 - lime
'#cddc39', // zone 3 - yellow
'#ffeb3b', // zone 4 - orange
'#ff9800', // zone 5 - deep orange
];
return colors[Math.min(zoneNumber - 1, colors.length - 1)] || '#999';
}
function toggleSection(header) {
var content = header.nextElementSibling;
var icon = header.querySelector('.toggle-icon');
var parentSection = header.parentElement;
if (content.style.display === 'none') {
content.style.display = 'block';
if (icon) icon.textContent = '▲';
parentSection.classList.remove('collapsed');
} else {
content.style.display = 'none';
if (icon) icon.textContent = '▼';
parentSection.classList.add('collapsed');
}
}
// ── 3D Scene Manipulation ─────────────────────────────────────────────────
/**
* Apply the X-ray overlay effect to the 3D scene.
* Dims non-contributing elements and highlights contributing links.
*/
function applyXRayOverlay(explanationData) {
if (!window.Viz3D) return;
// Save original material states and apply dimming
_saveAndDimScene();
// Highlight contributing links
_highlightContributingLinks(explanationData);
// Render Fresnel zone ellipsoids
_renderFresnelZones(explanationData);
}
/**
* Save original material states and dim the scene.
*/
function _saveAndDimScene() {
_originalMaterialStates.clear();
// Dim room elements using Viz3D callback
if (window.Viz3D.forEachRoomObject) {
Viz3D.forEachRoomObject(function(obj) {
_dimObject(obj);
});
}
// Dim all links first (will highlight contributing ones later)
if (window.Viz3D.forEachLink) {
Viz3D.forEachLink(function(line, linkID) {
_dimObject(line);
});
}
// Dim non-target blobs
if (window.Viz3D.forEachBlob) {
Viz3D.forEachBlob(function(obj, blobID) {
if (blobID !== _currentBlobID && obj.group) {
_dimObject(obj.group);
}
});
}
}
function _dimObject(obj) {
if (!obj || !obj.material) return;
var uuid = obj.uuid;
if (!uuid) return;
// Save original state
if (!_originalMaterialStates.has(uuid)) {
_originalMaterialStates.set(uuid, {
opacity: obj.material.opacity,
transparent: obj.material.transparent,
emissiveIntensity: obj.material.emissiveIntensity || 0
});
if (obj.material.emissive) {
_originalMaterialStates.get(uuid).emissiveColor = obj.material.emissive.getHex();
}
if (obj.material.color) {
_originalMaterialStates.get(uuid).color = obj.material.color.getHex();
}
}
// Apply dimming
obj.material.opacity = CONFIG.dimOpacity;
obj.material.transparent = true;
if (obj.material.emissive) {
obj.material.emissive.setHex(0x000000);
obj.material.emissiveIntensity = 0;
}
obj.material.needsUpdate = true;
}
/**
* Highlight contributing links with a glowing effect.
*/
function _highlightContributingLinks(explanationData) {
if (!explanationData.contributing_links || !window.Viz3D) return;
explanationData.contributing_links.forEach(function (link) {
if (window.Viz3D.highlightLink) {
Viz3D.highlightLink(link.link_id, CONFIG.highlightColor, CONFIG.highlightEmissive, 0.8);
_highlightedLinks.push(link.link_id);
}
});
}
/**
* Render Fresnel zone ellipsoids for contributing links.
*/
function _renderFresnelZones(explanationData) {
if (!explanationData.fresnel_zones) return;
explanationData.fresnel_zones.forEach(function (zone) {
if (window.Viz3D.addFresnelZone) {
var mesh = Viz3D.addFresnelZone(
zone.center_pos[0], zone.center_pos[1], zone.center_pos[2],
zone.semi_axes[0], zone.semi_axes[1], zone.semi_axes[2],
CONFIG.fresnelColor,
CONFIG.fresnelOpacity
);
if (mesh) {
_fresnelMeshes.push(mesh);
}
}
});
}
/**
* Restore the scene to its original state.
*/
function restoreScene() {
// Restore material states
if (window.Viz3D) {
_originalMaterialStates.forEach(function (state, uuid) {
if (window.Viz3D.restoreObjectMaterial) {
Viz3D.restoreObjectMaterial(uuid, state);
}
});
}
// Remove Fresnel zone meshes
_fresnelMeshes.forEach(function (mesh) {
if (window.Viz3D) {
Viz3D.removeFresnelZone(mesh);
}
});
_fresnelMeshes = [];
_highlightedLinks = [];
_originalMaterialStates.clear();
}
// ── API Integration ─────────────────────────────────────────────────────────
function fetchExplanation(blobID) {
return fetch('/api/explain/' + encodeURIComponent(blobID))
.then(function (response) {
if (!response.ok) {
throw new Error('Failed to fetch explanation: ' + response.statusText);
}
return response.json();
})
.then(function (data) {
_explanationData = data;
renderContent(data);
applyXRayOverlay(data);
})
.catch(function (error) {
console.error('[Explainability] Failed to load explanation:', error);
renderContent(null);
});
}
// ── Public API ─────────────────────────────────────────────────────────────
return {
/**
* Open the explainability view for a blob.
* @param {number} blobID - The blob ID to explain
*/
explain: function (blobID) {
if (_isActive) {
// Already open, just switch to new blob
close();
}
_isActive = true;
_currentBlobID = blobID;
// Create UI if needed
if (!_sidebarPanel) {
createSidebarPanel();
}
// Show sidebar
_sidebarPanel.style.display = 'block';
_sidebarPanel.classList.add('panel-sidebar-visible');
_sceneOverlay.style.display = 'block';
_sceneOverlay.classList.add('panel-overlay-visible');
// Fetch and display data
fetchExplanation(blobID);
},
/**
* Close the explainability view.
*/
close: function () {
if (!_isActive) return;
_isActive = false;
_currentBlobID = null;
_explanationData = null;
// Hide sidebar
if (_sidebarPanel) {
_sidebarPanel.classList.remove('panel-sidebar-visible');
_sidebarPanel.style.display = 'none';
}
if (_sceneOverlay) {
_sceneOverlay.classList.remove('panel-overlay-visible');
_sceneOverlay.style.display = 'none';
}
// Restore scene
restoreScene();
},
/**
* Check if explainability view is currently active.
*/
isActive: function () {
return _isActive;
},
/**
* Toggle the all links section.
*/
toggleSection: toggleSection,
/**
* Get current explanation data.
*/
getData: function () {
return _explanationData;
},
/**
* Get current blob ID.
*/
getCurrentBlobID: function () {
return _currentBlobID;
}
};
})();
// Make toggleSection available globally for onclick
window.Explainability = Explainability;