# 애니메이션 2종 실제 동작 확인 (실제 제품 RJ102-0020 부재 1개)
import asyncio, time
from playwright.async_api import async_playwright

URL = "http://127.0.0.1:8099/fab.html"
SHOT_D = r"C:\Users\user\AppData\Local\Temp\dzw_fea\anim_deform.png"
SHOT_Y = r"C:\Users\user\AppData\Local\Temp\dzw_fea\anim_yield.png"


async def main():
    errs = []
    async with async_playwright() as p:
        br = await p.chromium.launch()
        pg = await br.new_page(viewport={'width': 1600, 'height': 950})
        pg.on('pageerror', lambda e: errs.append(str(e)))
        await pg.goto(URL, timeout=30000)
        await pg.wait_for_timeout(3000)

        await pg.evaluate("""async () => {
            const r = await (await fetch('fab_models/parts/index.json')).json();
            const blk = r.blocks.find(b => b.model === 'RJ102-0020');
            await window.FAB.loadBlock('RJ102-0020', blk.parts);
        }""")
        await pg.wait_for_timeout(1500)
        await pg.evaluate("() => window.FAB.selectMode()")
        b = await pg.locator('#fab-canvas').bounding_box()
        cx, cy = b['x'] + b['width'] * 0.5, b['y'] + b['height'] * 0.5
        # 부품 중심을 화면좌표로 투영해 확실히 맞힌다(픽셀 운에 의존하지 않게)
        pts = await pg.evaluate("""() => {
            const T = window.FAB.THREE, cam = window.FAB.camera;
            const dom = window.FAB.renderer.domElement, r = dom.getBoundingClientRect();
            const out = [];
            window.FAB.scene.traverse(o => {
                if (!o.isMesh || o.name === '__fab_edge' || !o.geometry.attributes) return;
                if (o.userData && o.userData.__feaOverlay) return;
                const g = o.geometry; g.computeBoundingSphere();
                if (!g.boundingSphere) return;
                const v = g.boundingSphere.center.clone();
                o.localToWorld(v); v.project(cam);
                out.push({name: o.name || 'unnamed',
                          x: r.left + (v.x + 1) / 2 * r.width,
                          y: r.top + (1 - v.y) / 2 * r.height});
            });
            return out;
        }""")
        picked = None
        for q in pts[:14]:
            await pg.mouse.click(q['x'], q['y'])
            await pg.wait_for_timeout(200)
            picked = await pg.evaluate("() => { const s = window.FAB_FEA.findSelected(); const p = window.FAB_FEA.state.clickedPart; return (s && p) ? (p.name || 'unnamed') : null; }")
            if picked: break
        print('0) 클릭한 부품        :', picked)

        await pg.locator('#fab-fea-btn').click()
        await pg.wait_for_timeout(600)
        print('1) 범위/예상시간 표시 :', await pg.evaluate(
            "() => { const e=window.FAB_FEA; return document.getElementById('fab-fea-panel').innerText.split('\\n').filter(s=>s.indexOf('대상')>=0)[0] || '(없음)'; }"))
        print('   해석 대상 부품 수  :', await pg.evaluate("() => window.FAB_FEA.targetMeshes().length"),
              '/ 전체', await pg.evaluate("() => window.FAB_FEA.state.clickedPart ? 22 : '?'"))

        # 조건은 API 로 직접 넣는다(클릭→찍기 경로는 _fea_click_test.py 에서 따로 검증).
        #   선택한 부재의 실제 정점 양 끝을 고정점/하중점으로 쓴다.
        await pg.evaluate("""() => {
            const F = window.FAB_FEA, st = F.state, m = st.clickedPart;
            const a = m.geometry.attributes.position;
            let lo = null, hi = null, lox = 1e30, hix = -1e30;
            for (let i = 0; i < a.count; i++) {
                const x = a.array[i*3], y = a.array[i*3+1], z = a.array[i*3+2];
                if (x < lox) { lox = x; lo = [x, y, z]; }
                if (x > hix) { hix = x; hi = [x, y, z]; }
            }
            st.fixPoints = [{p: lo, pw: lo}];
            st.loadPoints = [{p: hi, pw: hi, dir: [0, 0, -1], dirw: [0, 0, -1], N: 1000}];
        }""")
        print('   고정점 :', await pg.evaluate("() => window.FAB_FEA.state.fixPoints.length"), '곳 / 하중점 :',
              await pg.evaluate("() => window.FAB_FEA.state.loadPoints.length"), '곳')
        t0 = time.time()
        await pg.evaluate("() => window.FAB_FEA.run()")
        for _ in range(80):
            if await pg.evaluate("() => !!window.FAB_FEA.state.lastResult"):
                break
            await pg.wait_for_timeout(500)
        wall = time.time() - t0
        r = await pg.evaluate("() => window.FAB_FEA.state.lastResult")
        if not r:
            txt = await pg.locator('#fab-fea-panel').inner_text()
            print('   [실패]', ' | '.join(txt.split(chr(10))[-6:]))
            await br.close(); return
        print('2) 해석 완료          : %.1f초 / 요소 %d / 절점 %d'
              % (wall, r['mesh']['elements'], r['mesh']['nodes']))
        print('   최대변위/최대응력   : %.4f mm / %.1f MPa (항복대비 %.0f%%)'
              % (r['result']['max_disp_mm'], r['result']['max_vonmises_MPa'], r['result']['yield_ratio_pct']))
        print('   가한 하중          : %.0f N' % r['applied_N'])
        print('   disp 배열 길이     : %d (절점 %d x3)' % (len(r['viz']['disp']), r['mesh']['nodes']))

        # ── B-1 변형 애니메이션 ──
        print('3) 변형 애니메이션')
        print('   자동 과장배율      :', await pg.evaluate("() => window.FAB_FEA.anim.scale") , '배')
        await pg.evaluate("() => window.FAB_FEA.play('deform')")
        await pg.wait_for_timeout(900)
        moved = await pg.evaluate("""() => {
            const a = window.FAB_FEA.anim;
            const cur = a.geo.attributes.position.array;
            let d = 0;
            for (let i = 0; i < cur.length; i++) d = Math.max(d, Math.abs(cur[i] - a.base[i]));
            return d;
        }""")
        print('   재생 중 실제 좌표 이동량: %.3f mm (0 이면 안 움직인 것)' % moved)
        print('   화면 안내문         :', ' / '.join(
            (await pg.evaluate("() => window.FAB_FEA.els().animInfo.innerText")).split('\n')))
        await pg.wait_for_timeout(700)
        await pg.screenshot(path=SHOT_D)
        await pg.evaluate("() => window.FAB_FEA.stop()")
        restored = await pg.evaluate("""() => {
            const a = window.FAB_FEA.anim; const cur = a.geo.attributes.position.array;
            let d = 0; for (let i = 0; i < cur.length; i++) d = Math.max(d, Math.abs(cur[i] - a.base[i]));
            return d;
        }""")
        print('   정지 후 복원 오차   : %.6f mm (0 이어야 원상복구)' % restored)

        # ── B-2 항복 확산 ──
        print('4) 항복 확산 애니메이션')
        await pg.evaluate("() => window.FAB_FEA.play('yield')")
        seen = []
        for k in range(11):
            await pg.wait_for_timeout(1000)
            seen.append(await pg.evaluate("() => window.FAB_FEA.els().animInfo.innerText.split(String.fromCharCode(10))[0]"))
        for i, ln in enumerate(seen):
            print('   %2d초: %s' % (i + 1, ln))
        changed = await pg.evaluate("() => window.FAB_FEA.anim.geo.attributes.color.version > 0")
        print('   색상 갱신 여부      :', changed)
        await pg.screenshot(path=SHOT_Y)
        await pg.evaluate("() => window.FAB_FEA.stop()")

        print('5) 먼저 항복하는 순서 (패널 표시)')
        txt = await pg.locator('#fab-fea-panel').inner_text()
        for line in txt.split('\n'):
            if '항복' in line and ('N 에서' in line or '순서' in line):
                print('   ', line.strip())
        await br.close()
    print('페이지 오류:', errs if errs else '없음')
    print('캡처:', SHOT_D, '/', SHOT_Y)

asyncio.run(main())
