diff --git a/ui_server.py b/ui_server.py
index 4e2e2e9..8570158 100644
--- a/ui_server.py
+++ b/ui_server.py
@@ -1449,9 +1449,9 @@ HTML = """
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-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"] 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="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; }
@@ -1476,13 +1476,25 @@ 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 .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-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-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 { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; }
+ .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 > 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-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 h2 { margin: 0; }
+ .network-visual-card { padding: 14px; }
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); }
@keyframes appearance-in { from { opacity: 0; transform: translateY(-5px) scale(.98); } to { opacity: 1; transform: none; } }
@@ -1495,9 +1507,12 @@ HTML = """
.appearance-trigger-chevron { display: none; }
.appearance-panel { position: fixed; top: 56px; right: 8px; width: min(390px, calc(100vw - 16px)); }
.appearance-options { grid-template-columns: repeat(3, minmax(0, 1fr)); }
- .network-canvas { grid-template-columns: 1fr; padding: 10px; gap: 10px; }
- .network-block { grid-column: 1; }
- .network-visual-card { margin: 0 10px 10px; padding: 10px; }
+ .network-workspace.has-structure { grid-template-columns: 1fr; }
+ .network-workspace.has-structure > .network-restore-panel, .network-workspace.has-structure > .network-canvas { grid-column: 1; }
+ .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)); } }
@@ -1808,9 +1823,21 @@ HTML = """
-
-
- Модель
+
+ Блоки сети
+
+
+
+ перетаскивайте блоки за свободное место
+
+
+
+
+
+ Модель
@@ -1821,9 +1848,9 @@ HTML = """
модель не выбрана
-
-
- Параметры до инференса
+
+
-
-
+
+
+ ⌫
+ отпустите здесь, чтобы удалить блок
+
@@ -2674,6 +2706,380 @@ HTML = """
selectConfigBlock(selectedConfigBlock);
$('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) {
const match = /^(\\d{1,5})x(\\d{1,5})$/i.exec(String(value || '').trim());
return match ? [Number(match[1]), Number(match[2])] : [1280, 720];
@@ -3568,6 +3974,7 @@ HTML = """
$('settingsTab').hidden = name !== 'settings';
$('modelTab').hidden = name !== 'model';
if (name === 'settings' && typeof applyStoredConfigLayout === 'function') requestAnimationFrame(applyStoredConfigLayout);
+ if (name === 'model' && typeof applyNetworkLayout === 'function') requestAnimationFrame(applyNetworkLayout);
if (name === 'archive') loadArchive();
if (updateHash && location.hash !== `#${name}`) history.replaceState(null, '', `#${name}`);
}
@@ -4030,6 +4437,7 @@ HTML = """
restoreAppearanceSettings();
initCollapsibleSections();
initConfigCanvas();
+ initNetworkCanvas();
loadArchive();
switchTab(location.hash.slice(1) || 'stream', false);