# -*- coding: utf-8 -*-
"""2110DN-0018-001 PLATE (SM490 12T) 외곽/홀 추출 → 12mm 압출 → OBJ.
그래프 가지치기(degree-1 제거)로 중심선·지시선·중심마크를 걷어내고, 남은 루프만 사용."""
import json, math, collections, numpy as np

SCR = r'C:\Users\user\AppData\Local\Temp\claude\E-----Claude--20260728-claude-code-2-1-219\8defc145-8ae5-48d0-9058-355f3d9ae8ed\scratchpad'
OUT = r'E:\도진팩토리\3D스캔및티칭시스템\fab_models\E500PRO_부재시범.obj'
X0, Y0, X1, Y1 = 297150, 5540, 298700, 5900
THK = 12.0
TOL = 0.3

d1 = json.load(open(SCR + r'\index.json', encoding='utf-8'))
d2 = json.load(open(SCR + r'\index2.json', encoding='utf-8'))

def inw(x, y): return X0 <= x <= X1 and Y0 <= y <= Y1

# ---- 영역 내 세그먼트 수집 (LINE, ARC) ----
segs = []   # dict(p0,p1,kind,arc=(cx,cy,r,a0,a1))
for l in d2['lines']:
    if inw(l[0], l[1]) and inw(l[2], l[3]):
        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]
    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)))
    if inw(p0[0], p0[1]) and inw(p1[0], p1[1]):
        segs.append({'p0': p0, 'p1': p1, 'kind': 'A', 'arc': (cx, cy, r, s, e)})
print('region segs:', len(segs))

# ---- 노드 부여 ----
nodes = []
def nid(p):
    for i, q in enumerate(nodes):
        if abs(q[0]-p[0]) < TOL and abs(q[1]-p[1]) < TOL: return i
    nodes.append(p); return len(nodes)-1
for s in segs:
    s['a'] = nid(s['p0']); s['b'] = nid(s['p1'])

# ---- degree-1 반복 제거 (중심선/지시선/중심마크 제거) ----
alive = [True]*len(segs)
while True:
    deg = collections.Counter()
    for i, s in enumerate(segs):
        if alive[i]: deg[s['a']] += 1; deg[s['b']] += 1
    drop = [i for i, s in enumerate(segs) if alive[i] and (deg[s['a']] < 2 or deg[s['b']] < 2)]
    if not drop: break
    for i in drop: alive[i] = False
segs = [s for i, s in enumerate(segs) if alive[i]]
print('after pruning:', len(segs))

# ---- 루프 탐색 ----
adj = collections.defaultdict(list)
for i, s in enumerate(segs):
    adj[s['a']].append((i, s['b'])); adj[s['b']].append((i, s['a']))

def seg_pts(s, frm):
    """세그먼트를 frm 노드에서 출발하는 점열로 (아크는 이산화)."""
    if s['kind'] == 'L':
        p = [s['p0'], s['p1']]
    else:
        cx, cy, r, a0, a1 = s['arc']
        if a1 <= a0: a1 += 360
        n = max(6, int(abs(a1-a0)/4))
        t = np.radians(np.linspace(a0, a1, n))
        p = list(zip(cx + r*np.cos(t), cy + r*np.sin(t)))
    if frm == s['b']: p = p[::-1]
    return p

used = set(); loops = []
for start in range(len(segs)):
    if start in used: continue
    # 단순 순회: 각 노드에서 아직 안 쓴 간선을 따라감
    cyc = [start]; used.add(start)
    s = segs[start]; n0, cur = s['a'], s['b']
    ok = False
    while True:
        nxt = [(i, o) for i, o in adj[cur] if i not in used]
        if not nxt:
            if any(i == start for i, o in adj[cur]) or cur == n0: ok = True
            break
        i, o = nxt[0]; used.add(i); cyc.append(i); cur = o
        if cur == n0: ok = True; break
    if ok and len(cyc) >= 3:
        pts = []
        node = n0
        for i in cyc:
            pp = seg_pts(segs[i], node)
            pts.extend(pp[:-1])
            node = segs[i]['b'] if node == segs[i]['a'] else segs[i]['a']
        loops.append(pts)

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)))

loops.sort(key=area, reverse=True)
print('loops found:', [round(area(l)) for l in loops[:8]])
outer = loops[0]
oa = np.array(outer)
print('outer: %d pts, bbox %.1f x %.1f mm, area %.0f mm2'
      % (len(outer), np.ptp(oa[:,0]), np.ptp(oa[:,1]), area(outer)))

# ---- 내부 루프 검증: 외곽 안에 완전히 들어와야 함 ----
from matplotlib.path import Path
opath = Path(oa)
inner_loops = []
for l in loops[1:]:
    la = np.array(l)
    if opath.contains_points(la).all() and area(l) > 100:
        inner_loops.append(l)
print('inner loops (cutouts):', [round(area(l)) for l in inner_loops])

# ---- 홀(CIRCLE): 외곽 내부 전체 포함 검사 ----
holes = []
for c in d1['circles']:
    _, _, cx, cy, r = c
    if not inw(cx, cy): continue
    t = np.radians(np.linspace(0, 360, 48, endpoint=False))
    ring = np.c_[cx + r*np.cos(t), cy + r*np.sin(t)]
    if opath.contains_points(ring).all():
        holes.append((cx, cy, r))
    else:
        print('  [reject circle] r=%.1f @(%.0f,%.0f) — 외곽을 벗어남(상세도 표시원 등)' % (r, cx, cy))
print('valid holes:', len(holes), 'radii:', sorted(set(round(h[2],1) for h in holes)))

# ---- 원점 이동 ----
ox, oy = oa[:,0].min(), oa[:,1].min()
print('model-space offset (mm): X=%.1f Y=%.1f' % (ox, oy))

import cadquery as cq
def wire(pts):
    v = [cq.Vector(p[0]-ox, p[1]-oy, 0) for p in pts]
    if (v[0]-v[-1]).Length > 1e-6: v.append(v[0])
    return cq.Wire.makePolygon(v, forConstruction=False)

ow = wire(outer)
iw = [wire(l) for l in inner_loops]
iw += [cq.Wire.makeCircle(r, cq.Vector(cx-ox, cy-oy, 0), cq.Vector(0,0,1)) for cx, cy, r in holes]
solid = cq.Solid.extrudeLinear(ow, iw, cq.Vector(0, 0, THK))
res = cq.Workplane('XY').newObject([solid])

stl = SCR + r'\part.stl'
cq.exporters.export(res, stl, tolerance=0.1, angularTolerance=0.1)
import trimesh
m = trimesh.load(stl)
m.export(OUT)
print('OBJ ->', OUT)
print('verts=%d faces=%d watertight=%s' % (len(m.vertices), len(m.faces), m.is_watertight))
print('bbox mm:', [round(v,2) for v in m.extents])
json.dump({'offset': [ox, oy], 'holes': holes, 'thk': THK,
           'extents': [float(v) for v in m.extents], 'faces': int(len(m.faces))},
          open(SCR + r'\build_result.json', 'w'))
