# 제관 조립 순서 애니메이션 게이트 (2026-07-20)
# 1) fab.html 로드 → JS 예외 0
# 2) ROCK_SAW_1980 블럭 로드 → mesh 6개 + assembly.json 스텝 6개 인식
# 3) 재생 시작 시 전 부품 숨김(S(0)) 확인
# 4) playAssembly 완료까지 대기(wait_for_function !playing) → 재생 중 스크린샷
# 5) [핵심] 종료 후 각 부품 position 이 로드 직후 원본 조립좌표와 일치(부동소수 오차 내)
# 6) prev/next 수동 스텝, restart 동작 확인
import json, os, sys, subprocess, time
from playwright.sync_api import sync_playwright

D = r"E:\도진팩토리\3D스캔및티칭시스템"
PORT = 8094
URL = "http://localhost:%d/fab.html" % PORT

srv = subprocess.Popen([sys.executable, "-m", "http.server", str(PORT)],
                       cwd=D, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(1.5)

result = {"js_exceptions": [], "ext_noise": [], "mesh_after_load": None,
          "step_count": None, "hidden_at_start": None, "played_ok": None,
          "pos_match": None, "max_dev": None, "next_step": None, "prev_step": None,
          "restart_step": None}

def _con(m):
    if m.type == "error":
        t = m.text
        if "ERR_CONNECTION_REFUSED" in t or "Failed to load resource" in t:
            result["ext_noise"].append(t)
        else:
            result["js_exceptions"].append("CONSOLE:" + t)

try:
    with sync_playwright() as p:
        b = p.chromium.launch()
        pg = b.new_page(viewport={"width": 1400, "height": 900})
        pg.on("pageerror", lambda e: result["js_exceptions"].append(str(e)))
        pg.on("console", _con)
        pg.goto(URL)
        pg.wait_for_function("window.FAB && window.FAB.loadBlock && window.FAB_ASM", timeout=30000)
        pg.wait_for_timeout(1000)

        # 2) 블럭 로드 (assembly.json 은 loadBlock 감싸기에서 자동 로드)
        pg.evaluate("""async () => {
            const r = await fetch('fab_models/parts/index.json',{cache:'no-store'});
            const d = await r.json();
            const blk = d.blocks.find(x=>x.model==='ROCK_SAW_1980');
            await window.FAB.loadBlock(blk.model, blk.parts);
        }""")
        pg.wait_for_function("window.FAB_ASM.stepCount > 0", timeout=10000)
        pg.wait_for_timeout(500)

        result["mesh_after_load"] = pg.evaluate(
            "() => { let n=0; window.FAB_ASM._group.traverse(o=>{if(o.isMesh)n++}); return n; }")
        result["step_count"] = pg.evaluate("() => window.FAB_ASM.stepCount")

        # 로드 직후 원본 조립좌표 포착 (ground truth) — 부품명별 position
        orig_pos = pg.evaluate("""() => {
            const out = {};
            window.FAB_ASM._group.children.forEach(o => {
                if (o.isMesh) out[o.name] = [o.position.x, o.position.y, o.position.z];
            });
            return out;
        }""")

        # 3) S(0) 스냅(restart)에서 전 부품 숨김 확인 (재생 시작 직전 상태)
        pg.evaluate("() => window.FAB_ASM.restart()")
        pg.wait_for_timeout(150)
        result["hidden_at_start"] = pg.evaluate("""() => {
            return window.FAB_ASM._group.children.filter(o=>o.isMesh).every(o=>o.visible===false)
                   && window.FAB_ASM.step===0;
        }""")

        # 재생 시작
        pg.evaluate("() => window.FAB_ASM.playAssembly()")
        pg.wait_for_timeout(120)

        # 4) 재생 중 스크린샷 → 완료 대기
        pg.wait_for_timeout(1800)
        try: os.makedirs(os.path.join(D, "shots"), exist_ok=True)
        except Exception: pass
        pg.screenshot(path=os.path.join(D, "shots", "fab_assembly_playing.png"))
        pg.wait_for_function("!window.FAB_ASM.playing", timeout=30000)
        pg.wait_for_timeout(300)
        result["played_ok"] = True

        # 5) 종료 후 좌표 일치 검증
        end_pos = pg.evaluate("""() => {
            const out = {};
            window.FAB_ASM._group.children.forEach(o => {
                if (o.isMesh) out[o.name] = [o.position.x, o.position.y, o.position.z];
            });
            return out;
        }""")
        max_dev = 0.0
        for name, op in orig_pos.items():
            ep = end_pos.get(name)
            if not ep: max_dev = 9e9; break
            for a, bb in zip(op, ep):
                max_dev = max(max_dev, abs(a - bb))
        result["max_dev"] = max_dev
        result["pos_match"] = max_dev < 1e-3
        all_visible = pg.evaluate(
            "() => window.FAB_ASM._group.children.filter(o=>o.isMesh).every(o=>o.visible===true)")

        # 6) 수동 스텝: restart → next → prev
        pg.evaluate("() => window.FAB_ASM.restart()")
        pg.wait_for_timeout(200)
        result["restart_step"] = pg.evaluate("() => window.FAB_ASM.step")   # 기대 0
        pg.evaluate("() => window.FAB_ASM.next()")
        pg.wait_for_function("!window.FAB_ASM.playing && window.FAB_ASM.step===1", timeout=6000)
        result["next_step"] = pg.evaluate("() => window.FAB_ASM.step")       # 기대 1
        pg.evaluate("() => window.FAB_ASM.prev()")
        pg.wait_for_timeout(300)
        result["prev_step"] = pg.evaluate("() => window.FAB_ASM.step")       # 기대 0

        b.close()
finally:
    srv.terminate()

ok = (len(result["js_exceptions"]) == 0
      and result["mesh_after_load"] == 6
      and result["step_count"] == 6
      and result["hidden_at_start"] is True
      and result["played_ok"] is True
      and result["pos_match"] is True
      and result["restart_step"] == 0
      and result["next_step"] == 1
      and result["prev_step"] == 0)

print(json.dumps(result, ensure_ascii=False, indent=1))
print("RESULT:", "PASS" if ok else "FAIL")
sys.exit(0 if ok else 1)
