ElectroPro

Проектирование · монтаж · сметы

Мои объекты

ВЕРСИЯ 2.5
Объекты0
Помещения0
Материалы0 ₽

Проекты

Данные сохраняются в памяти браузера на этом устройстве. Это демонстрационный прототип; перед очисткой данных браузера сделай экспорт резервной копии.
';downloadEstimate(html,'ElectroPro_Коммерческое_предложение.html','text/html;charset=utf-8')} // ElectroPro 2.0: execution journal. Project-scoped, backed by existing JSON backups. function progressState(){let p=activeProject()||projects[0];if(!p)return null;if(!p.progress)p.progress={stages:[],otherActual:0};return p.progress} function progressProject(){return activeProject()||projects[0]} function addStage(){let st=progressState();if(!st){alert('Сначала создайте объект');return}let name=document.getElementById('stageName').value.trim();if(!name){alert('Введите название этапа');return}let qty=Math.max(0,+document.getElementById('stageQty').value||0),budget=Math.max(0,+document.getElementById('stageBudget').value||0);st.stages.push({id:makeId(),name:name.slice(0,180),qty,unit:document.getElementById('stageUnit').value,done:0,budget,actual:0,status:'planned',note:''});document.getElementById('stageName').value='';save();renderProgress()} function updateStage(id,key,value){let st=progressState(),stage=st?.stages.find(x=>x.id===id);if(!stage)return;if(['done','actual','budget','qty'].includes(key)){let n=Number(value);if(!Number.isFinite(n)||n<0){alert('Введите неотрицательное число');renderProgress();return}stage[key]=n;if(key==='qty'&&stage.done>n)stage.done=n;if(key==='done'&&stage.done>stage.qty)stage.done=stage.qty}else if(key==='status'){if(!['planned','working','done'].includes(value))return;stage.status=value;if(value==='done')stage.done=stage.qty}else if(key==='note')stage.note=String(value).slice(0,500);save();renderProgress()} function removeStage(id){let st=progressState();if(!st||!confirm('Удалить этап из журнала?'))return;st.stages=st.stages.filter(x=>x.id!==id);save();renderProgress()} function progressStats(){let st=progressState();if(!st)return null;let a=st.stages,qty=a.reduce((v,x)=>v+(+x.qty||0),0),budget=a.reduce((v,x)=>v+(+x.budget||0),0),actual=a.reduce((v,x)=>v+(+x.actual||0),0),weighted=a.reduce((v,x)=>v+(+x.budget||0)*(x.qty>0?Math.min(x.done/x.qty,1):(x.status==='done'?1:0)),0);let avg=a.length?a.reduce((v,x)=>v+(x.qty>0?Math.min(x.done/x.qty,1):(x.status==='done'?1:0)),0)/a.length:0;return {budget,actual,completion:budget>0?weighted/budget:avg,otherActual:+st.otherActual||0}} function renderProgress(){let p=progressProject(),st=progressState(),root=document.getElementById('progressList'),summary=document.getElementById('progressSummary');document.getElementById('progressProject').textContent=p?'Объект: '+p.name:'Сначала создайте объект';if(!st){root.innerHTML='';summary.innerHTML='';return}let t=progressStats();let fb=financeBreakdown();let plannedRevenue=fb?.revenue||0;summary.innerHTML='
Готовность по этапам'+Math.round(t.completion*100)+'%
План затрат по этапам'+money(t.budget)+'
Фактические затраты по этапам'+money(t.actual)+'
Всего фактических затрат'+money(t.actual+t.otherActual)+'
Цена заказчику из финансовой сметы'+money(plannedRevenue)+'
Разница цены и учтённых затрат'+money(plannedRevenue-t.actual-t.otherActual)+'

Процент готовности взвешен по плановой стоимости этапов; если цены не указаны, считается среднее по этапам. Сумма затрат отражает только введённые фактические расходы.

';root.innerHTML=st.stages.map(x=>'
'+escapeHtml(x.name)+'
').join('')||'

Этапов пока нет.

'} function updateProgressOther(value){let st=progressState(),n=+value;if(!st||!Number.isFinite(n)||n<0){alert('Введите неотрицательное число');renderProgress();return}st.otherActual=n;save();renderProgress()} function exportProgressCsv(){let p=progressProject(),st=progressState();if(!p||!st)return;let rows=[['Объект','Этап','Статус','Единица','Плановый объём','Выполнено','План затрат','Факт затрат','Примечание'],...st.stages.map(x=>[p.name,x.name,x.status,x.unit,x.qty,x.done,x.budget,x.actual,x.note||''])];rows.push([p.name,'Прочие фактические затраты','','','','','',st.otherActual,'']);let csv='\ufeff'+rows.map(r=>r.map(v=>'"'+String(v??'').replace(/"/g,'""')+'"').join(';')).join('\r\n');downloadEstimate(csv,'electropro-progress.csv','text/csv;charset=utf-8')} // ElectroPro 2.0 — закупки и черновой акт, данные в проекте. function purchaseProject(){return activeProject()||projects[0]} function purchaseState(){let p=purchaseProject();if(!p)return null;if(!p.purchasing)p.purchasing={items:[],act:{number:'1',date:'',client:''}};if(!Array.isArray(p.purchasing.payments))p.purchasing.payments=[];if(!p.purchasing.act)p.purchasing.act={number:'1',date:'',client:''};return p.purchasing} function safeNumber(x){let n=Number(x);return Number.isFinite(n)&&n>=0?n:null} function addPurchase(){let st=purchaseState();if(!st)return;let name=document.getElementById('buyName').value.trim(),qty=safeNumber(document.getElementById('buyQty').value),price=safeNumber(document.getElementById('buyPrice').value);if(!name||qty===null||price===null){alert('Проверьте название, количество и цену');return}st.items.push({id:makeId(),name:name.slice(0,180),unit:document.getElementById('buyUnit').value,qty,price,bought:0,actualPrice:0});document.getElementById('buyName').value='';save();renderPurchases()} function updatePurchase(id,key,value){let st=purchaseState(),r=st?.items.find(x=>x.id===id);if(!r)return;let n=safeNumber(value);if(n===null){alert('Введите неотрицательное число');renderPurchases();return}if(key==='bought')n=Math.min(n,r.qty);if(key==='qty'&&r.bought>n)r.bought=n;if(!['qty','price','bought','actualPrice'].includes(key))return;r[key]=n;save();renderPurchases()} function removePurchase(id){let st=purchaseState();if(!st||!confirm('Удалить позицию закупки?'))return;st.items=st.items.filter(x=>x.id!==id);save();renderPurchases()} function purchaseTotals(){let items=purchaseState()?.items||[];return {planned:items.reduce((s,x)=>s+x.qty*x.price,0),actual:items.reduce((s,x)=>s+x.bought*x.actualPrice,0),remaining:items.reduce((s,x)=>s+Math.max(0,x.qty-x.bought)*x.price,0)}} function renderPurchases(){let p=purchaseProject(),st=purchaseState();document.getElementById('purchaseProject').textContent=p?'Объект: '+p.name:'Сначала создайте объект';if(!st)return;let t=purchaseTotals();document.getElementById('purchaseList').innerHTML=st.items.map(x=>`
${escapeHtml(x.name)}

Осталось приобрести: ${Math.max(0,x.qty-x.bought).toFixed(2)} ${escapeHtml(x.unit)}

`).join('')||'

Пока нет закупок.

';document.getElementById('purchaseSummary').innerHTML=`
План закупок${money(t.planned)}
Оплаченные материалы (по вводу)${money(t.actual)}
Остаток по плановым ценам${money(t.remaining)}
`;renderClientPayments();document.getElementById('actNumber').value=st.act.number||'';document.getElementById('actDate').value=st.act.date||'';document.getElementById('actClient').value=st.act.client||''} function updateActField(key,value){let st=purchaseState();if(!st||!['number','date','client'].includes(key))return;st.act[key]=String(value).slice(0,180);save()} function exportPurchasesCsv(){let p=purchaseProject(),st=purchaseState();if(!p||!st)return;let rows=[['Материал','Ед.','Нужно','Плановая цена','Куплено','Фактическая цена','Плановая сумма','Фактическая сумма'],...st.items.map(x=>[x.name,x.unit,x.qty,x.price,x.bought,x.actualPrice,x.qty*x.price,x.bought*x.actualPrice])];let csv='\uFEFF'+rows.map(r=>r.map(csvCell).join(';')).join('\r\n');downloadEstimate(csv,'ElectroPro_Закупки.csv','text/csv;charset=utf-8')} // ElectroPro 2.0: sync procurement from material estimate and track incoming payments. function syncPurchasesFromEstimate(){ let st=purchaseState();if(!st){alert('Сначала создайте объект');return} let rows=estimateRows().filter(r=>['Автоматически','Материал вручную'].includes(r.kind)&&Number(r.qty)>0); let added=0,updated=0; for(let r of rows){ let existing=st.items.find(x=>x.sourceKey===r.key); if(existing){existing.qty=Math.max(Number(existing.bought)||0,Number(r.qty)||0);existing.price=Math.max(0,Number(r.price)||0);updated++} else{st.items.push({id:makeId(),sourceKey:r.key,name:r.name,unit:r.unit,qty:Number(r.qty),price:Number(r.price)||0,bought:0,actualPrice:0});added++} } save();renderPurchases();alert('Добавлено: '+added+'. Обновлено: '+updated+'. Проверьте дубли и цены.'); } function addClientPayment(){ let st=purchaseState();if(!st)return; let raw=document.getElementById('payAmount').value,amount=Number(raw),date=document.getElementById('payDate').value; if(!raw.trim()||!Number.isFinite(amount)||amount<=0||!date){alert('Укажите дату и положительную сумму');return} st.payments.push({id:makeId(),date,amount,method:document.getElementById('payMethod').value,note:document.getElementById('payNote').value.slice(0,250)}); document.getElementById('payAmount').value='';document.getElementById('payNote').value='';save();renderPurchases(); } function removeClientPayment(id){let st=purchaseState();if(!st||!confirm('Удалить платёж?'))return;st.payments=st.payments.filter(p=>p.id!==id);save();renderPurchases()} function renderClientPayments(){ let st=purchaseState();if(!st)return; let received=st.payments.reduce((n,x)=>n+(Number(x.amount)||0),0); let contract=financeBreakdown()?.revenue||0; document.getElementById('paymentsList').innerHTML=st.payments.slice().sort((a,b)=>b.date.localeCompare(a.date)).map(x=>`
${escapeHtml(x.date)} — ${money(x.amount)}

${escapeHtml(x.method)} · ${escapeHtml(x.note||'Без комментария')}

`).join('')||'

Поступлений пока нет.

'; document.getElementById('paymentsSummary').innerHTML=`
Сумма предложения (ориентир)${money(contract)}
Получено от заказчика${money(received)}
${received>contract?'Превышение над предложением':'Осталось получить по предложению'}${money(Math.abs(contract-received))}

Сумма предложения может меняться при пересчёте сметы и не заменяет договорную стоимость.

`; } function exportPaymentsCsv(){let st=purchaseState();if(!st)return;let rows=[['Дата','Сумма','Способ','Комментарий'],...st.payments.map(x=>[x.date,x.amount,x.method,x.note])];let csv='\uFEFF'+rows.map(r=>r.map(csvCell).join(';')).join('\r\n');downloadEstimate(csv,'ElectroPro_Платежи.csv','text/csv;charset=utf-8')} function exportActHtml(){let p=purchaseProject(),st=purchaseState(),prog=progressState();if(!p||!st)return;let stages=(prog?.stages||[]).filter(x=>x.done>0);if(!stages.length){alert('Сначала внесите выполненные объёмы в «Журнал работ».');return}let sum=0;let rows=stages.map(x=>{let amount=x.qty>0?x.budget*Math.min(1,x.done/x.qty):0;sum+=amount;return `${escapeHtml(x.name)}${escapeHtml(x.unit)}${x.done}${amount.toFixed(2)}`}).join('');let html=`Акт № ${escapeHtml(st.act.number)}

Акт выполненных работ № ${escapeHtml(st.act.number||'—')}

Дата: ${escapeHtml(st.act.date||'—')}

Объект: ${escapeHtml(p.name)}

Заказчик: ${escapeHtml(st.act.client||'—')}

${rows}
РаботаЕд.ВыполненоПредварительная стоимость, ₽

Итого: ${sum.toFixed(2)} ₽

Объёмы и суммы требуют согласования с договором и подтверждения сторонами.

Заказчик: ___________________   Исполнитель: ___________________

`;downloadEstimate(html,'ElectroPro_Акт.html','text/html;charset=utf-8')} load();renderHome();navigate('dashboard');calcCable(); if('serviceWorker' in navigator && location.protocol==='https:'){window.addEventListener('load',()=>navigator.serviceWorker.register('./sw.js').catch(()=>{}));} // ElectroPro 3.4: deterministic local audit. No normative cable ampacity or RCD compliance claims. function epInputKey(){return 'electropro-input-36-'+(activeProject()?.id||activeProject()?.name||'default');} function epGetInput(){try{return Object.assign({panel:'apartment',device:'unknown',upstream:'unknown',isolate:'unknown'},JSON.parse(localStorage.getItem(epInputKey())||'{}'));}catch(e){return {panel:'apartment',device:'unknown',upstream:'unknown',isolate:'unknown'};}} function epSaveInput(){const o={panel:document.getElementById('epInputPanel').value,device:document.getElementById('epInputDevice').value,upstream:document.getElementById('epUpstream').value,isolate:document.getElementById('epIsolate').value};localStorage.setItem(epInputKey(),JSON.stringify(o));epLastAudit=null;} function epLoadInput(){const o=epGetInput();for(const [key,id] of Object.entries({panel:'epInputPanel',device:'epInputDevice',upstream:'epUpstream',isolate:'epIsolate'})){const el=document.getElementById(id);if(el)el.value=o[key];}} document.addEventListener('DOMContentLoaded',epLoadInput); let epLastAudit=null; // ElectroPro 4.2. Generated devices are clearly marked and can be reverted. function ep40Generate(){ const p=activeProject(),root=document.getElementById('ep40Result'); if(!p){alert('Сначала создайте проект');return} const b=getBoardLayout(),groups=boardGroups(),mode=document.getElementById('ep40RcdMode').value; if(!groups.length){root.textContent='Сначала добавьте группы.';return} if(b.ep40Backup){if(!confirm('Заменить предыдущую автокомпоновку? Ручные аппараты сохраняются.'))return;ep40Undo(true)} b.ep40Backup={devices:JSON.parse(JSON.stringify(b.devices||[])),order:[...(b.order||[])],positions:JSON.parse(JSON.stringify(b.positions||{})),rcdAssignments:{...(b.rcdAssignments||{})}}; b.devices=b.devices||[]; const generated=[]; const make=(type,label,width)=>{let d={id:makeId(),type,label,width,brand:'ElectroPro',epGenerated:true};b.devices.push(d);generated.push(d);return d}; // A switch is a placeholder only, not a protective breaker. const hasInput=b.devices.some(d=>d.type==='input'); if(!hasInput&&epGetInput().device==='breaker')make('input','Вводной автомат (параметры уточнить)',2); const rcds=[]; if(mode==='one')rcds.push(make('rcd','УЗО общее · номинал/IΔn уточнить',2)); if(mode==='pergroup')groups.forEach((g,i)=>rcds.push(make('rcd','УЗО '+(i+1)+' · '+g.name.slice(0,20),+g.voltage===400?4:2))); b.rcdAssignments=b.rcdAssignments||{}; if(mode==='one')groups.forEach(g=>b.rcdAssignments[g.id]=rcds[0].id); if(mode==='pergroup')groups.forEach((g,i)=>b.rcdAssignments[g.id]=rcds[i].id); const items=boardLayoutItems(),ordered=[...items.filter(d=>d.type==='input'),...items.filter(d=>d.type==='rcd'),...items.filter(d=>d.type==='rcbo'),...items.filter(d=>d.type==='group'),...items.filter(d=>!['input','rcd','rcbo','group'].includes(d.type))]; b.order=ordered.map(d=>d.id); b.positions={}; // Explicit positions cleared in generated draft, saved in backup. const cap=Number(b.capacity)||18,rows=Number(b.cabinet?.rows)||3,total=cap*rows; let used=ordered.reduce((n,d)=>n+d.width,0),reserve=Number(document.getElementById('ep40Reserve').value),overflow=used>total,reserveShort=used>total*(1-reserve); save();renderBoardLayout();renderSingleLine();epLastAudit=null; root.innerHTML='Черновик сформирован. Аппаратов: '+ordered.length+', занято '+used+' из '+total+' модулей. '+(overflow?'Ошибка: оборудование не помещается.':reserveShort?'Предупреждение: заданный резерв не обеспечен.':'Резерв по числу модулей выдержан.')+' Проверьте назначение УЗО, разделение N, номиналы, дифференциальную защиту, токи КЗ и соединения.'; } function ep40Undo(silent){ const b=getBoardLayout(),backup=b.ep40Backup; if(!backup){if(!silent)alert('Нет сохранённой автокомпоновки для отмены.');return} b.devices=backup.devices;b.order=backup.order;b.positions=backup.positions;b.rcdAssignments=backup.rcdAssignments;delete b.ep40Backup; save();renderBoardLayout();renderSingleLine();epLastAudit=null; if(!silent){const root=document.getElementById('ep40Result');if(root)root.textContent='Предыдущая компоновка восстановлена.'} } // 4.1: logical connection illustration, NOT a physical wiring diagram. function ep41Analyze(){ const b=getBoardLayout(),groups=boardGroups(),rcds=(b.devices||[]).filter(d=>d.type==='rcd'),assign=b.rcdAssignments||{},issues=[]; const rcdIds=new Set(rcds.map(d=>d.id)),buses=new Map(); for(const g of groups){const rid=assign[g.id];const key=rid&&rcdIds.has(rid)?rid:'main'; if(rid&&!rcdIds.has(rid))issues.push({level:'error',msg:'Группа «'+g.name+'» привязана к отсутствующему УЗО.'}); if(!rid)issues.push({level:'warning',msg:'Группа «'+g.name+'»: дифференциальная защита не указана; проверьте, требуется ли она.'}); if(!buses.has(key))buses.set(key,[]);buses.get(key).push(g); if(!g.cable||/не задан/i.test(g.cable))issues.push({level:'missing',msg:'Не задан кабель группы «'+g.name+'».'}); } if(!groups.length)issues.push({level:'missing',msg:'Нет групп для отображения.'}); if(!b.cabinet?.showN)issues.push({level:'warning',msg:'N-шина скрыта в настройках корпуса: проверьте нейтральные соединения.'}); if(!b.cabinet?.showPE)issues.push({level:'warning',msg:'PE-шина скрыта: проверьте защитный проводник и его подключение.'}); if(rcds.length&&groups.length)issues.push({level:'warning',msg:'Для каждого УЗО нужна отдельная N-шина/группа нейтралей; объединение N разных УЗО недопустимо. На схеме это лишь логические секции.'}); issues.push({level:'missing',msg:'Не проверены система заземления, реальные клеммы, сечения перемычек, шинные соединители, номиналы и типы УЗО, разделение PEN и фазировка.'}); return {b,groups,rcds,assign,issues,buses}; } function ep41Render(){ const d=ep41Analyze(),root=document.getElementById('ep41Diagram');if(!root)return; const esc=escapeHtml,w=960,row=76,h=Math.max(250,175+d.groups.length*row),a=[]; a.push(''); a.push('Электрощит · условные L / N / PE связиНЕ МОНТАЖНАЯ СХЕМА — требуется инженерная проверка'); const boxes=(x,y,ww,label,sub)=>''+esc(label).slice(0,28)+''+esc(sub).slice(0,32)+''; a.push(boxes(20,70,130,'Ввод','L / N / PE'));a.push(''); const neutralGroups=[...d.buses.keys()]; d.groups.forEach((g,i)=>{const y=120+i*row,rid=d.assign[g.id],rd=d.rcds.find(x=>x.id===rid),key=rd?rid:'main',idx=neutralGroups.indexOf(key),ny=78+idx*20; a.push(''); if(rd)a.push(boxes(225,y-24,180,'УЗО: '+rd.label,'условная привязка')); else a.push(''); a.push('',boxes(430,y-24,155,'QF '+g.breaker+' А',g.name)); a.push(''); a.push(''+esc(g.name).slice(0,36)+'L (коричневый) · N (синий) · PE (зелёный)'); a.push('N: '+esc(rd?rd.label:'общая секция').slice(0,22)+''); }); a.push('Цвета условные. Проводники не трассированы через реальные клеммы. Гребёнки не рассчитаны.'); root.innerHTML=a.join('');ep41Check(); } function ep41Check(){const d=ep41Analyze(),root=document.getElementById('ep41Status');if(!root)return; root.innerHTML='

Логические секции N: '+d.buses.size+'; групп: '+d.groups.length+'; УЗО: '+d.rcds.length+'.

'+d.issues.map(x=>'

'+(x.level==='error'?'⛔':x.level==='warning'?'⚠️':'ℹ️')+' '+escapeHtml(x.msg)+'

').join('')+'

Важно: отсутствие предупреждений не означает пригодности схемы к монтажу.

'; } // ElectroPro 4.2: non-authoritative labeling and printable draft. function ep42Model(){ const b=getBoardLayout(),items=boardLayoutItems(),groups=boardGroups(),a=b.annotations||{},assign=b.rcdAssignments||{},dev=b.devices||[]; const labels=items.map((d,i)=>({id:d.id,mark:a[d.id]?.mark||epMark(d),label:d.label,width:d.width,type:d.type,phase:a[d.id]?.phase||epPhase(d),row:b.positions?.[d.id]?.row??'—',slot:b.positions?.[d.id]?.slot??'—'})); const wiring=groups.map((g,i)=>({group:g.name,mark:a[g.id]?.mark||epMark({id:g.id,type:'group'}),cable:g.cable||'Не указан',phase:a[g.id]?.phase||'Не определена',n:assign[g.id]?('N-'+(dev.findIndex(d=>d.id===assign[g.id])+1)):'N-общая (проверить)',pe:'PE (проверить)',terminal:'X? (не назначена)',length:g.length??'—'})); const duplicates=labels.map(x=>x.mark).filter((x,i,all)=>all.indexOf(x)!==i); return {labels,wiring,duplicates,capacity:Number(b.capacity)||18,rows:Number(b.cabinet?.rows)||3,project:activeProject()?.name||'Без названия'}; } function ep42Render(){ const d=ep42Model(),root=document.getElementById('ep42Face');if(!root)return; let s='
'+escapeHtml(d.project)+' · лицевая панель (условно)
'; for(let r=1;r<=d.rows;r++){let items=d.labels.filter(x=>Number(x.row)===r);if(!items.length){items=d.labels.filter(x=>x.row==='—').slice((r-1)*d.capacity,r*d.capacity)} s+='
'+items.map(x=>'
'+escapeHtml(x.mark)+'
'+escapeHtml(x.label)+'
'+escapeHtml(x.phase)+'
').join('')+'
'; }root.innerHTML=s+'
'; document.getElementById('ep42Status').textContent=(d.duplicates.length?'Повторяющиеся обозначения: '+[...new Set(d.duplicates)].join(', ')+'. ':'')+'Не подтверждены клеммы, сечения внутренних соединений, шины, моменты затяжки и фактическая трассировка.'; } function ep42Print(){ const d=ep42Model(),esc=escapeHtml; const table=(headers,rows)=>''+headers.map(x=>'').join('')+''+rows.map(r=>''+r.map(x=>'').join('')+'').join('')+'
'+esc(x)+'
'+esc(x??'—')+'
'; const styles=''; const rows=d.labels.map(x=>[x.mark,x.label,x.type,x.phase,x.width,x.row,x.slot]); const wires=d.wiring.map(x=>[x.mark,x.group,x.phase,x.cable,x.length,x.n,x.pe,x.terminal]); const html='ElectroPro 4.2 — черновик документации'+styles+'

ElectroPro 4.2 — черновик документации

Проект: '+esc(d.project)+'

НЕ ДЛЯ МОНТАЖА. Логическая ведомость, а не проверенная монтажная схема. Отсутствуют реальные клеммы, трассировка, параметры внутренних проводников, токи КЗ и подтверждение соответствия нормам.

Лицевая панель — перечень аппаратов

'+table(['Марка','Аппарат','Тип','Фаза','Модули','Ряд','Место'],rows)+'

Маркировка отходящих линий и шин

'+table(['Марка','Группа','Фаза','Кабель','Длина, м','Секция N','PE','Клемма'],wires)+'

Контроль перед выпуском монтажной документации

□ Проверить питание и систему заземления □ Проверить защиту от КЗ и перегрузки □ Уточнить реальные клеммы и соединения □ Разделить N по УЗО □ Проверить PE □ Уточнить шинные системы и сечения перемычек □ Проверить маркировку и паспортные данные □ Подписать проект

'+(d.duplicates.length?'

Предупреждение: повторяющиеся обозначения '+esc([...new Set(d.duplicates)].join(', '))+'

':'')+''; const blob=new Blob([html],{type:'text/html;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='ElectroPro_4_2_documentation.html';a.click();setTimeout(()=>URL.revokeObjectURL(url),5000); // HTML is downloaded for reliable Safari use; open it and print/share as PDF. alert('Документ HTML сохранён. Откройте его в браузере и выберите «Печать» → «Сохранить в PDF».'); } // ElectroPro 4.3 — terminal-level draft; not a wiring or installation instruction. // 4.4 schematic terminal picker. SVG geometry is illustrative, never installation guidance. let ep44Start=null; function ep44Esc(v){return escapeHtml(String(v??''));} function ep44Nodes(){return ep43Nodes();} function ep44Coordinates(nodes){const coords=new Map();const w=920,left=75,step=110;nodes.forEach((n,i)=>{const col=i%7,row=Math.floor(i/7);coords.set(n.id,{x:left+col*step,y:80+row*102,row,col});});return {coords,width:w,height:Math.max(280,145+Math.ceil(nodes.length/7)*102)};} function ep44ResetSelection(){ep44Start=null;ep44Render();} function ep44Select(id){const nodes=ep44Nodes(),n=nodes.find(x=>x.id===id);if(!n)return; if(!ep44Start){ep44Start=id;ep44Render();return;} if(ep44Start===id){ep44Start=null;ep44Render();return;} const first=nodes.find(x=>x.id===ep44Start),wire=document.getElementById('ep43Wire')?.value||'L'; if(first.kind!==wire||n.kind!==wire){alert('Тип проводника должен соответствовать обеим выбранным клеммам.');return;} if(wire==='N'&&first.section&&n.section&&first.section!==n.section){alert('Запрещено соединять разные секции нейтрали N.');return;} document.getElementById('ep43From').value=first.id;document.getElementById('ep43To').value=n.id; ep43Add();ep44Start=null;ep44Render(); } function ep44Shift(id,delta){const c=ep43Connections().find(c=>c.id===id);if(!c)return;c.routeOffset=Math.max(-4,Math.min(4,(Number(c.routeOffset)||0)+delta));save();ep44Render();} // ElectroPro 4.5: symbolic frontal cabinet with selectable logical terminals. // ElectroPro 4.6: illustrative manufacturer-themed facades, NOT manufacturer-certified products. // ElectroPro 4.7 — user-supplied equipment data; no fabricated product specs. // ElectroPro 4.8: project BOM, grouped by exact catalog entry (not device label). function ep48Rows(){ const st=ep47Store(),items=boardLayoutItems(),map=new Map(); for(const d of items){ const entry=st.entries.find(e=>e.id===st.assign[d.id]); const key=entry?'sku:'+entry.id:'missing:'+d.type+':'+d.width; if(!map.has(key))map.set(key,{key,brand:entry?.brand||'Не указан',sku:entry?.sku||'',model:entry?.model||d.type,kind:entry?.type||d.type,amps:entry?.amps??'',pol:entry?.pol??'',ka:entry?.ka??'',width:entry?.width??d.width,source:entry?.source||'',verified:!!entry?.verified,qty:0,marks:[],missing:!entry}); const row=map.get(key);row.qty++;row.marks.push(epMark(d)); } return [...map.values()].sort((a,b)=>Number(a.missing)-Number(b.missing)||a.brand.localeCompare(b.brand,'ru')||a.sku.localeCompare(b.sku,'ru')); } function ep48State(){const b=getBoardLayout();if(!b.ep48)b.ep48={prices:{},currency:'RUB'};return b.ep48;} function ep48Price(key,value){const n=Number(value);if(value===''||!Number.isFinite(n)||n<0){delete ep48State().prices[key]}else ep48State().prices[key]=n;save();ep48Render();} function ep48Data(){const prices=ep48State().prices;return ep48Rows().map(r=>({...r,price:Object.prototype.hasOwnProperty.call(prices,r.key)?prices[r.key]:null}));} function ep48Render(){const target=document.getElementById('ep48Table'),summary=document.getElementById('ep48Summary');if(!target)return;const rows=ep48Data();const missing=rows.filter(r=>r.missing).reduce((n,r)=>n+r.qty,0),unverified=rows.filter(r=>!r.verified&&!r.missing).reduce((n,r)=>n+r.qty,0),noPrice=rows.filter(r=>r.price===null).length,total=rows.reduce((n,r)=>n+(r.price===null?0:r.price*r.qty),0);summary.textContent='Позиций: '+rows.length+' · аппаратов: '+rows.reduce((n,r)=>n+r.qty,0)+' · без артикула: '+missing+' · с неподтверждённым паспортом: '+unverified+' · без цены: '+noPrice+' · известная стоимость: '+total.toLocaleString('ru-RU')+' ₽ (не полный итог)';target.innerHTML=''+rows.map(r=>'').join('')+'
Производитель / артикулХарактеристикиМеткиШт.Цена, ₽Сумма, ₽
'+escapeHtml(r.brand)+'
'+escapeHtml(r.sku||'АРТИКУЛ НЕ УКАЗАН')+'
'+escapeHtml(r.model)+(r.verified?' ✓':' ⚠')+'
'+escapeHtml(r.kind)+' · '+escapeHtml(r.amps)+' А · '+escapeHtml(r.pol)+'P · '+escapeHtml(r.width)+' мод. · '+escapeHtml(r.ka)+' кА'+escapeHtml(r.marks.join(', '))+''+r.qty+''+(r.price===null?'—':(r.price*r.qty).toFixed(2))+'

Для закупки проверьте артикулы, паспорта, комплектующие, шины, клеммы, аксессуары и наличие у поставщика. Они не включены автоматически.

';} function ep48CsvText(){const rows=[['Производитель','Артикул','Модель','Тип','Номинал А','Полюсов','Отключающая способность кА','Ширина мод.','Количество','Маркировка','Цена за шт. RUB','Сумма RUB','Паспорт проверен','Источник','Статус']];for(const r of ep48Data())rows.push([r.brand,r.sku,r.model,r.kind,r.amps,r.pol,r.ka,r.width,r.qty,r.marks.join(' '),r.price??'',r.price===null?'':(r.price*r.qty).toFixed(2),r.verified?'Да':'Нет',r.source,r.missing?'Нет артикула':'']);return '\ufeff'+rows.map(row=>row.map(v=>'"'+String(v).replace(/"/g,'""')+'"').join(';')).join('\r\n');} function ep48Csv(){const blob=new Blob([ep48CsvText()],{type:'text/csv;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='ElectroPro_4_8_specification.csv';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1500);} function ep48Print(){ep48Render();const title='Спецификация электрощита · '+(activeProject()?.name||'Проект');const w=window.open('','_blank');if(!w){alert('Разрешите открытие вкладок для печати.');return;}w.document.write(''+escapeHtml(title)+'

'+escapeHtml(title)+'

'+escapeHtml(document.getElementById('ep48Summary').textContent)+'

'+document.getElementById('ep48Table').innerHTML+'

Черновая спецификация. Не подтверждает комплектность или соответствие нормам.

');w.document.close();w.focus();setTimeout(()=>w.print(),300);} function ep47Store(){const b=getBoardLayout();if(!b.ep47)b.ep47={entries:[],assign:{}};return b.ep47;} function ep47Add(){const v=id=>document.getElementById('ep47'+id)?.value?.trim()||'',sku=v('Sku'),brand=v('Brand'),width=Number(v('Width')),amps=Number(v('In')),ka=v('Ka')===''?null:Number(v('Ka')),pol=Number(v('Poles')),type=v('Type');const status=document.getElementById('ep47Status');if(!sku||!v('Model')||!(amps>0)||!(width>0)||!(pol>=1&&pol<=4)||!(ka===null||ka>0)){status.textContent='Заполните артикул, модель, номинал, полюса и ширину корректными значениями.';return}const st=ep47Store();if(st.entries.some(e=>e.brand===brand&&e.sku===sku)){status.textContent='Этот артикул уже внесён для производителя.';return}st.entries.push({id:makeId(),brand,sku,model:v('Model'),type,amps,pol,ka,width,source:v('Source'),verified:document.getElementById('ep47Verified').checked});save();ep47Render();status.textContent='Модель сохранена. Подтверждение характеристик зависит от предоставленного паспорта.';} function ep47Render(){const list=document.getElementById('ep47List'),dev=document.getElementById('ep47Device'),sel=document.getElementById('ep47Entry');if(!list||!dev||!sel)return;const st=ep47Store(),oldDev=dev.value,oldEntry=sel.value;list.innerHTML=st.entries.map(e=>'
'+escapeHtml(e.brand+' '+e.sku+' · '+e.model+' · '+e.amps+' А · '+e.pol+'P · '+e.width+' мод. '+(e.verified?'✓ Сверено пользователем':'⚠ Не подтверждено'))+'
').join('')||'Каталог пуст. Добавьте модель по паспорту.';dev.innerHTML=boardLayoutItems().map(d=>'').join('');sel.innerHTML=st.entries.map(e=>'').join('');if([...dev.options].some(o=>o.value===oldDev))dev.value=oldDev;if([...sel.options].some(o=>o.value===oldEntry))sel.value=oldEntry;} function ep47Delete(id){const st=ep47Store();st.entries=st.entries.filter(e=>e.id!==id);Object.keys(st.assign).forEach(k=>{if(st.assign[k]===id)delete st.assign[k]});save();renderBoardLayout();ep47Render();} function ep47Assign(){const id=document.getElementById('ep47Device')?.value,eid=document.getElementById('ep47Entry')?.value,st=ep47Store(),e=st.entries.find(x=>x.id===eid),d=boardLayoutItems().find(x=>x.id===id);if(!e||!d)return;const types={group:'mcb',input:'mcb',rcd:'rcd',rcbo:'rcbo'};if(types[d.type]&&types[d.type]!==e.type){document.getElementById('ep47Status').textContent='Тип модели не соответствует назначению аппарата.';return}st.assign[id]=eid;save();renderBoardLayout();ep45Render();ep47Render();document.getElementById('ep47Status').textContent='Привязано: '+e.brand+' '+e.sku+'. Ширина применяется к компоновке; электрическая совместимость не подтверждена.';} function ep47Unassign(){const id=document.getElementById('ep47Device')?.value;if(!id)return;delete ep47Store().assign[id];save();renderBoardLayout();ep45Render();ep47Render();} function ep47ForDevice(id){const st=ep47Store();return st.entries.find(e=>e.id===st.assign[id]);} function ep46Data(){const b=getBoardLayout();if(!b.ep46Facades)b.ep46Facades={};return b.ep46Facades;} function ep46Populate(){const el=document.getElementById('ep46Device');if(!el)return;const prev=el.value,items=boardLayoutItems();el.innerHTML=items.map(d=>'').join('');if(items.some(d=>d.id===prev))el.value=prev;} function ep46Apply(){const id=document.getElementById('ep46Device')?.value;if(!id)return;const d=boardLayoutItems().find(x=>x.id===id);if(!d)return;const brand=document.getElementById('ep46Brand').value,face=document.getElementById('ep46Face').value;ep46Data()[id]={brand,face};const raw=getBoardLayout().devices.find(x=>x.id===id);if(raw)raw.brand=brand;save();renderBoardLayout();ep45Render();const msg=document.getElementById('ep46Message');if(msg)msg.textContent='Оформление сохранено. Характеристики и артикул аппарата не менялись.';} function ep46Reset(){const id=document.getElementById('ep46Device')?.value;if(!id)return;delete ep46Data()[id];save();renderBoardLayout();ep45Render();const msg=document.getElementById('ep46Message');if(msg)msg.textContent='Оформление сброшено.';} function ep46Facade(d,x,y,ww){const f=ep46Data()[d.id],brand=f?.brand||d.brand||'ElectroPro',face=f?.face||({group:'mcb',input:'mcb',rcd:'rcd',rcbo:'rcbo'})[d.type]||'switch';const palettes={'ABB':['#e21e2c','#f6f6f6'],'Schneider Electric':['#198c55','#f7faf8'],'IEK':['#f1b919','#fffdf2'],'EKF':['#dc2932','#fdf7f7'],'ElectroPro':['#2b5d87','#f5f7fb']};const [accent,bg]=palettes[brand]||palettes.ElectroPro;const esc=ep44Esc;const label=face==='mcb'?'MCB':face==='rcd'?'RCD':face==='rcbo'?'RCBO':'SW';return ''+esc(brand.length>16?brand.slice(0,14):brand)+''+esc(epMark(d))+''+label+'DEMO'} function ep45Render(){ const root=document.getElementById('ep45Canvas');if(!root)return; const b=getBoardLayout(),items=boardLayoutItems(),nodes=ep43Nodes(),links=ep43Connections(); const rows=Math.max(1,Number(b.cabinet?.rows)||3),cap=Math.max(1,Number(b.capacity)||18),pitch=28,margin=65; const W=Math.max(660,margin*2+cap*pitch),H=145+rows*145+115; const nodePositions=new Map(),deviceMap=new Map(),parts=[]; const color={L:'#aa552c',N:'#2276b8',PE:'#218550'}; parts.push(''); parts.push('ElectroPro 4.6 · Визуальная модель · НЕ ДЛЯ МОНТАЖА'); // Use project ordering and optional manual positions, leaving overflow visible in status. let slot=0,row=0,overflow=false; for(const d of items){const w=Math.max(1,Number(d.width)||1);let pos=b.positions?.[d.id];let rr,ss; if(pos){rr=Math.max(0,Number(pos.row)-1);ss=Math.max(0,Number(pos.slot)-1)} else {if(slot+w>cap){row++;slot=0}rr=row;ss=slot;slot+=w} if(rr>=rows||ss+w>cap){overflow=true;continue} const x=margin+ss*pitch,y=74+rr*145,ww=w*pitch-2; deviceMap.set(d.id,{x,y,w:ww}); parts.push(''); parts.push(ep46Facade(d,x,y,ww)); for(const t of ['L','N'])nodePositions.set('DEV:'+d.id+':'+t,{x:x+ww*(t==='L'?.32:.68),y:y+5}); } // Dedicated busbars are illustrative, not physical terminals. const busY=H-94; for(const [kind,y] of [['N',busY],['PE',busY+38]]){ parts.push(''); parts.push(''+kind+' · условная шина'); } nodePositions.set('BUS:PE',{x:W-90,y:busY+47});nodePositions.set('BUS:N:MAIN',{x:W-90,y:busY+9}); const rcds=(b.devices||[]).filter(d=>d.type==='rcd'); rcds.forEach((d,i)=>nodePositions.set('BUS:N:'+d.id,{x:margin+110+i*80,y:busY+9})); const groups=boardGroups(),assign=b.rcdAssignments||{}; groups.forEach((g,i)=>{const d=deviceMap.get(g.id);if(!d)return; for(const kind of ['L','N','PE'])nodePositions.set('GROUP:'+g.id+':'+kind,{x:d.x+d.w*(kind==='L'?.25:kind==='N'?.5:.75),y:d.y+91}); }); const paths=[]; for(const c of links){const a=nodePositions.get(c.from),z=nodePositions.get(c.to);if(!a||!z)continue;const off=(Number(c.routeOffset)||0)*12,mid=Math.max(45,(a.y+z.y)/2+off),col=color[c.wire]||'#555'; paths.push(''); } const targets=[]; for(const n of nodes){const q=nodePositions.get(n.id);if(!q)continue;targets.push(''+ep44Esc(n.label)+'');} root.innerHTML=''+parts.join('')+paths.join('')+targets.join('')+''; ep46Populate();ep47Render();ep48Render(); root.querySelectorAll('[data-ep45]').forEach(el=>el.addEventListener('click',()=>ep44Select(el.getAttribute('data-ep45')))); const status=document.getElementById('ep45Status');if(status)status.textContent=(overflow?'Внимание: часть аппаратов не помещается в корпус. ':'')+'Показано '+items.length+' аппаратов и '+links.length+' логических соединений. Шины и клеммы условные; физические соединения не подтверждены.'; } function ep45Export(){ep45Render();const svg=document.querySelector('#ep45Canvas svg');if(!svg)return;const blob=new Blob([new XMLSerializer().serializeToString(svg)],{type:'image/svg+xml;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='ElectroPro_4_6_cabinet_draft.svg';a.click();setTimeout(()=>URL.revokeObjectURL(url),3000);} function ep44Render(){const root=document.getElementById('ep44Canvas'),controls=document.getElementById('ep44WireControls'),selection=document.getElementById('ep44Selection');if(!root)return; const nodes=ep44Nodes(),links=ep43Connections(),{coords,width,height}=ep44Coordinates(nodes); const colors={L:'#ad5723',N:'#2675bb',PE:'#198754'},paths=[]; for(const c of links){const a=coords.get(c.from),b=coords.get(c.to);if(!a||!b)continue; const color=colors[c.wire]||'#555',off=(Number(c.routeOffset)||0)*13,mid=(a.y+b.y)/2+off; const d=`M ${a.x} ${a.y} V ${mid} H ${b.x} V ${b.y}`; paths.push(''+ep44Esc(c.mark||c.wire)+''); } const boxes=nodes.map(n=>{const pt=coords.get(n.id),selected=ep44Start===n.id,color=colors[n.kind]||'#666';return ''+ep44Esc(n.kind)+''+ep44Esc(n.label.slice(0,15))+'';}); root.innerHTML='Условная схема клемм · не для монтажа'+paths.join('')+boxes.join('')+''; ep45Render(); root.querySelectorAll('[data-node]').forEach(el=>el.addEventListener('click',()=>ep44Select(el.getAttribute('data-node')))); if(selection)selection.textContent=ep44Start?'Выбрана начальная клемма: '+(nodes.find(n=>n.id===ep44Start)?.label||'')+'. Выберите конечную.':'Выберите начальную клемму'; if(controls)controls.innerHTML=links.map(c=>'
'+ep44Esc(c.mark||c.wire)+' · '+ep44Esc(c.wire)+'
').join(''); controls?.querySelectorAll('[data-shift]').forEach(el=>el.addEventListener('click',()=>ep44Shift(el.dataset.id,Number(el.dataset.shift)))); } function ep44ExportSvg(){ep44Render();const svg=document.querySelector('#ep44Canvas svg');if(!svg)return;const blob=new Blob([new XMLSerializer().serializeToString(svg)],{type:'image/svg+xml;charset=utf-8'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download='ElectroPro_4_4_schematic.svg';a.click();setTimeout(()=>URL.revokeObjectURL(url),3000);} function ep43Nodes(){ const b=getBoardLayout(),groups=boardGroups(),dev=b.devices||[],assign=b.rcdAssignments||{},nodes=[]; const add=(id,label,kind,section)=>nodes.push({id,label,kind,section:section||''}); add('BUS:PE','PE-шина','PE','PE');add('BUS:N:MAIN','N-шина до/без УЗО','N','MAIN'); for(const d of dev){if(d.type==='rcd')add('BUS:N:'+d.id,'N-шина после УЗО '+d.label,'N',d.id); for(const t of ['L','N'])add('DEV:'+d.id+':'+t,d.label+' · '+t,t,d.type==='rcd'?d.id:'');} for(const g of groups){let sec=assign[g.id]||'MAIN';for(const t of ['L','N','PE'])add('GROUP:'+g.id+':'+t,g.name+' · '+t,t,t==='N'?sec:'');} return nodes; } function ep43Connections(){const b=getBoardLayout();if(!Array.isArray(b.ep43Connections))b.ep43Connections=[];return b.ep43Connections;} function ep43Check(){const nodes=ep43Nodes(),byId=new Map(nodes.map(n=>[n.id,n])),links=ep43Connections(),issues=[],pairs=new Set(),marks=new Set(); for(const c of links){let a=byId.get(c.from),b=byId.get(c.to),pair=[c.from,c.to].sort().join('|')+'|'+c.wire; if(!a||!b){issues.push({level:'error',msg:'Связь '+(c.mark||c.id)+': клемма удалена или не существует.'});continue} if(c.from===c.to)issues.push({level:'error',msg:'Соединение клеммы с самой собой: '+a.label}); if(a.kind!==c.wire||b.kind!==c.wire)issues.push({level:'error',msg:'Несовместимый тип клеммы и проводника: '+a.label+' → '+b.label}); if(c.wire==='N'&&a.section&&b.section&&a.section!==b.section)issues.push({level:'error',msg:'Объединены разные секции N: '+a.label+' → '+b.label}); if(pairs.has(pair))issues.push({level:'warning',msg:'Дублирующая связь: '+a.label+' → '+b.label});pairs.add(pair); if(c.mark&&marks.has(c.mark))issues.push({level:'warning',msg:'Повтор маркировки: '+c.mark});if(c.mark)marks.add(c.mark); if(c.wire==='PE'&&(a.id!=='BUS:PE'&&b.id!=='BUS:PE'))issues.push({level:'warning',msg:'PE-связь не содержит главную PE-шину: '+a.label+' → '+b.label}); } issues.push({level:'missing',msg:'Не проверены реальные клеммы, допустимые сечения и число проводников, соединители, токи КЗ, непрерывность PE и соответствие схеме питания.'}); return issues; } function ep43Render(){const nodes=ep43Nodes(),links=ep43Connections(),a=document.getElementById('ep43From'),b=document.getElementById('ep43To'),list=document.getElementById('ep43Connections'),report=document.getElementById('ep43Report');if(!a||!b)return; const oldA=a.value,oldB=b.value,options=nodes.map(n=>'').join('');a.innerHTML=options;b.innerHTML=options;if(nodes.some(n=>n.id===oldA))a.value=oldA;if(nodes.some(n=>n.id===oldB))b.value=oldB; const map=new Map(nodes.map(n=>[n.id,n]));list.innerHTML=links.length?links.map((c,i)=>'
'+escapeHtml(c.mark||'W-'+(i+1))+' · '+escapeHtml(c.wire)+' · '+escapeHtml(map.get(c.from)?.label||'Удалена')+' → '+escapeHtml(map.get(c.to)?.label||'Удалена')+'
').join(''):'

Связи пока не добавлены.

'; ep44Render();const issues=ep43Check();report.innerHTML='Проверка логических соединений'+issues.map(x=>'

'+(x.level==='error'?'⛔':x.level==='warning'?'⚠️':'ℹ️')+' '+escapeHtml(x.msg)+'

').join(''); } function ep43Add(){const from=document.getElementById('ep43From')?.value,to=document.getElementById('ep43To')?.value,wire=document.getElementById('ep43Wire')?.value,mark=document.getElementById('ep43Mark')?.value.trim()||''; if(!from||!to)return alert('Выберите две клеммы.');ep43Connections().push({id:makeId(),from,to,wire,mark});save();ep43Render();epLastAudit=null; } function ep43Remove(i){ep43Connections().splice(i,1);save();ep43Render();epLastAudit=null;} function epAudit(){ const b=getBoardLayout(),groups=boardGroups(),items=boardLayoutItems(),c=b.cabinet||{},cap=Number(b.capacity)||18,rows=Number(c.rows)||3; const issues=[];const add=(level,code,title,detail,target)=>issues.push({level,code,title,detail,target:target||''}); if(!activeProject())add('missing','PROJECT','Нет активного проекта','Создайте или выберите проект.'); if(!items.length)add('missing','EMPTY','Нет оборудования','Добавьте устройства и группы для проверки.'); const cfg=epGetInput(), critical=['main','ats','industrial'].includes(cfg.panel); const manual=(b.devices||[]).some(d=>d.type==='input'); if(cfg.device==='unknown')add('missing','INPUT_TYPE','Не указан вводной аппарат','Выберите тип вводного аппарата и подтвердите его наличие в схеме.'); if(cfg.device==='breaker'&&!manual)add('warning','INPUT_LAYOUT','Вводной автомат не найден на компоновке','Добавьте вводной автомат на вид щита либо уточните, где он расположен.'); if(cfg.device!=='breaker'&&manual)add('warning','INPUT_MISMATCH','В компоновке есть вводной автомат','Сверьте выбранный тип вводного аппарата с компоновкой.'); if(cfg.upstream==='unknown')add('missing','UPSTREAM','Не подтверждена вышестоящая защита','Нужны номинал, характеристика, отключающая способность и проверка защиты питающей линии.'); if(cfg.upstream==='no'&&cfg.device!=='breaker'&&cfg.device!=='fuse')add('error','NO_SUPPLY_PROTECTION','Защита питающей линии не определена','Нельзя считать рубильник заменой автоматической защиты. Разработайте схему защиты.'); if(cfg.device==='none'&&cfg.upstream==='yes')add('warning','NO_LOCAL_INPUT','В щите нет вводного аппарата','Проверьте допустимость отсутствия местного отключения для этого назначения щита.'); if(cfg.device==='switch'&&cfg.upstream==='yes')add('warning','SWITCH_RATING','Проверьте номинал выключателя нагрузки','Подтвердите рабочий ток, полюсность, категорию применения и условия отключения.'); if(cfg.isolate==='unknown')add('missing','ISOLATION','Не подтверждено отключение для обслуживания','Укажите средство отключения и возможность безопасного обслуживания.'); if(cfg.isolate==='no')add('warning','NO_ISOLATION','Не предусмотрено отключение для обслуживания','Проверьте обязательность и расположение аппарата отключения.'); if(critical)add('warning','SPECIAL_PANEL','Специальные требования к вводам','Для ГРЩ/ВРУ, АВР и промышленных щитов необходима проверка конкретной схемы, защит и требований проекта.'); if(ep43Connections().length)for(const issue of ep43Check())if(issue.level==='error')add('error','TERMINAL_CONNECTION','Редактор клемм',issue.msg); for(const issue of ep41Analyze().issues)if(issue.level==='error')add('error','WIRING_LOGIC', 'Проверка связей',issue.msg); const placement=boardCells(); for(const d of placement.overflow)add('error','FIT','Аппарат не размещён: '+d.label,'Проверьте ширину, позицию и вместимость корпуса.',d.id); const used=items.reduce((sum,d)=>sum+Number(d.width||0),0),total=cap*rows; if(used>total)add('error','CAPACITY','Недостаточно модулей','Нужно '+used+', доступно '+total+'.'); else if(total>0&&(total-used)/total<0.2)add('warning','RESERVE','Малый резерв места','Свободно '+(total-used)+' из '+total+' модулей. Ориентир 20% не является нормативом.'); const seen=new Map();for(const d of items){const mark=String(epMark(d)||'').trim().toUpperCase();if(!mark)add('missing','MARK','Нет маркировки: '+d.label,'Укажите обозначение аппарата.',d.id);else if(seen.has(mark))add('error','DUP_MARK','Повтор маркировки '+mark,'Одинаковое обозначение у «'+seen.get(mark)+'» и «'+d.label+'».',d.id);else seen.set(mark,d.label)} if((b.devices||[]).some(d=>d.epGenerated))add('warning','AUTO_DRAFT','Автокомпоновка требует проектной проверки','УЗО и вводные устройства созданы как черновик: номиналы, токи утечки, нейтрали, селективность и схема подключения не подтверждены.'); const assigned=b.rcdAssignments||{},rcdIds=new Set((b.devices||[]).filter(d=>d.type==='rcd').map(d=>d.id)); for(const g of groups){ const current=groupAmps(g),breaker=Number(g.breaker),power=groupPower(g); if(!g.name?.trim())add('missing','GROUP_NAME','Нет названия группы','Заполните название.',g.id); if(!Number.isFinite(breaker)||breaker<=0)add('missing','BREAKER','Не указан автомат: '+g.name,'Укажите номинал защитного аппарата.',g.id); else if(Number.isFinite(current)&¤t>breaker)add('error','OVERLOAD','Оценочный ток выше номинала: '+g.name,current.toFixed(2)+' А > '+breaker+' А. Проверьте расчёт и защиту.',g.id); else if(Number.isFinite(current)&¤t>breaker*.8)add('warning','HIGH_LOAD','Высокая нагрузка: '+g.name,current.toFixed(2)+' А — более 80% номинала; это справочный порог.',g.id); if(!Number.isFinite(power)||power<=0)add('missing','POWER','Нет мощности: '+g.name,'Укажите нагрузку группы или точки с мощностью.',g.id); if(!g.cable||/не задан/i.test(g.cable))add('missing','CABLE','Не указан кабель: '+g.name,'Нужны марка, сечение, материал и способ прокладки.',g.id); if(!g.drop?.section||!['cu','al'].includes(g.drop?.material||''))add('missing','SECTION','Нет проверенных параметров жил: '+g.name,'В расчёте падения напряжения задайте материал и сечение; способ прокладки проверяется отдельно.',g.id); if(!(groupEffectiveLength(g)>0))add('missing','LENGTH','Не задана длина линии: '+g.name,'Укажите длину трассы или длину вручную.',g.id); if(assigned[g.id]&&!rcdIds.has(assigned[g.id]))add('error','RCD_LINK','Некорректная привязка УЗО: '+g.name,'Назначенное УЗО отсутствует в составе щита.',g.id); if(!assigned[g.id])add('missing','RCD_INFO','Не подтверждена дифференциальная защита: '+g.name,'Нет привязки к общему УЗО; проверьте, применяется ли дифавтомат и требуется ли защита для данной линии.',g.id); const ph=epPhase({id:g.id,type:'group',width:Number(g.voltage)===400?3:1,voltage:g.voltage}); if(Number(g.voltage)===400&&ph!=='L1/L2/L3')add('warning','PHASE','Маркировка трёхфазной группы: '+g.name,'Ожидается L1/L2/L3; фактические подключения не проверены.',g.id); } const totals=epPhaseTotals(),values=Object.values(totals.totals),avg=values.reduce((a,v)=>a+v,0)/3; if(avg>0&&(Math.max(...values)-Math.min(...values))/avg>0.2)add('warning','BALANCE','Неравномерность фаз','Разница расчётных установленных мощностей превышает 20% среднего. Это индикатор, не норматив.'); for(const issue of totals.issues)add('warning','PHASE_DATA','Проверьте фазу',issue); if(c.showN===false)add('missing','N_BUS','Шина N скрыта','Уточните необходимость и исполнение нейтральных клемм.'); if(c.showPE===false)add('missing','PE_BUS','Шина PE скрыта','Уточните исполнение защитного проводника и клемм.'); add('missing','ENGINEERING','Нет данных для полной электротехнической проверки','Не проверены токи КЗ, отключающая способность, петля повреждения, селективность, характеристики УЗО, система заземления, допустимый ток кабеля с учётом прокладки, нагрева и группировки, испытания и измерения.'); return {project:activeProject()?.name||'Без проекта',time:new Date().toLocaleString('ru-RU'),issues,counts:{error:issues.filter(x=>x.level==='error').length,warning:issues.filter(x=>x.level==='warning').length,missing:issues.filter(x=>x.level==='missing').length},checked:{groups:groups.length,devices:items.length,modules:used,capacity:total}}; } function epRunAudit(){epLastAudit=epAudit();const r=epLastAudit,root=document.getElementById('epAuditResults');if(!root)return; const color={error:'#b42318',warning:'#9a6700',missing:'#3869a5'},label={error:'Ошибка',warning:'Предупреждение',missing:'Недостаточно данных'}; root.innerHTML='

Объект: '+escapeHtml(r.project)+' · Аппаратов: '+r.checked.devices+' · Групп: '+r.checked.groups+'

Ошибки: '+r.counts.error+' · Предупреждения: '+r.counts.warning+' · Нет данных: '+r.counts.missing+'

'+(r.issues.length?r.issues.map((x,i)=>'
'+label[x.level]+' · '+escapeHtml(x.title)+'
'+escapeHtml(x.detail)+'
').join(''):'

По реализованным проверкам замечаний нет. Это не заключение о безопасности.

')+'

Результат предварительный; для монтажа требуется инженерная проверка и испытания.

'; } function epDownloadAudit(){if(!epLastAudit)epRunAudit();const r=epLastAudit;if(!r)return;const safe=escapeHtml;const body='Проверка ElectroPro 3.4

Отчёт проверки электрощита

Объект: '+safe(r.project)+' · '+safe(r.time)+'

Ошибки: '+r.counts.error+'; предупреждения: '+r.counts.warning+'; недостаточно данных: '+r.counts.missing+'

    '+r.issues.map(x=>'
  1. '+safe(x.title)+' ('+safe(x.level)+')

    '+safe(x.detail)+'

  2. ').join('')+'
Предварительная программная проверка. Не является заключением о соответствии нормам или допуском к эксплуатации.'; const url=URL.createObjectURL(new Blob([body],{type:'text/html;charset=utf-8'}));const a=document.createElement('a');a.href=url;a.download='electropro-audit-3-4.html';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000); } // ElectroPro 3.7: advisory calculation only; no automated normative approval. function ep37Calc(){ const get=id=>document.getElementById(id), result=get('ep37result');if(!result)return; const ph=Number(get('ep37phase').value),kw=Number(get('ep37kw').value),cos=Number(get('ep37cos').value),demand=Number(get('ep37demand').value),type=get('ep37type').value,up=get('ep37up').value; if(!Number.isFinite(kw)||kw<=0||!Number.isFinite(cos)||cos<=0||cos>1||!Number.isFinite(demand)||demand<=0||demand>1){result.textContent='Введите положительную мощность, cos φ от 0 до 1 и коэффициент одновременности от 0 до 1.';return;} const i=kw*1000*demand/(ph===3?Math.sqrt(3)*400*cos:230*cos); const nominal=[6,10,16,20,25,32,40,50,63,80,100,125,160,200,250,315,400,500,630,800,1000].find(n=>n>=i); let msg='Расчётный ток: '+i.toFixed(2)+' А. '; if(nominal)msg+='Ближайший стандартный номинал не ниже расчётного тока: '+nominal+' А (только ориентир, не окончательный подбор). '; else msg+='Ток выше диапазона предварительного подбора; требуется индивидуальное проектирование. '; if(type==='switch')msg+='Выключатель нагрузки не защищает от сверхтоков. '+(up==='yes'?'Вышестоящая защита заявлена; её пригодность нужно проверить.':'Нет подтверждённой вышестоящей защиты: выбор рубильника нельзя считать допустимым. '); else msg+='Для автомата необходимо проверить условия Ib ≤ In ≤ Iz, отключающую способность, характеристику срабатывания и токи КЗ. '; msg+='Проверьте номинал и тип по каталогу изготовителя, полюсность и применимые нормы.'; result.textContent=msg; } document.addEventListener('DOMContentLoaded',ep37Calc); // 3.9 preliminary calculations; independent input fields retained per project. const ep39Fields=['temp','x','limit','ikmin','ia','time','k','ith']; function ep39Key(){return 'electropro-line-39-'+(activeProject()?.id||'default');} function ep39Load(){let d={};try{d=JSON.parse(localStorage.getItem(ep39Key())||'{}')}catch(e){};for(const k of ep39Fields){let el=document.getElementById('ep39'+k);if(el)el.value=d[k]??(k==='temp'?'70':k==='x'?'0.08':'');}ep39Calc(false);} function ep39Calc(save=true){let d={},errors=[],missing=[],warnings=[];for(const k of ep39Fields)d[k]=document.getElementById('ep39'+k)?.value??'';if(save)localStorage.setItem(ep39Key(),JSON.stringify(d)); const num=(v)=>v===''?NaN:Number(v),a=k=>num(d[k]),v=k=>num(document.getElementById('ep38'+k)?.value??''),p=k=>num(document.getElementById('ep37'+k)?.value??''); const ph=p('phase')===3?3:1,U=ph===3?400:230,kw=p('kw'),cos=p('cos'),demand=p('demand'),section=v('section'),L=v('length'),temp=a('temp'),X=a('x'),limit=a('limit'); const Ib=kw>0&&cos>0&&cos<=1&&demand>0&&demand<=1?kw*1000*demand/(ph===3?Math.sqrt(3)*U*cos:U*cos):NaN; let delta=NaN,percent=NaN; if(!Number.isFinite(Ib))missing.push('Корректная мощность и cos φ из 3.7'); if(!(section>0))missing.push('Сечение кабеля из 3.8'); if(!(L>0))missing.push('Длина линии из 3.8'); if(!Number.isFinite(temp)||temp < -20||temp>120)errors.push('Температура жилы вне допустимого диапазона ввода'); if(!Number.isFinite(X)||X<0)errors.push('Некорректное реактивное сопротивление'); if(!(limit>0))missing.push('Проектный предел падения напряжения'); if(Number.isFinite(Ib)&§ion>0&&L>0&&Number.isFinite(temp)&&temp>=-20&&temp<=120&&Number.isFinite(X)&&X>=0){ const rho20=document.getElementById('ep38mat')?.value==='Алюминий'?0.028264:0.017241; const alpha=document.getElementById('ep38mat')?.value==='Алюминий'?0.00403:0.00393; const Rkm=rho20*(1+alpha*(temp-20))*1000/section; const sin=Math.sqrt(Math.max(0,1-cos*cos)); delta=(ph===3?Math.sqrt(3):2)*Ib*L/1000*(Rkm*cos+X*sin); percent=100*delta/U; if(limit>0&&percent>limit)errors.push('Расчётное падение напряжения выше заданного проектного предела'); warnings.push('Расчёт по приближённому сопротивлению жилы; не учтены контакты, гармоники, фактическая геометрия и температура по трассе'); } const ikmin=a('ikmin'),ia=a('ia'),t=a('time'),k=a('k'),ith=a('ith'); if(!(ikmin>0))missing.push('Минимальный ток КЗ в конце линии из расчёта петли'); if(!(ia>0))missing.push('Ток гарантированного отключения Ia по времятоковой характеристике'); if(ikmin>0&&ia>0&&ikmin0))missing.push('Время отключения при расчётном токе КЗ'); if(!(k>0))missing.push('Коэффициент k для материала и изоляции'); if(!(ith>0))missing.push('Ток КЗ для адиабатической проверки'); let smin=NaN; if(t>0&&k>0&&ith>0){smin=ith*Math.sqrt(t)/k;if(section>0&§ionString(x).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); let html='Результат 3.9'; if(Number.isFinite(delta))html+='

Оценка падения: '+delta.toFixed(2)+' В ('+percent.toFixed(2)+'%)

'; if(Number.isFinite(smin))html+='

Адиабатический минимум: '+smin.toFixed(2)+' мм²

'; if(errors.length)html+='

Ошибки: '+errors.map(esc).join('; ')+'

'; if(missing.length)html+='

Недостающие данные: '+missing.map(esc).join('; ')+'

'; html+='

Предупреждения: '+warnings.map(esc).join('; ')+'

'; if(!errors.length&&!missing.length)html+='

Числовые сравнения выполнены, но нормативное соответствие и безопасность не подтверждены.

'; const root=document.getElementById('ep39result');if(root)root.innerHTML=html; } document.addEventListener('DOMContentLoaded',ep39Load); // 3.8: advisory comparison only, not certification of a circuit. function ep38Key(){return 'electropro-cable-38-'+(activeProject()?.id||'default');} const ep38Fields=['mat','section','lay','iz','in','length','ik','icu']; function ep38Load(){let d={};try{d=JSON.parse(localStorage.getItem(ep38Key())||'{}')}catch(e){};for(const k of ep38Fields){const el=document.getElementById('ep38'+k);if(el)el.value=d[k]??'';}ep38Calc(false);} function ep38Calc(save=true){const get=k=>document.getElementById('ep38'+k),root=get('result');if(!root)return;const d={};for(const k of ep38Fields)d[k]=get(k).value;if(save)localStorage.setItem(ep38Key(),JSON.stringify(d));const n=k=>d[k].trim()===''?NaN:Number(d[k]);const errors=[],missing=[],warnings=[];const ph=Number(document.getElementById('ep37phase')?.value||1),kw=Number(document.getElementById('ep37kw')?.value),cos=Number(document.getElementById('ep37cos')?.value),dem=Number(document.getElementById('ep37demand')?.value);const ib=kw>0&&cos>0&&cos<=1&&dem>0&&dem<=1?kw*1000*dem/(ph===3?Math.sqrt(3)*400*cos:230*cos):NaN; if(!Number.isFinite(ib))missing.push('Корректная мощность, cos φ и коэффициент одновременности в расчёте 3.7'); if(!Number.isFinite(n('section'))||n('section')<=0)missing.push('Сечение кабеля'); if(!d.lay)missing.push('Способ прокладки'); if(!Number.isFinite(n('iz'))||n('iz')<=0)missing.push('Iz с поправками по применимым нормативным таблицам'); if(!Number.isFinite(n('in'))||n('in')<=0)missing.push('Номинал защитного автомата In'); if(!Number.isFinite(n('length'))||n('length')<=0)missing.push('Длина трассы для проверки падения напряжения'); if(!Number.isFinite(n('ik'))||n('ik')<=0)missing.push('Максимальный ток КЗ'); if(!Number.isFinite(n('icu'))||n('icu')<=0)missing.push('Отключающая способность автомата'); if(Number.isFinite(ib)&&Number.isFinite(n('in'))&&n('in')>0&&ib>n('in'))errors.push('Ib > In: номинал автомата ниже расчётного тока'); if(Number.isFinite(n('in'))&&n('in')>0&&Number.isFinite(n('iz'))&&n('iz')>0&&n('in')>n('iz'))errors.push('In > Iz: не выполняется базовое условие защиты от перегрузки'); if(Number.isFinite(ib)&&Number.isFinite(n('iz'))&&n('iz')>0&&ib>n('iz'))errors.push('Ib > Iz: допустимый ток кабеля ниже нагрузки'); if(Number.isFinite(n('ik'))&&n('ik')>0&&Number.isFinite(n('icu'))&&n('icu')>0&&n('ik')>n('icu'))errors.push('Отключающая способность ниже указанного максимального тока КЗ'); if(d.section&&d.iz)warnings.push('Iz принят как введённое значение; соответствие сечению и условиям прокладки не подтверждено программой'); warnings.push('Не выполнены проверки I₂ ≤ 1,45 Iz, минимального тока КЗ и времени отключения, термической стойкости, селективности, падения напряжения и системы заземления'); const escape=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); let html='Проверка 3.8'+(Number.isFinite(ib)?'

Расчётный Ib: '+ib.toFixed(2)+' А

':'');if(errors.length)html+='

Ошибки: '+errors.map(escape).join('; ')+'

';if(missing.length)html+='

Недостающие данные: '+missing.map(escape).join('; ')+'

';html+='

Предупреждения: '+warnings.map(escape).join('; ')+'

';if(!errors.length&&!missing.length)html+='

Базовые числовые сравнения пройдены. Полная пригодность кабеля и защиты НЕ подтверждена.

';root.innerHTML=html;} document.addEventListener('DOMContentLoaded',ep38Load); // ElectroPro 5.1: project-persisted multiroom footprint editor. const ep51={selected:null,drag:null}; function ep51Project(){return activeProject()||null} function ep51Rooms(){let p=ep51Project();if(!p)return [];if(!Array.isArray(p.ep51Rooms))p.ep51Rooms=[];return p.ep51Rooms} function ep51Refresh(){const sel=document.getElementById('ep51Selected');if(!sel)return;let rs=ep51Rooms();if(!rs.some(r=>r.id===ep51.selected))ep51.selected=rs[0]?.id||null;sel.innerHTML=rs.map(r=>'').join('');sel.value=ep51.selected||'';let r=rs.find(r=>r.id===ep51.selected);if(r){document.getElementById('ep51Name').value=r.name;document.getElementById('ep51Width').value=r.w;document.getElementById('ep51Depth').value=r.h}ep51Draw()} function ep51Select(){ep51.selected=document.getElementById('ep51Selected').value;ep51Refresh()} function ep51Number(id){let v=Number(document.getElementById(id).value);return Number.isFinite(v)&&v>=.5&&v<=30?v:null} function ep51Add(){if(!ep51Project()){alert('Сначала создайте объект');return}let w=ep51Number('ep51Width'),h=ep51Number('ep51Depth');if(!w||!h){alert('Размеры: от 0,5 до 30 м');return}let rs=ep51Rooms(),last=rs[rs.length-1];let r={id:makeId(),name:document.getElementById('ep51Name').value.trim().slice(0,70)||'Комната',x:last?last.x+last.w:0,y:last?last.y:0,w,h,openings:{}};rs.push(r);ep51.selected=r.id;save();ep51Refresh()} function ep51Update(){let r=ep51Rooms().find(r=>r.id===ep51.selected);if(!r)return;let w=ep51Number('ep51Width'),h=ep51Number('ep51Depth');if(!w||!h){alert('Размеры: от 0,5 до 30 м');return}Object.assign(r,{w,h,name:document.getElementById('ep51Name').value.trim().slice(0,70)||'Комната'});save();ep51Refresh()} function ep51Remove(){let p=ep51Project();if(!p||!ep51.selected)return;if(!confirm('Удалить выбранную комнату из 3D-планировки?'))return;p.ep51Rooms=ep51Rooms().filter(r=>r.id!==ep51.selected);ep51.selected=null;save();ep51Refresh()} function ep51SetOpening(){let r=ep51Rooms().find(r=>r.id===ep51.selected);if(!r)return;r.openings=r.openings||{};r.openings[document.getElementById('ep51Side').value]=document.getElementById('ep51Opening').value;save();ep51Draw()} function ep51View(){let rs=ep51Rooms(),minX=Math.min(0,...rs.map(r=>r.x)),minY=Math.min(0,...rs.map(r=>r.y)),maxX=Math.max(6,...rs.map(r=>r.x+r.w)),maxY=Math.max(4,...rs.map(r=>r.y+r.h));let scale=Math.min(720/(maxX-minX+1),420/(maxY-minY+1),90);return {minX,minY,scale,ox:40,oy:40}} function ep51Draw(){let c=document.getElementById('ep51Plan');if(!c)return;let ctx=c.getContext('2d'),v=ep51View(),rs=ep51Rooms();ctx.clearRect(0,0,c.width,c.height);ctx.fillStyle='#f1f5f9';ctx.fillRect(0,0,c.width,c.height);ctx.font='14px system-ui';for(let r of rs){let x=v.ox+(r.x-v.minX)*v.scale,y=v.oy+(r.y-v.minY)*v.scale,w=r.w*v.scale,h=r.h*v.scale;ctx.fillStyle=r.id===ep51.selected?'#ffedd5':'#e0effb';ctx.fillRect(x,y,w,h);ctx.strokeStyle=r.id===ep51.selected?'#df762c':'#506981';ctx.lineWidth=4;ctx.strokeRect(x,y,w,h);ctx.fillStyle='#23374b';ctx.fillText(r.name.slice(0,20),x+8,y+24);ctx.font='12px system-ui';ctx.fillText(r.w+' × '+r.h+' м',x+8,y+43);ctx.font='14px system-ui';for(let [side,type] of Object.entries(r.openings||{})){if(type==='none'||(r.ep55Openings||[]).some(o=>o.side===side))continue;let ax=side==='left'?x:side==='right'?x+w:x+w/2,ay=side==='top'?y:side==='bottom'?y+h:y+h/2;ctx.strokeStyle=type==='door'?'#d97706':'#0ea5e9';ctx.lineWidth=7;ctx.beginPath();if(side==='top'||side==='bottom'){ctx.moveTo(ax-16,ay);ctx.lineTo(ax+16,ay)}else{ctx.moveTo(ax,ay-16);ctx.lineTo(ax,ay+16)}ctx.stroke()}}document.getElementById('ep51Info').textContent=rs.length+' комнат · перетаскивайте выбранную комнату пальцем · оранжевый проём — дверь, синий — окно';if(ep5State.on)ep51Draw3D()} function ep51Draw3D(){let rs=ep51Rooms();if(!rs.length)return;let c=document.getElementById('ep5Canvas'),ctx=c.getContext('2d'),W=c.width,H=c.height;let minX=Math.min(...rs.map(r=>r.x)),maxX=Math.max(...rs.map(r=>r.x+r.w)),minY=Math.min(...rs.map(r=>r.y)),maxY=Math.max(...rs.map(r=>r.y+r.h)),cx=(minX+maxX)/2,cy=(minY+maxY)/2;let height=Math.max(1.5,Number(document.getElementById('ep5Height').value)||2.7),scale=Math.min(460/Math.max(1,maxX-minX,maxY-minY),290/height)*ep5State.zoom,ang=ep5State.angle,tilt=ep5State.tilt;ctx.fillStyle='#eaf0f5';ctx.fillRect(0,0,W,H);function proj(x,y,z){let a=x-cx,b=y-cy,rx=a*Math.cos(ang)-b*Math.sin(ang),ry=a*Math.sin(ang)+b*Math.cos(ang);return [W/2+rx*scale,H*.64+(ry*Math.sin(tilt)-z*Math.cos(tilt))*scale]};function poly(points,fill){ctx.beginPath();points.forEach((pt,i)=>{let q=proj(...pt);if(i)ctx.lineTo(...q);else ctx.moveTo(...q)});ctx.closePath();ctx.fillStyle=fill;ctx.fill();ctx.strokeStyle='#9daebc';ctx.lineWidth=2;ctx.stroke()};rs.forEach((r,i)=>poly((r.ep56Vertices?.length>=3?r.ep56Vertices:[[0,0],[r.w,0],[r.w,r.h],[0,r.h]]).map(q=>[r.x+q[0],r.y+q[1],0]),i%2?'#e4d6c3':'#f5e6d1'));let walls=[];rs.forEach(r=>{let vv=r.ep56Vertices?.length>=3?r.ep56Vertices:[[0,0],[r.w,0],[r.w,r.h],[0,r.h]];let edges=vv.map((q,i)=>[[r.x+q[0],r.y+q[1]],[r.x+vv[(i+1)%vv.length][0],r.y+vv[(i+1)%vv.length][1]],'wall:'+i]);for(let [a,b,side] of edges){let mid=proj((a[0]+b[0])/2,(a[1]+b[1])/2,0);walls.push({a,b,side,room:r,type:r.openings?.[side]||'none',sort:mid[1]})}});walls.sort((a,b)=>a.sort-b.sort);for(let w of walls){let {a,b,type}=w;let room=w.room, openings=(room?.ep55Openings||[]).filter(o=>ep57Side(room,o.side)===w.side&&!o.invalidGeometry);if(openings.length){let len=Math.hypot(b[0]-a[0],b[1]-a[1]),cuts=[0,len,...openings.flatMap(o=>[o.offset,o.offset+o.width])].sort((x,y)=>x-y);for(let j=1;jmid>=t.offset&&mid<=t.offset+t.width),bands=o?[[0,o.sill],[o.sill+o.height,height]]:[[0,height]];for(let [z0,z1] of bands){if(z1-z0<.001)continue;let u=lo/len,v=hi/len,ax=a[0]+(b[0]-a[0])*u,ay=a[1]+(b[1]-a[1])*u,bx=a[0]+(b[0]-a[0])*v,by=a[1]+(b[1]-a[1])*v;poly([[ax,ay,z0],[bx,by,z0],[bx,by,z1],[ax,ay,z1]],'rgba(204,221,234,.50)')}}}else{let sections=type==='door'?[[0,.36],[.64,1]]:type==='window'?[[0,.32],[.68,1]]:[[0,1]];for(let [u,v] of sections){let ax=a[0]+(b[0]-a[0])*u,ay=a[1]+(b[1]-a[1])*u,bx=a[0]+(b[0]-a[0])*v,by=a[1]+(b[1]-a[1])*v;poly([[ax,ay,0],[bx,by,0],[bx,by,height],[ax,ay,height]],'rgba(204,221,234,.50)')}}}ctx.fillStyle='#243b53';ctx.font='bold 14px system-ui';for(let r of rs){let q=proj(r.x+r.w/2,r.y+r.h/2,.08);ctx.fillText(r.name.slice(0,16),q[0]-25,q[1])}document.getElementById('ep5Info').textContent=rs.length+' комнат в 3D · проёмы условные · размещение электрических точек остаётся в 2D-плане выбранного помещения'} document.addEventListener('DOMContentLoaded',()=>{let c=document.getElementById('ep51Plan');if(!c)return;c.addEventListener('pointerdown',e=>{let rect=c.getBoundingClientRect(),px=(e.clientX-rect.left)*c.width/rect.width,py=(e.clientY-rect.top)*c.height/rect.height,v=ep51View();let rs=ep51Rooms();let r=[...rs].reverse().find(r=>px>=v.ox+(r.x-v.minX)*v.scale&&px<=v.ox+(r.x-v.minX+r.w)*v.scale&&py>=v.oy+(r.y-v.minY)*v.scale&&py<=v.oy+(r.y-v.minY+r.h)*v.scale);if(!r)return;ep51.selected=r.id;ep51.drag={id:r.id,px,py,x:r.x,y:r.y};c.setPointerCapture(e.pointerId);ep51Refresh()});c.addEventListener('pointermove',e=>{if(!ep51.drag)return;let rect=c.getBoundingClientRect(),px=(e.clientX-rect.left)*c.width/rect.width,py=(e.clientY-rect.top)*c.height/rect.height,v=ep51View(),r=ep51Rooms().find(r=>r.id===ep51.drag.id);if(!r)return;r.x=Math.round((ep51.drag.x+(px-ep51.drag.px)/v.scale)*10)/10;r.y=Math.round((ep51.drag.y+(py-ep51.drag.py)/v.scale)*10)/10;ep51Draw()});const end=()=>{if(ep51.drag){ep51.drag=null;save();ep51Draw()}};c.addEventListener('pointerup',end);c.addEventListener('pointercancel',end);ep51Refresh()}); // ElectroPro 5.3: project-scoped 3D electrical point editor. const ep52={selected:null}; function ep52Room(){return ep51Rooms().find(r=>r.id===ep51.selected)} function ep52Points(){let r=ep52Room();if(!r)return [];if(!Array.isArray(r.ep52Points))r.ep52Points=[];return r.ep52Points} function ep52Refresh(){let el=document.getElementById('ep52List');if(!el)return;let pts=ep52Points();if(!pts.some(p=>p.id===ep52.selected))ep52.selected=pts[0]?.id||null;let names={socket:'Розетка',switch:'Выключатель',light:'Светильник',box:'Распредкоробка'};el.innerHTML=pts.map(p=>'').join('');el.value=ep52.selected||'';document.getElementById('ep52Info').textContent=pts.length+' электроточек в выбранной комнате. Положение и высота сохраняются в проекте.'} function ep52Select(){ep52.selected=document.getElementById('ep52List').value;let p=ep52Points().find(p=>p.id===ep52.selected);if(!p)return;document.getElementById('ep52Type').value=p.type;document.getElementById('ep52Side').value=p.side;document.getElementById('ep52Pos').value=Math.round(p.pos*100);document.getElementById('ep52Height').value=p.height;ep51Draw()} function ep52Add(){let r=ep52Room();if(!r){alert('Сначала выберите комнату');return}let type=document.getElementById('ep52Type').value,side=document.getElementById('ep52Side').value,pos=Number(document.getElementById('ep52Pos').value),height=Number(document.getElementById('ep52Height').value),max=Number(document.getElementById('ep5Height').value)||2.7;if(!Number.isFinite(pos)||pos<0||pos>100||!Number.isFinite(height)||height<0||height>max){alert('Проверьте положение (0–100%) и высоту (0–'+max+' м)');return}let p={id:makeId(),type,side,pos:pos/100,height:side==='ceiling'?max:height};ep52Points().push(p);ep52.selected=p.id;save();ep52Refresh();ep51Draw()} function ep52Remove(){let r=ep52Room();if(!r||!ep52.selected)return;r.ep52Points=ep52Points().filter(p=>p.id!==ep52.selected);ep52.selected=null;save();ep52Refresh();ep51Draw()} function ep52Coord(r,p){let t=p.pos;switch(p.side){case 'top':return [r.x+r.w*t,r.y,p.height];case 'bottom':return [r.x+r.w*t,r.y+r.h,p.height];case 'left':return [r.x,r.y+r.h*t,p.height];case 'right':return [r.x+r.w,r.y+r.h*t,p.height];default:return [r.x+r.w*t,r.y+r.h*.5,p.height]}} const ep52Draw2D=ep51Draw;ep51Draw=function(){ep52Draw2D();let c=document.getElementById('ep51Plan');if(!c)return;let ctx=c.getContext('2d'),v=ep51View();for(let r of ep51Rooms())for(let p of r.ep52Points||[]){let [x,y]=ep52Coord(r,p),px=v.ox+(x-v.minX)*v.scale,py=v.oy+(y-v.minY)*v.scale;ctx.beginPath();ctx.arc(px,py,7,0,Math.PI*2);ctx.fillStyle=p.type==='socket'?'#2563eb':p.type==='switch'?'#ea580c':p.type==='light'?'#eab308':'#7c3aed';ctx.fill();ctx.strokeStyle='white';ctx.lineWidth=2;ctx.stroke()}ep52Refresh()}; const ep52Draw3D=ep51Draw3D;ep51Draw3D=function(){ep52Draw3D();let c=document.getElementById('ep5Canvas'),ctx=c.getContext('2d'),rs=ep51Rooms();if(!rs.length)return;let minX=Math.min(...rs.map(r=>r.x)),maxX=Math.max(...rs.map(r=>r.x+r.w)),minY=Math.min(...rs.map(r=>r.y)),maxY=Math.max(...rs.map(r=>r.y+r.h)),cx=(minX+maxX)/2,cy=(minY+maxY)/2,height=Math.max(1.5,Number(document.getElementById('ep5Height').value)||2.7),scale=Math.min(460/Math.max(1,maxX-minX,maxY-minY),290/height)*ep5State.zoom,ang=ep5State.angle,tilt=ep5State.tilt;for(let r of rs)for(let p of r.ep52Points||[]){let [x,y,z]=ep52Coord(r,p),a=x-cx,b=y-cy,rx=a*Math.cos(ang)-b*Math.sin(ang),ry=a*Math.sin(ang)+b*Math.cos(ang),px=c.width/2+rx*scale,py=c.height*.64+(ry*Math.sin(tilt)-z*Math.cos(tilt))*scale;ctx.beginPath();ctx.arc(px,py,Math.max(5,Math.min(10,scale*.08)),0,Math.PI*2);ctx.fillStyle=p.type==='socket'?'#2563eb':p.type==='switch'?'#ea580c':p.type==='light'?'#eab308':'#7c3aed';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke()}} const ep52RefreshRoom=ep51Refresh;ep51Refresh=function(){ep52RefreshRoom();ep52Refresh()}; // ElectroPro 5.3: room-scoped orthogonal cable routes and geometric estimates. const ep53={selected:null}; function ep53Routes(){let r=ep52Room();if(!r)return [];if(!Array.isArray(r.ep53Routes))r.ep53Routes=[];return r.ep53Routes} function ep53PointName(p,i){return ({socket:'Розетка',switch:'Выключатель',light:'Светильник',box:'Коробка'}[p.type]||'Точка')+' '+(i+1)} function ep53Path(room,route){let pts=room.ep52Points||[],a=pts.find(p=>p.id===route.from),b=pts.find(p=>p.id===route.to);if(!a||!b)return null;let A=ep52Coord(room,a),B=ep52Coord(room,b),z=Number(route.level);if(!Number.isFinite(z))return null;return [A,[A[0],A[1],z],[B[0],A[1],z],[B[0],B[1],z],B]} function ep53Length(path){if(!path)return 0;let total=0;for(let i=1;iv-path[i-1][j]));return total} function ep53Refresh(){let a=document.getElementById('ep53From');if(!a)return;let pts=ep52Points(),options=pts.map((p,i)=>'').join(''),oldA=a.value,b=document.getElementById('ep53To'),oldB=b.value;a.innerHTML=options;b.innerHTML=options;if(pts.some(p=>p.id===oldA))a.value=oldA;if(pts.some(p=>p.id===oldB))b.value=oldB;else if(pts.length>1)b.value=pts[1].id;let routes=ep53Routes(),room=ep52Room(),sel=document.getElementById('ep53List');if(!routes.some(r=>r.id===ep53.selected))ep53.selected=routes[0]?.id||null;sel.innerHTML=routes.map((r,i)=>'').join('');sel.value=ep53.selected||'';let total=routes.reduce((sum,r)=>sum+ep53Length(ep53Path(room,r))*(1+(Number(r.reserve)||0)/100),0);document.getElementById('ep53Info').textContent=routes.length+' трасс · '+total.toFixed(2)+' м с запасом. Расчёт предварительный: проёмы учитываются условно, препятствия и крепления требуют проверки.'} function ep53Select(){ep53.selected=document.getElementById('ep53List').value;let r=ep53Routes().find(x=>x.id===ep53.selected);if(!r)return;for(let [id,v] of [['ep53From',r.from],['ep53To',r.to],['ep53Level',r.level],['ep53Reserve',r.reserve],['ep53Cable',r.cable]])document.getElementById(id).value=v;document.getElementById('ep54Mode').value=r.mode||'legacy';ep51Draw()} function ep53Add(){let room=ep52Room();if(!room){alert('Сначала выберите комнату');return}let from=document.getElementById('ep53From').value,to=document.getElementById('ep53To').value,level=Number(document.getElementById('ep53Level').value),reserve=Number(document.getElementById('ep53Reserve').value),max=Number(document.getElementById('ep5Height').value)||2.7;if(!from||!to||from===to){alert('Выберите две разные электроточки');return}if(!Number.isFinite(level)||level<0||level>max||!Number.isFinite(reserve)||reserve<0||reserve>100){alert('Проверьте высоту прокладки и запас');return}let r={id:makeId(),from,to,level,reserve,cable:document.getElementById('ep53Cable').value.trim().slice(0,90)||'Кабель',mode:document.getElementById('ep54Mode').value};ep53Routes().push(r);ep53.selected=r.id;save();ep51Draw()} function ep53Remove(){let room=ep52Room();if(!room||!ep53.selected)return;room.ep53Routes=ep53Routes().filter(r=>r.id!==ep53.selected);ep53.selected=null;save();ep51Draw()} const ep53Old2D=ep51Draw;ep51Draw=function(){ep53Old2D();let c=document.getElementById('ep51Plan');if(!c)return;let ctx=c.getContext('2d'),v=ep51View();for(let room of ep51Rooms())for(let route of room.ep53Routes||[]){let path=ep53Path(room,route);if(!path)continue;ctx.beginPath();path.forEach((p,i)=>{let x=v.ox+(p[0]-v.minX)*v.scale,y=v.oy+(p[1]-v.minY)*v.scale;if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y)});ctx.strokeStyle=route.id===ep53.selected?'#dc2626':'#059669';ctx.lineWidth=3;ctx.setLineDash([8,5]);ctx.stroke();ctx.setLineDash([])}ep53Refresh()}; const ep53Old3D=ep51Draw3D;ep51Draw3D=function(){ep53Old3D();let rs=ep51Rooms();if(!rs.length)return;let c=document.getElementById('ep5Canvas'),ctx=c.getContext('2d'),minX=Math.min(...rs.map(r=>r.x)),maxX=Math.max(...rs.map(r=>r.x+r.w)),minY=Math.min(...rs.map(r=>r.y)),maxY=Math.max(...rs.map(r=>r.y+r.h)),cx=(minX+maxX)/2,cy=(minY+maxY)/2,height=Math.max(1.5,Number(document.getElementById('ep5Height').value)||2.7),scale=Math.min(460/Math.max(1,maxX-minX,maxY-minY),290/height)*ep5State.zoom,ang=ep5State.angle,tilt=ep5State.tilt;for(let room of rs)for(let route of room.ep53Routes||[]){let path=ep53Path(room,route);if(!path)continue;ctx.beginPath();path.forEach(([x,y,z],i)=>{let a=x-cx,b=y-cy,rx=a*Math.cos(ang)-b*Math.sin(ang),ry=a*Math.sin(ang)+b*Math.cos(ang),px=c.width/2+rx*scale,py=c.height*.64+(ry*Math.sin(tilt)-z*Math.cos(tilt))*scale;if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py)});ctx.strokeStyle=route.id===ep53.selected?'#dc2626':'#059669';ctx.lineWidth=4;ctx.stroke()}}; const ep53OldRefresh=ep52Refresh;ep52Refresh=function(){ep53OldRefresh();ep53Refresh()}; // ElectroPro 5.4: room-boundary route alternatives, simplified opening penalty and cable schedule. const ep54LegacyPath=ep53Path; function ep54OpeningPenalty(room, a, b, z){ const eps=1e-5, mid=(u,v)=>(u+v)/2; let side=null,t0=0,t1=0; if(Math.abs(a[1]-room.y)=.68)return 0; return type==='door'?(z<2.2?1000:3):(z>.7&&z<2.2?1000:3); } function ep54Perimeter(room,A,B,z){ let w=room.w,h=room.h,per=2*(w+h); function perimeter(p){let x=Math.max(0,Math.min(w,p[0]-room.x)),y=Math.max(0,Math.min(h,p[1]-room.y));let candidates=[[y,0],[w+y,w],[w+h+(w-x),h],[2*w+h+(h-y),0]];let d=[y,x===w?y:Infinity,h-y,x===0?h-y:Infinity]; let edges=[{t:x,d:Math.abs(y)},{t:w+y,d:Math.abs(x-w)},{t:w+h+(w-x),d:Math.abs(y-h)},{t:2*w+h+(h-y),d:Math.abs(x)}];edges.sort((a,b)=>a.d-b.d);return edges[0].t%per} function coord(t){t=(t%per+per)%per;if(t<=w)return [room.x+t,room.y,z];t-=w;if(t<=h)return [room.x+w,room.y+t,z];t-=h;if(t<=w)return [room.x+w-t,room.y+h,z];t-=w;return [room.x,room.y+h-t,z]} let a=perimeter(A),b=perimeter(B),options=[]; for(let dir of [1,-1]){let distance=dir===1?(b-a+per)%per:(a-b+per)%per,positions=[a],corners=[0,w,w+h,2*w+h];for(let c of corners){let d=dir===1?(c-a+per)%per:(a-c+per)%per;if(d>1e-6&&da.score-b.score);return options[0] } ep53Path=function(room,route){let base=ep54LegacyPath(room,route);if(!base||!route.mode||route.mode==='legacy')return base;let A=base[0],B=base[base.length-1],z=Number(route.level);if(route.mode==='ceiling'){let H=Math.max(z,Number(document.getElementById('ep5Height')?.value)||2.7);return [A,[A[0],A[1],H],[B[0],A[1],H],[B[0],B[1],H],B]} let p=ep54Perimeter(room,A,B,z);return [A,[A[0],A[1],z],...p.points,[B[0],B[1],z],B] }; function ep54Rows(){let rows=[];for(let room of ep51Rooms())for(let route of room.ep53Routes||[]){let path=ep53Path(room,route);if(!path)continue;let length=ep53Length(path),reserve=Number(route.reserve)||0;rows.push({room:room.name,cable:route.cable||'Кабель',length,reserve,total:length*(1+reserve/100),mode:route.mode||'legacy'})}return rows} function ep54Report(){let rows=ep54Rows(),agg=new Map();for(let r of rows){let a=agg.get(r.cable)||{count:0,length:0,total:0};a.count++;a.length+=r.length;a.total+=r.total;agg.set(r.cable,a)}let html='

Ведомость кабеля по объекту

Оценка по геометрии помещений; без точных размеров проёмов, допусков монтажа и скрытых препятствий.

';for(let [c,a] of agg)html+='';html+='
КабельТрассБез запаса, мС запасом, м
'+escapeHtml(c)+''+a.count+''+a.length.toFixed(2)+''+a.total.toFixed(2)+'

Всего: '+rows.reduce((a,r)=>a+r.total,0).toFixed(2)+' м, '+rows.length+' трасс. Проёмы учитываются условно по центру стены; перед монтажом трассы необходимо проверить.

';document.getElementById('ep54Report').innerHTML=html} function ep54Csv(){let rows=ep54Rows(),q=x=>'"'+String(x).replace(/"/g,'""')+'"',lines=[['Комната','Марка кабеля','Способ','Длина м','Запас %','Итого м'].map(q).join(';')];for(let r of rows)lines.push([r.room,r.cable,r.mode,r.length.toFixed(2),r.reserve,r.total.toFixed(2)].map(q).join(';'));let blob=new Blob(['\ufeff'+lines.join('\r\n')],{type:'text/csv;charset=utf-8'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='ElectroPro_5_4_cables.csv';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)} const ep51Original3D=ep5Draw;ep5Draw=function(){ep51Original3D();if(ep5State.on&&ep51Rooms().length)ep51Draw3D()}; const ep51OriginalMode=ep5SetMode;ep5SetMode=function(on){ep51OriginalMode(on);if(on)ep51Refresh()}; // ElectroPro 5.5: dimensioned openings and actual intersection checks. const ep55={selected:null}; function ep55Room(){return ep51Rooms().find(r=>r.id===ep51.selected)} function ep55Openings(r){if(!Array.isArray(r.ep55Openings))r.ep55Openings=[];return r.ep55Openings} function ep55Refresh(){let el=document.getElementById('ep55List');if(!el)return;let r=ep55Room(),list=r?ep55Openings(r):[];el.innerHTML=list.map(o=>'').join('');if(!list.some(o=>o.id===ep55.selected))ep55.selected=list[0]?.id||null;el.value=ep55.selected||'';document.getElementById('ep55Info').textContent=list.length+' точных проёмов. Старые условные проёмы остаются на стенах без точных размеров.'} function ep55Select(){ep55.selected=document.getElementById('ep55List').value;let o=ep55Openings(ep55Room()).find(x=>x.id===ep55.selected);if(!o)return;document.getElementById('ep51Side').value=o.side;document.getElementById('ep51Opening').value=o.type;for(let [k,v] of [['ep55Offset',o.offset],['ep55Width',o.width],['ep55Sill',o.sill],['ep55Height',o.height]])document.getElementById(k).value=v} function ep55Add(){let r=ep55Room();if(!r){alert('Сначала выберите комнату');return}let side=document.getElementById('ep51Side').value,type=document.getElementById('ep51Opening').value,offset=Number(document.getElementById('ep55Offset').value),width=Number(document.getElementById('ep55Width').value),sill=Number(document.getElementById('ep55Sill').value),height=Number(document.getElementById('ep55Height').value),length=(side==='top'||side==='bottom')?r.w:r.h,H=Number(document.getElementById('ep5Height').value)||2.7;if(type==='none'){alert('Выберите дверь или окно');return}if(![offset,width,sill,height].every(Number.isFinite)||offset<0||width<.1||offset+width>length+1e-7||sill<0||height<.1||sill+height>H+1e-7){alert('Проём не помещается в выбранную стену или превышает высоту помещения');return}if(ep55Openings(r).some(o=>o.side===side&&Math.max(offset,o.offset)o.id!==ep55.selected);ep55.selected=null;save();ep51Draw()} const ep55OldDraw=ep51Draw;ep51Draw=function(){ep55OldDraw();let c=document.getElementById('ep51Plan');if(!c)return;let ctx=c.getContext('2d'),v=ep51View();for(let r of ep51Rooms())for(let o of r.ep55Openings||[]){let x=v.ox+(r.x-v.minX)*v.scale,y=v.oy+(r.y-v.minY)*v.scale;let a=o.offset*v.scale,b=(o.offset+o.width)*v.scale;ctx.beginPath();if(o.side==='top'||o.side==='bottom'){let yy=y+(o.side==='bottom'?r.h*v.scale:0);ctx.moveTo(x+a,yy);ctx.lineTo(x+b,yy)}else{let xx=x+(o.side==='right'?r.w*v.scale:0);ctx.moveTo(xx,y+a);ctx.lineTo(xx,y+b)}ctx.strokeStyle=o.type==='door'?'#d97706':'#0284c7';ctx.lineWidth=9;ctx.stroke()}ep55Refresh()}; const ep55OldPenalty=ep54OpeningPenalty; ep54OpeningPenalty=function(room,a,b,z){let openings=room.ep55Openings||[],eps=1e-5,side=null,start=0,end=0;if(Math.abs(a[1]-room.y)o.side===side);if(!relevant.length)return ep55OldPenalty(room,a,b,z);return relevant.some(o=>Math.max(Math.min(start,end),o.offset)o.sill+1e-6&&zr.id===ep51.selected)} function ep56Verts(r){return r.ep56Vertices?.length>=3?r.ep56Vertices:[[0,0],[r.w,0],[r.w,r.h],[0,r.h]]} function ep56Valid(v){if(v.length<3||v.length>24||v.some(p=>!p.every(Number.isFinite)||p.some(t=>Math.abs(t)>50)))return false;let area=0;for(let i=0;i'').join('');sel.value=String(ep56.vertex);document.getElementById('ep56X').value=v[ep56.vertex]?.[0]??'';document.getElementById('ep56Y').value=v[ep56.vertex]?.[1]??'';document.getElementById('ep56Toggle').textContent=ep56.editing?'✓ Завершить редактирование':'Редактировать углы';document.getElementById('ep56Info').textContent=r?(v.length+' вершин · '+(ep56.editing?'перетаскивайте оранжевые точки':'включите редактирование')+' · электроточки и проёмы привязаны к сегментам; маршруты предварительные'):'Сначала выберите комнату'} function ep56Select(){ep56.vertex=Number(document.getElementById('ep56Vertex').value)||0;ep56Refresh()} function ep56Apply(){let r=ep56Room();if(!r)return;let v=ep56Verts(r).map(p=>p.slice());v[ep56.vertex]=[Number(document.getElementById('ep56X').value),Number(document.getElementById('ep56Y').value)];ep56Commit(v)} function ep56AddCorner(){let r=ep56Room();if(!r)return;let v=ep56Verts(r).map(p=>p.slice()),i=ep56.vertex,b=v[(i+1)%v.length];v.splice(i+1,0,[(v[i][0]+b[0])/2,(v[i][1]+b[1])/2]);ep56.vertex=i+1;ep56Commit(v)} function ep56RemoveCorner(){let r=ep56Room();if(!r)return;let v=ep56Verts(r).map(p=>p.slice());if(v.length<=3)return alert('Минимум три вершины');v.splice(ep56.vertex,1);ep56.vertex=Math.max(0,ep56.vertex-1);ep56Commit(v)} function ep56Reset(){let r=ep56Room();if(!r||!confirm('Вернуть прямоугольный контур этой комнаты?'))return;delete r.ep56Vertices;save();ep51Draw()} const ep56PreviousDraw=ep51Draw;ep51Draw=function(){ep56PreviousDraw();let c=document.getElementById('ep51Plan');if(!c)return;let ctx=c.getContext('2d'),view=ep51View();for(let r of ep51Rooms()){if(!r.ep56Vertices?.length&&!ep56.editing)continue;let verts=ep56Verts(r);ctx.beginPath();verts.forEach((p,i)=>{let x=view.ox+(r.x+p[0]-view.minX)*view.scale,y=view.oy+(r.y+p[1]-view.minY)*view.scale;if(i)ctx.lineTo(x,y);else ctx.moveTo(x,y)});ctx.closePath();ctx.fillStyle='rgba(255,255,255,.13)';ctx.fill();ctx.strokeStyle='#d97706';ctx.lineWidth=5;ctx.stroke();if(ep56.editing&&r.id===ep51.selected)verts.forEach((p,i)=>{let x=view.ox+(r.x+p[0]-view.minX)*view.scale,y=view.oy+(r.y+p[1]-view.minY)*view.scale;ctx.beginPath();ctx.arc(x,y,9,0,Math.PI*2);ctx.fillStyle=i===ep56.vertex?'#ea580c':'#fbbf24';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke()})}ep56Refresh()}; const ep56PreviousRefresh=ep51Refresh;ep51Refresh=function(){ep56PreviousRefresh();ep56Refresh()}; document.addEventListener('DOMContentLoaded',()=>{let c=document.getElementById('ep51Plan');if(!c)return;function point(e){let b=c.getBoundingClientRect(),v=ep51View();return {x:(e.clientX-b.left)*c.width/b.width,y:(e.clientY-b.top)*c.height/b.height,v}}c.addEventListener('pointerdown',e=>{if(!ep56.editing)return;let r=ep56Room();if(!r)return;let q=point(e),v=ep56Verts(r),i=v.findIndex(p=>Math.hypot(q.x-(q.v.ox+(r.x+p[0]-q.v.minX)*q.v.scale),q.y-(q.v.oy+(r.y+p[1]-q.v.minY)*q.v.scale))<22);if(i<0)return;e.stopImmediatePropagation();ep51.drag=null;ep56.vertex=i;ep56.drag={id:e.pointerId,index:i,start:v.map(p=>p.slice()),v:q.v};c.setPointerCapture(e.pointerId);ep56Refresh()},{capture:true});c.addEventListener('pointermove',e=>{if(!ep56.drag)return;e.stopImmediatePropagation();let r=ep56Room(),q=point(e),v=ep56.drag.start.map(p=>p.slice());v[ep56.drag.index]=[Math.round(((q.x-q.v.ox)/q.v.scale+q.v.minX-r.x)*10)/10,Math.round(((q.y-q.v.oy)/q.v.scale+q.v.minY-r.y)*10)/10];if(ep56Valid(v)){r.ep56Vertices=v;ep51Draw()}},{capture:true});function end(e){if(!ep56.drag)return;e.stopImmediatePropagation();ep56.drag=null;save();ep51Draw()}c.addEventListener('pointerup',end,{capture:true});c.addEventListener('pointercancel',end,{capture:true});c.addEventListener('dblclick',e=>{if(!ep56.editing)return;e.preventDefault();let r=ep56Room();if(!r)return;let q=point(e),v=ep56Verts(r).map(p=>p.slice()),best={dist:Infinity,i:0,p:null};v.forEach((a,i)=>{let b=v[(i+1)%v.length],ax=q.v.ox+(r.x+a[0]-q.v.minX)*q.v.scale,ay=q.v.oy+(r.y+a[1]-q.v.minY)*q.v.scale,bx=q.v.ox+(r.x+b[0]-q.v.minX)*q.v.scale,by=q.v.oy+(r.y+b[1]-q.v.minY)*q.v.scale,t=Math.max(0,Math.min(1,((q.x-ax)*(bx-ax)+(q.y-ay)*(by-ay))/(Math.hypot(bx-ax,by-ay)**2||1))),px=ax+t*(bx-ax),py=ay+t*(by-ay),dist=Math.hypot(q.x-px,q.y-py);if(diste.classList.contains('card')); function fold(label,nodes,parent,key,opened){ if(!nodes.length)return; const d=document.createElement('details');d.className='ep-fold';d.dataset.section=key; const saved=localStorage.getItem('ep-board-fold-'+key); d.open=saved===null?!!opened:saved==='1'; const sum=document.createElement('summary');sum.textContent=label; const body=document.createElement('div');body.className='ep-fold-body'; d.append(sum,body);parent.insertBefore(d,nodes[0]);nodes.forEach(n=>body.appendChild(n)); d.addEventListener('toggle',()=>localStorage.setItem('ep-board-fold-'+key,d.open?'1':'0')); return d; } if(cards[0])fold('+ Добавить группу',[cards[0]],view,'add-group',false); if(cards[1])fold('Список групп и нагрузка',[cards[1]],view,'group-list',false); const board=cards[2];if(board){ const title=board.querySelector(':scope > h3');if(title)title.textContent='Электрощит'; const kids=Array.from(board.children); const layout=board.querySelector(':scope > #boardLayout'); const deviceList=board.querySelector(':scope > #boardDeviceList'); const intro=kids.filter(e=>e.tagName==='P'&&e!==null)[0];if(intro)intro.remove(); const top=kids.filter(e=>e.classList.contains('formgrid')||e.classList.contains('actions')); fold('Добавить аппарат / настройки ряда',top,board,'add-device',false); const nested=Array.from(board.children).filter(e=>e.classList.contains('card')); const labels=['Корпус, шины и клеммы','Автокомпоновка','Спецификация','Каталог оборудования','Монтажная визуализация']; nested.forEach((e,i)=>{ let name=e.querySelector('h3,h4,b')?.textContent?.trim()||labels[i]||'Настройки'; name=name.replace(/^ElectroPro\s+[\d.]+\s*·\s*/,''); fold(name,[e],board,'panel-'+i,false); }); const assign=board.querySelector(':scope > .card'); // Additional assignment and audit blocks may follow the main cabinet. const bar=document.createElement('div');bar.className='ep-board-bar';bar.innerHTML='Вид щита и аппараты'; if(layout)board.insertBefore(bar,layout); const btn=bar.querySelector('button');btn.addEventListener('click',()=>{ const folds=Array.from(view.querySelectorAll('.ep-fold'));const expand=folds.some(d=>!d.open);folds.forEach(d=>{d.open=expand;localStorage.setItem('ep-board-fold-'+d.dataset.section,expand?'1':'0')});btn.textContent=expand?'Свернуть всё':'Развернуть всё'; }); // Compact the long explanatory paragraphs but retain access in an accordion. const help=Array.from(board.children).filter(e=>e.tagName==='P'&&e.classList.contains('tiny')); if(help.length)fold('Подсказки и ограничения',help,board,'help',false); // Group RCD assignment cards and remaining board editors in compact accordions. Array.from(board.children).filter(e=>e.classList.contains('card')).forEach((e,i)=>{ let name=e.querySelector('h3,h4,b')?.textContent?.trim()||'Дополнительные настройки'; fold(name,[e],board,'extra-'+i,false); }); } if(cards[3])fold('Падение напряжения и отчёт',[cards[3]],view,'voltage',false); if(cards[4])fold('Ведомость и экспорт',[cards[4]],view,'materials',false); } if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initCompactBoard);else initCompactBoard(); })();