llm-semantic-visualizer / src /ui /panels /ReliefPanel.js
hbauzan's picture
Release v4.17.7 — sync from GitHub main
819a47f
Raw
History Blame Contribute Delete
13.2 kB
// Relief Semántico panel (extracted from Sidebar.js in H4 4.2).
// Owns the entire `#relief-submode-controls` block: comparison modes A/B/C,
// active-document list, scale/render/thickness controls, PDF selector + ingestion
// + search, and the Mode-C dimension inspector. The SAE and Model sub-panels are
// nested here and composed from their own modules to keep the DOM identical.
//
// Cross-panel: publishes ctx.refresh.{reliefFiles,docList,dimList,comparisonModeUI,
// subModeUI}; reads ctx.refresh.modelPanel. Also mirrors renderDocList/renderDimList
// onto window.* for visualizer/DimensionInspector.js.
import { STATE } from '../../core/State.js';
import { updateThreadVisibility } from '../../visualizer/ThreadManager.js';
import { updateAllThreadPositions } from '../../visualizer/LayoutEngine.js';
import { clearRelief, updateReliefScale, updateReliefThickness } from '../../visualizer/ReliefRenderer.js';
import { hideDimensionInspector } from '../../visualizer/DimensionInspector.js';
import { saeMetricsMarkup } from './SaeMetricsPanel.js';
import { modelPanelMarkup } from './ModelPanel.js';
import { reliefIngestControlsMarkup, wireReliefIngest } from './relief/ReliefIngest.js';
import { reliefBootEmptyStateMarkup, wireReliefBootEmptyState } from './relief/ReliefAutoLoad.js';
import {
reliefFileSelectorMarkup,
wireReliefFileList,
} from './relief/ReliefFileList.js';
import { reliefSearchMarkup, wireReliefSearch } from './relief/ReliefSearch.js';
import { reliefComparisonMarkup, wireReliefComparison } from './relief/ReliefComparison.js';
/** @typedef {import('../types.js').PanelContext} PanelContext */
export function reliefIngestMarkup() {
return `
<div id="relief-ingest-controls" class="section-card" style="display: flex; flex-direction: column; gap: var(--spacing-xs);">
${modelPanelMarkup()}
<div style="border-top: 1px solid oklch(from var(--color-neutral-50) l c h / 0.05); padding-top: var(--spacing-xs); margin-top: 4px;">
${reliefFileSelectorMarkup()}
${reliefBootEmptyStateMarkup()}
${reliefIngestControlsMarkup()}
</div>
${saeMetricsMarkup()}
</div>
`;
}
export function reliefVisualizerMarkup() {
return `
<!-- Controles del Relieve Semántico (visibles en sub-modo RELIEF) -->
<div id="relief-submode-controls" style="display:${STATE.blanketSubMode === 'RELIEF' ? 'flex' : 'none'}; flex-direction:column; gap: var(--spacing-xs); margin-top: 8px;">
${reliefComparisonMarkup()}
<!-- Escala X -->
<div style="display:flex; flex-direction:column; gap:2px;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">X Compression:</label>
<span id="relief-scale-x-val" style="font-size: var(--font-size-caption); color: var(--color-brand-primary); font-family: var(--font-mono);">${STATE.reliefScaleX.toFixed(3)}</span>
</div>
<input type="range" id="relief-scale-x" min="0.005" max="0.2" step="0.005" value="${STATE.reliefScaleX}">
</div>
<!-- Escala Y -->
<div style="display:flex; flex-direction:column; gap:2px;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Y Elevation:</label>
<span id="relief-scale-y-val" style="font-size: var(--font-size-caption); color: var(--color-brand-primary); font-family: var(--font-mono);">${STATE.reliefScaleY.toFixed(1)}</span>
</div>
<input type="range" id="relief-scale-y" min="1.0" max="50.0" step="1.0" value="${STATE.reliefScaleY}">
</div>
<!-- Escala Z -->
<div style="display:flex; flex-direction:column; gap:2px;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Z Depth:</label>
<span id="relief-scale-z-val" style="font-size: var(--font-size-caption); color: var(--color-brand-primary); font-family: var(--font-mono);">${STATE.reliefScaleZ.toFixed(1)}</span>
</div>
<input type="range" id="relief-scale-z" min="0.1" max="10.0" step="0.1" value="${STATE.reliefScaleZ}">
</div>
<!-- Modo de render -->
<div style="display:flex; flex-direction:column; gap:2px; margin-top: 4px;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Render mode:</label>
<select id="relief-render-mode" class="input-field" style="background: oklch(from var(--color-neutral-950) l c h / 0.5);">
<option value="MESH" ${STATE.reliefRenderMode === 'MESH' ? 'selected' : ''}>Continuous Mesh</option>
<option value="POINTS" ${STATE.reliefRenderMode === 'POINTS' ? 'selected' : ''}>Point Cloud</option>
<option value="RIBBONS" ${STATE.reliefRenderMode === 'RIBBONS' ? 'selected' : ''}>🧵 Ribbons by Chunk (Divergent)</option>
</select>
</div>
<!-- Grosor de puntos (modo RIBBONS): chico = hilos separados, grueso = manta -->
<div id="relief-thickness-row" style="display:${STATE.reliefRenderMode === 'RIBBONS' ? 'flex' : 'none'}; flex-direction:column; gap:2px;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<label style="font-size: var(--font-size-caption); color: var(--color-neutral-400);">Point size:</label>
<span id="relief-thickness-val" style="font-size: var(--font-size-caption); color: var(--color-brand-primary); font-family: var(--font-mono);">${STATE.reliefThickness.toFixed(1)}</span>
</div>
<input type="range" id="relief-thickness" min="1.0" max="10.0" step="0.5" value="${STATE.reliefThickness}">
</div>
${reliefSearchMarkup()}
</div>
`;
}
/**
* Wire the relief controls. Publishes cross-panel refreshers and mirrors
* renderDocList/renderDimList onto window.* for DimensionInspector.
* @param {PanelContext} ctx
* @returns {void}
*/
export function wireReliefPanel(ctx) {
const { applyComparison } = ctx;
// --- Relieve Semántico Events ---
const btnSubThreads = document.getElementById('blanket-sub-threads');
const btnSubRelief = document.getElementById('blanket-sub-relief');
const threadsControls = document.getElementById('threads-submode-controls');
const reliefControls = document.getElementById('relief-submode-controls');
const reliefScaleX = document.getElementById('relief-scale-x');
const reliefScaleY = document.getElementById('relief-scale-y');
const reliefScaleZ = document.getElementById('relief-scale-z');
const valScaleX = document.getElementById('relief-scale-x-val');
const valScaleY = document.getElementById('relief-scale-y-val');
const valScaleZ = document.getElementById('relief-scale-z-val');
const reliefRenderMode = document.getElementById('relief-render-mode');
const fileList = wireReliefFileList(ctx);
wireReliefBootEmptyState();
wireReliefIngest(ctx);
wireReliefSearch(ctx, fileList);
const comparison = wireReliefComparison(ctx);
const updateSubModeUI = () => {
const subMode = STATE.blanketSubMode;
const layoutMode = STATE.layoutMode;
if (btnSubThreads) {
if (subMode === 'THREADS') {
btnSubThreads.style.background = 'var(--color-brand-primary)';
btnSubThreads.style.color = 'var(--color-neutral-950)';
} else {
btnSubThreads.style.background = '';
btnSubThreads.style.color = '';
}
}
if (btnSubRelief) {
if (subMode === 'RELIEF') {
btnSubRelief.style.background = 'var(--color-brand-secondary)';
btnSubRelief.style.color = 'var(--color-neutral-950)';
} else {
btnSubRelief.style.background = '';
btnSubRelief.style.color = '';
}
}
if (threadsControls) {
threadsControls.style.display = (subMode === 'THREADS' && layoutMode === 'BLANKET') ? 'block' : 'none';
}
const compareControls = document.getElementById('compare-controls');
if (compareControls) {
compareControls.style.display = (subMode === 'THREADS' && layoutMode === 'COMPARE') ? 'block' : 'none';
}
if (reliefControls) {
reliefControls.style.display = (subMode === 'RELIEF') ? 'flex' : 'none';
}
const compareModeContent = document.getElementById('compare-mode-content');
if (compareModeContent) {
compareModeContent.style.display = (subMode === 'THREADS' && layoutMode === 'COMPARE') ? 'block' : 'none';
}
if (subMode === 'THREADS') {
clearRelief(STATE.scene || window.scene);
hideDimensionInspector();
ctx.refresh.clearReliefSearch?.();
}
if (subMode === 'RELIEF') {
ctx.refresh.reliefFiles?.();
ctx.refresh.docList?.();
if (STATE.reliefDocs.length > 0) applyComparison();
} else {
const hud = document.getElementById('relief-hud');
if (hud) hud.style.display = 'none';
hideDimensionInspector();
}
};
ctx.refresh.subModeUI = updateSubModeUI;
if (btnSubThreads) {
btnSubThreads.onclick = () => {
STATE.blanketSubMode = 'THREADS';
updateSubModeUI();
updateThreadVisibility();
updateAllThreadPositions();
};
}
if (btnSubRelief) {
btnSubRelief.onclick = () => {
STATE.blanketSubMode = 'RELIEF';
updateSubModeUI();
updateThreadVisibility();
updateAllThreadPositions();
};
}
// Fast GPU scaling updates (User request 1: direct scale.set() calls, no geometry rebuild)
const updateScaleValues = () => {
const sx = STATE.reliefScaleX;
const sy = STATE.reliefScaleY;
const sz = STATE.reliefScaleZ;
updateReliefScale(sx, sy, sz);
};
if (reliefScaleX) {
reliefScaleX.oninput = function() {
STATE.reliefScaleX = parseFloat(this.value);
if (valScaleX) valScaleX.textContent = STATE.reliefScaleX.toFixed(3);
updateScaleValues();
};
}
if (reliefScaleY) {
reliefScaleY.oninput = function() {
STATE.reliefScaleY = parseFloat(this.value);
if (valScaleY) valScaleY.textContent = STATE.reliefScaleY.toFixed(1);
updateScaleValues();
};
}
if (reliefScaleZ) {
reliefScaleZ.oninput = function() {
STATE.reliefScaleZ = parseFloat(this.value);
if (valScaleZ) valScaleZ.textContent = STATE.reliefScaleZ.toFixed(1);
updateScaleValues();
};
}
const reliefThicknessRow = document.getElementById('relief-thickness-row');
const reliefThickness = document.getElementById('relief-thickness');
const valThickness = document.getElementById('relief-thickness-val');
if (reliefRenderMode) {
reliefRenderMode.onchange = function() {
STATE.reliefRenderMode = this.value;
// El slider de grosor solo aplica al modo RIBBONS
if (reliefThicknessRow) {
reliefThicknessRow.style.display = (this.value === 'RIBBONS') ? 'flex' : 'none';
}
// Cambiar el tipo de geometría requiere reconstruir; re-aplica la comparación.
if (STATE.reliefDocs.length > 0) applyComparison();
};
}
if (reliefThickness) {
reliefThickness.oninput = function() {
STATE.reliefThickness = parseFloat(this.value);
if (valThickness) valThickness.textContent = STATE.reliefThickness.toFixed(1);
// Cambio de tamaño en caliente (sin reconstruir geometría)
updateReliefThickness(STATE.reliefThickness);
};
}
fileList.setOnDocListRendered(comparison.updateSimilarityMetrics);
// Initialize comparison UI state (mode toggles + active doc list).
comparison.updateComparisonModeUI();
comparison.updateBSubUI();
ctx.refresh.docList?.();
// Initialize Relief UI state on load if already in relief mode
if (STATE.blanketSubMode === 'RELIEF') {
updateSubModeUI();
}
}