# -*- coding: utf-8 -*-
"""E500PRO 다부재 3D 뷰어 HTML 생성 (자기완결형: three.js + OBJ 전부 인라인).
기존 E500PRO_3D뷰어.html 은 건드리지 않고 새 파일로 만든다."""
import json, os, io, sys, re

SCR = r'C:\Users\user\AppData\Local\Temp\claude\E-----Claude--20260728-claude-code-2-1-219\8defc145-8ae5-48d0-9058-355f3d9ae8ed\scratchpad'
ROOT = r'E:\도진팩토리\3D스캔및티칭시스템'
# 출력 파일명을 인자로 받는다(기존 뷰어를 덮어쓰지 않고 v2 로 낼 수 있게).
_out = sys.argv[1] if len(sys.argv) > 1 else 'E500PRO_전체뷰어.html'
OUT = _out if os.path.isabs(_out) else SCR + '\\' + _out

# 이번 실행 결과 + 이전 실행 결과를 합쳐 E500PRO 부재를 전부 싣는다.
# (이전에 만든 부재는 이번 실행에서 '덮어쓰기 금지'로 skip 되므로 결과 파일에 ok 로 남지 않는다.)
res = {}
for fn in ('multi_result_backup_prev.json', 'multi_result.json'):
    p = SCR + '\\' + fn
    if not os.path.exists(p):
        continue
    for r in json.load(open(p, encoding='utf-8')):
        if r.get('status') == 'ok':
            res[r['pn']] = r
# 결과 JSON 은 실행할 때마다 덮어써져서 예전에 성공한 부재의 기록이 사라질 수 있다.
# manifest.json 이 채택 부재의 정본이므로, 결과 JSON 에 없는 E500PRO 부재는 여기서 보충한다.
try:
    man = json.load(open(ROOT + r'\fab_models\manifest.json', encoding='utf-8'))
    for it in man.get('items', []):
        f = it.get('file', '')
        if not os.path.basename(f).startswith('e500pro_'):
            continue
        m = re.search(r'\d{4}[A-Z]{2}-\d{4}-\d{3,4}', it.get('name', ''))
        if not m:
            continue
        pn = m.group(0)
        if pn in res:
            continue
        note = it.get('note', '')
        thk = note.split('두께 ')[1].split('T')[0] if '두께 ' in note else '?'
        if '원문 "' in note:
            ev = note.split('원문 "')[1].split('"')[0]
        elif 'VLM판독:' in note:
            # 그림으로 직접 읽은 두께는 따옴표 없이 원문을 그대로 적어 두었다.
            ev = 'VLM판독: ' + note.split('VLM판독:')[1].split('—')[0].strip()
        else:
            ev = ''
        res[pn] = dict(pn=pn, file=f, thk=thk, extents=it.get('size_mm', []),
                       faces=it.get('faces', 0), watertight=True, nholes=0,
                       evidence=ev, status='ok')
except Exception as ex:
    print('(manifest 보충 실패: %s)' % ex)

# 두 번째 인자를 주면 그 문자열이 도번에 든 부재만 싣는다(예: 0004 -> 0004 시트 전용 뷰어).
_filt = sys.argv[2] if len(sys.argv) > 2 else None
if _filt:
    res = {k: v for k, v in res.items() if _filt in k}
    print('필터 "%s" 적용 -> %d개' % (_filt, len(res)))
res = [res[k] for k in sorted(res)]
three = io.open(ROOT + r'\threejs\three.min.js', encoding='utf-8', errors='replace').read()

parts = []
for r in res:
    p = os.path.join(ROOT, r['file'].replace('/', '\\'))
    if not os.path.exists(p):
        print('  (파일 없음, 뷰어에서 제외)', r['file']); continue
    obj = io.open(p, encoding='utf-8', errors='replace').read()
    parts.append(dict(pn=r['pn'], thk=r['thk'], ext=r['extents'], faces=r['faces'],
                      wt=r['watertight'], holes=r['nholes'], obj=obj,
                      ev=r.get('evidence', '')))
print('뷰어에 실을 부재: %d개 — %s' % (len(parts), ', '.join(p['pn'] for p in parts)))

html = []
html.append('''<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8">
<title>E500PRO 2110DN-0004 FRAME 시트 부재 3D 뷰어</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#12161c;font-family:"Malgun Gothic",sans-serif}
#side{position:fixed;left:0;top:0;bottom:0;width:250px;z-index:20;background:#161b22;
      border-right:1px solid #232c37;display:flex;flex-direction:column}
#side h1{margin:0;padding:14px 14px 6px;font-size:14px;color:#e8eef6;letter-spacing:.4px}
#side h1 span{display:block;font-size:11px;color:#7b8ea3;margin-top:3px;font-weight:normal}
#q{margin:8px 12px;padding:7px 9px;background:#0f1319;border:1px solid #2c3a49;border-radius:6px;
   color:#c9d6e4;font-size:12px;font-family:inherit;outline:none}
#q:focus{border-color:#2f6feb}
#list{flex:1;overflow-y:auto;padding:4px 8px 12px}
#list button{display:block;width:100%;text-align:left;background:#1b2530;color:#c9d6e4;
   border:1px solid #2c3a49;border-radius:6px;padding:7px 10px;margin:4px 0;
   font-size:12px;cursor:pointer;font-family:inherit;line-height:1.45}
#list button:hover{background:#243244}
#list button.on{background:#2f6feb;border-color:#4a86ff;color:#fff}
#list button small{display:block;color:#8fa3b8;font-size:11px}
#list button.on small{color:#d8e4f7}
#label{position:fixed;left:270px;top:16px;color:#e8eef6;z-index:10;pointer-events:none}
#label b{font-size:19px;letter-spacing:.5px}
#label div{font-size:13px;color:#8fa3b8;margin-top:4px;line-height:1.6}
#label i{color:#6f8299;font-style:normal;font-size:12px}
#hint{position:fixed;right:18px;bottom:14px;color:#7b8ea3;font-size:12px;z-index:10}
</style></head><body>
<div id="side">
  <h1>E500PRO 부재<span id="cnt"></span></h1>
  <input id="q" placeholder="도번·두께 검색 (예: 0019, 22T)">
  <div id="list"></div>
</div>
<div id="label"></div>
<div id="hint">마우스 좌드래그=회전 · 휠=줌 · 우드래그=이동</div>
<script>''')
html.append(three)
html.append('</script>\n<script id="partdata" type="application/json">')
html.append(json.dumps(parts, ensure_ascii=False))
html.append('''</script>
<script>
var PARTS = JSON.parse(document.getElementById('partdata').textContent);

function parseOBJ(txt){
  var lines = txt.split('\\n'), verts=[], pos=[];
  for (var i=0;i<lines.length;i++){
    var l = lines[i].trim();
    if (l.length<2) continue;
    if (l.charAt(0)==='v' && l.charAt(1)===' '){
      var p=l.split(/\\s+/); verts.push([parseFloat(p[1]),parseFloat(p[2]),parseFloat(p[3])]);
    } else if (l.charAt(0)==='f' && l.charAt(1)===' '){
      var q=l.split(/\\s+/), idx=[];
      for (var k=1;k<q.length;k++){ var s=q[k].split('/')[0]; if(!s) continue;
        var n=parseInt(s,10); idx.push(n<0? verts.length+n : n-1); }
      for (var t=1;t+1<idx.length;t++){
        var tri=[idx[0],idx[t],idx[t+1]];
        for (var m=0;m<3;m++){ var v=verts[tri[m]]; if(!v) continue; pos.push(v[0],v[1],v[2]); }
      }
    }
  }
  var geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(pos,3));
  geo.computeVertexNormals(); geo.computeBoundingBox();
  var ctr=new THREE.Vector3(); geo.boundingBox.getCenter(ctr);
  geo.translate(-ctr.x,-ctr.y,-ctr.z);
  return {geo:geo, nv:verts.length, ntri:pos.length/9};
}

var scene = new THREE.Scene();
scene.background = new THREE.Color(0x12161c);
var renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(window.devicePixelRatio||1);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.1, 100000);
scene.add(new THREE.HemisphereLight(0xbcd4ff, 0x20262e, 0.9));
var d1=new THREE.DirectionalLight(0xffffff,0.9), d2=new THREE.DirectionalLight(0xffffff,0.45);
scene.add(d1); scene.add(d2);
var mat = new THREE.MeshStandardMaterial({color:0x9aa3ab, metalness:0.75, roughness:0.35, side:THREE.DoubleSide});
var mesh=null, edges=null, grid=null, radius=1000;
var target=new THREE.Vector3(0,0,0), dist=1500, theta=0.9, phi=1.1;

function updateCam(){
  phi=Math.max(0.05,Math.min(Math.PI-0.05,phi));
  camera.position.set(target.x+dist*Math.sin(phi)*Math.sin(theta),
                      target.y+dist*Math.cos(phi),
                      target.z+dist*Math.sin(phi)*Math.cos(theta));
  camera.lookAt(target);
}

function show(i){
  var p = PARTS[i];
  if (mesh){ scene.remove(mesh); scene.remove(edges); scene.remove(grid); mesh.geometry.dispose(); }
  var r = parseOBJ(p.obj);
  mesh = new THREE.Mesh(r.geo, mat); scene.add(mesh);
  edges = new THREE.LineSegments(new THREE.EdgesGeometry(r.geo,25),
            new THREE.LineBasicMaterial({color:0x1b2027})); scene.add(edges);
  var size=new THREE.Vector3(); r.geo.boundingBox.getSize(size);
  radius = Math.max(size.x,size.y,size.z);
  grid = new THREE.GridHelper(radius*2, 20, 0x2a3340, 0x1e242c);
  grid.position.y = -size.y/2 - radius*0.02; scene.add(grid);
  d1.position.set(1,1.4,1).multiplyScalar(radius);
  d2.position.set(-1.2,-0.6,-0.8).multiplyScalar(radius);
  camera.near = radius*0.001; camera.far = radius*100; camera.updateProjectionMatrix();
  // 부재마다 크기가 다르므로 전환할 때 카메라를 매번 새로 맞춘다
  target.set(0,0,0); dist = radius*1.6; updateCam();
  document.getElementById('label').innerHTML =
    '<b>'+p.pn+' &nbsp;'+p.thk+'T</b><div>'+
    p.ext[0].toFixed(1)+' &times; '+p.ext[1].toFixed(1)+' &times; '+p.ext[2].toFixed(1)+' mm'+
    ' &nbsp;·&nbsp; 홀 '+p.holes+'개 &nbsp;·&nbsp; 삼각형 '+r.ntri+
    ' &nbsp;·&nbsp; watertight(닫힌 입체) '+(p.wt?'✔':'✘')+
    (p.ev?'<br><i>두께 근거: '+p.ev+'</i>':'')+'</div>';
  var bs = document.querySelectorAll('#list button');
  for (var k=0;k<bs.length;k++) bs[k].className = (+bs[k].dataset.i===i?'on':'');
}

var list = document.getElementById('list');
document.getElementById('cnt').textContent = PARTS.length+'개 · 두께 근거 확인된 부재만';
function render(f){
  list.innerHTML='';
  var n=0;
  PARTS.forEach(function(p,i){
    var key=(p.pn+' '+p.thk+'T').toLowerCase();
    if(f && key.indexOf(f)<0) return;
    n++;
    var b=document.createElement('button');
    b.dataset.i=i;
    b.innerHTML=p.pn.replace('2110DN-','')+' &nbsp;'+p.thk+'T<small>'+
      p.ext[0].toFixed(0)+'×'+p.ext[1].toFixed(0)+' · 홀'+p.holes+'</small>';
    b.onclick=function(){ show(i); };
    list.appendChild(b);
  });
  if(!n) list.innerHTML='<div style="color:#7b8ea3;font-size:12px;padding:8px">검색 결과 없음</div>';
}
document.getElementById('q').addEventListener('input',function(e){
  render(e.target.value.trim().toLowerCase());
});
render('');

var dragging=0,lx=0,ly=0, el=renderer.domElement;
el.addEventListener('contextmenu',function(e){e.preventDefault();});
el.addEventListener('mousedown',function(e){dragging=e.button===2?2:1;lx=e.clientX;ly=e.clientY;});
window.addEventListener('mouseup',function(){dragging=0;});
window.addEventListener('mousemove',function(e){
  if(!dragging) return;
  var dx=e.clientX-lx, dy=e.clientY-ly; lx=e.clientX; ly=e.clientY;
  if(dragging===1){ theta-=dx*0.005; phi-=dy*0.005; }
  else { var pan=dist*0.0015;
    var right=new THREE.Vector3().setFromMatrixColumn(camera.matrix,0);
    var up=new THREE.Vector3().setFromMatrixColumn(camera.matrix,1);
    target.addScaledVector(right,-dx*pan); target.addScaledVector(up,dy*pan); }
  updateCam();
});
el.addEventListener('wheel',function(e){ e.preventDefault();
  dist *= (e.deltaY>0?1.1:0.9);
  dist = Math.max(radius*0.05, Math.min(radius*20, dist)); updateCam();
},{passive:false});
window.addEventListener('resize',function(){
  camera.aspect=window.innerWidth/window.innerHeight; camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth,window.innerHeight);
});
show(0);
(function loop(){ requestAnimationFrame(loop); renderer.render(scene,camera); })();
</script>
</body></html>''')

io.open(OUT, 'w', encoding='utf-8').write(''.join(html))
print('viewer ->', OUT, '%.1f MB' % (os.path.getsize(OUT)/1048576), '| parts:', len(parts))
