|
|
|
|
@ -1486,6 +1486,7 @@ HTML = """<!doctype html>
|
|
|
|
|
.network-block:hover { border-color: color-mix(in srgb, var(--ui-accent) 48%, var(--ui-border)) !important; transform: translateY(-2px); }
|
|
|
|
|
.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-custom-block { border-style: dashed !important; }
|
|
|
|
|
.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; }
|
|
|
|
|
@ -1835,6 +1836,11 @@ HTML = """<!doctype html>
|
|
|
|
|
<div id="networkWorkspace" class="network-workspace">
|
|
|
|
|
<aside id="networkRestorePanel" class="config-restore-panel network-restore-panel" hidden>
|
|
|
|
|
<span class="config-restore-title">Структура сети</span>
|
|
|
|
|
<div class="config-structure-create">
|
|
|
|
|
<label>Шаблон<select id="networkTemplateSelect"><option value="model">Модель</option><option value="model-params">Параметры</option><option value="netron">Netron</option></select></label>
|
|
|
|
|
<label>Название<input id="networkBlockTitle" type="text" maxlength="48" placeholder="Мой блок"></label>
|
|
|
|
|
<button id="networkCreateBlock" type="button">создать блок</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="networkRestoreList" class="config-restore-list"></div>
|
|
|
|
|
</aside>
|
|
|
|
|
<div id="networkCanvas" class="model-grid network-canvas">
|
|
|
|
|
@ -2735,6 +2741,12 @@ HTML = """<!doctype html>
|
|
|
|
|
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 networkTemplateKey(block) { return block?.dataset.custom === 'true' ? (block.dataset.template || 'model') : block?.dataset.section; }
|
|
|
|
|
function nextNetworkKey() {
|
|
|
|
|
let index = 1;
|
|
|
|
|
while (networkBlocks.some(block => block.dataset.section === `network-custom-${index}`)) index += 1;
|
|
|
|
|
return `network-custom-${index}`;
|
|
|
|
|
}
|
|
|
|
|
function updateNetworkBlockScale(block) {
|
|
|
|
|
if (!block) return;
|
|
|
|
|
const width = Math.max(networkMinWidth(), block.clientWidth || block.offsetWidth || networkMinWidth());
|
|
|
|
|
@ -2858,7 +2870,7 @@ HTML = """<!doctype html>
|
|
|
|
|
label.textContent = networkTitle(block);
|
|
|
|
|
const stateNode = document.createElement('span');
|
|
|
|
|
stateNode.className = 'config-item-state';
|
|
|
|
|
stateNode.textContent = state === 'deleted' ? 'в корзине' : state === 'hidden' ? 'скрыт' : 'активен';
|
|
|
|
|
stateNode.textContent = state === 'deleted' ? 'в корзине' : state === 'hidden' ? 'скрыт' : block.dataset.custom === 'true' ? 'свой' : 'активен';
|
|
|
|
|
const actions = document.createElement('span');
|
|
|
|
|
actions.className = 'config-item-actions';
|
|
|
|
|
const visibility = document.createElement('button');
|
|
|
|
|
@ -2869,6 +2881,25 @@ HTML = """<!doctype html>
|
|
|
|
|
setNetworkBlockState(block, state ? '' : 'hidden');
|
|
|
|
|
});
|
|
|
|
|
actions.appendChild(visibility);
|
|
|
|
|
if (block.dataset.custom === 'true' && !state) {
|
|
|
|
|
const duplicate = document.createElement('button');
|
|
|
|
|
duplicate.type = 'button';
|
|
|
|
|
duplicate.textContent = 'дубль';
|
|
|
|
|
duplicate.addEventListener('click', event => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
duplicateNetworkBlock(block);
|
|
|
|
|
});
|
|
|
|
|
actions.appendChild(duplicate);
|
|
|
|
|
const remove = document.createElement('button');
|
|
|
|
|
remove.type = 'button';
|
|
|
|
|
remove.textContent = 'удалить';
|
|
|
|
|
remove.title = 'Удалить пользовательский блок без восстановления';
|
|
|
|
|
remove.addEventListener('click', event => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
removeNetworkBlockForever(block);
|
|
|
|
|
});
|
|
|
|
|
actions.appendChild(remove);
|
|
|
|
|
}
|
|
|
|
|
item.append(label, stateNode, actions);
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
if (!state) selectNetworkBlock(block);
|
|
|
|
|
@ -2903,6 +2934,55 @@ HTML = """<!doctype html>
|
|
|
|
|
renderNetworkStructure();
|
|
|
|
|
selectNetworkBlock(networkBlocks.find(candidate => !networkState(candidate)) || null);
|
|
|
|
|
}
|
|
|
|
|
function createNetworkBlock(templateKey, title, existingKey = '') {
|
|
|
|
|
const template = networkBlocks.find(block => block.dataset.custom !== 'true' && block.dataset.section === templateKey);
|
|
|
|
|
if (!template || !networkCanvas) return null;
|
|
|
|
|
const key = existingKey || nextNetworkKey();
|
|
|
|
|
const clone = template.cloneNode(true);
|
|
|
|
|
clone.dataset.section = key;
|
|
|
|
|
clone.dataset.custom = 'true';
|
|
|
|
|
clone.dataset.template = templateKey;
|
|
|
|
|
clone.classList.add('network-custom-block');
|
|
|
|
|
clone.querySelectorAll('[id]').forEach(node => node.removeAttribute('id'));
|
|
|
|
|
clone.querySelectorAll('[for]').forEach(node => node.removeAttribute('for'));
|
|
|
|
|
const summary = clone.querySelector(':scope > .network-summary');
|
|
|
|
|
if (summary) summary.textContent = title || `Новый блок ${key.replace('network-custom-', '')}`;
|
|
|
|
|
networkCanvas.appendChild(clone);
|
|
|
|
|
networkBlocks.push(clone);
|
|
|
|
|
networkLayout[key] = {...(networkLayout[key] || {}), custom: true, template: templateKey, title: networkTitle(clone)};
|
|
|
|
|
makeNetworkBlockInteractive(clone);
|
|
|
|
|
return clone;
|
|
|
|
|
}
|
|
|
|
|
function positionNewNetworkBlock(block) {
|
|
|
|
|
const bottom = networkBlocks.filter(item => item !== block && !networkState(item)).reduce((value, item) => Math.max(value, (item.offsetTop || 0) + item.offsetHeight), 0);
|
|
|
|
|
const width = networkCanvasWidth();
|
|
|
|
|
block.style.left = '12px';
|
|
|
|
|
block.style.top = `${Math.max(12, bottom + 18)}px`;
|
|
|
|
|
block.style.width = `${Math.min(width, Math.max(networkMinWidth(), Math.round(width * .46)))}px`;
|
|
|
|
|
updateNetworkBlockScale(block);
|
|
|
|
|
}
|
|
|
|
|
function duplicateNetworkBlock(block) {
|
|
|
|
|
const clone = createNetworkBlock(networkTemplateKey(block), `${networkTitle(block)} · копия`);
|
|
|
|
|
if (!clone) return;
|
|
|
|
|
positionNewNetworkBlock(clone);
|
|
|
|
|
bringNetworkBlockFront(clone);
|
|
|
|
|
captureNetworkLayout(clone);
|
|
|
|
|
saveNetworkLayout();
|
|
|
|
|
renderNetworkStructure();
|
|
|
|
|
selectNetworkBlock(clone);
|
|
|
|
|
setText('networkLayoutState', `создан блок «${networkTitle(clone)}»`);
|
|
|
|
|
}
|
|
|
|
|
function removeNetworkBlockForever(block) {
|
|
|
|
|
if (!block || block.dataset.custom !== 'true') return;
|
|
|
|
|
const key = block.dataset.section;
|
|
|
|
|
block.remove();
|
|
|
|
|
networkBlocks = networkBlocks.filter(item => item !== block);
|
|
|
|
|
if (key) delete networkLayout[key];
|
|
|
|
|
saveNetworkLayout();
|
|
|
|
|
renderNetworkStructure();
|
|
|
|
|
selectNetworkBlock(networkBlocks.find(candidate => !networkState(candidate)) || null);
|
|
|
|
|
setText('networkLayoutState', 'пользовательский блок удалён');
|
|
|
|
|
}
|
|
|
|
|
function networkResizeEdge(event, block) {
|
|
|
|
|
const rect = block.getBoundingClientRect();
|
|
|
|
|
const edge = 9;
|
|
|
|
|
@ -3035,7 +3115,11 @@ HTML = """<!doctype html>
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function resetNetworkLayout() {
|
|
|
|
|
networkLayout = {};
|
|
|
|
|
const custom = {};
|
|
|
|
|
networkBlocks.forEach(block => {
|
|
|
|
|
if (block.dataset.custom === 'true') custom[block.dataset.section] = {custom: true, template: networkTemplateKey(block), title: networkTitle(block)};
|
|
|
|
|
});
|
|
|
|
|
networkLayout = custom;
|
|
|
|
|
networkZIndex = 1;
|
|
|
|
|
networkBlocks.forEach(block => {
|
|
|
|
|
block.style.left = '';
|
|
|
|
|
@ -3059,6 +3143,10 @@ HTML = """<!doctype html>
|
|
|
|
|
if (!networkCanvas) return;
|
|
|
|
|
networkLayout = readNetworkLayout();
|
|
|
|
|
networkBlocks = Array.from(networkCanvas.children).filter(node => node.classList.contains('network-block'));
|
|
|
|
|
Object.entries(networkLayout).forEach(([key, entry]) => {
|
|
|
|
|
if (!entry || entry.custom !== true || networkBlocks.some(block => block.dataset.section === key)) return;
|
|
|
|
|
createNetworkBlock(entry.template || 'model', entry.title || 'Новый блок', key);
|
|
|
|
|
});
|
|
|
|
|
networkBlocks.forEach(block => {
|
|
|
|
|
makeNetworkBlockInteractive(block);
|
|
|
|
|
const state = networkLayout[block.dataset.section]?.state;
|
|
|
|
|
@ -3076,6 +3164,20 @@ HTML = """<!doctype html>
|
|
|
|
|
networkStructureOpen = !networkStructureOpen;
|
|
|
|
|
renderNetworkStructure();
|
|
|
|
|
});
|
|
|
|
|
$('networkCreateBlock')?.addEventListener('click', () => {
|
|
|
|
|
const templateKey = $('networkTemplateSelect')?.value || 'model';
|
|
|
|
|
const title = String($('networkBlockTitle')?.value || '').trim().slice(0, 48) || 'Новый блок';
|
|
|
|
|
const block = createNetworkBlock(templateKey, title);
|
|
|
|
|
if (!block) return;
|
|
|
|
|
positionNewNetworkBlock(block);
|
|
|
|
|
bringNetworkBlockFront(block);
|
|
|
|
|
captureNetworkLayout(block);
|
|
|
|
|
saveNetworkLayout();
|
|
|
|
|
if ($('networkBlockTitle')) $('networkBlockTitle').value = '';
|
|
|
|
|
renderNetworkStructure();
|
|
|
|
|
selectNetworkBlock(block);
|
|
|
|
|
setText('networkLayoutState', `создан блок «${networkTitle(block)}»`);
|
|
|
|
|
});
|
|
|
|
|
$('networkContentScale')?.addEventListener('input', event => applyNetworkContentScale(event.target.value));
|
|
|
|
|
window.addEventListener('resize', applyNetworkLayout);
|
|
|
|
|
if (window.ResizeObserver) {
|
|
|
|
|
|