# -*- coding: utf-8 -*-
# [2026-07-22] 판 과선택 해소 실측 — CEO "판 1장이 아니라 제품이 통째로 잡힌다"
# 진짜 마우스로 서로 다른 넓은 면 3곳을 클릭해, 선택영역 bbox 를 측정한다.
# 판정: 최소축 <= 30mm (판 1장), 최대축 >= 300mm (쓸 수 있는 판), 클릭점-선택영역 거리 = 0
import sys, os, json
from playwright.sync_api import sync_playwright

URL = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8090/DOZIKWORKS_OS_v5_locked.dc.html"

FIND_HITS = r"""() => {
  const a = window.__dzw;
  const cam = a.three.cam || a.three.camera, dom = a.three.rnd.domElement;
  const rect = dom.getBoundingClientRect();
  const RC = (window.THREE && THREE.Raycaster) ? THREE.Raycaster : a._ray.constructor;
  const ray = new RC();
  const hits = [];
  for (let gy = 0.15; gy <= 0.86; gy += 0.04) {
    for (let gx = 0.15; gx <= 0.86; gx += 0.04) {
      const cx = rect.left + rect.width * gx, cy = rect.top + rect.height * gy;
      const mx = ((cx - rect.left) / rect.width) * 2 - 1;
      const my = -((cy - rect.top) / rect.height) * 2 + 1;
      ray.setFromCamera({x: mx, y: my}, cam);
      const h = ray.intersectObjects([a._knuckleMesh], true);
      if (h.length) {
        const n = h[0].face.normal;
        hits.push({x: cx, y: cy, nx: n.x, ny: n.y, nz: n.z, fi: h[0].faceIndex});
      }
    }
  }
  return hits;
}"""

# 선택영역 bbox + 클릭점과 선택영역 최단거리 (mm)
MEASURE = r"""(fi) => {
  const a = window.__dzw, E = window.DZW5_EDGEPATH;
  const reg = a._faceARegion;
  if (!reg || !reg.size) return {err: 'empty region'};
  const ex = E._regionExtent(a._knuckleMesh, reg);
  const pos = a._knuckleMesh.geometry.attributes.position;
  const p = a._planeAHit && a._planeAHit.point;
  let dmin = -1, hasSeed = null;
  if (p) {
    const inv = a._knuckleMesh.matrixWorld.clone().invert();
    const lp = p.clone().applyMatrix4(inv);
    // 클릭 삼각형이 선택영역에 실제로 들어있는가 (f882cb2 번호변환 유지 확인)
    hasSeed = reg.has(E._origFaceIndex(a._knuckleMesh, fi));
    // 클릭점 → 선택영역 삼각형까지의 진짜 최단거리 (정점거리 아님: 판이 거대 삼각형 몇 장이라 정점은 수백mm 떨어져 있다)
    const V = THREE.Vector3, tri = new THREE.Triangle(new V(), new V(), new V()), cp = new V();
    dmin = Infinity;
    for (const t of reg) {
      const i = t*3;
      tri.a.set(pos.getX(i), pos.getY(i), pos.getZ(i));
      tri.b.set(pos.getX(i+1), pos.getY(i+1), pos.getZ(i+1));
      tri.c.set(pos.getX(i+2), pos.getY(i+2), pos.getZ(i+2));
      tri.closestPointToPoint(lp, cp);
      const d = cp.distanceTo(lp); if (d < dmin) dmin = d;
    }
  }
  return {n: reg.size, dx: ex.dx, dy: ex.dy, dz: ex.dz, min: ex.min, max: ex.max, clickDist: dmin, hasSeed: hasSeed};
}"""

# 비교용: 수정 전 방식(그룹 전체)으로 같은 seed 를 선택했을 때의 bbox
OLD_MEASURE = r"""(fi) => {
  const a = window.__dzw, E = window.DZW5_EDGEPATH;
  const orig = E._origFaceIndex(a._knuckleMesh, fi);
  const reg = E._selectGroupRegion(a, a._knuckleMesh, orig);
  const ex = E._regionExtent(a._knuckleMesh, reg);
  return {n: reg.size, min: ex.min, max: ex.max, dx: ex.dx, dy: ex.dy, dz: ex.dz,
          grp: a._faceGroupId ? a._faceGroupId[orig] : -1};
}"""

READ = ("() => ({mode: window.__dzw._faceMode, "
        "a: window.__dzw._faceARegion?window.__dzw._faceARegion.size:0})")


def click_el(pg, sel):
    loc = pg.locator(sel).first
    try:
        box = loc.bounding_box()
    except Exception:
        return False
    if not box or box["width"] == 0:
        return False
    pg.mouse.click(box["x"] + box["width"]/2, box["y"] + box["height"]/2)
    return True


def click_text(pg, text, exact=True):
    loc = pg.get_by_text(text, exact=exact)
    for i in range(loc.count()):
        try:
            box = loc.nth(i).bounding_box()
        except Exception:
            continue
        if box and box["width"] > 0:
            pg.mouse.click(box["x"] + box["width"]/2, box["y"] + box["height"]/2)
            return True
    return False


def pick_three(hits):
    """법선이 서로 확실히 다른 넓은 면 3곳 (같은 판 중복 방지)"""
    picked = []
    for h in hits:
        if all(abs(h["nx"]*q["nx"] + h["ny"]*q["ny"] + h["nz"]*q["nz"]) < 0.7 for q in picked):
            picked.append(h)
        if len(picked) == 3:
            break
    i = 0
    while len(picked) < 3 and i < len(hits):
        if hits[i] not in picked:
            picked.append(hits[i])
        i += 1
    return picked


def main():
    rows = []
    with sync_playwright() as p:
        br = p.chromium.launch(headless=True)
        pg = br.new_page(viewport={"width": 1600, "height": 900})
        errs = []
        pg.on("console", lambda m: errs.append(m.text) if m.type == "error" else None)
        pg.goto(URL)
        pg.wait_for_function("window.__dzw && window.__dzw._knuckleMesh && window.DZW5_EDGEPATH && window.__dzw.three", timeout=60000)
        pg.wait_for_timeout(5000)
        if pg.evaluate("() => { const w=document.getElementById('dzwb-wrap'); return !!(w && w.className.includes('shown')); }"):
            click_el(pg, "#dzwb-x"); pg.wait_for_timeout(800)
        click_text(pg, "FIT"); pg.wait_for_timeout(1500)

        hits = pg.evaluate(FIND_HITS)
        if not hits:
            print("FAIL: raycast 히트 없음"); br.close(); return 1
        pts = pick_three(hits)

        # 선택툴 → 제품 클릭 → 티칭패널 → 정합 탭
        click_el(pg, '[data-dzw-pick="select"]'); pg.wait_for_timeout(500)
        pg.mouse.click(pts[0]["x"], pts[0]["y"]); pg.wait_for_timeout(1000)
        click_el(pg, '.dzw-tp-tab[data-tab="align"]'); pg.wait_for_timeout(500)

        for i, pt in enumerate(pts, 1):
            click_el(pg, "#dzw-edge-reset-btn"); pg.wait_for_timeout(600)
            st = pg.evaluate(READ)
            if st["mode"] != 1:
                click_el(pg, "#dzw-curve-mode-btn"); pg.wait_for_timeout(500)
            old = pg.evaluate(OLD_MEASURE, pt["fi"])
            pg.mouse.move(pt["x"], pt["y"]); pg.mouse.down(); pg.mouse.up()
            pg.wait_for_timeout(1500)
            new = pg.evaluate(MEASURE, pt["fi"])
            # 철칙 제5조 — CEO가 눈으로 확인할 수 있게 클릭마다 화면 저장
            outdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plate_shot_260722")
            os.makedirs(outdir, exist_ok=True)
            pg.screenshot(path=os.path.join(outdir, f"click{i}_red.png"))
            rows.append({"click": i, "screen": [round(pt["x"]), round(pt["y"])], "old": old, "new": new})

        br.close()

    print(json.dumps(rows, ensure_ascii=False, indent=2))
    print("\n{:<6}{:<34}{:<34}{}".format("클릭", "수정전(그룹전체)", "수정후(평면1장)", "판정"))
    ok_all = True
    for r in rows:
        o, n = r["old"], r["new"]
        if "err" in n:
            print(f"{r['click']:<6}{'-':<34}{n['err']:<34}FAIL"); ok_all = False; continue
        ok = n["min"] <= 30.0 and n["max"] >= 300.0 and n["clickDist"] <= 1.0 and n["hasSeed"]
        ok_all = ok_all and ok
        print("{:<6}{:<34}{:<34}{}".format(
            r["click"],
            f"{o['n']}면 최소{o['min']:.0f} 최대{o['max']:.0f}mm",
            f"{n['n']}면 최소{n['min']:.0f} 최대{n['max']:.0f}mm d={n['clickDist']:.2f} seed={n['hasSeed']}",
            "PASS" if ok else "FAIL"))
    print("\n전체:", "PASS" if ok_all else "FAIL")
    return 0 if ok_all else 1


if __name__ == "__main__":
    sys.exit(main())
