# 제관 정본 계층 트리 — 픽/분해 신계약 게이트 (2026-07-21 재작성)
# 옛 계약(pivot·objectChange·__fabPickPivot·userData.isoPath 주입)은 폐기.
# 신계약: 계층이 실제 씬그래프(중첩 THREE.Group)로 존재한다.
#   1) 블럭 '1톤-복합식준설차-설우3D' 로드 → root.children===12, meshesOf===55, pathOf(p001_1)===['G01','G01-1']
#   2) depth0 픽 → 루트 그룹 반환(제품 전체)
#   3) G01 진입 → 픽이 실제 Group 반환(피벗 아님), gizmo 네이티브 이동(group.position.x+=300)으로 자식 부품 이동
#   4) 최상위 분해 = 12 유닛(블록), t=0 정확복귀
#   5) exitAll → depth0 픽 다시 루트
#   6) 통짜(단일 mesh) 블럭 '06_Zone_-_Housing' → depth0 픽이 루트 그룹(부품 아님), 분해 차단
#   7) JS 예외 0
import json, os, sys, subprocess, time
from playwright.sync_api import sync_playwright

D = r"E:\도진팩토리\3D스캔및티칭시스템"
PORT = 8097
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": [],
          "struct": None, "whole": None, "enter_pick": None, "explode": None,
          "exit_restore": None, "single_mesh": 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_ISO && window.FAB_ISO.pickTarget && window.FAB_EXP && window.FAB_EXP.setOpen)",
            timeout=30000)
        pg.wait_for_timeout(1200)

        # 1) 블럭 로드 + 구조 (path 는 로드시 씬그래프로 조립 → children===12 를 준비신호로 폴링)
        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==='1톤-복합식준설차-설우3D');
            await window.FAB.loadBlock(blk.model, blk.parts);
        }""")
        pg.wait_for_function(
            "() => { const r=window.FAB_ISO._dbg.getRoot(); return r && r.children.length===12; }",
            timeout=30000)
        pg.wait_for_timeout(400)

        result["struct"] = pg.evaluate("""() => {
            const DB = window.FAB_ISO._dbg, root = DB.getRoot();
            const p1 = DB.meshesOf(root).find(m => m.name === 'p001_1');
            return { children: root.children.length, meshes: DB.meshesOf(root).length,
                     fabRoot: !!root.userData.__fabRoot,
                     pathP001_1: p1 ? DB.pathOf(p1) : null };
        }""")

        # 2) depth0 전체선택 → 루트 그룹
        result["whole"] = pg.evaluate("""() => {
            const ISO = window.FAB_ISO, DB = ISO._dbg, root = DB.getRoot();
            const m = DB.meshesOf(root)[0];
            const t = window.__fabPickTarget(m);
            const tc = DB.getTC(); tc.attach(t);
            return { isoLen: ISO.state.isoPath.length, isRoot: t === root,
                     isGroup: !!(t && t.isGroup), tcIsGroup: tc.object === root, notMesh: t !== m };
        }""")

        # 3) G01 진입 + 스코프 내 픽 → 실제 Group, 네이티브 이동
        result["enter_pick"] = pg.evaluate("""() => {
            const ISO = window.FAB_ISO, DB = ISO._dbg, THREE = window.THREE, root = DB.getRoot();
            const target = DB.meshesOf(root).find(m => DB.pathOf(m)[0] === 'G01');
            const entered = ISO.enterAt(target);
            const isoLen = ISO.state.isoPath.length;
            const ctx = DB.contextNode();
            const t = window.__fabPickTarget(target);
            const tc = DB.getTC(); tc.attach(t);
            const isGroup = !!(t && t.isGroup);
            const notPivot = !(t && t.name === '__fabPickPivot');
            const underCtx = (t.parent === ctx);
            // 네이티브 이동: 픽한 Group 의 position 을 옮기면 자식 부품 월드좌표가 함께 이동
            const child = DB.meshesOf(t)[0];
            const before = new THREE.Vector3(); child.getWorldPosition(before);
            const others = DB.meshesOf(root).filter(m => DB.pathOf(m)[0] !== 'G01');
            const beforeOther = others.map(m => { const v=new THREE.Vector3(); m.getWorldPosition(v); return v; });
            t.position.x += 300; root.updateMatrixWorld(true);
            const after = new THREE.Vector3(); child.getWorldPosition(after);
            const moved = Math.abs(after.x - before.x - 300) < 1;
            const othersStill = others.every((m,i)=>{ const v=new THREE.Vector3(); m.getWorldPosition(v); return v.distanceTo(beforeOther[i]) < 0.01; });
            t.position.x -= 300;
            return { entered, isoLen, isGroup, notPivot, underCtx, tcSet: tc.object === t, moved, othersStill };
        }""")

        # 4) 최상위 분해 = 12 유닛 + 0 복귀
        result["explode"] = pg.evaluate("""() => {
            const ISO = window.FAB_ISO, DB = ISO._dbg, EXP = window.FAB_EXP, THREE = window.THREE;
            ISO.exitAll();
            const root = DB.getRoot(), meshes = DB.meshesOf(root);
            const wp = o => { const v=new THREE.Vector3(); o.getWorldPosition(v); return v; };
            const home = meshes.map(wp);
            EXP.setOpen(true);
            const units = EXP._st.units ? EXP._st.units.length : -1;
            EXP.applyT(1.0);
            const moved = meshes.map(wp).some((v,i)=>v.distanceTo(home[i]) > 10);
            EXP.applyT(0);
            const exact = meshes.map(wp).every((v,i)=>v.distanceTo(home[i]) < 1e-6);
            EXP.setOpen(false);
            return { units, moved, exact };
        }""")

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

        # 5) exitAll → 전체선택 복귀
        result["exit_restore"] = pg.evaluate("""() => {
            const ISO = window.FAB_ISO, DB = ISO._dbg;
            ISO.exitAll();
            const root = DB.getRoot();
            const t = window.__fabPickTarget(DB.meshesOf(root)[0]);
            return { isoLen: ISO.state.isoPath.length, isRoot: t === root };
        }""")

        # 6) 통짜(단일 mesh) 블럭
        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==='06_Zone_-_Housing');
            await window.FAB.loadBlock(blk.model, blk.parts);
        }""")
        pg.wait_for_timeout(2000)
        result["single_mesh"] = pg.evaluate("""() => {
            const ISO = window.FAB_ISO, DB = ISO._dbg, EXP = window.FAB_EXP, root = DB.getRoot();
            const meshes = DB.meshesOf(root);
            const t = window.__fabPickTarget(meshes[0]);
            EXP.setOpen(true);   // 통짜 → 차단(open=false 유지)
            return { parts: meshes.length, isoLen: ISO.state.isoPath.length,
                     isRoot: t === root, isGroup: !!(t && t.isGroup), notMesh: t !== meshes[0],
                     explodeBlocked: EXP.open === false };
        }""")
        pg.screenshot(path=os.path.join(D, "shots", "fab_pick_single.png"))
        b.close()
finally:
    srv.terminate()

st = result["struct"] or {}
w  = result["whole"] or {}
ep = result["enter_pick"] or {}
ex = result["explode"] or {}
er = result["exit_restore"] or {}
sm = result["single_mesh"] or {}

ok = (len(result["js_exceptions"]) == 0
      and st.get("children") == 12 and st.get("meshes") == 55
      and st.get("fabRoot") is True and st.get("pathP001_1") == ["G01", "G01-1"]
      and w.get("isoLen") == 0 and w.get("isRoot") is True and w.get("isGroup") is True
      and w.get("tcIsGroup") is True and w.get("notMesh") is True
      and ep.get("entered") is True and ep.get("isoLen") == 1
      and ep.get("isGroup") is True and ep.get("notPivot") is True and ep.get("underCtx") is True
      and ep.get("tcSet") is True and ep.get("moved") is True and ep.get("othersStill") is True
      and ex.get("units") == 12 and ex.get("moved") is True and ex.get("exact") is True
      and er.get("isoLen") == 0 and er.get("isRoot") is True
      and sm.get("parts") == 1 and sm.get("isRoot") is True and sm.get("isGroup") is True
      and sm.get("notMesh") is True and sm.get("explodeBlocked") is True)

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