feat(ui): make network blocks interactive

main
AlexsandrSnytkin 14 hours ago
parent 20a198d634
commit 6895fb81ff

@ -1449,9 +1449,9 @@ HTML = """<!doctype html>
body[data-density="spacious"] .settings-group, body[data-density="spacious"] .model-group { padding: 15px 16px; gap: 10px 12px; } body[data-density="spacious"] .settings-group, body[data-density="spacious"] .model-group { padding: 15px 16px; gap: 10px 12px; }
body[data-density="spacious"] .quickbar { gap: 10px; } body[data-density="spacious"] .quickbar { gap: 10px; }
body[data-radius="sharp"] .stage, body[data-radius="sharp"] .settings-panel, body[data-radius="sharp"] .model-panel, body[data-radius="sharp"] .archive-panel, body[data-radius="sharp"] .log-drawer, body[data-radius="sharp"] .player-box, body[data-radius="sharp"] .netron-panel { border-radius: 5px; } body[data-radius="sharp"] .stage, body[data-radius="sharp"] .settings-panel, body[data-radius="sharp"] .model-panel, body[data-radius="sharp"] .archive-panel, body[data-radius="sharp"] .log-drawer, body[data-radius="sharp"] .player-box, body[data-radius="sharp"] .netron-panel { border-radius: 5px; }
body[data-radius="sharp"] button, body[data-radius="sharp"] select, body[data-radius="sharp"] input, body[data-radius="sharp"] textarea, body[data-radius="sharp"] .config-block { border-radius: 4px !important; } body[data-radius="sharp"] button, body[data-radius="sharp"] select, body[data-radius="sharp"] input, body[data-radius="sharp"] textarea, body[data-radius="sharp"] .config-block, body[data-radius="sharp"] .network-block { border-radius: 4px !important; }
body[data-radius="round"] .stage, body[data-radius="round"] .settings-panel, body[data-radius="round"] .model-panel, body[data-radius="round"] .archive-panel, body[data-radius="round"] .log-drawer, body[data-radius="round"] .player-box, body[data-radius="round"] .netron-panel { border-radius: 20px; } body[data-radius="round"] .stage, body[data-radius="round"] .settings-panel, body[data-radius="round"] .model-panel, body[data-radius="round"] .archive-panel, body[data-radius="round"] .log-drawer, body[data-radius="round"] .player-box, body[data-radius="round"] .netron-panel { border-radius: 20px; }
body[data-radius="round"] button, body[data-radius="round"] select, body[data-radius="round"] input, body[data-radius="round"] textarea, body[data-radius="round"] .config-block { border-radius: 14px !important; } body[data-radius="round"] button, body[data-radius="round"] select, body[data-radius="round"] input, body[data-radius="round"] textarea, body[data-radius="round"] .config-block, body[data-radius="round"] .network-block { border-radius: 14px !important; }
body[data-contrast="soft"] { --ui-muted: var(--ui-muted-contrast); --ui-border: var(--ui-border-contrast); } body[data-contrast="soft"] { --ui-muted: var(--ui-muted-contrast); --ui-border: var(--ui-border-contrast); }
body[data-contrast="high"] { --ui-muted: var(--ui-muted-contrast); --ui-border: var(--ui-border-contrast); } body[data-contrast="high"] { --ui-muted: var(--ui-muted-contrast); --ui-border: var(--ui-border-contrast); }
body[data-motion="none"] *, body[data-motion="none"] *::before, body[data-motion="none"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } body[data-motion="none"] *, body[data-motion="none"] *::before, body[data-motion="none"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }
@ -1476,13 +1476,25 @@ HTML = """<!doctype html>
.config-block.settings-group input:not([type="checkbox"]):not([type="file"]), .config-block.settings-group select, .config-block.settings-group button { min-height: calc(27px * var(--block-scale)); height: calc(27px * var(--block-scale)); padding-left: calc(7px * var(--block-scale)); padding-right: calc(7px * var(--block-scale)); font-size: clamp(8px, calc(12px * var(--block-scale)), 26px); } .config-block.settings-group input:not([type="checkbox"]):not([type="file"]), .config-block.settings-group select, .config-block.settings-group button { min-height: calc(27px * var(--block-scale)); height: calc(27px * var(--block-scale)); padding-left: calc(7px * var(--block-scale)); padding-right: calc(7px * var(--block-scale)); font-size: clamp(8px, calc(12px * var(--block-scale)), 26px); }
.config-block.settings-group .control-fields, .config-block.settings-group .advanced-grid, .config-block.settings-group .packet-grid { gap: calc(6px * var(--block-scale)) calc(8px * var(--block-scale)); } .config-block.settings-group .control-fields, .config-block.settings-group .advanced-grid, .config-block.settings-group .packet-grid { gap: calc(6px * var(--block-scale)) calc(8px * var(--block-scale)); }
.network-panel { overflow: hidden; } .network-panel { overflow: hidden; }
.network-canvas { position: relative; display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 14px; padding: 14px; background: radial-gradient(circle at 10% 0%, color-mix(in srgb, var(--ui-accent) 7%, transparent), transparent 32%), color-mix(in srgb, var(--ui-bg) 74%, var(--ui-panel)); } .network-workspace { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; }
.network-block { grid-column: span 6; min-width: 0; margin: 0; padding: 14px; border: 1px solid var(--ui-border) !important; border-radius: 14px !important; background: color-mix(in srgb, var(--ui-panel) 94%, var(--ui-accent)) !important; box-shadow: 0 8px 22px color-mix(in srgb, var(--ui-bg) 20%, transparent); overflow: hidden; transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease; } .network-workspace.has-structure { grid-template-columns: minmax(190px, 260px) minmax(0, 1fr); align-items: start; }
.network-workspace.has-structure > .network-restore-panel { grid-column: 1; border-right: 1px solid var(--ui-border); border-bottom: 0; }
.network-workspace.has-structure > .network-canvas { grid-column: 2; }
.network-restore-panel { min-height: 0; max-height: calc(100vh - 128px); }
.network-canvas { position: relative; display: block; min-height: 720px; padding: 14px; overflow: auto; background: radial-gradient(circle at 10% 0%, color-mix(in srgb, var(--ui-accent) 7%, transparent), transparent 32%), color-mix(in srgb, var(--ui-bg) 74%, var(--ui-panel)); }
.network-block { position: absolute; min-width: 240px; min-height: 110px; margin: 0; padding: 14px; border: 1px solid var(--ui-border) !important; border-radius: 14px !important; background: color-mix(in srgb, var(--ui-panel) 94%, var(--ui-accent)) !important; box-shadow: 0 8px 22px color-mix(in srgb, var(--ui-bg) 20%, transparent); overflow: auto; transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease; cursor: grab; touch-action: none; }
.network-block:hover { border-color: color-mix(in srgb, var(--ui-accent) 48%, var(--ui-border)) !important; transform: translateY(-2px); } .network-block:hover { border-color: color-mix(in srgb, var(--ui-accent) 48%, var(--ui-border)) !important; transform: translateY(-2px); }
.network-block > summary { grid-column: 1 / -1; margin: -2px 0 5px; padding-bottom: 8px; border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 80%, transparent); } .network-block.is-selected { border-color: color-mix(in srgb, var(--ui-accent) 70%, var(--ui-border)) !important; box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-accent) 12%, transparent), 0 8px 22px color-mix(in srgb, var(--ui-bg) 20%, transparent); }
.network-block.is-dragging, .network-block.is-resizing { z-index: 5; border-color: var(--ui-accent) !important; box-shadow: 0 10px 28px color-mix(in srgb, var(--ui-accent) 16%, transparent); transform: none; transition: none; user-select: none; }
.network-block.is-deleted, .network-block.is-hidden { display: none !important; }
.network-summary { display: flex; align-items: center; gap: 6px; margin: -2px 0 8px; padding-bottom: 8px; border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 80%, transparent); color: var(--ui-text); font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.network-summary::before { content: ""; color: var(--ui-accent); font-size: 11px; }
.network-block.settings-group { --block-scale: 1; padding: calc(14px * var(--block-scale)); gap: calc(8px * var(--block-scale)); }
.network-block.settings-group label, .network-block.settings-group .model-state { gap: calc(3px * var(--block-scale)); font-size: clamp(8px, calc(11px * var(--block-scale)), 22px); }
.network-block.settings-group input:not([type="checkbox"]):not([type="file"]), .network-block.settings-group select, .network-block.settings-group button { min-height: calc(28px * var(--block-scale)); height: calc(28px * var(--block-scale)); padding-left: calc(7px * var(--block-scale)); padding-right: calc(7px * var(--block-scale)); font-size: clamp(8px, calc(12px * var(--block-scale)), 26px); }
.network-block.settings-group .network-summary { margin-bottom: calc(8px * var(--block-scale)); padding-bottom: calc(8px * var(--block-scale)); font-size: clamp(9px, calc(12px * var(--block-scale)), 24px); }
.network-block .model-actions { padding-top: 3px; } .network-block .model-actions { padding-top: 3px; }
.network-visual-card { margin: 0 14px 14px; padding: 14px; border: 1px solid var(--ui-border); border-radius: 14px; background: color-mix(in srgb, var(--ui-panel) 94%, var(--ui-accent)); box-shadow: 0 8px 22px color-mix(in srgb, var(--ui-bg) 20%, transparent); } .network-visual-card { padding: 14px; }
.network-visual-card h2 { margin: 0; }
body[data-card-style="flat"] .network-block, body[data-card-style="flat"] .network-visual-card { box-shadow: none; border-radius: 8px !important; } body[data-card-style="flat"] .network-block, body[data-card-style="flat"] .network-visual-card { box-shadow: none; border-radius: 8px !important; }
body[data-card-style="neon"] .network-block, body[data-card-style="neon"] .network-visual-card { border-color: color-mix(in srgb, var(--ui-accent) 48%, var(--ui-border)) !important; box-shadow: 0 0 0 1px color-mix(in srgb, var(--ui-accent) 12%, transparent), 0 10px 24px color-mix(in srgb, var(--ui-accent) 10%, transparent); } body[data-card-style="neon"] .network-block, body[data-card-style="neon"] .network-visual-card { border-color: color-mix(in srgb, var(--ui-accent) 48%, var(--ui-border)) !important; box-shadow: 0 0 0 1px color-mix(in srgb, var(--ui-accent) 12%, transparent), 0 10px 24px color-mix(in srgb, var(--ui-accent) 10%, transparent); }
@keyframes appearance-in { from { opacity: 0; transform: translateY(-5px) scale(.98); } to { opacity: 1; transform: none; } } @keyframes appearance-in { from { opacity: 0; transform: translateY(-5px) scale(.98); } to { opacity: 1; transform: none; } }
@ -1495,9 +1507,12 @@ HTML = """<!doctype html>
.appearance-trigger-chevron { display: none; } .appearance-trigger-chevron { display: none; }
.appearance-panel { position: fixed; top: 56px; right: 8px; width: min(390px, calc(100vw - 16px)); } .appearance-panel { position: fixed; top: 56px; right: 8px; width: min(390px, calc(100vw - 16px)); }
.appearance-options { grid-template-columns: repeat(3, minmax(0, 1fr)); } .appearance-options { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.network-canvas { grid-template-columns: 1fr; padding: 10px; gap: 10px; } .network-workspace.has-structure { grid-template-columns: 1fr; }
.network-block { grid-column: 1; } .network-workspace.has-structure > .network-restore-panel, .network-workspace.has-structure > .network-canvas { grid-column: 1; }
.network-visual-card { margin: 0 10px 10px; padding: 10px; } .network-workspace.has-structure > .network-restore-panel { border-right: 0; border-bottom: 1px solid var(--ui-border); max-height: 42vh; }
.network-canvas { min-height: 900px; padding: 10px; }
.network-block { min-width: 220px; max-width: calc(100% - 8px); }
.network-visual-card { padding: 10px; }
} }
@media (max-width: 420px) { .appearance-options { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 420px) { .appearance-options { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
</style> </style>
@ -1808,9 +1823,21 @@ HTML = """<!doctype html>
</div> </div>
<div id="modelTab" class="tab-page model-tab" hidden> <div id="modelTab" class="tab-page model-tab" hidden>
<section class="model-panel network-panel"> <section class="model-panel network-panel">
<div class="model-grid network-canvas"> <div class="config-toolbar network-toolbar">
<details class="model-group settings-group network-block" data-section="model" open> <span class="config-toolbar-title">Блоки сети</span>
<summary>Модель</summary> <button id="networkResetLayout" type="button">сбросить раскладку</button>
<button id="networkStructureToggle" type="button" aria-expanded="false">структура</button>
<label class="config-scale-control" title="Масштаб элементов и текста выбранного сетевого блока"><span>масштаб блока</span><input id="networkContentScale" type="range" min="50" max="200" step="5" value="100" aria-label="Масштаб содержимого выбранного сетевого блока" disabled><output id="networkContentScaleValue">100%</output></label>
<span id="networkLayoutState" class="config-layout-state">перетаскивайте блоки за свободное место</span>
</div>
<div id="networkWorkspace" class="network-workspace">
<aside id="networkRestorePanel" class="config-restore-panel network-restore-panel" hidden>
<span class="config-restore-title">Структура сети</span>
<div id="networkRestoreList" class="config-restore-list"></div>
</aside>
<div id="networkCanvas" class="model-grid network-canvas">
<section class="model-group settings-group network-block" data-section="model">
<div class="network-summary">Модель</div>
<label class="model-wide">Файл весов <label class="model-wide">Файл весов
<select id="modelSelect"><option value="">загрузка списка...</option></select> <select id="modelSelect"><option value="">загрузка списка...</option></select>
</label> </label>
@ -1821,9 +1848,9 @@ HTML = """<!doctype html>
<span id="modelState" class="model-state">модель не выбрана</span> <span id="modelState" class="model-state">модель не выбрана</span>
</div> </div>
<label class="model-wide">Путь<input id="modelPath" type="text" readonly></label> <label class="model-wide">Путь<input id="modelPath" type="text" readonly></label>
</details> </section>
<details class="model-group settings-group network-block" data-section="model-params" open> <section class="model-group settings-group network-block" data-section="model-params">
<summary>Параметры до инференса</summary> <div class="network-summary">Параметры до инференса</div>
<label>Устройство <label>Устройство
<select id="modelDevice"> <select id="modelDevice">
<option value="-1">CPU</option> <option value="-1">CPU</option>
@ -1837,10 +1864,9 @@ HTML = """<!doctype html>
<label>Размер полного кадра<input id="modelFull" type="number" min="128" max="4096" step="32" value="1280"></label> <label>Размер полного кадра<input id="modelFull" type="number" min="128" max="4096" step="32" value="1280"></label>
<label>Макс. детекций<input id="modelMaxDet" type="number" min="1" max="300" step="1" value="60"></label> <label>Макс. детекций<input id="modelMaxDet" type="number" min="1" max="300" step="1" value="60"></label>
<span class="model-state model-wide">Параметры применяются при следующем старте.</span> <span class="model-state model-wide">Параметры применяются при следующем старте.</span>
</details> </section>
</div> <section class="netron-section network-visual-card network-block" data-section="netron">
<section class="netron-section network-visual-card"> <div class="network-summary">Визуализация сети</div>
<h2>Визуализация сети</h2>
<div class="netron-toolbar"> <div class="netron-toolbar">
<button id="modelNetron" class="command primary" type="button">Показать в Netron</button> <button id="modelNetron" class="command primary" type="button">Показать в Netron</button>
<span id="netronState" class="netron-state">граф Netron не загружен</span> <span id="netronState" class="netron-state">граф Netron не загружен</span>
@ -1849,6 +1875,12 @@ HTML = """<!doctype html>
<iframe id="netronFrame" class="netron-frame" title="Визуализация архитектуры сети в Netron" loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe> <iframe id="netronFrame" class="netron-frame" title="Визуализация архитектуры сети в Netron" loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe>
</div> </div>
</section> </section>
</div>
</div>
<div id="networkTrash" class="config-trash network-trash" role="button" tabindex="0" aria-label="Перетащите сетевой блок сюда для удаления" hidden>
<span class="config-trash-icon" aria-hidden="true"></span>
<span>отпустите здесь, чтобы удалить блок</span>
</div>
</section> </section>
</div> </div>
<div id="archiveTab" class="tab-page archive-tab" hidden> <div id="archiveTab" class="tab-page archive-tab" hidden>
@ -2674,6 +2706,380 @@ HTML = """<!doctype html>
selectConfigBlock(selectedConfigBlock); selectConfigBlock(selectedConfigBlock);
$('configContentScale')?.addEventListener('input', event => applyConfigContentScale(event.target.value)); $('configContentScale')?.addEventListener('input', event => applyConfigContentScale(event.target.value));
} }
let networkCanvas = null;
let networkBlocks = [];
let networkLayout = {};
let networkZIndex = 1;
let networkSelectedBlock = null;
let networkStructureOpen = false;
let networkContentScale = 1;
function readNetworkLayout() {
try {
const saved = JSON.parse(localStorage.getItem('fpv-network-layout-v1') || '{}');
return saved && typeof saved === 'object' ? saved : {};
} catch (_error) { return {}; }
}
function saveNetworkLayout() {
try {
localStorage.setItem('fpv-network-layout-v1', JSON.stringify(networkLayout));
if (networkCanvas) localStorage.setItem('fpv-network-canvas-width', String(Math.round(networkCanvasWidth())));
} catch (_error) {}
}
function networkMinWidth() { return window.innerWidth <= 760 ? 220 : 240; }
function networkCanvasWidth() { return Math.max(networkMinWidth(), (networkCanvas?.clientWidth || 0) - 28); }
function networkState(block) { return block?.dataset.blockState || ''; }
function networkTitle(block) { return block?.querySelector(':scope > .network-summary')?.textContent?.trim() || block?.dataset.section || 'Блок'; }
function updateNetworkBlockScale(block) {
if (!block) return;
const width = Math.max(networkMinWidth(), block.clientWidth || block.offsetWidth || networkMinWidth());
const height = Math.max(110, block.clientHeight || block.offsetHeight || 110);
const area = Math.max(.56, Math.min(1.85, (width / 720) * (height / 320)));
const entry = networkLayout[block.dataset.section] || {};
const userScale = Number.isFinite(Number(block._networkUserScale)) ? block._networkUserScale : (Number(entry.scale) || 100) / 100;
block._networkUserScale = Math.max(.5, Math.min(2, userScale));
block.style.setProperty('--block-scale', Math.max(.5, Math.min(2, Math.sqrt(area) * block._networkUserScale)).toFixed(3));
}
function selectNetworkBlock(block) {
if (!block || networkState(block)) {
networkSelectedBlock = null;
networkBlocks.forEach(item => item.classList.remove('is-selected'));
const control = $('networkContentScale');
if (control) control.disabled = true;
return;
}
networkSelectedBlock = block;
networkBlocks.forEach(item => item.classList.toggle('is-selected', item === block));
const value = Math.max(50, Math.min(200, Math.round(Number(networkLayout[block.dataset.section]?.scale) || 100)));
networkContentScale = value / 100;
setValue('networkContentScale', value);
setText('networkContentScaleValue', `${value}%`);
const control = $('networkContentScale');
if (control) control.disabled = false;
updateNetworkBlockScale(block);
}
function applyNetworkContentScale(value) {
networkContentScale = Math.max(.5, Math.min(2, (Number(value) || 100) / 100));
const block = networkSelectedBlock;
if (!block || networkState(block)) return;
block._networkUserScale = networkContentScale;
const entry = networkLayout[block.dataset.section] || {};
entry.scale = Math.round(networkContentScale * 100);
networkLayout[block.dataset.section] = entry;
setText('networkContentScaleValue', `${Math.round(networkContentScale * 100)}%`);
updateNetworkBlockScale(block);
saveNetworkLayout();
}
function captureNetworkLayout(block) {
const key = block?.dataset.section;
if (!key) return;
const entry = networkLayout[key] || {};
entry.left = Math.round(parseFloat(block.style.left) || block.offsetLeft || 0);
entry.top = Math.round(parseFloat(block.style.top) || block.offsetTop || 0);
entry.width = Math.round(block.offsetWidth);
if (block.style.height && block.style.height !== 'auto') entry.height = Math.round(block.offsetHeight);
else delete entry.height;
if (Number.isFinite(Number(block._networkUserScale)) && Math.abs(block._networkUserScale - 1) > .001) entry.scale = Math.round(block._networkUserScale * 100);
else delete entry.scale;
if (block.style.zIndex) entry.z = Number(block.style.zIndex);
networkLayout[key] = entry;
}
function placeNetworkBlock(block, left, top, width) {
if (!block) return;
const canvasWidth = networkCanvasWidth();
const minWidth = networkMinWidth();
const safeLeft = Math.max(0, Math.round(Math.min(left, Math.max(0, canvasWidth - minWidth))));
const safeWidth = Math.max(minWidth, Math.min(canvasWidth - safeLeft, Math.round(width)));
block.style.left = `${safeLeft}px`;
block.style.top = `${Math.max(0, Math.round(top))}px`;
block.style.width = `${safeWidth}px`;
}
function updateNetworkCanvasHeight() {
if (!networkCanvas) return;
const bottom = networkBlocks.reduce((value, block) => networkState(block) ? value : Math.max(value, (block.offsetTop || 0) + block.offsetHeight), 0);
networkCanvas.style.minHeight = `${Math.max(window.innerWidth <= 760 ? 900 : 720, bottom + 28)}px`;
}
function applyNetworkLayout() {
if (!networkCanvas) return;
const width = networkCanvasWidth();
const gap = 14;
const visible = block => block && !networkState(block);
const model = networkBlocks.find(block => block.dataset.section === 'model' && visible(block));
const params = networkBlocks.find(block => block.dataset.section === 'model-params' && visible(block));
const netron = networkBlocks.find(block => block.dataset.section === 'netron' && visible(block));
const placeDefault = (block, left, top, blockWidth) => {
if (!block) return;
const entry = networkLayout[block.dataset.section] || {};
if (!Number.isFinite(Number(entry.left)) || !Number.isFinite(Number(entry.top))) placeNetworkBlock(block, left, top, blockWidth);
};
const half = Math.max(networkMinWidth(), (width - gap) / 2);
placeDefault(model, 0, 14, half);
placeDefault(params, half + gap, 14, half);
const rowHeight = Math.max(model?.offsetHeight || 110, params?.offsetHeight || 110);
placeDefault(netron, 0, rowHeight + 28, width);
networkBlocks.forEach(block => {
const entry = networkLayout[block.dataset.section];
if (!entry || networkState(block)) return;
const left = Number.isFinite(Number(entry.left)) ? Math.max(0, Math.min(width - networkMinWidth(), Number(entry.left))) : block.offsetLeft;
block.style.left = `${Math.round(left)}px`;
if (Number.isFinite(Number(entry.top))) block.style.top = `${Math.max(0, Math.round(Number(entry.top)))}px`;
if (Number.isFinite(Number(entry.width))) block.style.width = `${Math.max(networkMinWidth(), Math.min(width - left, Number(entry.width)))}px`;
if (Number.isFinite(Number(entry.height))) block.style.height = `${Math.max(110, Number(entry.height))}px`;
if (Number.isFinite(Number(entry.z))) { block.style.zIndex = String(Math.max(1, Number(entry.z))); networkZIndex = Math.max(networkZIndex, Number(entry.z)); }
updateNetworkBlockScale(block);
});
updateNetworkCanvasHeight();
}
function bringNetworkBlockFront(block) {
if (!block || networkState(block)) return;
block.style.zIndex = String(++networkZIndex);
captureNetworkLayout(block);
saveNetworkLayout();
}
function renderNetworkStructure() {
const panel = $('networkRestorePanel');
const list = $('networkRestoreList');
const toggle = $('networkStructureToggle');
const workspace = $('networkWorkspace');
if (!panel || !list || !toggle) return;
list.replaceChildren();
networkBlocks.forEach(block => {
const state = networkState(block);
const item = document.createElement('div');
item.className = `config-restore-item${state ? ' is-hidden' : ''}`;
item.title = 'Нажмите, чтобы выбрать блок';
const label = document.createElement('span');
label.className = 'config-item-title';
label.textContent = networkTitle(block);
const stateNode = document.createElement('span');
stateNode.className = 'config-item-state';
stateNode.textContent = state === 'deleted' ? 'в корзине' : state === 'hidden' ? 'скрыт' : 'активен';
const actions = document.createElement('span');
actions.className = 'config-item-actions';
const visibility = document.createElement('button');
visibility.type = 'button';
visibility.textContent = state ? 'вернуть' : 'скрыть';
visibility.addEventListener('click', event => {
event.stopPropagation();
setNetworkBlockState(block, state ? '' : 'hidden');
});
actions.appendChild(visibility);
item.append(label, stateNode, actions);
item.addEventListener('click', () => {
if (!state) selectNetworkBlock(block);
item.classList.add('is-selected');
bringNetworkBlockFront(block);
});
list.appendChild(item);
});
const hidden = networkBlocks.filter(block => networkState(block) === 'hidden').length;
const deleted = networkBlocks.filter(block => networkState(block) === 'deleted').length;
toggle.textContent = hidden || deleted ? `структура · ${hidden + deleted}` : 'структура';
toggle.setAttribute('aria-expanded', String(networkStructureOpen));
panel.hidden = !networkStructureOpen;
workspace?.classList.toggle('has-structure', networkStructureOpen);
setText('networkLayoutState', `${networkBlocks.length - hidden - deleted} блоков · ${hidden} скрыто · ${deleted} в корзине`);
}
function setNetworkBlockState(block, state) {
if (!block) return;
const key = block.dataset.section;
if (key && state) networkLayout[key] = {...(networkLayout[key] || {}), state};
else if (key && networkLayout[key]) {
delete networkLayout[key].state;
if (!Object.keys(networkLayout[key]).length) delete networkLayout[key];
}
block.dataset.blockState = state || '';
block.classList.toggle('is-hidden', state === 'hidden');
block.classList.toggle('is-deleted', state === 'deleted');
if (state) block.setAttribute('aria-hidden', 'true');
else block.removeAttribute('aria-hidden');
saveNetworkLayout();
applyNetworkLayout();
renderNetworkStructure();
selectNetworkBlock(networkBlocks.find(candidate => !networkState(candidate)) || null);
}
function networkResizeEdge(event, block) {
const rect = block.getBoundingClientRect();
const edge = 9;
const left = event.clientX - rect.left <= edge;
const right = rect.right - event.clientX <= edge;
const top = event.clientY - rect.top <= edge;
const bottom = rect.bottom - event.clientY <= edge;
return `${top ? 'n' : bottom ? 's' : ''}${left ? 'w' : right ? 'e' : ''}`;
}
function networkDragBlocked(block, event) {
const target = event.target;
if (!(target instanceof Element)) return true;
return !!target.closest('input, select, textarea, button, a, iframe, [contenteditable="true"], .netron-panel');
}
function networkTrashDrop(event) {
const trash = $('networkTrash');
if (!trash || trash.hidden || !event) return false;
const rect = trash.getBoundingClientRect();
return event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom;
}
function showNetworkTrash() {
const trash = $('networkTrash');
if (trash) { trash.hidden = false; trash.classList.remove('is-over'); }
}
function finishNetworkDrag(block, event, dragged) {
const trash = $('networkTrash');
const droppedOnTrash = networkTrashDrop(event);
if (trash) { trash.classList.remove('is-over'); trash.hidden = true; }
if (!dragged) return;
captureNetworkLayout(block);
if (droppedOnTrash) setNetworkBlockState(block, 'deleted');
else { saveNetworkLayout(); renderNetworkStructure(); }
block._networkSuppressClick = true;
window.setTimeout(() => { block._networkSuppressClick = false; }, 0);
}
function startNetworkDrag(block, event) {
if (event.button !== 0 || !networkCanvas || networkDragBlocked(block, event)) return;
const startX = event.clientX;
const startY = event.clientY;
const startLeft = parseFloat(block.style.left) || block.offsetLeft || 0;
const startTop = parseFloat(block.style.top) || block.offsetTop || 0;
let dragged = false;
showNetworkTrash();
block.setPointerCapture?.(event.pointerId);
const move = moveEvent => {
if (!dragged && Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < 4) return;
if (!dragged) { dragged = true; moveEvent.preventDefault(); block.classList.add('is-dragging'); }
moveEvent.preventDefault();
const maxLeft = Math.max(0, networkCanvasWidth() - block.offsetWidth);
block.style.left = `${Math.max(0, Math.min(maxLeft, Math.round(startLeft + moveEvent.clientX - startX)))}px`;
block.style.top = `${Math.max(0, Math.round(startTop + moveEvent.clientY - startY))}px`;
const trash = $('networkTrash');
if (trash) trash.classList.toggle('is-over', networkTrashDrop(moveEvent));
updateNetworkCanvasHeight();
};
const end = endEvent => {
block.releasePointerCapture?.(event.pointerId);
block.removeEventListener('pointermove', move);
block.removeEventListener('pointerup', end);
block.removeEventListener('pointercancel', end);
block.classList.remove('is-dragging');
finishNetworkDrag(block, endEvent, dragged);
};
block.addEventListener('pointermove', move);
block.addEventListener('pointerup', end, {once: true});
block.addEventListener('pointercancel', end, {once: true});
}
function makeNetworkBlockInteractive(block) {
if (!block || block._networkInteractive) return;
block._networkInteractive = true;
block.addEventListener('pointerdown', event => {
selectNetworkBlock(block);
bringNetworkBlockFront(block);
const edge = networkResizeEdge(event, block);
if (!edge || networkDragBlocked(block, event)) return;
event.preventDefault();
event.stopPropagation();
const startX = event.clientX;
const startY = event.clientY;
const startLeft = parseFloat(block.style.left) || block.offsetLeft || 0;
const startTop = parseFloat(block.style.top) || block.offsetTop || 0;
const startWidth = block.offsetWidth;
const startHeight = block.offsetHeight;
block.classList.add('is-resizing');
block.setPointerCapture?.(event.pointerId);
const move = moveEvent => {
const dx = moveEvent.clientX - startX;
const dy = moveEvent.clientY - startY;
const minWidth = networkMinWidth();
const canvasWidth = networkCanvasWidth();
let left = startLeft;
let top = startTop;
let width = startWidth;
let height = startHeight;
if (edge.includes('w')) { left = Math.max(0, Math.min(startLeft + dx, startLeft + startWidth - minWidth)); width = startWidth - (left - startLeft); }
if (edge.includes('e')) width = Math.max(minWidth, Math.min(canvasWidth - left, startWidth + dx));
if (edge.includes('n')) { top = Math.max(0, Math.min(startTop + dy, startTop + startHeight - 110)); height = startHeight - (top - startTop); }
if (edge.includes('s')) height = Math.max(110, startHeight + dy);
block.style.left = `${Math.round(left)}px`;
block.style.top = `${Math.round(top)}px`;
block.style.width = `${Math.round(width)}px`;
block.style.height = `${Math.round(height)}px`;
updateNetworkBlockScale(block);
updateNetworkCanvasHeight();
};
const end = () => {
block.releasePointerCapture?.(event.pointerId);
block.removeEventListener('pointermove', move);
block.removeEventListener('pointerup', end);
block.removeEventListener('pointercancel', end);
block.classList.remove('is-resizing');
captureNetworkLayout(block);
saveNetworkLayout();
block._networkSuppressClick = true;
window.setTimeout(() => { block._networkSuppressClick = false; }, 0);
};
block.addEventListener('pointermove', move);
block.addEventListener('pointerup', end, {once: true});
block.addEventListener('pointercancel', end, {once: true});
});
block.addEventListener('pointermove', event => {
if (!block.classList.contains('is-resizing')) {
const edge = networkResizeEdge(event, block);
block.style.cursor = edge ? `${edge}-resize` : '';
}
});
block.addEventListener('pointerdown', event => {
if (event.defaultPrevented || block.classList.contains('is-resizing')) return;
startNetworkDrag(block, event);
});
}
function resetNetworkLayout() {
networkLayout = {};
networkZIndex = 1;
networkBlocks.forEach(block => {
block.style.left = '';
block.style.top = '';
block.style.width = '';
block.style.height = '';
block.style.zIndex = '';
block._networkUserScale = 1;
block.dataset.blockState = '';
block.classList.remove('is-hidden', 'is-deleted');
block.removeAttribute('aria-hidden');
});
saveNetworkLayout();
applyNetworkLayout();
renderNetworkStructure();
selectNetworkBlock(networkBlocks[0] || null);
setText('networkLayoutState', 'раскладка сети сброшена');
}
function initNetworkCanvas() {
networkCanvas = $('networkCanvas');
if (!networkCanvas) return;
networkLayout = readNetworkLayout();
networkBlocks = Array.from(networkCanvas.children).filter(node => node.classList.contains('network-block'));
networkBlocks.forEach(block => {
makeNetworkBlockInteractive(block);
const state = networkLayout[block.dataset.section]?.state;
if (state) {
block.dataset.blockState = state;
block.classList.add(state === 'deleted' ? 'is-deleted' : 'is-hidden');
block.setAttribute('aria-hidden', 'true');
}
});
applyNetworkLayout();
renderNetworkStructure();
selectNetworkBlock(networkBlocks.find(block => !networkState(block)) || null);
$('networkResetLayout')?.addEventListener('click', resetNetworkLayout);
$('networkStructureToggle')?.addEventListener('click', () => {
networkStructureOpen = !networkStructureOpen;
renderNetworkStructure();
});
$('networkContentScale')?.addEventListener('input', event => applyNetworkContentScale(event.target.value));
window.addEventListener('resize', applyNetworkLayout);
if (window.ResizeObserver) {
const observer = new ResizeObserver(entries => {
entries.forEach(entry => updateNetworkBlockScale(entry.target));
updateNetworkCanvasHeight();
});
networkBlocks.forEach(block => observer.observe(block));
}
}
function parseFrameSize(value) { function parseFrameSize(value) {
const match = /^(\\d{1,5})x(\\d{1,5})$/i.exec(String(value || '').trim()); const match = /^(\\d{1,5})x(\\d{1,5})$/i.exec(String(value || '').trim());
return match ? [Number(match[1]), Number(match[2])] : [1280, 720]; return match ? [Number(match[1]), Number(match[2])] : [1280, 720];
@ -3568,6 +3974,7 @@ HTML = """<!doctype html>
$('settingsTab').hidden = name !== 'settings'; $('settingsTab').hidden = name !== 'settings';
$('modelTab').hidden = name !== 'model'; $('modelTab').hidden = name !== 'model';
if (name === 'settings' && typeof applyStoredConfigLayout === 'function') requestAnimationFrame(applyStoredConfigLayout); if (name === 'settings' && typeof applyStoredConfigLayout === 'function') requestAnimationFrame(applyStoredConfigLayout);
if (name === 'model' && typeof applyNetworkLayout === 'function') requestAnimationFrame(applyNetworkLayout);
if (name === 'archive') loadArchive(); if (name === 'archive') loadArchive();
if (updateHash && location.hash !== `#${name}`) history.replaceState(null, '', `#${name}`); if (updateHash && location.hash !== `#${name}`) history.replaceState(null, '', `#${name}`);
} }
@ -4030,6 +4437,7 @@ HTML = """<!doctype html>
restoreAppearanceSettings(); restoreAppearanceSettings();
initCollapsibleSections(); initCollapsibleSections();
initConfigCanvas(); initConfigCanvas();
initNetworkCanvas();
loadArchive(); loadArchive();
switchTab(location.hash.slice(1) || 'stream', false); switchTab(location.hash.slice(1) || 'stream', false);
</script> </script>

Loading…
Cancel
Save