# dzw_translate.py — 번역기 (2층): 제품 작업기술서 → 로봇별 프로그램 [CEO 핵심기술 2026-07-19]
#   사상: 티칭 지식(작업기술서)은 하나, 로봇은 실행자일 뿐.
#   입력: .workspec.json (제품 좌표계 — dzw_workspec.js가 생성)
#   출력: --robot kuka   → KRL .src   (LIN + A,B,C 오일러각)
#         --robot abb    → RAPID .mod (MoveL + 쿼터니언)
#         --robot csv    → 범용 CSV   (어떤 시스템이든 읽는 중간 산출물)
#   야스카와(JBI)는 뷰어의 검증된 생성기(segsToJbi)가 담당 — 여기서 중복 구현하지 않는다.
#   현대·화낙은 3층에서 RoboDK 기구학 경유로 추가 예정.
#
#   자세 규약(v1): 공구 Z+ = 토치가 제품을 향하는 방향 = -tool_dir.
#     X+ = 용접 진행 방향(경로 접선)을 Z에 직교화 + 진행각(travel_angle) 기울임.
#     제품→로봇 베이스 변환은 --base-shift x,y,z (mm)로 지정 (기본 0,0,0 = 제품좌표 그대로).
#
#   사용: python dzw_translate.py 제품.workspec.json --robot kuka
#         python dzw_translate.py 제품.workspec.json --robot abb --base-shift 1200,0,577.4
import json, math, sys, os

def norm(v):
    L = math.sqrt(sum(x*x for x in v)) or 1.0
    return [x/L for x in v]

def cross(a, b):
    return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]]

def dot(a, b): return sum(x*y for x, y in zip(a, b))

def frame_at(pts, i, tool_dir, travel_deg):
    """경로 i번째 점의 공구 좌표계(회전행렬 3x3, 열=X,Y,Z축) — 제품 좌표계 기준"""
    z = norm([-tool_dir[0], -tool_dir[1], -tool_dir[2]])          # 공구 Z+ = 제품을 향함
    j = min(i+1, len(pts)-1); k = max(i-1, 0)
    t = norm([pts[j][0]-pts[k][0], pts[j][1]-pts[k][1], pts[j][2]-pts[k][2]])  # 진행 접선
    x = [t[0]-dot(t, z)*z[0], t[1]-dot(t, z)*z[1], t[2]-dot(t, z)*z[2]]       # Z에 직교화
    if math.sqrt(dot(x, x)) < 1e-9: x = [1, 0, 0]
    x = norm(x)
    # 진행각: 토치를 진행 방향으로 travel_deg 기울임 (Z를 X쪽으로 회전)
    a = math.radians(travel_deg)
    z2 = norm([z[0]*math.cos(a)+x[0]*math.sin(a), z[1]*math.cos(a)+x[1]*math.sin(a), z[2]*math.cos(a)+x[2]*math.sin(a)])
    x2 = norm([x[0]-dot(x, z2)*z2[0], x[1]-dot(x, z2)*z2[1], x[2]-dot(x, z2)*z2[2]])
    y2 = cross(z2, x2)
    return [[x2[0], y2[0], z2[0]], [x2[1], y2[1], z2[1]], [x2[2], y2[2], z2[2]]]

def mat_to_kuka_abc(R):
    """KUKA A,B,C (도) — ZYX 오일러: A=Z회전, B=Y회전, C=X회전"""
    B = math.asin(max(-1, min(1, -R[2][0])))
    if abs(math.cos(B)) > 1e-9:
        A = math.atan2(R[1][0], R[0][0]); C = math.atan2(R[2][1], R[2][2])
    else:
        A = math.atan2(-R[0][1], R[1][1]); C = 0.0
    return [math.degrees(A), math.degrees(B), math.degrees(C)]

def mat_to_quat(R):
    """ABB 쿼터니언 [q1..q4] = [w,x,y,z]"""
    tr = R[0][0]+R[1][1]+R[2][2]
    if tr > 0:
        s = math.sqrt(tr+1)*2
        return [s/4, (R[2][1]-R[1][2])/s, (R[0][2]-R[2][0])/s, (R[1][0]-R[0][1])/s]
    i = max(range(3), key=lambda k: R[k][k])
    j, k = (i+1) % 3, (i+2) % 3
    s = math.sqrt(max(1e-12, 1+R[i][i]-R[j][j]-R[k][k]))*2
    q = [0, 0, 0, 0]
    q[0] = (R[k][j]-R[j][k])/s
    q[i+1] = s/4; q[j+1] = (R[j][i]+R[i][j])/s; q[k+1] = (R[k][i]+R[i][k])/s
    return q

def points(ws, base):
    """작업기술서 → [(구간번호, [x,y,z], R행렬, 속도cmmin, 태그)] 나열"""
    out = []
    for s in ws["seams"]:
        pts = s["pts_mm"]; td = s.get("tool_dir", [0, 0, 1]); tv = s.get("travel_angle_deg", 10)
        sp = ws.get("weld_params", {}).get("speed_line_cmmin", 45)
        for i, p in enumerate(pts):
            R = frame_at(pts, i, td, tv)
            out.append((s["no"], [p[0]+base[0], p[1]+base[1], p[2]+base[2]], R, sp, s.get("tag")))
    return out

def emit_kuka(ws, base):
    L = ["&ACCESS RVP", "DEF " + name(ws) + "()",
         "; DOZIKWORKS 작업기술서 번역 — 제품 중심 티칭의 KUKA 실행본",
         "; 규약: 공구Z+=제품방향, 진행각 반영. base-shift=" + str(base),
         "$VEL.CP=" + f'{ws.get("weld_params",{}).get("speed_line_cmmin",45)/6000:.4f}',  # cm/min→m/s
         "PTP $POS_ACT"]
    cur = None
    for segno, p, R, sp, tag in points(ws, base):
        if segno != cur:
            L.append("; ---- 구간 %d%s ----" % (segno, " [%s]" % tag if tag else ""))
            cur = segno
        A, B, C = mat_to_kuka_abc(R)
        L.append("LIN {X %.2f,Y %.2f,Z %.2f,A %.2f,B %.2f,C %.2f} C_DIS" % (p[0], p[1], p[2], A, B, C))
    L += ["END"]
    return "\n".join(L), ".src"

def emit_abb(ws, base):
    L = ["MODULE " + name(ws),
         "! DOZIKWORKS 작업기술서 번역 — 제품 중심 티칭의 ABB 실행본",
         "PERS tooldata tWeld:=[TRUE,[[0,0,0],[1,0,0,0]],[1,[0,0,1],[1,0,0,0],0,0,0]];",
         "PROC main()"]
    v = "v%d" % max(5, round(ws.get("weld_params", {}).get("speed_line_cmmin", 45)/6))  # cm/min→mm/s
    cur = None
    for segno, p, R, sp, tag in points(ws, base):
        if segno != cur:
            L.append("    ! ---- 구간 %d%s ----" % (segno, " [%s]" % tag if tag else ""))
            cur = segno
        q = mat_to_quat(R)
        L.append("    MoveL [[%.2f,%.2f,%.2f],[%.5f,%.5f,%.5f,%.5f],[0,0,0,0],[9E9,9E9,9E9,9E9,9E9,9E9]],%s,z1,tWeld;"
                 % (p[0], p[1], p[2], q[0], q[1], q[2], q[3], v))
    L += ["ENDPROC", "ENDMODULE"]
    return "\n".join(L), ".mod"

def emit_csv(ws, base):
    L = ["seg,x_mm,y_mm,z_mm,qw,qx,qy,qz,speed_cmmin,tag"]
    for segno, p, R, sp, tag in points(ws, base):
        q = mat_to_quat(R)
        L.append("%d,%.2f,%.2f,%.2f,%.5f,%.5f,%.5f,%.5f,%s,%s" % (segno, p[0], p[1], p[2], q[0], q[1], q[2], q[3], sp, tag or ""))
    return "\n".join(L), ".csv"

def name(ws):
    n = ws.get("product", {}).get("name", "dzw")
    return "".join(c if c.isalnum() else "_" for c in os.path.splitext(n)[0])[:24] or "dzw"

BACKENDS = {"kuka": emit_kuka, "abb": emit_abb, "csv": emit_csv}

# ── 산업용 백엔드: RoboDK 포스트프로세서 (C:/RoboDK/Posts, Apache 2.0, 150종) ──
#   [2026-07-19 조사] 현대·화낙·두산·모토만·KUKA(Arc 변형 포함) 등 전 제조사 지원.
#   업계 표준 구조(범용 프로그램 → 포스트) 그대로 — 우리 1층(작업기술서)이 범용 프로그램 역할.
POSTS_DIR = "C:/RoboDK/Posts"

def emit_via_post(ws, base, post_name, out_dir):
    import importlib, sys as _sys
    if POSTS_DIR not in _sys.path: _sys.path.insert(0, POSTS_DIR)
    from robodk import robomath
    mod = importlib.import_module(post_name)
    rp = mod.RobotPost(post_name, name(ws), 6)
    rp.ProgStart(name(ws))
    rp.RunMessage("DOZIKWORKS workspec v%s — product-centric weld program" % ws.get("version", "1"), True)
    mmps = ws.get("weld_params", {}).get("speed_line_cmmin", 45)/6.0   # cm/min → mm/s
    try: rp.setSpeed(mmps)
    except Exception: pass
    cur = None
    for segno, p, R, sp, tag in points(ws, base):
        if segno != cur:
            rp.RunMessage("seam %d%s" % (segno, " [%s]" % tag if tag else ""), True)
            cur = segno
        pose = robomath.Mat([[R[0][0], R[0][1], R[0][2], p[0]],
                             [R[1][0], R[1][1], R[1][2], p[1]],
                             [R[2][0], R[2][1], R[2][2], p[2]],
                             [0, 0, 0, 1]])
        rp.MoveL(pose, [0]*6, None)   # 관절값 미지(좌표 기반) — 포스트가 pose로 기록
    rp.ProgFinish(name(ws))
    saved = rp.ProgSave(out_dir, name(ws), False, False)   # show=False — 편집기 자동 열림 금지
    return getattr(rp, "PROG_FILES", None) or saved

def main(argv):
    if len(argv) < 2 or "--robot" not in argv:
        print(__doc__ or "사용법: python dzw_translate.py <워크스펙> --robot kuka|abb|csv [--base-shift x,y,z]")
        return 1
    src = argv[1]
    robot_raw = argv[argv.index("--robot")+1]
    robot = robot_raw.lower()
    base = [0.0, 0.0, 0.0]
    if "--base-shift" in argv:
        base = [float(x) for x in argv[argv.index("--base-shift")+1].split(",")]
    if robot == "yaskawa":
        print("야스카와는 뷰어의 검증된 JBI 생성기 사용 — 뷰어에서 'JBI 초안' 버튼 (중복 구현 금지)"); return 0
    if robot.startswith("post:"):   # 예: --robot post:Hyundai / post:Motoman / post:Fanuc_R30i
        ws = json.load(open(src, encoding="utf-8"))
        assert ws.get("format") == "dozikworks-workspec", "작업기술서 파일이 아님"
        post = robot_raw[5:]   # 포스트 파일명은 대소문자 구분 (Hyundai.py 등)
        files = emit_via_post(ws, base, post, os.path.dirname(os.path.abspath(src)))
        print("번역 완료(RoboDK 포스트 %s): %s" % (post, files)); return 0
    if robot not in BACKENDS:
        print("지원: " + ", ".join(BACKENDS) + " / post:<포스트명> (150종, C:/RoboDK/Posts) / yaskawa=뷰어"); return 1
    ws = json.load(open(src, encoding="utf-8"))
    assert ws.get("format") == "dozikworks-workspec", "작업기술서 파일이 아님"
    body, ext = BACKENDS[robot](ws, base)
    out = os.path.splitext(src)[0].replace(".workspec", "") + "_" + robot + ext
    open(out, "w", encoding="utf-8").write(body)
    n = sum(len(s["pts_mm"]) for s in ws["seams"])
    print("번역 완료: %s → %s (구간 %d, 점 %d)" % (robot.upper(), out, len(ws["seams"]), n))
    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv))
