# 제관 복사·붙여넣기 게이트 (2026-07-20)
# 1) fab.html 로드 → JS 예외 0
# 2) ROCK_SAW_1980 블럭 로드 → mesh 6개 확인
# 3) 전체 복제 → mesh 12개(2배)·복제본 1 확인
# 4) [핵심] fab_scene 과 '동일한' 레이캐스트로 복제본 부품이 실제로 선택되는지 검증
#    (tc.attach 우회가 아니라, 복제본 부품 world중심을 NDC로 투영 → intersectObject(root,true) → 히트가 복제 group 자손인지)
# 5) 복제본 부품 이동 가능 확인 → Delete 로 복제본 제거 → mesh 6개 복귀
import json, os, sys, subprocess, time
from playwright.sync_api import sync_playwright

D = r"E:\도진팩토리\3D스캔및티칭시스템"
PORT = 8091
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,
          "mesh_after_copy": None, "clone_count": None, "pick_hits_clone": None,
          "moved_ok": None, "mesh_after_delete": None, "clone_after_delete": 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_COPY", timeout=30000)
        pg.wait_for_timeout(1200)

        # 2) 블럭 로드
        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_timeout(2500)
        result["mesh_after_load"] = pg.evaluate("() => window.FAB_COPY.meshCount")

        # 3) 전체 복제
        pg.evaluate("() => window.FAB_COPY.copyWholeBlock()")
        pg.wait_for_timeout(600)
        result["mesh_after_copy"] = pg.evaluate("() => window.FAB_COPY.meshCount")
        result["clone_count"] = pg.evaluate("() => window.FAB_COPY.cloneCount")

        # 4) 핵심: fab_scene 과 동일 경로 레이캐스트로 복제본 선택 검증
        #    복제 group(마지막에 붙은 clones 원소)의 한 부품 world중심 → 카메라 투영 NDC → intersectObject(root,true)
        pick = pg.evaluate("""() => {
            const T = window.THREE, FAB = window.FAB, CP = window.FAB_COPY;
            const root = CP.root;
            // 복제 group 찾기 (clones set 의 원소 중 root 자식 group)
            let cloneUnit = null;
            CP._clones.forEach(u => { if (u.parent === root) cloneUnit = u; });
            if (!cloneUnit) return {err:'no clone unit'};
            const part = cloneUnit.children[0];
            // 카메라 얻기: 씬 렌더 루프의 카메라는 tc.camera 로 접근
            const tc = CP._findTC();
            const cam = tc.camera;
            // 복제 부품 world 중심
            const box = new T.Box3().setFromObject(part);
            const c = new T.Vector3(); box.getCenter(c);
            const ndc = c.clone().project(cam);
            const ray = new T.Raycaster();
            ray.setFromCamera({x:ndc.x, y:ndc.y}, cam);
            const hits = ray.intersectObject(root, true);   // ★ fab_scene 과 완전히 동일한 호출
            if (!hits.length) return {err:'no hit', ndc:[ndc.x,ndc.y]};
            const hit = hits[0].object;
            // 히트가 복제 group 의 자손인지
            let a = hit, inClone = false;
            while (a) { if (a === cloneUnit) { inClone = true; break; } a = a.parent; }
            return { hitName: hit.name, inClone: inClone };
        }""")
        result["pick_hits_clone"] = pick

        # 5) 복제본 부품 선택(tc.attach 로 선택 상태 재현) → 이동 → Delete
        moved = pg.evaluate("""() => {
            const CP = window.FAB_COPY, root = CP.root;
            let cloneUnit = null;
            CP._clones.forEach(u => { if (u.parent === root) cloneUnit = u; });
            const part = cloneUnit.children[0];
            const tc = CP._findTC();
            tc.attach(part);
            const before = part.position.x;
            part.position.x += 500;   // 끌어내기(이동) 시뮬레이션
            document.getElementById('fab-selname').textContent = part.name;
            return { attached: tc.object === part, moved: part.position.x !== before };
        }""")
        result["moved_ok"] = moved

        pg.evaluate("() => window.FAB.fit && window.FAB.fit()")
        pg.wait_for_timeout(400)
        try: os.makedirs(os.path.join(D, "shots"), exist_ok=True)
        except Exception: pass
        pg.screenshot(path=os.path.join(D, "shots", "fab_copy.png"))

        # Delete (복제본 제거)
        pg.evaluate("() => window.FAB_COPY.deleteSelected()")
        pg.wait_for_timeout(500)
        result["mesh_after_delete"] = pg.evaluate("() => window.FAB_COPY.meshCount")
        result["clone_after_delete"] = pg.evaluate("() => window.FAB_COPY.cloneCount")
        b.close()
finally:
    srv.terminate()

pick = result["pick_hits_clone"] or {}
moved = result["moved_ok"] or {}
ok = (len(result["js_exceptions"]) == 0
      and result["mesh_after_load"] == 6
      and result["mesh_after_copy"] == 12
      and result["clone_count"] == 1
      and pick.get("inClone") is True
      and moved.get("attached") is True and moved.get("moved") is True
      and result["mesh_after_delete"] == 6
      and result["clone_after_delete"] == 0)

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