import * as THREE from 'three';
import { OrbitControls } from './vendor/OrbitControls.js';
import { buildTubeMeshData } from './tube-mesh.mjs';
import { createARController } from './ar.mjs';
import { sampleRows, buildCenterline, validateRows, parseTable } from './geometry.mjs';
const $ = id => document.getElementById(id);
const clone = x => JSON.parse(JSON.stringify(x));
const format = (n, digits=2) => new Intl.NumberFormat('es-ES',{minimumFractionDigits:digits,maximumFractionDigits:digits}).format(Math.abs(n)<1e-8?0:n);
const num = n => String(Math.round(n * 1000) / 1000);
const pad = n => String(n).padStart(2,'0');
const icons = {
ar:'',
phone:'',
apple:'',
cube:'',
upload:'',
download:'',
sliders:'',
help:'',
info:'',
tube:'',
chevron:'',
plus:'', minus:'',
close:'',
fit:'',
grid:'',
mouse:'',
pointer:'',
spark:'',
reset:'',
undo:'',
redo:'',
flag:'',
trash:'',
arrow:''
};
function icon(name) {return ``;}
function hydrateIcons(root=document){root.querySelectorAll('[data-icon]').forEach(el=>el.innerHTML=icon(el.dataset.icon));}
hydrateIcons();
const STORAGE='tubolab.project.v1';
let state={schemaVersion:1,name:'Tubo de referencia',rows:clone(sampleRows),baseline:clone(sampleRows),diameter:50,thickness:1.2,rollSign:1};
function checkProject(p){
if(!p||typeof p!=='object') throw Error('El proyecto JSON no es válido.');
const diameter=Number(p.diameter??50),thickness=Number(p.thickness??1.2),rollSign=Number(p.rollSign??1);
const errors=validateRows(p.rows,diameter).concat(validateRows(p.baseline??p.rows,diameter));
if(!Number.isFinite(diameter)||diameter<1||diameter>1000) errors.push('El diámetro debe estar entre 1 y 1000 mm.');
if(!Number.isFinite(thickness)||thickness<=0||thickness>=diameter/2) errors.push('El espesor debe ser positivo y menor que la mitad del diámetro.');
if(![1,-1].includes(rollSign)) errors.push('El sentido de giro debe ser +1 o −1.');
if(errors.length) throw Error(errors[0]);
const withIds=rows=>rows.map((r,i)=>({...r,id:typeof r.id==='string'&&r.id.length<100?r.id:'legacy-'+i}));
const rows=withIds(p.rows),baseline=withIds(p.baseline??p.rows);
if(new Set(rows.map(r=>r.id)).size!==rows.length||new Set(baseline.map(r=>r.id)).size!==baseline.length)throw Error('El proyecto contiene identificadores de fila repetidos.');
return {schemaVersion:1,name:String(p.name||'Pieza importada').slice(0,100),rows,baseline,diameter,thickness,rollSign};
}
try{const stored=localStorage.getItem(STORAGE);if(stored)state=checkProject(JSON.parse(stored));}catch{}
state=checkProject(state);
let selected=state.rows.length>1?1:0,history=[clone(state)],historyIndex=0,compare=false,currentModel,baselineModel;
let renderer,scene,camera,controls,modelGroup,originalGroup,grid,extent=900,currentView='iso',pickables=[],labelItems=[];
let toastTimer,arController;
function toast(msg){$('toast').textContent=msg;$('toast').classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>$('toast').classList.remove('show'),3200);}
function save(){try{localStorage.setItem(STORAGE,JSON.stringify(state));$('saveState').innerHTML=' Guardado en este navegador';}catch{$('saveState').textContent='Sin guardado automático';}}
function snapshot(){history=history.slice(0,historyIndex+1);history.push(clone(state));if(history.length>60)history.shift();historyIndex=history.length-1;save();updateHistory();}
function updateHistory(){$('undoBtn').disabled=historyIndex===0;$('redoBtn').disabled=historyIndex===history.length-1;}
function baseRow(i){return state.baseline.find(row=>row.id===state.rows[i]?.id);}
function rowChanged(i){const base=baseRow(i);return !base||['x','y','z','r'].some(k=>Math.abs(state.rows[i][k]-base[k])>1e-7);}
function valid(next){try{checkProject(next);$('validationBanner').hidden=true;return true;}catch(e){$('validationBanner').textContent=e.message;$('validationBanner').hidden=false;setTimeout(()=>$('validationBanner').hidden=true,6000);return false;}}
function commit(next,{fit=false,autocompare=true}={}){if(!valid(next)){renderUI();return false;}state=checkProject(next);selected=Math.min(Math.max(selected,0),state.rows.length-1);if(autocompare)compare=true;snapshot();refresh({fit});return true;}
function changeField(k,value){if(!Number.isFinite(value)){toast('Introduce una medida numérica válida.');renderUI();return false;}if(state.rows[selected][k]===value)return true;const next=clone(state);next.rows[selected][k]=value;return commit(next);}
function renderUI(){
$('projectName').value=state.name;$('diameter').value=num(state.diameter);$('thickness').value=num(state.thickness);$('rollSign').value=state.rollSign;
$('compareToggle').checked=compare;$('originalLegend').hidden=!compare;
const row=state.rows[selected],base=baseRow(selected);
const isBend=row.y!==0;
$('selectedTitle').textContent=(isBend?'Curva ':'Tramo ')+pad(selected+1);$('selectedChip').textContent=pad(selected+1);
for(const key of ['x','y','z','r']){
const field=$('edit'+key.toUpperCase());field.value=num(row[key]);field.classList.toggle('changed',!!base&&Math.abs(row[key]-base[key])>1e-7);
}
$('rotationRange').value=Math.max(-180,Math.min(180,row.z));
for(const [key,unit] of [['x','mm'],['z','°']]){
const el=$('diff'+key.toUpperCase());const diff=base?row[key]-base[key]:0;
el.textContent=base?`Original: ${format(base[key],key==='x'?3:1)} ${unit}${Math.abs(diff)>1e-7?' · '+(diff>0?'+':'')+format(diff,2)+' '+unit:''}`:'Fila añadida a la propuesta';el.classList.toggle('changed',Math.abs(diff)>1e-7);
}
$('resetRow').disabled=!base||!rowChanged(selected);
$('rowsBadge').textContent=state.rows.length+' tramos';
const tbody=$('rowsBody'),existing=[...tbody.children];
const reuse=existing.length===state.rows.length&&existing.every((tr,i)=>tr.dataset.rowId===state.rows[i].id);
if(!reuse)tbody.replaceChildren();
state.rows.forEach((r,i)=>{
const base=baseRow(i);
if(reuse){
const tr=existing[i];tr.className=i===selected?'selected':'';
for(const input of tr.querySelectorAll('input')){const k=input.dataset.key;if(input!==document.activeElement)input.value=num(r[k]);input.classList.toggle('changed',!!base&&Math.abs(r[k]-base[k])>1e-7);}
const badge=tr.querySelector('.row-badge');badge.className='row-badge'+(rowChanged(i)?' changed':r.y===0?' tail':'');badge.textContent=rowChanged(i)?'Modificado':r.y===0?(i===state.rows.length-1?'Recta final':'Recta'):'Original';return;
}
const tr=document.createElement('tr');tr.className=i===selected?'selected':'';tr.dataset.index=i;tr.dataset.rowId=r.id;
const first=document.createElement('td');first.textContent=pad(i+1);first.tabIndex=0;first.setAttribute('aria-label','Seleccionar fila '+(i+1));tr.append(first);
for(const [k,name] of [['x','Xc'],['y','Yc'],['z','Zc'],['r','CLR']]){
const td=document.createElement('td'),input=document.createElement('input');input.type='number';input.step='0.001';input.value=num(r[k]);input.dataset.key=k;input.setAttribute('aria-label',`${name} de la fila ${i+1}`);if(base&&Math.abs(r[k]-base[k])>1e-7)input.classList.add('changed');td.append(input);tr.append(td);
}
const td=document.createElement('td'),badge=document.createElement('span');badge.className='row-badge'+(rowChanged(i)?' changed':r.y===0?' tail':'');badge.textContent=rowChanged(i)?'Modificado':r.y===0?(i===state.rows.length-1?'Recta final':'Recta'):'Original';td.append(badge);tr.append(td);
const del=document.createElement('td');del.innerHTML=``;tr.append(del);tbody.append(tr);
});
updateHistory();
}
function updateMetrics(){
const delta=currentModel.length-baselineModel.length;
const endDelta=Math.hypot(...currentModel.end.map((v,i)=>v-baselineModel.end[i]));
$('lengthMetric').innerHTML=format(currentModel.length)+'mm';
$('bendsMetric').innerHTML=state.rows.filter(r=>r.y!==0).length+'curvas';
$('lengthDeltaMetric').innerHTML=(delta>1e-7?'+':'')+format(delta)+'mm';
$('lengthDeltaMetric').classList.toggle('changed',Math.abs(delta)>1e-7);
$('endpointMetric').innerHTML=format(endDelta)+'mm';$('endpointMetric').classList.toggle('changed',endDelta>1e-7);
$('viewSubtitle').textContent=state.rows.some((r,i)=>rowChanged(i))||state.rows.length!==state.baseline.length?'Propuesta modificada':'Geometría nominal';
}
function refresh({fit=false}={}){
renderUI();currentModel=buildCenterline(state.rows,{rollSign:state.rollSign});baselineModel=buildCenterline(state.baseline,{rollSign:state.rollSign});updateMetrics();
if(renderer){drawModel();if(fit)fitCamera(currentView);}else arController?.sync();
}
function selectRow(index){selected=index;renderUI();if(renderer)drawModel();else arController?.sync();}
function undo(delta){const n=historyIndex+delta;if(n<0||n>=history.length)return;historyIndex=n;state=clone(history[n]);selected=Math.min(selected,state.rows.length-1);save();refresh({fit:true});}
// Analytic frames generate hollow tube walls directly; centerline data stay in millimeters.
function sectionGeometry(samples,outer,inner){
const {positions,normals,indices}=buildTubeMeshData(samples,outer,inner);
const geometry=new THREE.BufferGeometry();geometry.setAttribute('position',new THREE.Float32BufferAttribute(positions,3));geometry.setAttribute('normal',new THREE.Float32BufferAttribute(normals,3));geometry.setIndex(indices);geometry.computeBoundingSphere();return geometry;
}
const materials={
straight:new THREE.MeshStandardMaterial({color:0xbccbd2,metalness:.6,roughness:.31,side:THREE.DoubleSide}),
bend:new THREE.MeshStandardMaterial({color:0x8faab6,metalness:.57,roughness:.34,side:THREE.DoubleSide}),
selected:new THREE.MeshStandardMaterial({color:0xef996c,metalness:.38,roughness:.3,side:THREE.DoubleSide}),
ghost:new THREE.MeshBasicMaterial({color:0x7cbbd5,transparent:true,opacity:.11,depthWrite:false,side:THREE.DoubleSide}),
line:new THREE.LineDashedMaterial({color:0x87c3db,dashSize:8,gapSize:5,transparent:true,opacity:.8,depthTest:false}),
};
function clearGroup(group){while(group.children.length){const child=group.children[0];group.remove(child);child.geometry?.dispose();if(child.userData.disposableMaterial)child.material?.dispose();}}
function drawModel(){
clearGroup(modelGroup);clearGroup(originalGroup);pickables=[];
const outer=state.diameter/2,inner=outer-state.thickness;
currentModel.sections.forEach(section=>{
const mesh=new THREE.Mesh(sectionGeometry(section.samples,outer,inner),section.rowIndex===selected?materials.selected:materials[section.type]);mesh.userData.rowIndex=section.rowIndex;modelGroup.add(mesh);pickables.push(mesh);
});
if(compare){
baselineModel.sections.forEach(section=>originalGroup.add(new THREE.Mesh(sectionGeometry(section.samples,outer,inner),materials.ghost)));
const line=new THREE.Line(new THREE.BufferGeometry().setFromPoints(baselineModel.points.map(s=>new THREE.Vector3(...s.p))),materials.line);line.computeLineDistances();line.renderOrder=3;originalGroup.add(line);
const a=new THREE.Vector3(...baselineModel.end),b=new THREE.Vector3(...currentModel.end);
if(a.distanceTo(b)>.05){const line=new THREE.Line(new THREE.BufferGeometry().setFromPoints([a,b]),new THREE.LineDashedMaterial({color:0xe7a283,dashSize:4,gapSize:4,transparent:true,opacity:.65,depthTest:false}));line.computeLineDistances();line.userData.disposableMaterial=true;originalGroup.add(line);}
}
const minZ=Math.min(currentModel.bounds.min[2],compare?baselineModel.bounds.min[2]:Infinity)-outer-15;
const center=new THREE.Vector3(...currentModel.bounds.min.map((v,i)=>(v+currentModel.bounds.max[i])/2));grid.position.set(center.x,center.y,minZ);
$('labels').replaceChildren();labelItems=[];
state.rows.forEach((row,i)=>{
const sections=currentModel.sections.filter(s=>s.rowIndex===i);const s=sections.find(s=>s.type==='bend')||sections[0];if(!s)return;
let p=s.samples[Math.floor((s.samples.length-1)/2)];if(s.type==='straight')p={p:s.samples[0].p.map((v,j)=>(v+s.samples[1].p[j])/2),n:s.samples[0].n};
const position=new THREE.Vector3(...p.p.map((v,j)=>v+(outer+19)*p.n[j]));const el=document.createElement('button');el.className='curve-label'+(i===selected?' selected':'');el.textContent=pad(i+1);el.title=(row.y?'Curva ':'Tramo ')+(i+1);el.setAttribute('aria-label',el.title);el.addEventListener('click',()=>selectRow(i));$('labels').append(el);labelItems.push({el,position});
});
for(const [text,p] of [['INICIO',currentModel.points[0].p],['FIN',currentModel.end]]){
const el=document.createElement('span');el.className='endpoint-label';el.textContent=text;$('labels').append(el);labelItems.push({el,position:new THREE.Vector3(...p)});
}
positionLabels();arController?.sync();
}
function cameraBounds(){
const box=new THREE.Box3();for(const m of compare?[currentModel,baselineModel]:[currentModel]){box.expandByPoint(new THREE.Vector3(...m.bounds.min));box.expandByPoint(new THREE.Vector3(...m.bounds.max));}box.expandByScalar(state.diameter/2+35);return box;
}
function fitCamera(view='iso'){
if(!camera)return;currentView=view;
document.querySelectorAll('[data-view]').forEach(btn=>btn.classList.toggle('active',btn.dataset.view===view));
const box=cameraBounds(),center=box.getCenter(new THREE.Vector3()),size=box.getSize(new THREE.Vector3());
extent=Math.max(size.x,size.y,size.z,100);
let direction=new THREE.Vector3(...({iso:[1,-1.5,1.1],front:[0,-1,0],top:[0,0,1],side:[1,0,0]}[view]||[1,-1.5,1.1])).normalize();
camera.up.set(...(view==='top'?[0,1,0]:[0,0,1]));
camera.position.copy(center).addScaledVector(direction,extent*2);camera.lookAt(center);camera.updateMatrixWorld();
const right=new THREE.Vector3().setFromMatrixColumn(camera.matrixWorld,0),up=new THREE.Vector3().setFromMatrixColumn(camera.matrixWorld,1);
let width=0,height=0,depth=0;
for(let i=0;i<8;i++){
const corner=new THREE.Vector3(i&1?box.max.x:box.min.x,i&2?box.max.y:box.min.y,i&4?box.max.z:box.min.z).sub(center);
width=Math.max(width,Math.abs(corner.dot(right))*2);height=Math.max(height,Math.abs(corner.dot(up))*2);depth=Math.max(depth,Math.abs(corner.dot(direction)));
}
const vFov=THREE.MathUtils.degToRad(camera.fov),distance=Math.max(height/2/Math.tan(vFov/2),width/2/(Math.tan(vFov/2)*camera.aspect))*1.27+depth*.5;
camera.position.copy(center).addScaledVector(direction,Math.max(distance,100));camera.near=.05;camera.far=Math.max(100000,distance*10);camera.updateProjectionMatrix();controls.target.copy(center);controls.minDistance=5;controls.maxDistance=Math.max(20000,extent*30);controls.update();
}
function positionLabels(){if(!camera)return;
const inverse=camera.quaternion.clone().invert(),axis=document.querySelector('.axis');
[[0,0,1,'z'],[0,1,0,'y'],[1,0,0,'x']].forEach(([x,y,z,name],i)=>{const v=new THREE.Vector3(x,y,z).applyQuaternion(inverse);axis.querySelectorAll('path')[i].setAttribute('d',`M29 34L${29+v.x*23} ${34-v.y*23}`);const label=axis.querySelector('.axis-'+name);label.style.left=(26+v.x*31)+'px';label.style.top=(29-v.y*31)+'px';label.style.right='auto';label.style.bottom='auto';});
const w=$('viewport').clientWidth,h=$('viewport').clientHeight;for(const item of labelItems){const p=item.position.clone().project(camera);item.el.style.left=(p.x*.5+.5)*w+'px';item.el.style.top=(-p.y*.5+.5)*h+'px';item.el.style.visibility=p.z>1||p.z< -1||Math.abs(p.x)>.95||Math.abs(p.y)>.87?'hidden':'visible';}}
function init3D(){
try{
const viewport=$('viewport');renderer=new THREE.WebGLRenderer({alpha:true,antialias:true,powerPreference:'high-performance'});renderer.setPixelRatio(Math.min(devicePixelRatio,2));renderer.setSize(viewport.clientWidth,viewport.clientHeight);renderer.outputColorSpace=THREE.SRGBColorSpace;renderer.toneMapping=THREE.ACESFilmicToneMapping;renderer.toneMappingExposure=1.25;renderer.domElement.setAttribute('aria-label','Modelo tridimensional del tubo. Arrastra para girar; utiliza la tabla para editar.');viewport.prepend(renderer.domElement);
scene=new THREE.Scene();camera=new THREE.PerspectiveCamera(33,viewport.clientWidth/viewport.clientHeight,.05,100000);camera.up.set(0,0,1);
controls=new OrbitControls(camera,renderer.domElement);controls.enableDamping=true;controls.dampingFactor=.085;controls.enablePan=true;controls.zoomSpeed=.8;
scene.add(new THREE.HemisphereLight(0xe0edf5,0x223b49,3));
for(const [color,intensity,pos] of [[0xffffff,3,[500,-300,1200]],[0x99bcd5,2,[-600,300,700]],[0xf2c7a8,2,[200,1200,400]]]){const light=new THREE.DirectionalLight(color,intensity);light.position.set(...pos);scene.add(light);}
grid=new THREE.GridHelper(5000,100,0x577184,0x3c5364);grid.rotation.x=Math.PI/2;grid.material.transparent=true;grid.material.opacity=.28;grid.material.depthWrite=false;scene.add(grid);
modelGroup=new THREE.Group();originalGroup=new THREE.Group();scene.add(modelGroup,originalGroup);
const raycaster=new THREE.Raycaster();let down;
renderer.domElement.addEventListener('pointerdown',e=>down=[e.clientX,e.clientY]);
renderer.domElement.addEventListener('pointerup',e=>{if(!down||Math.hypot(e.clientX-down[0],e.clientY-down[1])>5)return;const r=renderer.domElement.getBoundingClientRect();raycaster.setFromCamera(new THREE.Vector2((e.clientX-r.left)/r.width*2-1,-(e.clientY-r.top)/r.height*2+1),camera);const hits=raycaster.intersectObjects(pickables);if(hits.length)selectRow(hits[0].object.userData.rowIndex);});
renderer.domElement.addEventListener('webglcontextlost',e=>{e.preventDefault();$('webglMessage').hidden=false;$('webglMessage').textContent='Se ha interrumpido la vista 3D. Recarga la página para recuperarla. Tus medidas siguen guardadas.';});
new ResizeObserver(()=>{const w=viewport.clientWidth,h=viewport.clientHeight;if(!w||!h)return;renderer.setSize(w,h);camera.aspect=w/h;camera.updateProjectionMatrix();fitCamera(currentView);positionLabels();}).observe(viewport);
renderer.setAnimationLoop(()=>{if(arController?.active)return;controls.update();renderer.render(scene,camera);positionLabels();});
}catch(e){renderer=null;$('webglMessage').hidden=false;$('webglMessage').textContent='La vista 3D necesita WebGL 2. Prueba con Chrome, Edge o Firefox y activa la aceleración gráfica. Puedes seguir editando y exportando las medidas.';console.error(e);}
}
// Editing, importing, comparison and project persistence.
for(const field of document.querySelectorAll('[data-field]'))field.addEventListener('change',()=>changeField(field.dataset.field,field.value===''?NaN:Number(field.value)));
for(const btn of document.querySelectorAll('[data-step]'))btn.addEventListener('click',()=>{const [key,delta]=btn.dataset.step.split(':');changeField(key,Math.round((state.rows[selected][key]+Number(delta))*1000)/1000);});
$('rotationRange').addEventListener('input',e=>{const next=clone(state);next.rows[selected].z=Number(e.target.value);if(!valid(next))return;state=next;compare=true;refresh();});
$('rotationRange').addEventListener('change',()=>{snapshot();});
$('rowsBody').addEventListener('focusin',e=>{const tr=e.target.closest('tr');if(tr&&selected!==Number(tr.dataset.index)){selected=Number(tr.dataset.index);renderUI();if(renderer)drawModel();}});
$('rowsBody').addEventListener('change',e=>{if(e.target.dataset.key){selected=Number(e.target.closest('tr').dataset.index);changeField(e.target.dataset.key,e.target.value===''?NaN:Number(e.target.value));}});
$('rowsBody').addEventListener('click',e=>{const tr=e.target.closest('tr');if(!tr)return;const i=Number(tr.dataset.index);if(e.target.closest('.delete-row')){if(state.rows.length===1){toast('Conserva al menos un tramo.');return;}const next=clone(state);next.rows.splice(i,1);selected=Math.min(i,next.rows.length-1);commit(next,{fit:true});}else if(!e.target.matches('input'))selectRow(i);});
$('rowsBody').addEventListener('keydown',e=>{if((e.key==='Enter'||e.key===' ')&&e.target.tagName==='TD'){e.preventDefault();selectRow(Number(e.target.closest('tr').dataset.index));}});
$('addRow').addEventListener('click',()=>{if(state.rows.length>=100){toast('El máximo es de 100 filas.');return;}const next=clone(state),index=next.rows.at(-1).y===0?next.rows.length-1:next.rows.length;next.rows.splice(index,0,{id:'row-'+crypto.randomUUID(),x:100,y:90,z:0,r:Math.max(75,state.diameter)});selected=index;commit(next,{fit:true});});
$('resetRow').addEventListener('click',()=>{const base=baseRow(selected);if(!base)return;const next=clone(state);next.rows[selected]=clone(base);commit(next);});
$('undoBtn').addEventListener('click',()=>undo(-1));$('redoBtn').addEventListener('click',()=>undo(1));
window.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'&&!e.target.matches('input,textarea')){e.preventDefault();undo(e.shiftKey?1:-1);}if(e.key==='Escape')$('exportMenu').hidden=true;});
$('projectName').addEventListener('change',e=>{const next=clone(state);next.name=e.target.value.trim().slice(0,100)||'Pieza sin nombre';commit(next,{autocompare:false});});
for(const key of ['diameter','thickness','rollSign'])$(key).addEventListener('change',e=>{const next=clone(state);next[key]=e.target.value===''?NaN:Number(e.target.value);commit(next,{fit:key==='rollSign',autocompare:false});});
function setCompare(value){compare=!!value;$('compareToggle').checked=compare;$('originalLegend').hidden=!compare;if(renderer){drawModel();fitCamera(currentView);}else arController?.sync();}
$('compareToggle').addEventListener('change',e=>setCompare(e.target.checked));
$('setBaseline').addEventListener('click',()=>{const next=clone(state);next.baseline=clone(next.rows);commit(next,{autocompare:false});toast('La propuesta es ahora el original. Puedes deshacer este cambio.');});
for(const el of document.querySelectorAll('[data-view]'))el.addEventListener('click',()=>fitCamera(el.dataset.view));
$('fitBtn').addEventListener('click',()=>fitCamera(currentView));
for(const [id,factor] of [['zoomIn',.8],['zoomOut',1.25]])$(id).addEventListener('click',()=>{if(!camera)return;camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);controls.update();});
$('gridToggle').addEventListener('click',e=>{if(!grid)return;grid.visible=!grid.visible;$('gridToggle').classList.toggle('active',grid.visible);$('gridToggle').setAttribute('aria-pressed',grid.visible);});
function openHelp(){$('helpDialog').showModal();}
for(const id of ['helpBtn','topHelp','modelHelp'])$(id).addEventListener('click',openHelp);
$('railSettings').addEventListener('click',()=>{$('specCard').scrollIntoView({behavior:'smooth',block:'center'});$('diameter').focus({preventScroll:true});});
for(const btn of document.querySelectorAll('.close-modal'))btn.addEventListener('click',()=>btn.closest('dialog').close());
for(const dialog of document.querySelectorAll('dialog'))dialog.addEventListener('click',e=>{if(e.target===dialog){const r=dialog.getBoundingClientRect();if(e.clientXr.right||e.clientYr.bottom)dialog.close();}});
let jsonImport=null;
function openImport(){jsonImport=null;$('pasteData').value='';$('fileInput').value='';$('importError').hidden=true;$('importDialog').showModal();}
for(const id of ['importBtn','railImport'])$(id).addEventListener('click',openImport);
$('pasteData').addEventListener('input',()=>{jsonImport=null;$('importError').hidden=true;});
$('fileInput').addEventListener('change',async e=>{const file=e.target.files[0];if(!file)return;try{if(file.size>1000000)throw Error('El archivo debe pesar menos de 1 MB.');const text=await file.text();if(file.name.toLowerCase().endsWith('.json')){jsonImport=checkProject(JSON.parse(text));$('pasteData').value='Xc;Yc;Zc;CLR\n'+jsonImport.rows.map(r=>[r.x,r.y,r.z,r.r].join(';')).join('\n');}else{jsonImport=null;$('pasteData').value=text;}$('importError').hidden=true;}catch(e){jsonImport=null;$('importError').textContent=e.message;$('importError').hidden=false;}});
$('importConfirm').addEventListener('click',()=>{try{let next;if(jsonImport)next=checkProject(jsonImport);else{const rows=parseTable($('pasteData').value);next=checkProject({...state,name:'Pieza importada',rows,baseline:clone(rows)});}selected=0;compare=false;if(commit(next,{fit:true,autocompare:false})){$('importDialog').close();toast('Pieza cargada. El original está listo para comparar.');}else throw Error($('validationBanner').textContent);}catch(e){$('importError').textContent=e.message;$('importError').hidden=false;}});
$('exampleSelect').addEventListener('change',e=>{if(!e.target.value)return;const five=e.target.value==='five';const rows=five?[{x:120,y:65,z:0,r:80},{x:90,y:80,z:85,r:120},{x:75,y:70,z:-160,r:120},{x:150,y:70,z:85,r:80},{x:80,y:45,z:45,r:90},{x:100,y:0,z:0,r:0}]:clone(sampleRows);const next={schemaVersion:1,name:five?'Pieza de prueba · 5 curvas':'Tubo de referencia',rows,baseline:clone(rows),diameter:50,thickness:1.2,rollSign:1};selected=five?2:1;compare=false;commit(next,{fit:true,autocompare:false});e.target.value='';toast(five?'Ejemplo de cinco curvas cargado.':'Datos de la captura VGP3D cargados.');});
$('exportBtn').addEventListener('click',e=>{e.stopPropagation();$('exportMenu').hidden=!$('exportMenu').hidden;});document.addEventListener('click',e=>{if(!e.target.closest('.export-wrap'))$('exportMenu').hidden=true;});
function download(content,type,extension){const blob=new Blob([content],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=(state.name.replace(/[^a-zA-Z0-9áéíóúñ_-]/g,'_')||'tubo')+extension;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);$('exportMenu').hidden=true;}
$('exportCSV').addEventListener('click',()=>{download('\uFEFFXc;Yc;Zc;CLR\r\n'+state.rows.map(r=>[r.x,r.y,r.z,r.r].join(';')).join('\r\n'),'text/csv;charset=utf-8','.csv');toast('Tabla exportada para revisión.');});
$('exportJSON').addEventListener('click',()=>{download(JSON.stringify({...state,conventions:{length:'mm',angle:'degrees',xc:'tangent-to-tangent straight',yc:'bend angle',zc:'relative roll before bend',clr:'centerline radius'}},null,2),'application/json','.tubolab.json');toast('Proyecto exportado con original y propuesta.');});
refresh();init3D();if(renderer){drawModel();fitCamera('iso');}
function physicalModels(){
const proposal=new THREE.Group(),original=new THREE.Group();proposal.name='proposal';original.name='original';
const outer=state.diameter/2,inner=outer-state.thickness;
currentModel.sections.forEach(s=>proposal.add(new THREE.Mesh(sectionGeometry(s.samples,outer,inner),(s.rowIndex===selected?materials.selected:materials[s.type]).clone())));
baselineModel.sections.forEach(s=>original.add(new THREE.Mesh(sectionGeometry(s.samples,outer,inner),new THREE.MeshStandardMaterial({color:0x63b1dc,metalness:.15,roughness:.5,transparent:true,opacity:.23,depthWrite:false,side:THREE.DoubleSide}))));
return {proposal,original};
}
arController=createARController({getProject:()=>clone(state),getModels:physicalModels,getSelected:()=>selected,getCompare:()=>compare,onEdit:changeField,onSelect:selectRow,onCompare:setCompare,notify:toast,onExit:()=>fitCamera(currentView)});
for(const button of document.querySelectorAll('[data-mobile-target]'))button.addEventListener('click',()=>{
document.body.dataset.mobilePanel=button.dataset.mobileTarget;
document.querySelectorAll('[data-mobile-target]').forEach(b=>{const active=b===button;b.setAttribute('aria-selected',active);b.setAttribute('aria-pressed',active);b.tabIndex=active?0:-1;});
if(button.dataset.mobileTarget==='model')requestAnimationFrame(()=>fitCamera(currentView));
});
document.querySelector('.mobile-work-tabs').addEventListener('keydown',e=>{if(e.key==='ArrowLeft'||e.key==='ArrowRight'){e.preventDefault();const target=document.querySelector(`[data-mobile-target="${document.body.dataset.mobilePanel==='model'?'sequence':'model'}"]`);target.click();target.focus();}});
// Read-only diagnostics support reproducible geometry checks.
window.tubolab={getProject:()=>clone(state),getMetrics:()=>({length:currentModel.length,end:clone(currentModel.end),baselineLength:baselineModel.length}),is3DReady:()=>!!renderer};