# 쉬운 모드(프리셋) 전 과정 검증 — 실제 500PRO-R 센터프레임 부재로 해석까지 돌린다.
#   확인 항목: ① 프리셋 조건 자동생성 ② 해석 성공 ③ jet 히트맵 실제 렌더
#              ④ 한국어 요약배너 ⑤ 피로 간이평가 표시
# 실행 전제: python fea_server.py (8097) / python dzw_server.py 8090
import asyncio, json
from playwright.async_api import async_playwright

URL = "http://localhost:8090/fab.html"
SHOT = r"C:\Users\user\AppData\Local\Temp\dzw_fea\fea_easy.png"
# 실제 제품 부재 — 2110DN-0031 센터 프레임 (CEO 관심 부위)
PARTS = ["e500pro_0031_001.obj", "e500pro_0031_004.obj", "e500pro_0031_009.obj"]


async def main():
    errs = []
    async with async_playwright() as p:
        br = await p.chromium.launch()
        pg = await br.new_page(viewport={'width': 1600, 'height': 1000})
        pg.on('pageerror', lambda e: errs.append(str(e)))
        await pg.goto(URL, timeout=60000)
        await pg.wait_for_timeout(3500)

        # 1) 실제 센터프레임 부재 로드
        n = await pg.evaluate("""async (files) => {
            const parts = files.map((f,i)=>({name:f.replace('.obj',''), file:'fab_models/'+f, path:'G01/P'+i}));
            await window.FAB.loadBlock('500PRO_R_CENTERFRAME', parts);
            let c=0; window.FAB.scene.traverse(o=>{ if(o.isMesh && o.name) c++; });
            return c;
        }""", PARTS)
        print('1) 센터프레임 로드 : mesh %d개' % n)

        # 2) 진짜 클릭 경로로 부재 선택 (watchClicks 가 clickedPart 를 잡아야 한다)
        await pg.evaluate("() => window.FAB.selectMode()")
        box = await pg.locator('#fab-canvas').bounding_box()
        picked = None
        for (dx, dy) in [(0.5,0.5),(0.45,0.5),(0.55,0.5),(0.5,0.45),(0.5,0.55),(0.42,0.42),(0.6,0.6)]:
            await pg.mouse.click(box['x']+box['width']*dx, box['y']+box['height']*dy)
            await pg.wait_for_timeout(300)
            # ★ clickedPart 를 직접 본다. targetMeshes 만 보면 '클릭 실패로 전체가 잡힌 것' 과
            #   '클릭 성공으로 하나가 잡힌 것' 을 구분하지 못한다(실측으로 놓쳤던 부분).
            picked = await pg.evaluate("""() => {
                const s = window.FAB_FEA.state, m = window.FAB_FEA.targetMeshes();
                if(!m.length) return null;
                return {클릭잡힘: !!s.clickedPart, 클릭부품: s.clickedPart? (s.clickedPart.name||'?') : null,
                        해석대상수: m.length, 범위: s.scope};
            }""")
            if picked and picked.get('클릭잡힘'): break
        print('2) 클릭 선택      :', json.dumps(picked, ensure_ascii=False))

        # 3) 구조해석 패널 열기 — 쉬운 모드가 기본으로 떠야 한다
        await pg.locator('#fab-fea-btn').click()
        await pg.wait_for_timeout(600)
        ui = await pg.evaluate("""() => {
            const s = window.FAB_FEA.state;
            const e = window.FAB_FEA.els();
            return {쉬운모드기본: !s.pro, 기본프리셋: s.preset, 색기준: s.heat,
                    프리셋버튼수: (e.presetBtns||[]).length,
                    전문가줄_숨김: (e.proRows||[]).every(x=>!x||x.style.display==='none'),
                    큰버튼높이: e.easyRun ? getComputedStyle(e.easyRun).minHeight : null};
        }""")
        print('3) 쉬운 모드 UI   :', json.dumps(ui, ensure_ascii=False))

        # 4) 프리셋 'boom'(붐·마스트 조립 상태) — CEO 최우선 케이스이자 기본 선택값.
        #    마운트 좌표를 형상에서 추정하므로 '엉뚱한 곳에 스냅되지 않는지' 가 핵심 확인점.
        await pg.evaluate("() => window.FAB_FEA.choosePreset('boom')")
        await pg.wait_for_timeout(200)
        cond = await pg.evaluate("""() => {
            window.FAB_FEA.runPreset();
            const s = window.FAB_FEA.state;
            // 조건점이 '실제 부재 위' 인지 — 외곽상자 대비 어디에 찍혔는지 같이 본다
            const ms = window.FAB_FEA.targetMeshes();
            let lo=[1e9,1e9,1e9], hi=[-1e9,-1e9,-1e9];
            ms.forEach(m=>{ const a=m.geometry.attributes.position.array;
                for(let i=0;i<a.length;i+=3) for(let d=0;d<3;d++){
                    if(a[i+d]<lo[d]) lo[d]=a[i+d]; if(a[i+d]>hi[d]) hi[d]=a[i+d]; } });
            const fmt3 = p => p.map(x=>Math.round(x)).join(',');
            return {고정점: s.fixPoints.length, 하중점: s.loadPoints.length,
                    총하중N: Math.round(s.loadPoints.reduce((a,b)=>a+b.N,0)),
                    부재외곽_min: fmt3(lo), 부재외곽_max: fmt3(hi),
                    고정점_예시: s.fixPoints.slice(0,4).map(f=>fmt3(f.p)),
                    하중점_예시: s.loadPoints.slice(0,2).map(l=>fmt3(l.p))};
        }""")
        print('4) 자동 생성 조건 :', json.dumps(cond, ensure_ascii=False))

        # 해석 대기 (최대 90초)
        for _ in range(90):
            await pg.wait_for_timeout(1000)
            done = await pg.evaluate("() => !window.FAB_FEA.state.busy && !!window.FAB_FEA.state.lastResult")
            if done: break

        res = await pg.evaluate("""() => {
            const r = window.FAB_FEA.state.lastResult;
            if(!r) return null;
            return {ok:r.ok, 방식:r.method_kr, 최대응력MPa:+r.result.max_vonmises_MPa.toFixed(1),
                    항복대비pct:+r.result.yield_ratio_pct.toFixed(1),
                    최대변위mm:+r.result.max_disp_mm.toFixed(3),
                    절점:r.mesh.nodes, 계산초:r.solve_sec, 경고:r.warn||null};
        }""")
        print('5) 해석 결과      :', json.dumps(res, ensure_ascii=False))

        # 5) 히트맵이 '실제로' 칠해졌는지 — 정점색 분포를 직접 센다
        heat = await pg.evaluate("""() => {
            let g=null;
            window.FAB.scene.traverse(o=>{ if(o.name==='__FEA_RESULT__')
                o.traverse(c=>{ if(c.isMesh && c.geometry.attributes.color && !g) g=c.geometry; }); });
            if(!g) return {오류:'결과 오버레이 없음'};
            const col=g.attributes.color.array, n=col.length/3;
            let blue=0, green=0, yellow=0, red=0;
            for(let i=0;i<n;i++){
                const r=col[i*3], gg=col[i*3+1], b=col[i*3+2];
                if(b>0.5 && r<0.3) blue++;
                else if(gg>0.5 && r<0.5) green++;
                else if(r>0.7 && gg>0.5) yellow++;
                else if(r>0.6 && gg<0.4) red++;
            }
            return {정점:n, 파랑:blue, 초록:green, 노랑:yellow, 빨강:red,
                    삼각형: g.index? g.index.count/3 : 0};
        }""")
        print('6) jet 히트맵     :', json.dumps(heat, ensure_ascii=False))

        # 6) 요약 배너 + 피로 평가 문구
        txt = await pg.evaluate(
            "() => document.querySelector('#fab-fea-panel > div:last-child').innerText")
        print('7) 결과 패널 글 (앞 22줄):')
        for line in [l.strip() for l in txt.split('\n') if l.strip()][:22]:
            print('     ', line)
        checks = {
            '요약배너_색안내': ('빨간 부위가 약한 곳' in txt and '파란 부위가 튼튼한 곳' in txt),
            '가장약한부위': ('가장 약한 부위' in txt),
            '피로_순위': ('피로 위험 순위' in txt),
            '피로_간이문구': ('간이 평가' in txt and '전문기관' in txt),
        }
        print('8) 문구 점검      :', json.dumps(checks, ensure_ascii=False))

        # 7) 피로 색 기준으로 전환해도 다시 칠해지는지
        await pg.evaluate("() => window.FAB_FEA.setHeat && window.FAB_FEA.setHeat('fatigue')")
        await pg.wait_for_timeout(800)
        fat = await pg.evaluate("""() => {
            const t = document.querySelector('#fab-fea-panel > div:last-child').innerText;
            return {색기준: window.FAB_FEA.state.heat, 피로배너: t.includes('피로')};
        }""")
        print('9) 피로 모드 전환 :', json.dumps(fat, ensure_ascii=False))

        # 8) 아래로 처지는가 — UP=+Z 가정이 실제로 맞는지 변위 벡터로 확인
        #    (자중 케이스는 변위가 0.000mm 라 방향을 증명하지 못했다. boom 은 하중이 커서 보인다)
        dirchk = await pg.evaluate("""() => {
            const r = window.FAB_FEA.state.lastResult;
            if(!r || !r.viz.disp) return null;
            const d = r.viz.disp; let bi=0, bm=0;
            for(let i=0;i<d.length;i+=3){
                const m = d[i]*d[i]+d[i+1]*d[i+1]+d[i+2]*d[i+2];
                if(m>bm){ bm=m; bi=i; }
            }
            return {최대변위점_dx:+d[bi].toFixed(4), dy:+d[bi+1].toFixed(4), dz:+d[bi+2].toFixed(4),
                    아래로_처짐: d[bi+2] < 0};
        }""")
        print('10) 변위 방향     :', json.dumps(dirchk, ensure_ascii=False))

        # 9) 상대/항복 스케일 전환이 실제로 색을 바꾸는지
        sc = await pg.evaluate("""() => {
            function count(){
                let g=null;
                window.FAB.scene.traverse(o=>{ if(o.name==='__FEA_RESULT__')
                    o.traverse(c=>{ if(c.isMesh && c.geometry.attributes.color && !g) g=c.geometry; }); });
                if(!g) return null;
                const col=g.attributes.color.array, n=col.length/3; let red=0, blue=0;
                for(let i=0;i<n;i++){ const r=col[i*3],gg=col[i*3+1],b=col[i*3+2];
                    if(r>0.6 && gg<0.4) red++; if(b>0.5 && r<0.3) blue++; }
                return {빨강:red, 파랑:blue};
            }
            window.FAB_FEA.setScale('rel'); const rel = count();
            window.FAB_FEA.setScale('abs'); const abs = count();
            window.FAB_FEA.setScale('rel');
            return {상대스케일: rel, 항복스케일: abs};
        }""")
        print('11) 색 스케일     :', json.dumps(sc, ensure_ascii=False))
        leg = await pg.evaluate("() => window.FAB_FEA.els().leg.innerText")
        print('12) 범례 문구     :', ' / '.join([l.strip() for l in leg.split('\\n') if l.strip()]))

        await pg.screenshot(path=SHOT, full_page=False)

        # 10) 나머지 프리셋도 조건 생성이 되는지 (해석까지는 돌리지 않고 조건만)
        # 11) 스냅이 프리셋 의도 지점에서 얼마나 밀렸는지 — 많이 밀리면 프리셋 이름값을 못 한다
        snap = await pg.evaluate("""() => {
            const F = window.FAB_FEA, ms = F.targetMeshes();
            if(!ms.length) return null;
            const g = F._geo(ms);
            // boom 이 의도한 지점(정규좌표)과 실제 스냅 결과 거리
            const La = g.size[0] >= g.size[1] ? 0 : 1;
            const want = [];
            let u1=[0.5,0.30,1.0], u2=[0.5,0.70,1.0]; u1[La]=0.85; u2[La]=0.85;
            [u1,u2].forEach(u=>want.push(u));
            [[0.12,0.12,0],[0.88,0.12,0],[0.12,0.88,0],[0.88,0.88,0]].forEach(u=>want.push(u));
            const diag = Math.sqrt(g.size[0]**2+g.size[1]**2+g.size[2]**2);
            return want.map(u=>{
                const t=[0,1,2].map(d=>g.lo[d]+g.size[d]*u[d]);
                let bd=Infinity;
                for(let i=0;i<g.n;i++){
                    const dx=g.arr[i*3]-t[0], dy=g.arr[i*3+1]-t[1], dz=g.arr[i*3+2]-t[2];
                    const s=dx*dx+dy*dy+dz*dz; if(s<bd) bd=s;
                }
                return {의도: t.map(x=>Math.round(x)).join(','),
                        밀림mm: +Math.sqrt(bd).toFixed(1),
                        밀림pct: +(Math.sqrt(bd)/diag*100).toFixed(1)};
            });
        }""")
        print('13) 프리셋 스냅 밀림 (의도 지점 → 실제 정점):')
        for s in (snap or []):
            flag = '  ← 5% 초과' if s['밀림pct'] > 5 else ''
            print('      의도 %-14s 밀림 %6.1fmm (%.1f%%)%s' % (s['의도'], s['밀림mm'], s['밀림pct'], flag))

        print('14) 다른 프리셋 조건 생성:')
        for pid in ['self', 'lift', 'drill', 'auto']:
            c = await pg.evaluate("""(pid) => {
                const F = window.FAB_FEA, s = F.state;
                F.choosePreset(pid);
                const ps = F.presets.filter(x=>x.id===pid)[0];
                const ms = F.targetMeshes();
                if(!ms.length) return {오류:'대상없음'};
                try {
                    const bc = ps.build(F._geo ? F._geo(ms) : null, ps.defN||0);
                    return {고정: bc.fix.length, 하중: bc.load.length};
                } catch(e){ return {예외: String(e.message||e)}; }
            }""", pid)
            print('      %-6s' % pid, json.dumps(c, ensure_ascii=False))

        await br.close()
    print('페이지 오류:', errs if errs else '없음')
    print('스크린샷:', SHOT)

asyncio.run(main())
