Строповка 3D
Планировщик подъёма секции · статика, геометрия, каталог · v17 · один файл
центр тяжести
обухи (наведите — покажет нагрузки)
гаки и ветви, цвет = гак
ненагруженная ветвь
перегруз / несовместимо
оси X, Y, Z — в мм
Вращение — мышь или касание · масштаб — колесо или щипок · панорама — ПКМ, Shift или два пальца.
Диагностика: проверки вычислительного ядра не запускались

Проверка программы на контрольных примерах с заранее известным ответом — не сертификация подъёма и не подтверждение грузоподъёмности оснастки. Каждый тест работает на собственной копии демонстрационного каталога и не изменяет проект пользователя. Пока идёт рабочий автоподбор, проверки не запускаются.

Что проверяется

Единицы, вертикальная ветвь, симметрия и асимметрия, несовместимая длина, трилатерация 3/4 точек, невозможный момент, LP-диапазоны натяжений, перегрузки стропа/обуха/гака, выбор LA, скобы и их количество, стороны обуха, две трактовки XYZ, зоны со знаками, профиль Голиафа, применение и отмена, отказ устаревшему применению, отмена поиска, сериализация, изоляция индикатора 3D.

Что не проверяется

Реальная прочность оснастки и сварки, динамика крана, деформации стропов, фактические паспортные данные. Результат проверок не является разрешением на подъём.

Статусы

Пройдено — факт совпал с ожиданием в допуске. Ошибка — не совпал. Не запускалось — обязательный тест выполнить не удалось (например, нет данных каталога). Сводка считается из фактических результатов.

Полный HTML-код приложения

Это весь документ целиком — от <!DOCTYPE html> до </html>: стили и скрипты находятся внутри этого текста. Без внешних скриптов, стилей, шрифтов, модулей и сети: сохраните его как END.html и откройте на другом компьютере — приложение заработает офлайн из file://. Текущий проект и каталог вложены как безопасно экранированный снимок JSON; после переноса потребуется пересчёт.

Редактирование позиции

. При старте загруженного файла сперва используется встроенный снимок (см. boot()), и только потом — собственное localStorage, при этом чужой localStorage снимок не перетирает. Результаты расчёта в снимок не входят: после переноса помечается «нужен пересчёт» (importState обнуляет результат). Никаких platform bootstrap, auth и data-скриптов и сетевых бейджей сюда не добавляется. */ function buildStandaloneHtml(){ var clone = document.documentElement.cloneNode(true); var i, el; /* 1. Очистка динамического контента копии. */ var wipe = ['panels','results','resHead','selftestOut']; for(i=0;i\n' + clone.outerHTML; } /* Проверка автономности собранного документа: ищем внешние зависимости. */ function auditStandalone(html){ var issues = []; try{ var doc = new DOMParser().parseFromString(html, 'text/html'); var srcScripts = doc.querySelectorAll('script[src]'); if(srcScripts.length) issues.push(srcScripts.length + ' script src'); var links = doc.querySelectorAll('link'); var ext = 0; for(var i=0;i= 0) ext++; else if(/^(https?:)?\/\//i.test(h)) ext++; } if(ext) issues.push(ext + ' link (внешний CSS или ресурс)'); if(doc.querySelectorAll('link[rel="import"]').length) issues.push('link rel=import'); var styles = doc.querySelectorAll('style'); for(i=0;i 0) || !S.lugs.length || !S.hooks.length) return; if(S.mode !== 'manual') return; await compute({ silent:true }); }, 900); } /* --- набор гаков --- */ function setHookCount(n){ n = Math.max(1, Math.min(3, Math.round(num(n,1)))); S.crane.hooks = n; var names = ['Гак 1','Гак 2','Гак 3']; while(S.hooks.length < n){ var i = S.hooks.length; S.hooks.push({ id:uid('H'), name:names[i] || ('Гак '+(i+1)), wll_t:80, limits:null }); } if(S.hooks.length > n){ S.hooks = S.hooks.slice(0, n); } var nL = S.lugs.length; for(var j=0;j 0; for(var i=0;i 0)) throw new Error('Масса секции должна быть больше нуля.'); if(!S.lugs.length) throw new Error('Не задано ни одного обуха.'); if(!S.hooks.length) throw new Error('Не задано ни одного гака.'); S.progress.sub = useManual ? 'проверка по заданным XYZ гаков' : 'положения гаков вычисляются по принятым длинам ветвей'; S.progress.updated = clockHMS(); if(!silent) renderResults(); await sleep(0); if(useManual) runGivenSchemeWith(manualPos); else runGivenSchemeScript(); }catch(e){ S.result = null; S.checks = null; S.status = { kind:'err', text:'Расчёт не выполнен' }; S.issues = [{ sev:'err', tab:'calc', text:'Расчёт не выполнен: '+(e && e.message || e), why:'Проверка входных данных до запуска решателя.', fix:'Проверьте шаги 1–2 и повторите.' }]; }finally{ S.computing = false; S.progress = null; if((S.rev||0) !== rev && S.result){ S.staleNote = 'Данные изменены во время расчёта — нужен пересчёт'; S.status = { kind:'part', text:'Данные изменены во время расчёта — нужен пересчёт' }; } saveStateDebounced(); renderPanels(); renderResults(); if(typeof V3 !== 'undefined'){ V3.refresh(); V3.schedule(); } } return { status:'done' }; } function runGivenSchemeScript(){ runGivenSchemeWith(null); } /* --- примеры --- */ function baseItems(shackleId, slingId){ var arr = []; if(shackleId) arr.push({ id:uid('it'), type:'shackle', catId:shackleId }); if(slingId) arr.push({ id:uid('it'), type:'sling', catId:slingId }); return arr; } function loadExample(kind){ S.catalog = S.catalog || deepClone(DEFAULT_CATALOG); if(!Array.isArray(S.catalog.rigging)) S.catalog.rigging = deepClone(DEFAULT_CATALOG.rigging); S.rules = Object.assign(deepClone(DEFAULT_RULES), S.rules || {}); S.goliath = false; S.rules.goliathUseSpread = false; S.manualHookPos = {}; S.result = null; S.checks = null; S.issues = []; S.advice = []; S.candidates = []; S.candidatesRev = -1; S.appliedCandidate = null; S.undo = null; S.crane = { hooks:1, type:'bridge' }; if(kind === 'one'){ S.crane = { hooks:1, type:'bridge' }; S.section = { mass_kg:42000, cog:{x:0,y:0,z:1200}, size:{x:4600,y:2400,z:2400} }; S.lugs = [ { id:'L1', name:'Обух L1', catId:'LA-63', pos:{x:-2000,y:0,z:2400} }, { id:'L2', name:'Обух L2', catId:'LA-63', pos:{x: 2000,y:0,z:2400} } ]; S.hooks = [{ id:'H1', name:'Гак 1', wll_t:80, limits:null }]; S.branches = { L1:{ hookId:'H1', items:baseItems('SH-35','SL-32-8') }, L2:{ hookId:'H1', items:baseItems('SH-35','SL-32-8') } }; } if(kind === 'two'){ S.crane = { hooks:2, type:'bridge' }; S.section = { mass_kg:84000, cog:{x:0,y:0,z:1400}, size:{x:7000,y:3000,z:2800} }; S.lugs = [ { id:'A1', name:'Обух A1', catId:'LA-100', pos:{x:-2500,y:-1200,z:2800} }, { id:'A2', name:'Обух A2', catId:'LA-100', pos:{x:-2500,y: 1200,z:2800} }, { id:'B1', name:'Обух B1', catId:'LA-100', pos:{x: 2500,y:-1200,z:2800} }, { id:'B2', name:'Обух B2', catId:'LA-100', pos:{x: 2500,y: 1200,z:2800} } ]; S.hooks = [{ id:'H1', name:'Гак 1', wll_t:120, limits:null }, { id:'H2', name:'Гак 2', wll_t:120, limits:null }]; S.branches = { A1:{ hookId:'H1', items:baseItems('SH-55','SL-32-9') }, A2:{ hookId:'H1', items:baseItems('SH-55','SL-32-9') }, B1:{ hookId:'H2', items:baseItems('SH-55','SL-32-9') }, B2:{ hookId:'H2', items:baseItems('SH-55','SL-32-9') } }; } if(kind === 'goliath'){ S.crane = { hooks:2, type:'gantry' }; S.rules.goliathUseSpread = true; S.goliath = true; var g = S.rules.goliathSpread_mm || [16000,18500]; S.section = { mass_kg:520000, cog:{x:0,y:0,z:1700}, size:{x:22000,y:8000,z:3400} }; S.lugs = [ { id:'A1', name:'A1', catId:'LA-250', pos:{x:-8000,y:-2000,z:3400} }, { id:'A2', name:'A2', catId:'LA-250', pos:{x:-8000,y: 2000,z:3400} }, { id:'B1', name:'B1', catId:'LA-250', pos:{x: 8000,y:-2000,z:3400} }, { id:'B2', name:'B2', catId:'LA-250', pos:{x: 8000,y: 2000,z:3400} } ]; S.hooks = [ { id:'H1', name:'Гак Голиаф 1', wll_t:num(S.rules.goliathHookWLL_t,350), limits:null }, { id:'H2', name:'Гак Голиаф 2', wll_t:num(S.rules.goliathHookWLL_t,350), limits:null } ]; S.branches = { A1:{ hookId:'H1', items:baseItems('SH-200','SL-160-12') }, A2:{ hookId:'H1', items:baseItems('SH-200','SL-160-12') }, B1:{ hookId:'H2', items:baseItems('SH-200','SL-160-12') }, B2:{ hookId:'H2', items:baseItems('SH-200','SL-160-12') } }; void g; } if(kind === 'negative'){ S.crane = { hooks:1, type:'bridge' }; S.section = { mass_kg:60000, cog:{x:0,y:0,z:1200}, size:{x:5000,y:5000,z:2400} }; var R = 2000; S.lugs = [ { id:'N1', name:'Обух 1', catId:'LA-63', pos:{x:R, y:0, z:2400} }, { id:'N2', name:'Обух 2', catId:'LA-63', pos:{x:-R*0.5, y: R*0.866, z:2400} }, { id:'N3', name:'Обух 3', catId:'LA-63', pos:{x:-R*0.5, y:-R*0.866, z:2400} }, { id:'N4', name:'Обух 4 (неработающая ветвь)', catId:'LA-63', pos:{x:0, y:0, z:2400} } ]; S.hooks = [{ id:'H1', name:'Гак 1', wll_t:120, limits:null }]; S.branches = { N1:{ hookId:'H1', items:baseItems('SH-35','SL-32-8') }, N2:{ hookId:'H1', items:baseItems('SH-35','SL-32-8') }, N3:{ hookId:'H1', items:baseItems('SH-35','SL-32-8') }, N4:{ hookId:'H1', items:baseItems('SH-35','SL-32-12') } }; } S.status = { kind:'empty', text:'Пример загружен — нажмите расчёт' }; saveStateDebounced(); setTab('calc'); renderPanels(); renderResults(); V3.fit(); V3.schedule(); } function loadExampleSilent(kind){ var keep = { mode:S.mode, tab:S.tab, result:S.result, checks:S.checks, status:S.status, issues:S.issues, advice:S.advice, candidates:S.candidates, appliedCandidate:S.appliedCandidate, undo:S.undo, resultRev:S.resultRev, candidatesRev:S.candidatesRev }; loadExample(kind); S.result = null; S.checks = null; S.issues = []; S.advice = []; S.mode = keep.mode; S.tab = keep.tab; S.status = keep.status; S.candidates = keep.candidates || []; S.appliedCandidate = keep.appliedCandidate; S.undo = keep.undo; S.resultRev = keep.resultRev; S.candidatesRev = keep.candidatesRev; } /* --- глобальные обработчики --- */ function mainRun(){ if(S.mode === 'auto') runAutoSearch(); else compute(); } function bindGlobal(){ document.addEventListener('click', function(e){ var t = e.target; var closest = (t && t.closest) ? t.closest('[data-act],[data-cam],[data-zoom]') : null; if(!closest) return; var act = closest.getAttribute('data-act'); var camAttr = closest.getAttribute('data-cam'); var zoomAttr = closest.getAttribute('data-zoom'); try{ if(camAttr){ if(!V3.cam) return; var c = S.section.cog || {x:0,y:0,z:1500}; if(camAttr === 'top'){ V3.cam.az = -Math.PI/2; V3.cam.el = 1.4; V3.cam.target = { x:c.x, y:c.y, z:c.z }; } else if(camAttr === 'front'){ V3.cam.az = -Math.PI/2; V3.cam.el = 0.08; V3.cam.target = { x:c.x, y:c.y, z:c.z }; } else if(camAttr === 'side'){ V3.cam.az = 0; V3.cam.el = 0.08; V3.cam.target = { x:c.x, y:c.y, z:c.z }; } else if(camAttr === 'iso'){ V3.cam.az = 1.02; V3.cam.el = 0.42; V3.cam.target = { x:c.x, y:c.y, z:c.z }; } V3.fit(); return; } if(zoomAttr){ if(!V3.cam) return; if(zoomAttr === 'in') V3.cam.dist = clamp(V3.cam.dist*0.78, 600, 4e6); else if(zoomAttr === 'out') V3.cam.dist = clamp(V3.cam.dist*1.28, 600, 4e6); else if(zoomAttr === 'reset'){ V3.cam.az = 1.02; V3.cam.el = 0.42; V3.fit(); } V3.schedule(); return; } if(act === 'mainRun'){ mainRun(); return; } if(act === 'legendToggle'){ legendToggle(); return; } /* подача диалога «Полный HTML-код» обрабатывается ниже в общем списке */ if(act === 'retry3d'){ V3.refresh(); V3.fit(); return; } if(act === 'copylog'){ var txt = 'END — журнал 3D\n' + V3.info(); try{ if(navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(txt); }catch(err){} return; } if(act === 'stopSearch' || act === 'stopsearch'){ stopAutoSearch(); return; } if(act === 'selftest'){ runSelfTest(); return; } if(act === 'showSoft'){ S.softHidden = false; renderResults(); return; } if(act === 'hideSoft'){ S.softHidden = true; renderResults(); return; } if(act === 'addlug'){ var first = catList('lugs')[0]; var id = uid('L'); S.lugs.push({ id:id, name:'Обух '+(S.lugs.length+1), catId:first ? first.id : '', pos:{x:0,y:0,z:0} }); invalidate('Добавлен обух'); renderPanels(); return; } if(act === 'dellug'){ var did = closest.dataset.id; if(!confirm('Удалить обух? Его ветвь будет удалена.')) return; S.lugs = S.lugs.filter(function(l){ return l.id !== did; }); delete S.branches[did]; invalidate('Обух удалён'); renderPanels(); return; } if(act === 'editlug'){ var lid = closest.dataset.id; var l = lugInst(lid); if(!l) return; var names = catList('lugs').map(function(c){ return c.id+' — '+c.name; }).join('\n'); var v = prompt('Тип обуха (ID из каталога):\n'+names, l.catId); if(v != null && v.trim()){ var id2 = v.trim().split(' ')[0]; if(!lugById(id2)){ alert('Такого ID нет в каталоге.'); return; } l.catId = id2; } var nm = prompt('Имя обуха:', l.name); if(nm != null) l.name = nm; var x = prompt('X, мм:', l.pos.x); if(x != null) l.pos.x = num(x,0); var y = prompt('Y, мм:', l.pos.y); if(y != null) l.pos.y = num(y,0); var z = prompt('Z, мм:', l.pos.z); if(z != null) l.pos.z = num(z,0); invalidate('Обух изменён'); renderPanels(); return; } if(act === 'delhook'){ var hid = closest.dataset.id; if(S.hooks.length <= 1){ alert('Нельзя удалить единственный гак.'); return; } if(!confirm('Удалить гак? Его обухи станут неназначенными.')) return; S.hooks = S.hooks.filter(function(x2){ return x2.id !== hid; }); S.crane.hooks = S.hooks.length; for(var lk in S.branches) if(S.branches[lk].hookId === hid) S.branches[lk].hookId = S.hooks[0].id; delete S.manualHookPos[hid]; S.rules.goliathUseSpread = false; S.goliath = false; invalidate('Гак удалён'); renderPanels(); return; } if(act === 'togglelug'){ var tl = closest.dataset.lug, th = closest.dataset.hook; if(!S.branches[tl]) S.branches[tl] = { hookId:null, items:[] }; S.branches[tl].hookId = (S.branches[tl].hookId === th) ? null : th; invalidate('Назначение ветви изменено'); renderPanels(); return; } if(act === 'clearMhp'){ var cid = closest.dataset.id; delete S.manualHookPos[cid]; invalidate('Координаты гака очищены — положение будет вычислено по длинам'); renderPanels(); renderResults(); return; } if(act === 'branchadd'){ var blid = closest.dataset.lug; var sel = P.querySelector('[data-branchadd="'+blid+'"]'); if(!sel || !sel.value){ alert('Выберите позицию каталога.'); return; } var parts = sel.value.split(':'); var type = parts[0], catId = parts.slice(1).join(':'); if(!S.branches[blid]) S.branches[blid] = { hookId:null, items:[] }; S.branches[blid].items.push({ id:uid('it'), type:type, catId:catId }); invalidate(type === 'rigging' ? 'Добавлен демо-элемент без паспортных данных' : 'Позиция добавлена в сборку'); renderPanels(); return; } if(act === 'delitem' || act === 'repeatitem' || act === 'moveitem'){ var ilid = closest.dataset.lug, idx = +closest.dataset.idx; var bb = S.branches[ilid]; if(!bb) return; if(act === 'delitem') bb.items.splice(idx,1); else if(act === 'repeatitem'){ var it = bb.items[idx]; if(it) bb.items.splice(idx+1, 0, { id:uid('it'), type:it.type, catId:it.catId }); } else { var dir = +closest.dataset.dir, j = idx+dir; if(j >= 0 && j < bb.items.length){ var tmp = bb.items[idx]; bb.items[idx] = bb.items[j]; bb.items[j] = tmp; } } invalidate('Сборка изменена'); renderPanels(); return; } if(act === 'catgroup'){ catUI.group = closest.dataset.group; renderPanels(); return; } if(act === 'catEditRig'){ var rid = closest.dataset.id; var rg = riggingById(rid); if(!rg) return; var rn = prompt('Наименование демо-элемента:', rg.name); if(rn != null && rn.trim()) rg.name = rn.trim(); var rnote = prompt('Примечание (какие данные нужно задать):', rg.note || ''); if(rnote != null) rg.note = rnote; invalidate('Демо-элемент отредактирован (данные по-прежнему не заданы)'); renderPanels(); return; } if(act === 'catDel'){ var dk = closest.dataset.kind, did2 = closest.dataset.id; if(!confirm('Удалить позицию «'+did2+'» из каталога?')) return; S.catalog[dk] = catList(dk).filter(function(r){ return r.id !== did2; }); invalidate('Позиция каталога удалена'); renderPanels(); return; } if(act === 'catdemo'){ ['slings','shackles','lugs','blocks','rigging'].forEach(function(kind){ var have = {}; catList(kind).forEach(function(r){ have[r.id] = true; }); (DEFAULT_CATALOG[kind]||[]).forEach(function(r){ if(!have[r.id]) S.catalog[kind].push(deepClone(r)); }); }); invalidate('Каталог дополнен демонстрационными позициями'); renderPanels(); return; } if(act === 'catAddBranch'){ var ck = closest.dataset.kind, cid2 = closest.dataset.id; var ctype = (ck === 'slings') ? 'sling' : (ck === 'rigging' ? 'rigging' : 'shackle'); if(!S.lugs.length){ alert('Сначала добавьте обухи.'); return; } var list = S.lugs.map(function(l,i){ var b2 = S.branches[l.id] || (S.branches[l.id] = { hookId:null, items:[] }); var hk = b2.hookId ? hookById(b2.hookId) : null; return (i+1)+'. '+(l.name||l.id)+' → '+(hk ? hk.name : 'гак не назначен'); }); var pick = prompt('В какую ветвь добавить позицию?\n'+list.join('\n'), '1'); if(!pick) return; var idx2 = Math.max(0, Math.min(S.lugs.length-1, num(pick,1)-1)); var lid2 = S.lugs[idx2].id; if(!S.branches[lid2]) S.branches[lid2] = { hookId:null, items:[] }; S.branches[lid2].items.push({ id:uid('it'), type:ctype, catId:cid2 }); invalidate('Позиция каталога добавлена в ветвь'); renderPanels(); return; } if(act === 'example'){ loadExample(closest.dataset.kind); return; } if(act === 'autoAssign'){ autoAssignLugsToHooks(); return; } if(act === 'applyCandidate'){ applyCandidate(+closest.dataset.idx); return; } if(act === 'undoApply'){ undoApply(); return; } if(act === 'exportCSV'){ exportCSV(false); return; } if(act === 'exportCSVforce'){ exportCSV(true); return; } if(act === 'downloadHtml'){ downloadStandaloneHtml(); return; } if(act === 'showFullHtml'){ openFullHtml(); return; } if(act === 'fullHtmlClose'){ closeFullHtml(); return; } if(act === 'fullHtmlSelect'){ selectFullHtml(); return; } if(act === 'fullHtmlCopy'){ copyFullHtml(); return; } if(act === 'fullHtmlDownload'){ downloadStandaloneHtml(); return; } }catch(err){ bootErr('Действие интерфейса: '+(err && err.message || err)); S.issues = S.issues || []; S.issues.unshift({ sev:'err', text:'Сбой обработки действия: '+(err && err.message || err), why:'Внутренняя ошибка интерфейса.', fix:'Повторите действие или перезагрузите страницу.' }); renderResults(); } }); /* подсказки hover/focus/tap */ document.addEventListener('mouseover', function(e){ var el = e.target && e.target.closest ? e.target.closest('[data-tip]') : null; if(el) showTipEl(el, null); }); document.addEventListener('mouseout', function(e){ var el = e.target && e.target.closest ? e.target.closest('[data-tip]') : null; if(el) hideTipEl(); }); document.addEventListener('focusin', function(e){ var el = e.target && e.target.closest ? e.target.closest('[data-tip]') : null; if(el) showTipEl(el, null); }); document.addEventListener('focusout', function(){ hideTipEl(); }); document.addEventListener('click', function(e){ var el = e.target && e.target.closest ? e.target.closest('[data-tip]') : null; if(el) showTipEl(el, e); }); document.addEventListener('keydown', function(e){ if(e.key === 'Escape') hideTipEl(); }); var mb = document.getElementById('modebar'); if(mb) mb.addEventListener('click', function(e){ var b = e.target.closest ? e.target.closest('.modepick') : null; if(b) setMode(b.dataset.mode); }); var tb = document.getElementById('tabs'); if(tb) tb.addEventListener('click', function(e){ var b = e.target.closest ? e.target.closest('.tab') : null; if(b) setTab(b.dataset.tab); }); var recalc = document.getElementById('chkRecalc'); if(recalc){ recalc.checked = !!S.rules.autoRecalc; recalc.addEventListener('change', function(){ S.rules.autoRecalc = !!recalc.checked; saveStateDebounced(); if(S.rules.autoRecalc) scheduleAutoRecalc(); }); } /* индикатор 3D: hover, focus, tap */ var v3s = document.getElementById('v3stat'); var v3p = document.getElementById('v3statPop'); if(v3s && v3p){ var togglePop = function(force){ var show = (typeof force === 'boolean') ? force : v3p.hidden; v3p.hidden = !show; v3s.setAttribute('aria-expanded', show ? 'true' : 'false'); }; v3s.addEventListener('mouseenter', function(){ togglePop(true); }); v3s.addEventListener('mouseleave', function(){ togglePop(false); }); v3s.addEventListener('focus', function(){ togglePop(true); }); v3s.addEventListener('click', function(e){ e.stopPropagation(); togglePop(); }); v3s.addEventListener('keydown', function(e){ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); togglePop(); } if(e.key === 'Escape'){ togglePop(false); } }); document.addEventListener('click', function(e){ if(v3p.hidden) return; if(v3s.contains(e.target) || v3p.contains(e.target)) return; togglePop(false); }); } } /* --- запуск приложения --- */ function boot(){ try{ var src = null, i; /* Приоритет 1: ВСТРОЕННЫЙ СНИМОК автономного файла (#end-snapshot). Он физически лежит в этом документе, поэтому чужой localStorage его не перетирает. Расчётные результаты в снимок не входят: после переноса приложение сразу помечает «нужен пересчёт» через importState. */ try{ var snapEl = document.getElementById('end-snapshot'); if(snapEl && snapEl.textContent){ var sd = JSON.parse(snapEl.textContent); if(sd && sd.project && importState(sd.project, false)) src = 'встроенный снимок END.html'; } }catch(eSnap){ bootErr('Снимок автономного файла: '+(eSnap && eSnap.message || eSnap)); } /* Приоритет 2: собственное сохранение пользователя (если снимка нет). */ if(!src){ try{ var raw = localStorage.getItem(LS_KEY); if(raw){ var d = JSON.parse(raw); if(importState(d, false)) src = 'v17'; } }catch(e){ bootErr('Чтение сохранения: '+(e && e.message || e)); } } if(!src){ for(i=0;i'; var r = document.getElementById('results'); if(r) r.innerHTML = '
Ошибка запуска. Обновите страницу.
'; }catch(_){} } } window.addEventListener('error', function(e){ try{ bootErr((e && (e.message || (e.error && e.error.message))) || 'неизвестная ошибка страницы'); }catch(_){} try{ S.computing = false; S.progress = null; if(S.status && S.status.kind === 'busy') S.status = { kind:'err', text:'Сбой во время расчёта' }; renderResults(); }catch(_){} }); window.addEventListener('unhandledrejection', function(e){ try{ bootErr('Необработанный отказ промиса: '+((e && e.reason && (e.reason.message || e.reason)) || 'без текста')); }catch(_){} try{ S.computing = false; S.progress = null; renderResults(); }catch(_){} }); boot(); ⚡ Сделано в tenzorai.ru