# -*- coding: utf-8 -*-
"""2110DN-0031 CENTER FRAME 조립 시범 뷰어 생성 (자기완결형: three.js + OBJ 인라인).
make_viewer.py 와 같은 방식(three.min.js 인라인)을 재사용한다."""
import json, os, io, sys

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스캔및티칭시스템'
ASM = ROOT + r'\fab_models\assemblies\2110DN-0031_assembly.json'
OUT = SCR + r'\E500PRO_조립시범.html'

meta = json.load(open(ASM, encoding='utf-8'))

COL = ['#4f8cff', '#ff8a3d', '#3ecf8e', '#e05c8a', '#c78bff',
       '#f2c14e', '#5fd0d8', '#9aa7b8', '#ff6b6b', '#7ee081', '#d4a5ff']


def read_obj(p):
    V, F = [], []
    for ln in io.open(p, encoding='utf-8', errors='replace'):
        if ln.startswith('v '):
            V.append([float(x) for x in ln.split()[1:4]])
        elif ln.startswith('f '):
            idx = [int(t.split('/')[0]) - 1 for t in ln.split()[1:]]
            for k in range(1, len(idx) - 1):
                F += [idx[0], idx[k], idx[k + 1]]
    return V, F


parts = []
for i, p in enumerate(meta['parts']):
    fp = os.path.join(ROOT, p['obj'].replace('/', os.sep))
    if not os.path.exists(fp):
        print('!! 없음', fp); continue
    V, F = read_obj(fp)
    parts.append({'item': p['item'], 'pn': p['pn'], 'size': p['size_mm'],
                  'rot': p['rot_deg_xyz'], 'pos': p['pos_mm'], 'conf': p['confidence'],
                  'ceo': p['ceo_check'], 'ev': p['evidence'], 'note': p['note'],
                  'col': COL[i % len(COL)],
                  'v': [round(c, 2) for xyz in V for c in xyz], 'f': F})

three = io.open(ROOT + r'\threejs\three.min.js', encoding='utf-8', errors='replace').read()

H = []
H.append(u'''<!DOCTYPE html><html lang="ko"><head><meta charset="utf-8">
<title>2110DN-0031 CENTER FRAME 조립 시범</title><style>
html,body{margin:0;height:100%;overflow:hidden;background:#10141a;font-family:"Malgun Gothic",sans-serif}
#side{position:fixed;left:0;top:0;bottom:0;width:300px;z-index:20;background:#151a21;
 border-right:1px solid #232c37;display:flex;flex-direction:column}
#side h1{margin:0;padding:14px 14px 4px;font-size:14px;color:#e8eef6}
#side h1 span{display:block;font-size:11px;color:#7b8ea3;margin-top:4px;font-weight:normal;line-height:1.5}
#list{flex:1;overflow-y:auto;padding:6px 10px 12px}
#list button{display:block;width:100%;text-align:left;background:#1b2530;color:#c9d6e4;
 border:1px solid #2c3a49;border-left:5px solid #444;border-radius:6px;padding:7px 9px;margin:5px 0;
 font-size:12px;cursor:pointer;font-family:inherit;line-height:1.5}
#list button:hover{background:#243244}
#list button.on{background:#2f6feb;color:#fff}
#list button small{display:block;color:#8fa3b8;font-size:11px}
#list button.ceo small b{color:#ffb545}
#info{position:fixed;left:318px;top:14px;right:14px;color:#e8eef6;z-index:10;pointer-events:none;
 font-size:13px;line-height:1.65;text-shadow:0 1px 3px #000}
#info b{font-size:18px}
#info em{color:#ffb545;font-style:normal}
#hint{position:fixed;right:16px;bottom:12px;color:#6f8299;font-size:12px;z-index:10}
#miss{position:fixed;left:318px;bottom:12px;color:#ff9c6b;font-size:12px;z-index:10;line-height:1.6}
</style></head><body>
<div id="side"><h1>2110DN-0031 CENTER FRAME 조립 시범<span>발주도면 확인 기반 배치 · 반투명 = [CEO확인] 필요</span></h1><div id="list"></div></div>
<div id="info"></div><div id="miss"></div>
<div id="hint">드래그=회전 · 휠=확대 · 우클릭드래그=이동 · 부재 클릭 시 도번 표시</div>
<script id="ASM" type="application/json">''')
H.append(json.dumps({'meta': {k: v for k, v in meta.items() if k != 'parts'},
                     'parts': parts}, ensure_ascii=False))
H.append(u'</script>\n<script>\n')
H.append(three)
H.append(u'''
</script><script>
var D=JSON.parse(document.getElementById('ASM').textContent);
var scene=new THREE.Scene(); scene.background=new THREE.Color(0x10141a);
var W=innerWidth,Hh=innerHeight;
var cam=new THREE.PerspectiveCamera(45,W/Hh,1,40000);
var rnd=new THREE.WebGLRenderer({antialias:true}); rnd.setSize(W,Hh); document.body.appendChild(rnd.domElement);
scene.add(new THREE.AmbientLight(0xffffff,0.62));
var dl=new THREE.DirectionalLight(0xffffff,0.85); dl.position.set(1,1.4,1); scene.add(dl);
var dl2=new THREE.DirectionalLight(0xffffff,0.35); dl2.position.set(-1,-0.6,-1); scene.add(dl2);
var root=new THREE.Group(); scene.add(root);
var meshes=[];
D.parts.forEach(function(p,i){
  var g=new THREE.BufferGeometry();
  g.setAttribute('position',new THREE.Float32BufferAttribute(p.v,3));
  g.setIndex(p.f); g.computeVertexNormals();
  var m=new THREE.MeshLambertMaterial({color:new THREE.Color(p.col),
     transparent:p.ceo,opacity:p.ceo?0.42:1.0,side:THREE.DoubleSide});
  var o=new THREE.Mesh(g,m);
  o.rotation.set(p.rot[0]*Math.PI/180,p.rot[1]*Math.PI/180,p.rot[2]*Math.PI/180);
  o.position.set(p.pos[0],p.pos[1],p.pos[2]);
  o.userData=p; root.add(o); meshes.push(o);
  var e=new THREE.LineSegments(new THREE.EdgesGeometry(g,25),
     new THREE.LineBasicMaterial({color:0x0b0e12,transparent:true,opacity:0.55}));
  e.rotation.copy(o.rotation); e.position.copy(o.position); root.add(e);
});
// Z-up 도면 좌표를 화면 Y-up 으로
root.rotation.x=-Math.PI/2;
var bb=new THREE.Box3().setFromObject(root); var ctr=bb.getCenter(new THREE.Vector3());
var sz=bb.getSize(new THREE.Vector3()); var R=Math.max(sz.x,sz.y,sz.z);
root.position.sub(ctr);
cam.position.set(R*1.5,R*1.1,R*1.8); cam.lookAt(0,0,0);
// 간단 궤도 조작
var st={d:false,x:0,y:0,th:0.9,ph:0.9,r:R*2.6,px:0,py:0,pan:false};
function upd(){cam.position.set(st.r*Math.sin(st.ph)*Math.cos(st.th)+st.px,
 st.r*Math.cos(st.ph)+st.py, st.r*Math.sin(st.ph)*Math.sin(st.th));
 cam.lookAt(st.px,st.py,0);}
upd();
rnd.domElement.addEventListener('mousedown',function(e){st.d=true;st.pan=(e.button===2);st.x=e.clientX;st.y=e.clientY;});
addEventListener('mouseup',function(){st.d=false;});
addEventListener('mousemove',function(e){if(!st.d)return;var dx=e.clientX-st.x,dy=e.clientY-st.y;st.x=e.clientX;st.y=e.clientY;
 if(st.pan){st.px-=dx*st.r/900;st.py+=dy*st.r/900;}else{st.th+=dx*0.008;st.ph=Math.max(0.05,Math.min(3.09,st.ph-dy*0.008));}upd();});
rnd.domElement.addEventListener('contextmenu',function(e){e.preventDefault();});
rnd.domElement.addEventListener('wheel',function(e){e.preventDefault();st.r*=(e.deltaY>0?1.12:0.89);upd();});
var ray=new THREE.Raycaster(),mo=new THREE.Vector2();
function show(p){document.getElementById('info').innerHTML=
 '<b>'+p.pn+'</b> &nbsp;<span style="color:#8fa3b8">항번 '+p.item+' · '+p.size.map(function(v){return v+'';}).join(' x ')+' mm</span>'
 +(p.ceo?'<br><em>[CEO확인] 신뢰도 '+p.conf+'</em>':'<br><span style="color:#3ecf8e">신뢰도 '+p.conf+'</span>')
 +'<br><span style="color:#9fb2c6">근거: '+p.ev+'</span>'
 +(p.note?'<br><span style="color:#8a99ab">'+p.note+'</span>':'');
 [].forEach.call(document.querySelectorAll('#list button'),function(b){b.classList.toggle('on',b.dataset.k===p.pn+'#'+p.item);});}
rnd.domElement.addEventListener('click',function(e){
 mo.x=e.clientX/W*2-1; mo.y=-e.clientY/Hh*2+1; ray.setFromCamera(mo,cam);
 var h=ray.intersectObjects(meshes); if(h.length) show(h[0].object.userData);});
var L=document.getElementById('list');
D.parts.forEach(function(p){var b=document.createElement('button');
 b.style.borderLeftColor=p.col; b.dataset.k=p.pn+'#'+p.item;
 if(p.ceo)b.className='ceo';
 b.innerHTML='항번 '+p.item+' &nbsp;'+p.pn+'<small>'+p.size.join(' x ')+' mm · '
  +(p.ceo?'<b>[CEO확인] '+p.conf+'</b>':'신뢰도 high')+'</small>';
 b.onclick=function(){show(p);}; L.appendChild(b);});
document.getElementById('miss').innerHTML='미배치(단품 OBJ 없음): '+D.meta['BOM_15항목'];
addEventListener('resize',function(){W=innerWidth;Hh=innerHeight;cam.aspect=W/Hh;cam.updateProjectionMatrix();rnd.setSize(W,Hh);});
(function loop(){requestAnimationFrame(loop);rnd.render(scene,cam);})();
</script></body></html>''')
io.open(OUT, 'w', encoding='utf-8').write(u''.join(H))
print(OUT, os.path.getsize(OUT) // 1024, 'KB', len(parts), '부재')
