PageSourceSearch

https://grupoaltavoltagem.com.br/dashboard/assets/modules/engineering-ui.js?v=c2ed51bdd15f82ff

js grupoaltavoltagem.com.br collected 2026-10-04 07:04:00 UTC 51,826 bytes, 247 lines download raw bytes

1'use strict';
2(() => {
3  const tr=(a,pt,en)=>a.state.lang==='pt'?pt:en;
4  const esc=(a,value)=>a.esc(value??'');
5  const canEdit=(a,data)=>data?.can_edit===true&&!a.readOnlySeason?.();
6  const managed=data=>Boolean(data.sync?.configured&&data.sync?.enabled);
7  let presentationLibrary;
8  function localURL(value){try{if(!value)return '';const base=new URL('.',document.baseURI),url=new URL(value,base);return url.origin===base.origin&&url.pathname.startsWith(base.pathname)&&!url.username&&!url.password?url.href:'';}catch{return '';}}
9  function onshapeLink(a,value,label){try{const url=new URL(value);if(url.protocol!=='https:'||url.username||url.password||!['cad.onshape.com','forum.onshape.com','www.onshape.com','onshape.com'].includes(url.hostname))return '';return '<a class="button" href="'+esc(a,url.href)+'" target="_blank" rel="noopener noreferrer">'+a.icon('external')+esc(a,label)+'</a>';}catch{return '';}}
10  function modelStats(a,model){const stats=model?.stats||{},points=model?.presentation?.points,format=value=>new Intl.NumberFormat(a.state.lang==='pt'?'pt-BR':'en').format(Number(value)),rows=[['vertices',tr(a,'Vértices','Vertices')],['triangles',tr(a,'Triângulos','Triangles')],['meshes',tr(a,'Malhas','Meshes')]].filter(([key])=>stats[key]!==null&&stats[key]!==undefined&&Number.isFinite(Number(stats[key]))).map(([key,label])=>[label,stats[key]]);if(points!==null&&points!==undefined&&Number.isFinite(Number(points)))rows.unshift([tr(a,'Pontos','Points'),points]);if(!rows.length)return '';return '<dl class="engineering-model-stats">'+rows.map(([label,value])=>'<div><dt>'+label+'</dt><dd>'+format(value)+'</dd></div>').join('')+'</dl>';}
11  function uploadButton(a,data){return canEdit(a,data)&&data.robot_id&&!managed(data)?'<button type="button" class="button primary" data-engineering-upload>'+a.icon('plus')+tr(a,'Enviar nova versão','Upload new version')+'</button>':'';}
12  function shareButton(a,data){return canEdit(a,data)&&data.robot_id&&data.model?.presentation?.state==='ready'?'<button type="button" class="button" data-robot-share>'+a.icon('share')+tr(a,'Compartilhar robô','Share robot')+'</button>':'';}
13  function renderConfiguration(a,data,robotOnly=false){
14    const model=data.model,presentation=localURL(model?.presentation?.url);const heading=robotOnly?(canEdit(a,data)?'<div class="engineering-catalog-actions"><button type="button" class="button" data-engineering-edit>'+a.icon('edit')+a.t('edit')+'</button>'+(!managed(data)?'<button type="button" class="button" data-engineering-upload>'+a.icon('plus')+tr(a,'Enviar nova versão','Upload new version')+'</button>':'')+'</div>':''):a.pageHeading('engineering','engineeringSubtitle',(window.AltaVoltagemResponsibilities?.slot(a,'engineering')||'')+(canEdit(a,data)?(data.robot_registry_available?'<button type="button" class="button primary" data-robot-new>'+a.icon('plus')+tr(a,'Novo robô','New robot')+'</button>':'')+(data.robot_id||!data.robot_registry_available?'<button type="button" class="button" data-engineering-edit>'+a.icon('edit')+a.t('edit')+'</button>'+(!managed(data)?'<button type="button" class="button" data-engineering-upload>'+a.icon('plus')+tr(a,'Enviar nova versão','Upload new version')+'</button>':''):''):''));return heading+'<section class="panel"><div class="panel-head engineering-panel-head"><h2>'+esc(a,data.title||tr(a,'Robô da temporada','Season robot'))+'</h2>'+modelStats(a,model)+shareButton(a,data)+'</div><div class="panel-body">'+(managed(data)?'<p class="notice">'+tr(a,'Execute a manutenção desta atualização para habilitar a publicação manual.','Run this update’s maintenance to enable manual publishing.')+'</p>':'')+versionCaption(a,data)+(data.description?'<p class="detail-copy">'+esc(a,data.description)+'</p>':'')+presentationControls(a,model,presentation)+'<div class="engineering-viewer engineering-points-surface" data-engineering-viewer aria-busy="'+String(!!presentation)+'">'+(presentation?'<div data-model-status role="status">'+a.loading()+'</div><canvas data-presentation-canvas aria-label="'+esc(a,data.title||tr(a,'Apresentação 3D do robô','3D robot presentation'))+'"></canvas><span class="engineering-pivot-cross" data-pivot-cross role="img" aria-label="'+tr(a,'Centro de giro','Rotation center')+'">+</span>':'<div data-presentation-unavailable role="status" class="engineering-presentation-message">'+(model?.presentation?.state==='pending'?tr(a,'A apresentação está em preparação.','The presentation is being prepared.'):model?.presentation?.state==='unavailable'?tr(a,'A apresentação não está disponível para este arquivo.','The presentation is unavailable for this file.'):tr(a,'A apresentação do robô ainda não está disponível.','The robot presentation is not available yet.'))+'</div>')+'</div>'+(presentation?'<div class="engineering-viewer-tools"><p class="muted detail-copy">'+tr(a,'Escolha uma vista para posicionar o centro. Libere o giro para arrastar; use a roda do mouse para aproximar.','Choose a view to position the center. Unlock rotation to drag; use the mouse wheel to zoom.')+'</p><button type="button" class="button small" data-presentation-fullscreen>'+tr(a,'Tela cheia','Full screen')+'</button></div>
14':'')+(presentation?cameraControls(a):'')+viewControls(a,data)+versionsHTML(a,data)+'<div class="inline-fields section-gap">'+onshapeLink(a,data.onshape_document_url,tr(a,'Abrir documento no Onshape','Open Onshape document'))+onshapeLink(a,data.onshape_community_url,tr(a,'Comunidade Onshape','Onshape community'))+'</div><p class="muted detail-copy section-gap">'+tr(a,'A equipe vê a última versão publicada. Alterações no Onshape aparecem aqui depois de um novo envio.','The team sees the latest version. Changes in Onshape appear here after a new upload.')+'</p></div></section>'+(robotOnly?'':robotCards(a,data)+'<div data-resource-links="engineering"></div>');
15  }
16  function presentationControls(a,model,url){
17    if(!model||url||model.presentation?.state!=='pending'&&model.presentation?.state!=='unavailable')return '';
18    if(model.presentation?.state==='pending')return '<p class="muted detail-copy">'+tr(a,'A apresentação está em preparação.','The presentation is being prepared.')+'</p>';
19    if(model.presentation?.state==='unavailable')return '<p class="muted detail-copy">'+tr(a,'A apresentação não está disponível para este arquivo.','The presentation is unavailable for this file.')+'</p>';
20    return '';
21  }
22  function cameraControls(a){return '<div class="engineering-camera-controls"><div class="engineering-camera-presets" role="group" aria-label="'+tr(a,'Vistas do robô','Robot views')+'">'+[['front','Frente','Front'],['top','Topo','Top'],['left','Esquerda','Left'],['right','Direita','Right'],['back','Trás','Back'],['bottom','Base','Bottom']].map(([view,pt,en])=>'<button type="button" class="button small" data-camera-view="'+view+'" aria-pressed="'+String(view==='front')+'">'+tr(a,pt,en)+'</button>').join('')+'</div><button type="button" class="button small" data-camera-unlock aria-pressed="false">'+tr(a,'Liberar giro','Unlock rotation')+'</button></div>';}
23  function pivotControls(a,settings){
24    const cornerChoices=[['50,50,50',tr(a,'Centro do robô','Robot center')],...Array.from({length:8},(_,i)=>[[i&1?100:0,i&2?100:0,i&4?100:0].join(','),tr(a,'Canto ','Corner ')+(i+1)])];
25    return '<fieldset class="engineering-pivot"><legend>'+tr(a,'Centro de giro','Rotation center')+'</legend><p class="muted">'+tr(a,'Escolha o centro ou um canto e ajuste os três eixos. A cruz amarela marca o ponto de giro na prévia.','Choose the center or a corner and adjust the three axes. The yellow cross marks the pivot in the preview.')+'</p><label>'+tr(a,'Ponto inicial','Starting point')+'<select data-pivot-preset><option value="">'+tr(a,'Personalizado','Custom')+'</option>'+cornerChoices.map(([value,label])=>'<option value="'+value+'">'+label+'</option>').join('')+'</select></label><div class="engineering-pivot-axes">'+['x','y','z'].map(key=>'<label>'+key.toUpperCase()+' (%)<input type="number" min="0" max="100" step="1" data-pivot-axis="'+key+'" value="'+Number(settings.pivot?.[key]??50)+'"></label>').join('')+'</div></fieldset>';
26  }
27  function xrayControls(a,settings){
28    return '<fieldset class="engineering-xray"><legend>'+tr(a,'Raio-X através das paredes','X-ray through walls')+'</legend><p class="muted">'+tr(a,'Use o mesmo grupo de raio-X nas peças de uma roda mecanum e grupos diferentes para cada roda. As peças do mesmo grupo não escondem umas às outras. Escolha quais componentes aparecem atrás das paredes. Clique no robô ou use Localizar para destacar um grupo em amarelo; a numeração corresponde aos grupos do arquivo enviado.','Choose components visible behind walls. Click the robot or use Locate to highlight a group in yellow; numbers identify groups in the uploaded file.')+'</p><label>'+tr(a,'Visibilidade interna','Internal visibility')+'<select data-xray-mode>'+[['none','Ocultar tudo atrás das paredes','Hide everything behind walls'],['manual','Escolher componentes','Choose components'],['auto','Automático','Automatic']].map(([id,pt,en])=>'<option value="'+id+'" '+((settings.xray_mode||'auto')===id?'selected':'')+'>'+tr(a,pt,en)+'</option>').join('')+'</select></label><button type="button" class="button small" data-xray-clear>'+tr(a,'Limpar destaque','Clear highlight')+'</button><div data-xray-components style="max-height:250px;overflow:auto;margin-top:12px">'+tr(a,'Carregando componentes…','Loading components…')+'</div></fieldset>';
29  }
30  function viewControls(a,data){
31    const settings=data.view_settings||{},editor=canEdit(a,data)&&data.robot_id;
32    return editor?'<details class="engineering-orientation" open><summary>'+tr(a,'Orientação e centro de giro','Orientation and rotation center')+'</summary><p class="muted">'+tr(a,'Ajuste a orientação e o ponto de giro; a prévia de partículas será atualizada antes de salvar.','Adjust orientation and rotation point; the particle preview updates before saving.')+'</p><div class="inline-fields"><label>'+tr(a,'Eixo de cima','Up axis')+'<select class="filter-input" data-model-top>'+['+Y','-Y','+Z','-Z','+X','-X'].map(axis=>'<option value="'+axis+'" '+((settings.up_axis||'+Y')===axis?'selected':'')+'>'+axis+'</option>').join('')+'</select></label><label>'+tr(a,'Direção da frente (graus)','Front direction (degrees)')+'<input class="filter-input" data-model-heading type="number" min="0" max="359" step="1" value="'+(Number(settings.heading)||0)+'"></label></div>'+pivotControls(a,settings)+xrayControls(a,settings)+'<label class="engineering-initial-zoom">'+tr(a,'Zoom inicial','Initial zoom')+' <output data-model-zoom-label>'+Number(settings.zoom??100)+'%</output><input type="range" min="50" max="160" step="1" data-model-zoom value="'+Number(settings.zoom??100)+'"></label><div class="section-gap"><button type="button" class="button small" data-model-snapshot>Usar visão atual como thumbnail</button><div data-model-snapshot-preview></div></div><button type="button" class="button small primary" data-model-view-save>'+tr(a,'Salvar visualização','Save view')+'</button></details>':'';
33  }
34  function robotCards(a,data){
35    if(!data.robot_registry_available)return '';
36    return '<section class="section-gap"><h2>'+tr(a,'Robôs da equipe','Team robots')+'</h2><div class="resource-grid">'+(data.robots||[]).map(robot=>{const preview=robot.view_settings?.snapshot, image=preview?.model===robot.model?.sha256&&/^data:image\/webp;base64,[A-Za-z0-9+/=]+$/.test(preview?.data||'')?preview.data:'';return '<article class="resource-card engineering-robot-card '+(robot.id===data.robot_id?'is-selected':'')+'"><button type="button" class="engineering-robot-preview" data-robot-select="'+robot.id+'" aria-label="'+esc(a,robot.title)+'"><span class="engineering-robot-name">'+esc(a,robot.title)+'</span>'+(image?'<img class="engineering-robot-snapshot" src="'+esc(a,image)+'" alt="'+esc(a,robot.title)+'" loading="lazy">':'<span class="engineering-robot-preview-empty">'+tr(a,'Prévia indisponível','Preview unavailable')+'</span>')+'</button>'+(canEdit(a,data)&&!managed(data)?'<button type="button" class="button small" data-robot-upload="'+robot.id+'">'+tr(a,'Enviar nova versão','Upload new version')+'</button>':'')+(robot.is_primary?'<span class="library-featured-badge">★ '+tr(a,'Robô atual','Current robot')+'</span>':'')+'</article>';}).join('')+'</div></section>';
37  }
38  function openRobotActions(a,data,robot){
39    const editable=canEdit(a,data),season=a.state.seasonId,context=a.readContextKey?.(),current=()=>a.state.seasonId===season&&(context===undefined||context===a.readContextKey?.()),selected={...data,...robot,robot_id:robot.id,robots:data.robots},initial=JSON.stringify(robot.view_settings||{});
40    const footer='<button type="button" class="button" data-dialog-close>
40'+tr(a,'Voltar à tela','Back to screen')+'</button>'+(editable&&!robot.is_primary?'<button type="button" class="button primary" data-robot-action-primary>'+tr(a,'Tornar robô atual','Make current robot')+'</button>':'');
41    const dialog=a.openDialog(robot.title,'<div data-engineering-configuration></div>',footer);dialog.classList.add('engineering-configuration-dialog');dialog.dataset.removalType='engineering_robot';dialog.dataset.removalId=String(robot.id);
42    const host=dialog.querySelector('[data-engineering-configuration]');host.innerHTML=renderConfiguration(a,selected,true);bindConfiguration(a,host,selected);window.AltaVoltagemContributions?.mount(a,host,{page:'engineering',robotId:robot.id});
43    dialog.addEventListener('close',()=>{if(current()&&JSON.stringify(robot.view_settings||{})!==initial)a.loadPage();},{once:true});
44    dialog.querySelector('[data-robot-action-primary]')?.addEventListener('click',async event=>{if(!editable||!canEdit(a,data)||!current())return;const button=event.currentTarget;button.disabled=true;try{await a.api('engineering.robot.primary',{season_id:season,id:robot.id,version:robot.version},'POST');if(a.state.seasonId!==season||context!==undefined&&context!==a.readContextKey?.())return;dialog.close();await a.loadPage();}catch(error){a.toast(error.message,true);}finally{if(button.isConnected)button.disabled=false;}});
45  }
46  function exportGuide(a){
47    return '<details class="section-gap"><summary>'+tr(a,'Como exportar o modelo no Onshape','How to export the model from Onshape')+'</summary><ol><li>'+tr(a,'Clique com o botão direito na aba da montagem completa (Assembly) e escolha Export.','Right-click the complete Assembly tab and choose Export.')+'</li><li>'+tr(a,'Format: GLB (.glb).','Format: GLB (.glb).')+'</li><li>'+tr(a,'Resolution: Coarse. Deixe Compress desmarcado.','Resolution: Coarse. Leave Compress unchecked.')+'</li><li>'+tr(a,'Exporte em um único arquivo; desmarque Export unique parts as individual files, se aparecer.','Export as one file; uncheck Export unique parts as individual files, if shown.')+'</li><li>'+tr(a,'Options: Download. Envie o arquivo .glb abaixo (até 1 GiB).','Options: Download. Upload the .glb file below (up to 1 GiB).')+'</li></ol><p class="muted">'+tr(a,'Se ultrapassar o tamanho ou os detalhes aceitos, reduza a complexidade da montagem antes de exportar. O arquivo é validado antes de substituir a versão atual.','If size or detail limits are exceeded, reduce assembly complexity before exporting. The file is validated before replacing the current version.')+'</p><a href="https://cad.onshape.com/help/Content/File/exporting_files.htm" target="_blank" rel="noopener noreferrer">'+tr(a,'Instruções do Onshape','Onshape instructions')+'</a></details>';
48  }
49  function selectedVersion(data){return (data.model_versions||[]).find(item=>data.selected_model_version?item.id===data.selected_model_version:item.current);}
50  function versionCaption(a,data){const item=selectedVersion(data);return item?'<div class="section-gap"><h3>'+esc(a,item.label||item.model.filename)+'</h3><p class="muted">'+esc(a,item.author)+' · '+esc(a,item.created_at)+' UTC'+(item.current?' · '+tr(a,'Versão atual','Current version'):'')+'</p>'+(item.notes?'<p class="detail-copy">'+esc(a,item.notes)+'</p>':'')+'</div>':'';}
51  function versionsHTML(a,data){
52    const items=data.model_versions||[];if(!items.length)return '';
53    const selected=selectedVersion(data);
54    return '<section class="section-gap"><h3>'+tr(a,'Versões publicadas','Published versions')+'</h3><ul class="engineering-versions">'+items.map(item=>'<li><div><strong>'+esc(a,item.label||item.model.filename)+'</strong><p class="muted">'+esc(a,item.author)+' · '+esc(a,item.created_at)+' UTC · '+new Intl.NumberFormat(a.state.lang==='pt'?'pt-BR':'en',{maximumFractionDigits:1}).format(item.model.size_bytes/1048576)+' MiB'+(item.current?' · '+tr(a,'Atual','Current'):'')+'</p>'+(item.notes?'<p class="detail-copy">'+esc(a,item.notes)+'</p>':'')+'</div><button type="button" class="button small" data-engineering-version="'+item.id+'" '+(selected?.id===item.id?'disabled':'')+'>'+tr(a,selected?.id===item.id?'Em visualização':'Visualizar',selected?.id===item.id?'Viewing':'View')+'</button></li>').join('')+'</ul></section>';
55  }
56  async function bindConfiguration(a,container,data){
57    const viewer=container.querySelector('[data-engineering-viewer]'),body=viewer?.parentElement;
58    if(container.closest('.engineering-configuration-dialog')&&body&&!body.classList.contains('engineering-editor-split')){
59      const preview=document.createElement('div'),controls=document.createElement('div');preview.className='engineering-editor-preview';
59controls.className='engineering-editor-controls';controls.tabIndex=0;controls.setAttribute('role','region');controls.setAttribute('aria-label',tr(a,'Configurações do robô','Robot settings'));
60      for(const child of [...body.children]){if(child===viewer||child.matches('.engineering-viewer-tools,.engineering-camera-controls'))preview.append(child);else controls.append(child);}
61      body.classList.add('engineering-editor-split');body.append(preview,controls);
62    }
63    container.querySelector('[data-robot-share]')?.addEventListener('click',()=>window.AltaVoltagemRobotSharing?.open(a,data));
64    window.AltaVoltagemResourceLinks?.mount(a,container.querySelector('[data-resource-links="engineering"]'),'engineering');
65
66    container.querySelector('[data-robot-new]')?.addEventListener('click',()=>editRobot(a,data));
67    container.querySelectorAll('[data-robot-upload]').forEach(button=>button.onclick=()=>{const robot=data.robots.find(robot=>robot.id===Number(button.dataset.robotUpload));if(robot)upload(a,{...data,...robot,robot_id:robot.id});});
68    container.querySelectorAll('[data-robot-select]').forEach(button=>button.onclick=()=>{const robot=data.robots.find(robot=>robot.id===Number(button.dataset.robotSelect));if(robot)openRobotActions(a,data,robot);});
69
70    container.querySelectorAll('[data-engineering-version]').forEach(button=>button.addEventListener('click',()=>{
71      const item=(data.model_versions||[]).find(item=>item.id===Number(button.dataset.engineeringVersion));if(!item)return;
72      const selected={...data,model:item.model,selected_model_version:item.id};if(selected.view_settings?.xray_model!==item.model.sha256)selected.view_settings={...selected.view_settings,component_tooltips:[],tooltip_groups:[],xray_groups:[]};if(selected.view_settings?.xray_mode==='manual'&&selected.view_settings.xray_model!==item.model.sha256)selected.view_settings={...selected.view_settings,xray_mode:'none',xray_parts:[]};container.innerHTML=renderConfiguration(a,selected,!!container.closest('.engineering-configuration-dialog'));bindConfiguration(a,container,selected);
73    }));
74    container.querySelector('[data-engineering-edit]')?.addEventListener('click',()=>data.robot_id?editRobot(a,data,data.robots.find(robot=>robot.id===data.robot_id)):edit(a,data));
75    container.querySelector('[data-engineering-upload]')?.addEventListener('click',()=>upload(a,data));
76    const surface=container.querySelector('[data-engineering-viewer]'),status=container.querySelector('[data-model-status]'),canvas=container.querySelector('[data-presentation-canvas]');if(!surface)return;
77    const url=localURL(data.model?.presentation?.url);let presentation=null,generation=0,rotationUnlocked=false,cameraView='front';
78    const zoom=container.querySelector('[data-model-zoom]'),top=container.querySelector('[data-model-top]'),heading=container.querySelector('[data-model-heading]'),save=container.querySelector('[data-model-view-save]'),pivotFields=[...container.querySelectorAll('[data-pivot-axis]')],preset=container.querySelector('[data-pivot-preset]');
79    const xrayMode=container.querySelector('[data-xray-mode]'),xrayList=container.querySelector('[data-xray-components]'),xrayParts=new Set(data.view_settings?.xray_parts||[]);
80    function locatePickedComponent(id){
81      if(selectTooltipParts)toggleTooltipPart(id);
82      const input=xrayList?.querySelector('[data-xray-part="'+id+'"]');if(!input)return;
83      const details=input.closest('details');if(details)details.open=true;
84      xrayList.querySelectorAll('.is-located').forEach(row=>row.classList.remove('is-located'));input.parentElement.parentElement.classList.add('is-located');
85      input.scrollIntoView({block:'nearest',inline:'nearest'});input.focus({preventScroll:true});
86    }
87    const xrayGroups=new Map((data.view_settings?.xray_groups||[]).map(item=>[item.part,item.group]));
88    const tooltipGroups=(data.view_settings?.tooltip_groups||[]).map(item=>({...item,parts:[...item.parts]}));
89    if(!data.view_settings?.tooltip_groups){for(const item of data.view_settings?.component_tooltips||[]){let group=tooltipGroups.find(g=>g.title===item.title);
89if(!group){group={title:item.title,description:item.description||'',parts:[]};tooltipGroups.push(group);}group.parts.push(item.part);}}
90    let activeTooltip=tooltipGroups.length?0:-1,selectTooltipParts=false;
91    const applyTooltips=()=>presentation?.setTooltips(tooltipGroups.flatMap(group=>group.parts.map(part=>({part,title:group.title,description:group.description}))));
92    const tooltipEditor=document.createElement('fieldset');tooltipEditor.className='engineering-tooltip-editor';
93    tooltipEditor.innerHTML='<legend>Tooltips do robô</legend><p class="muted">Crie um tooltip e selecione os componentes. Com o giro travado, clique e arraste para vincular várias peças visíveis de uma vez.</p><div class="inline-fields"><select data-tooltip-select aria-label="Tooltip"></select><button type="button" class="button small" data-tooltip-add>Adicionar tooltip</button><button type="button" class="button small" data-tooltip-remove>Remover</button></div><div data-tooltip-fields><label>Título<input data-tooltip-title maxlength="120"></label><label>Descrição<textarea data-tooltip-description maxlength="500" rows="3"></textarea></label><button type="button" class="button small" data-tooltip-pick aria-pressed="false">Selecionar componentes</button><p data-tooltip-count class="muted"></p></div>';
94    xrayList?.closest('fieldset')?.before(tooltipEditor);
95    const tooltipSelect=tooltipEditor.querySelector('[data-tooltip-select]'),tooltipTitle=tooltipEditor.querySelector('[data-tooltip-title]'),tooltipDescription=tooltipEditor.querySelector('[data-tooltip-description]'),tooltipPick=tooltipEditor.querySelector('[data-tooltip-pick]');
96    function syncTooltipEditor(){tooltipSelect.replaceChildren(...tooltipGroups.map((group,i)=>{const option=document.createElement('option');option.value=i;option.textContent=group.title||'Sem título';return option;}));tooltipSelect.value=String(activeTooltip);const group=tooltipGroups[activeTooltip];tooltipEditor.querySelector('[data-tooltip-fields]').hidden=!group;tooltipEditor.querySelector('[data-tooltip-remove]').disabled=!group;tooltipTitle.value=group?.title||'';tooltipDescription.value=group?.description||'';tooltipPick.setAttribute('aria-pressed',String(selectTooltipParts));tooltipPick.textContent=selectTooltipParts?'Concluir seleção':'Selecionar componentes';tooltipEditor.querySelector('[data-tooltip-count]').textContent=(group?.parts.length||0)+' componentes vinculados';xrayList?.querySelectorAll('[data-tooltip-part]').forEach(input=>{input.disabled=!group;input.checked=!!group?.parts.includes(Number(input.dataset.tooltipPart));});}
97    function toggleTooltipPart(id,checked){const group=tooltipGroups[activeTooltip];if(!group)return;const parts=new Set(group.parts);if(checked??!parts.has(id))parts.add(id);else parts.delete(id);group.parts=[...parts];applyTooltips();syncTooltipEditor();}
98    tooltipSelect.onchange=()=>{activeTooltip=Number(tooltipSelect.value);syncTooltipEditor();};
99    tooltipEditor.querySelector('[data-tooltip-add]').onclick=()=>{if(tooltipGroups.length>=50){a.toast('Use até 50 tooltips por modelo.');return;}tooltipGroups.push({title:'Tooltip '+(tooltipGroups.length+1),description:'',parts:[]});activeTooltip=tooltipGroups.length-1;syncTooltipEditor();tooltipTitle.focus();};
100    tooltipEditor.querySelector('[data-tooltip-remove]').onclick=()=>{if(activeTooltip<0)return;tooltipGroups.splice(activeTooltip,1);activeTooltip=tooltipGroups.length?Math.min(activeTooltip,tooltipGroups.length-1):-1;selectTooltipParts=false;applyTooltips();syncTooltipEditor();};
101    tooltipTitle.oninput=()=>{const group=tooltipGroups[activeTooltip];if(group){group.title=tooltipTitle.value;tooltipSelect.options[activeTooltip].textContent=group.title||'Sem título';applyTooltips();}};
102    tooltipDescription.oninput=()=>{const group=tooltipGroups[activeTooltip];if(group){group.description=tooltipDescription.value;applyTooltips();}};
103    tooltipPick.onclick=()=>{selectTooltipParts=!selectTooltipParts;presentation?.setSelectionEnabled(selectTooltipParts);syncTooltipEditor();};syncTooltipEditor();
104    const applyXray=()=>presentation?.setXray(xrayMode?.value||data.view_settings?.xray_mode||'auto',[...xrayParts]);
105    function paintComponents(){
106      if(!xrayList)return;
107      const components=presentation?.components||[];
108      xrayList.innerHTML=components.length?components.map(({id})=>'<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;padding:7px 0"><label style="flex:1"><input type="checkbox" data-xray-part="'+id+'" '+(xrayParts.has(id)?'checked':'')+'> '+tr(a,'Componente ','Component ')+(id+1)+'</label><button type="button" class="button small" data-xray-locate="'+id+'">'+tr(a,'Localizar','Locate')+'</button><div class="engineering-tooltip-fields"><input data-xray-group="'+id+'" maxlength="60" aria-label="Grupo de raio-X" placeholder="Grupo de raio-X (ex.: roda dianteira esquerda)" value="'+esc(a,xrayGroups.get(id)||'')+'"><label><input type="checkbox" data-tooltip-part="'+id+'"> Vincular ao tooltip selecionado</label></div></div>').join(''):tr(a,'Nenhum componente disponível nesta prévia.','No components available in this preview.');
109      xrayList.querySelectorAll('[data-xray-group]').forEach(input=>input.addEventListener('change',()=>{const id=Number(input.dataset.xrayGroup),group=input.value.trim();if(group)xrayGroups.set(id,group);else xrayGroups.delete(id);presentation?.setXrayGroups([...xrayGroups].map(([part,group])=>({part,group})));}));
110      xrayList.querySelectorAll('[data-tooltip-part]').forEach(input=>input.onchange=()=>toggleTooltipPart(Number(input.dataset.tooltipPart),input.checked));syncTooltipEditor();
111      xrayList.querySelectorAll('[data-xray-part]').forEach(input=>input.onchange=()=>{const id=Number(input.dataset.xrayPart);input.checked?xrayParts.add(id):xrayParts.delete(id);xrayMode.value='manual';applyXray();});
112      xrayList.querySelectorAll('[data-xray-locate]').forEach(button=>button.onclick=()=>presentation?.highlightComponent(Number(button.dataset.xrayLocate)));
113    }
114    xrayMode?.addEventListener('change',applyXray);
115    container.querySelector('[data-xray-clear]')?.addEventListener('click',()=>presentation?.highlightComponent(null));
116    const preferences=()=>({xray_groups:[...xrayGroups].map(([part,group])=>({part,group})),component_tooltips:[],tooltip_groups:tooltipGroups.map(group=>({...group,title:group.title.trim()})),xray_mode:xrayMode?.value||data.view_settings?.xray_mode||'auto',xray_parts:[...xrayParts],xray_model:data.model?.sha256||'',up_axis:top?.value||data.view_settings?.up_axis||'+Y',heading:Number(heading?.value??data.view_settings?.heading??0),edges:!!data.view_settings?.edges,zoom:Number(zoom?.value??data.view_settings?.zoom??100),pivot:Object.fromEntries(pivotFields.map(input=>[input.dataset.pivotAxis,Number(input.value)]))});
117    const valid=()=>!heading||heading.reportValidity()&&pivotFields.every(input=>input.value!==''&&input.reportValidity());
118    let pendingSnapshot=data.view_settings?.snapshot?.model===data.model?.sha256?data.view_settings.snapshot:null;
119    const snapshotButton=container.querySelector('[data-model-snapsh
119ot]'),snapshotPreview=container.querySelector('[data-model-snapshot-preview]');
120    const showSnapshot=()=>{if(!snapshotPreview)return;snapshotPreview.replaceChildren();if(pendingSnapshot){const img=document.createElement('img');img.src=pendingSnapshot.data;img.alt=tr(a,'Thumbnail da visão escolhida','Thumbnail of selected view');img.className='engineering-robot-snapshot';snapshotPreview.append(img);}};showSnapshot();
121    if(snapshotButton)snapshotButton.onclick=()=>{const image=presentation?.snapshot();if(!image){a.toast(tr(a,'Aguarde o robô terminar de carregar.','Wait for the robot to finish loading.'));return;}pendingSnapshot={model:data.model.sha256,data:image};showSnapshot();a.toast(tr(a,'Thumbnail capturada. Clique em Salvar visualização para aplicar.','Thumbnail captured. Click Save view to apply.'));};
122    const season=a.state.seasonId,context=a.readContextKey?.();
123    async function remount(){const request=++generation;if(!canvas?.isConnected||!url||!valid())return;presentation?.dispose();presentation=null;status.hidden=false;surface.setAttribute('aria-busy','true');try{presentationLibrary??=import(new URL('assets/modules/engineering-presentation.js?v=c9779fa57a269fa4',document.baseURI).href).catch(error=>{presentationLibrary=null;throw error;});const visual=await presentationLibrary;if(request!==generation||!canvas.isConnected||a.state.seasonId!==season||context!==undefined&&context!==a.readContextKey?.())return;const mounted=await visual.mount(canvas,url,preferences(),{autoRotate:false,rotationEnabled:rotationUnlocked,assembled:true,onComponentPick:locatePickedComponent,onComponentsPick:ids=>{if(selectTooltipParts){const group=tooltipGroups[activeTooltip];if(group){group.parts=[...new Set([...group.parts,...ids])];applyTooltips();syncTooltipEditor();}}}});if(request!==generation||!canvas.isConnected){mounted.dispose();return;}mounted.setPivot(preferences().pivot);mounted.setView(cameraView);presentation=mounted;mounted.setSelectionEnabled(selectTooltipParts);paintComponents();status.hidden=true;surface.setAttribute('aria-busy','false');}catch{if(request!==generation)return;status.hidden=false;status.className='model-error';status.textContent=tr(a,'Não foi possível carregar a apresentação de partículas.','The particle presentation could not be loaded.');surface.setAttribute('aria-busy','false');}}
124    const cameraButtons=[...container.querySelectorAll('[data-camera-view]')],unlock=container.querySelector('[data-camera-unlock]');
125    cameraButtons.forEach(button=>button.addEventListener('click',()=>{cameraView=button.dataset.cameraView;presentation?.setView(cameraView);cameraButtons.forEach(item=>item.setAttribute('aria-pressed',String(item===button)));}));
126    unlock?.addEventListener('click',()=>{rotationUnlocked=!rotationUnlocked;presentation?.setRotationEnabled(rotationUnlocked);unlock.setAttribute('aria-pressed',String(rotationUnlocked));unlock.textContent=tr(a,rotationUnlocked?'Travar giro':'Liberar giro',rotationUnlocked?'Lock rotation':'Unlock rotation');});
127    if(canvas&&url)remount();
128    const pivot=()=>{if(!valid())return;preset&&(preset.value='');presentation?.setPivot(preferences().pivot);};
129    pivotFields.forEach(input=>input.addEventListener('input',pivot));preset?.addEventListener('change',()=>{if(!preset.value)return;pivotFields.forEach((input,i)=>input.value=preset.value.split(',')[i]);pivot();});
130    zoom?.addEventListener('input',()=>{container.querySelector('[data-model-zoom-label]').textContent=zoom.value+'%';presentation?.setZoom(Number(zoom.value));});
131    top?.addEventListener('change',remount);heading?.addEventListener('change',remount);
132    const full=container.querySelector('[data-presentation-fullscreen]');if(full){full.hidden=typeof surface.requestFullscreen!=='function';full.onclick=async()=>{if(!document.fullscreenElement&&canvas?.dataset.presentationReady!=='true')return;try{if(document.fullscreenElement)await document.exitFullscreen();else await surface.requestFullscreen();}catch{a.toast(tr(a,'O navegador não permitiu abrir a tela cheia.','The browser did not allow full screen.'));}};}
133    if(save){let revision=data.version;save.onclick=async()=>
133{if(!valid()||url&&!canvas?.isConnected||a.state.seasonId!==season||context!==undefined&&context!==a.readContextKey?.())return;save.disabled=true;try{presentation?.highlightComponent(null);const settings=preferences();if(settings.tooltip_groups.some(group=>!group.title))throw Error('Preencha o título de todos os tooltips.');if(pendingSnapshot)settings.snapshot=pendingSnapshot;const updated=await a.api('engineering.robot.save',{season_id:season,id:data.robot_id,version:revision,view_settings:settings},'POST');if(a.state.seasonId!==season||context!==undefined&&context!==a.readContextKey?.()||url&&!canvas?.isConnected)return;revision=updated.version;data.version=revision;data.view_settings=settings;const robot=data.robots?.find(r=>r.id===data.robot_id);if(robot){robot.version=revision;robot.view_settings=settings;}a.toast(tr(a,'Visualização salva para a equipe.','View saved for the team.'));}catch(error){a.toast(error.message,true);}finally{save.disabled=false;}};}
134    const dialog=container.closest('.engineering-configuration-dialog'),foot=dialog?.querySelector('.dialog-foot'),actions=container.querySelector(':scope > .engineering-catalog-actions');
135    if(foot&&actions){foot.querySelector('.engineering-footer-actions')?.remove();actions.classList.add('engineering-footer-actions');foot.prepend(actions);}
136  }
137  function edit(a,data){if(!canEdit(a,data))return;a.openDialog(tr(a,'Engenharia · configuração','Engineering · settings'),a.field('title',a.t('title'),data.title||'','text','required maxlength="180"')+a.textArea('description',a.t('description'),data.description||'','maxlength="10000"')+a.field('onshape_document_url',tr(a,'Link do projeto no Onshape','Onshape project link'),data.onshape_document_url||'','url','maxlength="2000" placeholder="https://cad.onshape.com/documents/…" '+(managed(data)?'readonly':''))+(managed(data)?'<p class="muted detail-copy">'+tr(a,'Altere o documento em Configurar conexão. Para enviar um GLB manual, desconecte primeiro a atualização automática.','Change the document in Configure connection. Disconnect automatic updates before uploading a manual GLB.')+'</p>':'')+a.field('onshape_community_url',tr(a,'Link da comunidade Onshape','Onshape community link'),data.onshape_community_url||'','url','maxlength="2000"'),a.editorFooter(),async(form,dialog)=>{await a.api('engineering.save',{season_id:a.state.seasonId,version:data.version,...a.formObject(form)},'POST');await a.saved(dialog);});}
138  async function sendModel(a,body,dialog){
139    dialog.querySelector('[data-model-upload-progress]')?.remove();
140    const box=document.createElement('div');box.className='engineering-upload-progress';box.dataset.modelUploadProgress='';box.setAttribute('aria-busy','true');
141    box.innerHTML='<div><span data-upload-stage role="status" aria-live="polite"></span><strong data-upload-percent>0%</strong></div><progress max="100" value="0" aria-label="'+tr(a,'Envio do GLB','GLB upload')+'"></progress><div data-model-processing hidden><span data-processing-label></span> <strong data-processing-percent></strong><progress max="100" aria-label="'+tr(a,'Processamento do GLB','GLB processing')+'"></progress></div>';
142    const input=dialog.querySelector('input[type=file]');(input?.closest('.field')||dialog.querySelector('form')||dialog).append(box);
143    const stage=box.querySelector('[data-upload-stage]'),percent=box.querySelector('[data-upload-percent]'),bar=box.querySelector('progress');
144    // Keep the server job across a failed connection or a retry of this editor.
145    if(!dialog._modelUpload||dialog._modelUpload.terminal)dialog._modelUpload={nonce:Array.from(crypto.getRandomValues(new Uint8Array(32)),v=>v.toString(16).padStart(2,'0')).join('')};
146    const saved=dialog._modelUpload,background=dialog._backgroundUpload,controller=background?.controller||new AbortController(),signal=controller.signal,csrf=a.state.csrf;
147    const phase=message=>{stage.textContent=message;if(background){background.message=message;background.card.querySelector('[data-phase]').textContent=message;}};
148    const processing=status=>{const kind=status.phase||'queued',value=Number(status.progress?.percent??status.val
148idation_progress),details={label:kind==='validation'?tr(a,'Validando arquivo…','Validating file…'):kind==='presentation'?tr(a,'Preparando visualização…','Preparing visualization…'):tr(a,'Aguardando processamento…','Waiting for processing…'),percent:kind==='validation'&&Number.isFinite(value)?Math.max(0,Math.min(100,Math.floor(value))):null};saved.processing=status;const area=box.querySelector('[data-model-processing]');area.hidden=false;area.querySelector('[data-processing-label]').textContent=details.label;area.querySelector('[data-processing-percent]').textContent=details.percent===null?'':details.percent+'%';const meter=area.querySelector('progress');if(details.percent===null)meter.removeAttribute('value');else meter.value=details.percent;window.AltaVoltagemBackgroundUploads?.processing(dialog,details);};
149    const transferred=()=>{saved.transferPercent=100;bar.value=100;percent.textContent='100%';phase(tr(a,'Arquivo enviado.','File uploaded.'));processing({phase:'queued'});};
150    if(Number.isFinite(saved.transferPercent)){bar.value=saved.transferPercent;percent.textContent=saved.transferPercent+'%';if(background&&saved.transferPercent===100)background.files.forEach(file=>background.progress.set(file,file.size));}
151    const cancelled=()=>Object.assign(Error(tr(a,'Envio cancelado.','Upload cancelled.')),{name:'AbortError'});
152    const request=async(endpoint,options={})=>{const response=await fetch(new URL(endpoint,document.baseURI),{credentials:'same-origin',headers:{Accept:'application/json','X-CSRF-Token':csrf,...options.headers},...options});const payload=await response.json();if(!response.ok||payload?.ok!==true)throw Error(payload?.error?.message||a.t('invalidResponse'));return payload.data;};
153    let cancelRequest=null;
154    const cancel=()=>{if(!cancelRequest)cancelRequest=request('model-upload-cancel.php',{method:'POST',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-Token':csrf},body:JSON.stringify({job_token:saved.token,upload_nonce:saved.nonce,season_id:Number(body.get('season_id'))})}).then(status=>{if(status.state==='cancelled')saved.terminal=true;return status;}).catch(error=>{cancelRequest=null;throw error;});cancelRequest?.catch(()=>{});};
155    const close=()=>controller.abort();dialog.addEventListener('close',close,{once:true});signal.addEventListener('abort',cancel);
156    try{
157      if(saved.done)return saved.model;
158      if(!saved.token){
159        if(signal.aborted)throw cancelled();phase(tr(a,'Enviando arquivo…','Uploading file…'));body.set('upload_nonce',saved.nonce);
160        const payload=await new Promise((resolve,reject)=>{
161          const xhr=new XMLHttpRequest(),abort=()=>xhr.abort();let finished=false;
162          const complete=(error,value)=>{if(finished)return;finished=true;signal.removeEventListener('abort',abort);error?reject(error):resolve(value);};
163          xhr.open('POST',new URL('model-upload.php',document.baseURI));xhr.withCredentials=true;xhr.timeout=3600000;xhr.responseType='json';xhr.setRequestHeader('Accept','application/json');xhr.setRequestHeader('X-CSRF-Token',csrf);
164          xhr.upload.onprogress=event=>{if(event.lengthComputable){const value=Math.min(100,Math.floor(event.loaded/event.total*100));saved.transferPercent=value;bar.value=value;percent.textContent=value+'%';}else{bar.removeAttribute('value');percent.textContent='';}};
165          xhr.upload.onload=transferred;
166          xhr.onload=()=>{const result=xhr.response;if(xhr.status<200||xhr.status>=300||result?.ok!==true)complete(Error(result?.error?.message||a.t('invalidResponse')));else complete(null,result.data);};
167          xhr.onerror=xhr.ontimeout=()=>complete(Error(a.t('uncertain')));xhr.onabort=()=>complete(cancelled());signal.addEventListener('abort',abort,{once:true});
168          try{xhr.send(body);}catch(error){complete(error);}
169        });
170        if(payload?.state==='cancelled'){saved.terminal=true;throw cancelled();}
171        if(payload?.state==='failed'){saved.terminal=true;throw Error(payload.error?.message||a.t('invalidResponse'));}
172        if(!payload?.job_token)throw Error(a.t('invalidResponse'));
173        saved.token=payload.job_token;
174      }
175      phase(tr(a,'Arquivo enviado.','File uploaded.'));processing(saved.processing||{phase:'queued'});const deadline=Date.now()+3600000;let failures=0;
176      while(Date.now()<deadline){
177        if(signal.aborted){phase(tr(a,'Cancelando processamento…','Cancelling processing…'));cancel();if(cancelRequest)await cancelRequest;}
178        let status;
179        try{status=await request('model-upload-status.php?job_token='+encodeURIComponent(saved.token));failures=0;}
180        catch(error){if(++failures>=3)throw error;await new Promise(resolve=>setTimeout(resolve,2000));continue;}
181        if(status.state==='ready'){saved.done=true;saved.model=status.result??status.model;phase(tr(a,'Versão salva.','Version saved.'));box.querySelector('[data-model-processing]').hidden=true;
181window.AltaVoltagemBackgroundUploads?.processing(dialog,null);return saved.model;}
182        if(status.state==='cancelled'){saved.terminal=true;throw cancelled();}
183        if(status.state==='failed'){saved.terminal=true;throw Error(status.error?.message||a.t('invalidResponse'));}
184        if(!['pending','processing'].includes(status.state))throw Error(a.t('invalidResponse'));
185        if(!signal.aborted)processing(status);
186        await new Promise(resolve=>setTimeout(resolve,2000));
187      }
188      throw Error(tr(a,'O processamento continua. Use Tentar novamente para consultar este mesmo envio.','Processing is continuing. Use Retry to check this same upload.'));
189    }catch(error){if(signal.aborted){cancel();try{await cancelRequest;}catch{error=Error(tr(a,'Não foi possível confirmar o cancelamento. Tente novamente para consultar o envio.','Cancellation could not be confirmed. Retry to check the upload.'));}}phase(error.message);throw error;}
190    finally{dialog.removeEventListener('close',close);signal.removeEventListener('abort',cancel);box.setAttribute('aria-busy','false');}
191  }
192  function editRobot(a,data,robot=null){a=window.AltaVoltagemBackgroun
192dUploads?.isolate(a)||a;
193    if(!canEdit(a,data))return;
194    const season=a.state.seasonId,context=a.readContextKey?.(),current=()=>a.state.seasonId===season&&(context===undefined||context===a.readContextKey?.());let stored=robot,savedRecord=false;
195    const body=a.field('title',tr(a,'Nome do robô','Robot name'),robot?.title||'','text','required maxlength="180"')+a.field('onshape_document_url',tr(a,'Link do projeto no Onshape','Onshape project link'),robot?.onshape_document_url||'','url','maxlength="2000" placeholder="https://cad.onshape.com/documents/…"')+a.textArea('description',a.t('description'),robot?.description||'','maxlength="10000"')+(robot?'':a.field('file',tr(a,'Visual 3D — arquivo GLB (opcional)','3D visual — GLB file (optional)'),'','file','accept=".glb,model/gltf-binary"')+a.field('label',tr(a,'Nome da primeira versão','First version name'),'V1','text','maxlength="180"')+exportGuide(a));
196    a.openDialog(robot?tr(a,'Editar robô','Edit robot'):tr(a,'Novo robô','New robot'),body,a.editorFooter(),async(form,dialog)=>{
197      if(!current())throw Error(tr(a,'Reabra o cadastro na temporada correta.','Reopen the editor in the correct season.'));
198      const file=form.get('file');if(file?.size&&managed(data))throw Error(tr(a,'Desconecte a atualização automática antes de enviar um GLB manual.','Disconnect automatic updates before uploading a manual GLB.'));if(file?.size&&(file.size>1024*1024*1024||!file.name.toLowerCase().endsWith('.glb')))throw Error(tr(a,'Escolha um GLB de até 1 GiB.','Choose a GLB up to 1 GiB.'));
199      if(!savedRecord){stored=await a.api('engineering.robot.save',{season_id:season,id:stored?.id,version:stored?.version,title:form.get('title'),description:form.get('description'),onshape_document_url:form.get('onshape_document_url')},'POST');savedRecord=true;}
200      if(file?.size){if(!current()||!dialog.open)throw Error(tr(a,'O cadastro mudou. Reabra o robô.','The editor changed. Reopen the robot.'));const upload=new FormData();upload.append('file',file);upload.append('season_id',String(season));upload.append('robot_id',String(stored.id));upload.append('version',String(stored.version));upload.append('label',String(form.get('label')||'V1'));await sendModel(a,upload,dialog);}
201      await a.saved(dialog);
202    });
203  }
204  function upload(a,data){a=window.AltaVoltagemBackgroundUploads?.isolate(a)||a;
205    if(!canEdit(a,data))return;
206    if(managed(data)){a.toast(tr(a,'Desconecte a atualização automática antes de enviar um GLB manual.','Disconnect automatic updates before uploading a manual GLB.'));return;}
207    const season=Number(a.state.seasonId),context=a.readContextKey();
208    a.openDialog(tr(a,'Enviar nova versão','Upload new version')+' · '+data.title,'<p class="notice">'+tr(a,'Envie um GLB (.glb) de até 1 GiB, sem a opção Compress. A versão anterior será preservada.','Upload a GLB (.glb) up to 1 GiB, with Compress disabled. The previous version will be preserved.')+'</p>'+exportGuide(a)+a.field('label',tr(a,'Nome da versão','Version name'),'V'+(Math.max(0,...(data.model_versions||[]).map(item=>Number(item.version)||0))+1),'text','required maxlength="180" placeholder="V1 — Chassi inicial"')+a.textArea('notes',tr(a,'O que mudou?','What changed?'),'','maxlength="10000"')+a.field('file','GLB','','file','required accept=".glb,model/gltf-binary"'),a.editorFooter(),async(form,dialog)=>{
209      if(!a.readDialogCurrent(dialog,context))return;
210      const file=form.get('file');
211      if(!file?.size||file.size>1024*1024*1024||!file.name.toLowerCase().endsWith('.glb'))throw Error(tr(a,'Escolha um arquivo GLB de até 1 GiB.','Choose a GLB file up to 1 GiB.'));
212      const body=new FormData();body.append('file',file);body.append('season_id',String(season));body.append('version',String(data.version));body.append('label',String(form.get('label')||''));body.append('notes',String(form.get('notes')||''));
213      if(data.robot_id)body.append('robot_id',String(data.robot_id));await sendModel(a,body,dialog);
214      await a.saved(dialog);
215    });
216  }
217  function showcaseSeasonLogo(a){const season=(a.state.bootstrap?.seasons||[]).find(item=>String(item.id)===String(a.state.seasonId)),url=localURL(season?.logo_url)||'assets/brand/biobuzz-wordmark-white.png';return '<img class="engineering-season-logo" src="'+esc(a,url)+'" alt="'+esc(a,season?.name||'BIOBUZZ')+'">';}
218  function renderPage(a,data){
219    const model=data.model||null,presentation=localURL(model?.presentation?.url),name=data.title||tr(a,'Robô da temporada','Season robot');
220    const actions=uploadButton(a,data)+shareButton(a,data)+(window.AltaVoltagemResponsibilities?.slot(a,'engineering')||'')+(canEdit(a,data)?'<button type="button" class="button" data-engineering-config>'+a.icon('settings')+tr(a,'Configuração','Configuration')+'</button>':'')+onshapeLink(a,data.onshape_document_url,tr(a,'Abrir robô no Onshape','Open robot in Onshape'));
221    const visual=presentation?'<div data-engineering-showcase-stage class="engineering-showcase-stage" aria-busy="true"><canvas data-engineering-showcase-canvas aria-label="'+esc(a,name)+'"></canvas></div>':'<div class="engineering-showcase-empty" role="status"><p>'+tr(a,model?.presentation?.state==='pending'?'A apresentação está em preparação.':model?.presentation?.state==='unavailable'?'A apresentação não está disponível.':'A apresentação do robô ainda não está disponível.','The particle presentation is not available yet.')+'</p></div>';
222    return a.pageHeading('engineering','engineeringSubtitle',actions)+'<section class="engineering-showcase" aria-label="'+esc(a,name)+'">'+visual+'<div class="engineering-showcase-caption"><p class="eyebrow">FTC #25706</p><div class="engineering-showcase-heading"><h2>'+esc(a,name)+'</h2></div></div>'+showcaseSeasonLogo(a)+(presentation?'<button type="button" class="engineering-fullscreen-toggle" data-engineering-fullscreen aria-label="'+tr(a,'Tela cheia','Full screen')+'"></button>':'')+'</section>';
223  }
224  function openConfiguration(a,data){
225    if(!canEdit(a,data))return;
226    const dialog=a.openDialog(tr(a,'Robôs da equipe','Team robots'),'<div data-engineering-configuration></div>','<button type="button" class="button" data-dialog-close>
226'+tr(a,'Voltar à tela','Back to screen')+'</button>');
227    dialog.classList.add('engineering-configuration-dialog');const host=dialog.querySelector('[data-engineering-configuration]');
228    host.innerHTML=data.robot_registry_available?'<div class="engineering-catalog-actions"><button type="button" class="button primary" data-robot-new>'+a.icon('plus')+tr(a,'Novo robô','New robot')+'</button></div>'+robotCards(a,data)+'<div data-resource-links="engineering"></div>':renderConfiguration(a,data);
229    bindConfiguration(a,host,data);
230  }
231  async function bindPage(a,container,data){
232    container.querySelector('[data-engineering-upload]')?.addEventListener('click',()=>upload(a,data));
233    container.querySelector('[data-robot-share]')?.addEventListener('click',()=>window.AltaVoltagemRobotSharing?.open(a,data));
234    container.querySelector('[data-engineering-config]')?.addEventListener('click',()=>openConfiguration(a,data));
235    window.AltaVoltagemEngineeringFullscreen?.bind(a,container.querySelector('.engineering-showcase'),container.querySelector('[data-engineering-fullscreen]'));
236    const stage=container.querySelector('[data-engineering-showcase-stage]'),canvas=stage?.querySelector('[data-engineering-showcase-canvas]');if(!canvas)return;
237    try{
238      presentationLibrary??=import(new URL('assets/modules/engineering-presentation.js?v=c9779fa57a269fa4',document.baseURI).href).catch(error=>{presentationLibrary=null;throw error;});
239      const visual=await presentationLibrary;if(!canvas.isConnected)return;
240      await visual.mount(canvas,localURL(data.model?.presentation?.url),data.view_settings||{});if(stage.isConnected)stage.setAttribute('aria-busy','false');
241    }catch{
242      if(!stage?.isConnected)return;stage.setAttribute('aria-busy','false');stage.innerHTML='<p class="notice">'+tr(a,'Não foi possível carregar a apresentação de partículas.','The particle presentation could not be loaded.')+'</p>';
243    }
244  }
245  window.AltaVoltagemEngineering={renderPage,bindPage};
246})();
247

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.