# -*- coding: utf-8 -*-
"""Pass 8: 이미 만든 E500PRO 부재 전수 재검사 — '도면의 내부 홈 개수' vs 'OBJ의 관통구멍 개수'.

OBJ 쪽 개수는 오일러 지표로 센다: 판재를 관통하는 구멍이 N개면 위상적으로 종수(genus) N 이고
V - E + F = 2 - 2N 이다. 눈으로 세지 않고 수치로 확정할 수 있다.
도면 쪽 개수는 부재 외곽 안에 들어오는 (a) 같은 덩어리의 내부 루프 (b) 별개 덩어리의 닫힌 루프
(c) 원(CIRCLE) 을 모두 센다. (b) 가 이번에 발견된 누락 원인이다.
"""
import json, math, os, re, collections
import numpy as np, trimesh
from matplotlib.path import Path

SCR = os.path.dirname(os.path.abspath(__file__))
ROOT = r'E:\도진팩토리\3D스캔및티칭시스템'
FAB = ROOT + r'\fab_models'
TOL = 0.5
PN = re.compile(r'2110DN-\d{4}-\d{3,4}')

d1 = json.load(open(SCR + r'\index.json', encoding='utf-8'))
d2 = json.load(open(SCR + r'\index2.json', encoding='utf-8'))

segs = []
for l in d2['lines']:
    segs.append({'p0': (l[0], l[1]), 'p1': (l[2], l[3]), 'kind': 'L'})
for a in d2['arcs']:
    cx, cy, r, s, e = a[:5]
    segs.append({'p0': (cx + r*math.cos(math.radians(s)), cy + r*math.sin(math.radians(s))),
                 'p1': (cx + r*math.cos(math.radians(e)), cy + r*math.sin(math.radians(e))),
                 'kind': 'A', 'arc': (cx, cy, r, s, e)})

node = {}
def nid(p):
    k = (round(p[0]/TOL), round(p[1]/TOL))
    for dx in (-1, 0, 1):
        for dy in (-1, 0, 1):
            kk = (k[0]+dx, k[1]+dy)
            if kk in node: return node[kk]
    v = len(node); node[k] = v; return v
par = {}
def find(x):
    while par.get(x, x) != x: par[x] = par.get(par[x], par[x]); x = par[x]
    return x
def uni(a, b):
    ra, rb = find(a), find(b)
    if ra != rb: par[ra] = rb
for s in segs:
    a, b = nid(s['p0']), nid(s['p1'])
    par.setdefault(a, a); par.setdefault(b, b)
    s['a'], s['b'] = a, b; uni(a, b)
comp = collections.defaultdict(list)
for i, s in enumerate(segs):
    comp[find(s['a'])].append(i)
comps = []
for root, idxs in comp.items():
    pts = np.array([p for i in idxs for p in (segs[i]['p0'], segs[i]['p1'])], float)
    x0, y0 = pts.min(0); x1, y1 = pts.max(0)
    comps.append(dict(idxs=idxs, bbox=(x0, y0, x1, y1), w=x1-x0, h=y1-y0))

def area(p):
    a = np.array(p); x, y = a[:, 0], a[:, 1]
    return 0.5*abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))

def seg_pts(s, frm):
    if s['kind'] == 'A':
        cx, cy, r, a0, a1 = s['arc']
        if a1 < a0: a1 += 360
        t = np.radians(np.linspace(a0, a1, 24))
        pp = [(cx + r*math.cos(u), cy + r*math.sin(u)) for u in t]
        return pp if frm == s['a'] else pp[::-1]
    return [s['p0'], s['p1']] if frm == s['a'] else [s['p1'], s['p0']]

def outline(sub):
    alive = [True]*len(sub)
    while True:
        deg = collections.Counter()
        for i, s in enumerate(sub):
            if alive[i]: deg[s['a']] += 1; deg[s['b']] += 1
        drop = [i for i, s in enumerate(sub) if alive[i] and (deg[s['a']] < 2 or deg[s['b']] < 2)]
        if not drop: break
        for i in drop: alive[i] = False
    sub = [s for i, s in enumerate(sub) if alive[i]]
    if len(sub) < 3: return None, None
    adj = collections.defaultdict(list)
    for i, s in enumerate(sub):
        adj[s['a']].append((i, s['b'])); adj[s['b']].append((i, s['a']))
    used_s = set(); loops = []
    for start in range(len(sub)):
        if start in used_s: continue
        cyc = [start]; used_s.add(start)
        n0, cur = sub[start]['a'], sub[start]['b']; ok = False
        while True:
            nxt = [(i, o) for i, o in adj[cur] if i not in used_s]
            if not nxt:
                if any(i == start for i, o in adj[cur]) or cur == n0: ok = True
                break
            i, o = nxt[0]; used_s.add(i); cyc.append(i); cur = o
            if cur == n0: ok = True; break
        if ok and len(cyc) >= 3:
            pts = []; nd = n0
            for i in cyc:
                pp = seg_pts(sub[i], nd); pts.extend(pp[:-1])
                nd = sub[i]['b'] if nd == sub[i]['a'] else sub[i]['a']
            if len(pts) >= 3: loops.append(pts)
    if not loops: return None, None
    loops.sort(key=area, reverse=True)
    return loops[0], loops[1:]


def drawing_holes(ext):
    """OBJ 크기와 일치하는 덩어리를 찾아 도면상 내부 홈 개수를 센다."""
    w0, h0 = ext[0], ext[1]
    cand = [c for c in comps
            if abs(c['w']-w0)/max(w0, 1) < 0.01 and abs(c['h']-h0)/max(h0, 1) < 0.01]
    if not cand:
        return None, '도면에서 같은 크기의 덩어리를 못 찾음'
    c = cand[0]
    o, rest = outline([segs[i] for i in c['idxs']])
    if o is None:
        return None, '외곽 루프 재구성 실패'
    op = Path(np.array(o)); x0, y0, x1, y1 = c['bbox']
    n_same = sum(1 for l in (rest or [])
                 if area(l) > 100 and op.contains_points(np.array(l)).all())
    n_sep = 0
    for c2 in comps:
        if c2 is c: continue
        bx0, by0, bx1, by1 = c2['bbox']
        if not (x0 <= bx0 and by0 >= y0 and bx1 <= x1 and by1 <= y1): continue
        o2, rest2 = outline([segs[i] for i in c2['idxs']])
        for l in ([o2] if o2 else []) + (rest2 or []):
            if area(l) > 100 and op.contains_points(np.array(l)).all(): n_sep += 1
    n_cir = 0
    for _, _, cx, cy, r in d1['circles']:
        if not (x0 <= cx <= x1 and y0 <= cy <= y1): continue
        t = np.radians(np.linspace(0, 360, 48, endpoint=False))
        if op.contains_points(np.c_[cx + r*np.cos(t), cy + r*np.sin(t)]).all(): n_cir += 1
    return dict(같은덩어리=n_same, 별개덩어리=n_sep, 원=n_cir,
                합계=n_same + n_sep + n_cir), None


man = json.load(open(FAB + r'\manifest.json', encoding='utf-8'))
rows = []
for it in man['items']:
    f = it.get('file', '')
    if 'e500pro_' not in os.path.basename(f) and 'E500PRO' not in f:
        continue
    m0 = PN.search(it.get('name', '')) or PN.search(f)
    pn = m0.group() if m0 else f
    p = os.path.join(ROOT, f.replace('/', '\\'))
    if not os.path.exists(p):
        rows.append((pn, None, None, 'OBJ 파일 없음')); continue
    m = trimesh.load(p)
    V, E, F = len(m.vertices), len(m.edges_unique), len(m.faces)
    genus = int(round((2 - (V - E + F)) / 2))          # 관통구멍 개수
    dh, err = drawing_holes([float(v) for v in m.extents])
    rows.append((pn, genus, dh, err))

print('%-20s %8s %8s   %s' % ('도번', 'OBJ구멍', '도면홈', '판정'))
bad = []
for pn, genus, dh, err in rows:
    if dh is None:
        print('%-20s %8s %8s   [확인불가] %s' % (pn, genus, '-', err)); continue
    ok = (genus == dh['합계'])
    tag = 'OK' if ok else '### 불일치'
    if not ok: bad.append((pn, genus, dh))
    print('%-20s %8d %8d   %s  (같은덩어리%d/별개덩어리%d/원%d)'
          % (pn, genus, dh['합계'], tag, dh['같은덩어리'], dh['별개덩어리'], dh['원']))
print('\n총 %d개 중 불일치 %d개' % (len(rows), len(bad)))
json.dump([{'pn': p, 'obj': g, 'dwg': d} for p, g, d in bad],
          open(SCR + r'\hole_audit_bad.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
