# -*- coding: utf-8 -*-
"""2110DN-0031 CENTER FRAME 조립 배치 산출.

좌표계(조립 원점):
  X = 정면도 도면 x - 328801.51   (프레임 좌측 외면 = X0, 폭 250)
  Z = 입면도 도면 y - 4375.2      (정면도 최하단 = Z0, 높이 약 986)
  Y = 좌측면도 도면 x - 327257.6  (좌측면도 좌단 = Y0, 깊이 약 910)
정면/좌측면/우측면 3개 입면도의 도면 y 범위가 4375~5361 로 일치 → 3각법 Z 공유 실측 확인.

부재 방향은 '두께 PL 만큼 떨어진 평행선 쌍' 실측으로 정했다(추정 아님).
실측되지 않은 축은 confidence 를 낮추고 [CEO확인] 으로 표시한다.
위치는 '회전 후 bbox 최소점을 목표 좌표에 맞춘다' 방식으로 계산해 회전 순서 실수를 차단한다.
"""
import json, os, math
import numpy as np

FX, FZ, LY = 328801.51, 4375.2, 327257.6
# SECTION B-B <-> 좌측면도 정합(실측): 공통 부재 = 10T 바닥판.
#   좌측면도  간격10.0 수평쌍  x 327328.1~327728.1 (길이 400.0), y 4491.6~4501.6
#   SECTION B-B 간격10.0 수평쌍 x 330305.7~330705.7 (길이 400.0), y 6112.0~6122.0
#   → 좌·우 끝이 둘 다 정확히 400.0 로 일치 = 같은 부재. 오프셋을 그대로 쓴다.
BY, BZ = 330305.7 - 327328.1 + LY, 6112.0 - 4491.6 + FZ   # SECB 도면좌표에서 빼면 전역 Y,Z
ROOT = r'E:\도진팩토리\3D스캔및티칭시스템'
OUT = ROOT + r'\fab_models\assemblies'

DIM = {
    '2110DN-0031-001': (388.0, 250.0, 30.0),
    '2110DN-0031-002': (440.0, 250.0, 30.0),
    '2110DN-0031-003': (650.0, 726.0, 25.0),
    '2110DN-0031-004': (250.0, 344.46, 30.0),
    '2110DN-0031-005': (607.0, 196.0, 25.0),
    '2110DN-0031-006': (613.0, 197.0, 20.0),
    '2110DN-0031-008': (198.0, 83.0, 20.0),
    '2110DN-0031-009': (150.0, 40.0, 12.0),
    '2110DN-0031-010': (198.0, 83.0, 20.0),
    '2110DN-0031-011': (253.0, 150.0, 30.0),
    '2110DN-0031-012': (90.0, 90.0, 6.0),
}


def Rx(a):
    c, s = math.cos(a), math.sin(a)
    return np.array([[1, 0, 0], [0, c, -s], [0, s, c]])


def Ry(a):
    c, s = math.cos(a), math.sin(a)
    return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])


def Rz(a):
    c, s = math.cos(a), math.sin(a)
    return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])


def euler_xyz(R):
    """three.js Euler order 'XYZ' (M = Rx·Ry·Rz) 로 분해."""
    sy = R[0, 2]
    sy = max(-1.0, min(1.0, sy))
    y = math.asin(sy)
    if abs(sy) < 0.999999:
        x = math.atan2(-R[1, 2], R[2, 2])
        z = math.atan2(-R[0, 1], R[0, 0])
    else:
        x = math.atan2(R[2, 1], R[1, 1]); z = 0.0
    return [round(math.degrees(v), 3) for v in (x, y, z)]


# 방향 프리셋: 로컬 z(두께)가 어느 전역축으로 가는가
ORIENT = {
    'ZUP_XY':   (Rz(math.radians(90)), '두께=전역Z(수평판). 로컬 388/250 → Y/X'),
    'XNORM':    (Ry(math.radians(90)), '두께=전역X(좌우 수직판)'),
    'YNORM':    (Rx(math.radians(90)), '두께=전역Y(앞뒤 수직판)'),
    'ZUP_0':    (Rz(0.0), '두께=전역Z(수평판). 로컬 a/b → X/Y (긴 변이 깊이방향)'),
}
# 사재(경사판): 두께=전역X, 그 위에 Y-Z 평면 안에서 57° 경사
ORIENT['XNORM_57'] = (Rx(math.radians(57)) @ Ry(math.radians(90)),
                      '두께=전역X + Y-Z 평면 내 57° 경사(SECTION B-B 실측 123°)')
# 항번9 -011: 연직판이지만 X-Y 평면 안에서 12.99° 틀어져 있다(2026-08-17 각도 재검).
#   왜 처음에 놓쳤나 — 좌측면도에서 면선이 '정확히 90.00°(연직)' 으로 보여 직각 부재로 읽었다.
#   실제로는 겉보기 두께가 29.23 으로 공칭 30 보다 작았고, 그 0.77mm 가 비스듬한 판을 옆에서
#   본 투영 축소였다(30·cos12.99 = 29.232, 오차 0.002mm).
#   SECTION C-C 에서 같은 판이 102.99°(연직 대비 12.99°)로 참두께 30.00 을 보이고,
#   끝단(두께면) 선분 길이도 정확히 30.0 이라 2뷰에서 값이 닫힌다.
#   회전축이 Z 인 근거: 후보 4개(Rx90 / Rz@Rx90 / Ry@Rx90 / Rx90@Rz) 중 좌측면도 투영에서
#   면선각 90.00° 와 간격 29.232 를 동시에 재현하는 것은 Rz@Rx90 뿐이다(나머지는 전부 30.000).
ORIENT['YNORM_13'] = (Rz(math.radians(12.99)) @ Rx(math.radians(90)),
                      '두께≈전역Y + X-Y 평면 내 12.99° 틀어짐(SECTION C-C 실측 102.99°, '
                      '좌측면도 겉보기 두께 29.23 = 30·cos12.99)')

P = []


def add(item, pn, orient, gmin, conf, ev, note=''):
    """gmin = 목표 전역 bbox 최소점 [X,Y,Z] (None 인 축은 0)"""
    a, b, t = DIM[pn]
    R = ORIENT[orient][0]
    corners = np.array([[x, y, z] for x in (0, a) for y in (0, b) for z in (0, t)])
    rc = corners @ R.T
    pos = np.array([g if g is not None else 0.0 for g in gmin]) - rc.min(0)
    ext = (rc.max(0) - rc.min(0))
    P.append({'item': item, 'pn': pn,
              'obj': 'fab_models/e500pro_0031_%s.obj' % pn[-3:],
              'size_mm': [a, b, t], 'orient': orient,
              'orient_설명': ORIENT[orient][1],
              'rot_deg_xyz': euler_xyz(R),
              'pos_mm': [round(float(v), 2) for v in pos],
              'bbox_global': [[round(float(v), 1) for v in (pos + rc.min(0))],
                              [round(float(v), 1) for v in (pos + rc.max(0))]],
              'ext_global': [round(float(v), 1) for v in ext],
              'confidence': conf, 'ceo_check': conf != 'high',
              'evidence': ev, 'note': note})


# ── 항번1 -001 : 정면도 하부 수평판 (PL30 수평쌍 y4576.64~4606.64, x328801.51~329051.51)
add(1, '2110DN-0031-001', 'ZUP_XY', [0.0, 0.0, 4576.64 - FZ], 'medium',
    '정면도: PL30 평행선쌍 실측 간격 30.0 / 수평(0°) / 구간폭 250.0 = 부재 250 치수 일치. X 0~250, Z 201.4~231.4 실측 확정.',
    'Y(깊이)는 정면도만으로 못 읽음 — 프레임 앞면 Y=0 가정. [CEO확인]')

# ── 항번2 -002 : 정면도 상부 수평판 (PL30 수평쌍 y5217.64~5247.64)
add(2, '2110DN-0031-002', 'ZUP_XY', [0.0, 0.0, 5217.64 - FZ], 'medium',
    '정면도: PL30 평행선쌍 간격 30.0 / 수평 / x328811.5~329041.5. Z 842.4~872.4 실측 확정.',
    'Y 미실측(Y=0 가정). 보이는 폭 230 < 부재 250 (가림). [CEO확인]')

# ── 항번3 -003 (2EA) : 프레임 좌우 측판. 정면도에서 PL25 수직쌍 2조를 실측
for x0, xd in ((0.0, 328801.51), (225.0, 329026.51)):
    add(3, '2110DN-0031-003', 'XNORM', [x0, 0.0, 4491.6 - FZ], 'medium',
        '정면도: PL25 평행선쌍 간격 25.0 / 수직(90°) / 도면x %.2f~%.2f → X %.0f~%.0f. 2EA 가 좌·우 측판으로 실측됨. 두 판의 외면 간격 250.0 = 항번1(-001) 폭 250 과 일치.'
        % (xd, xd + 25, x0, x0 + 25),
        'Y(깊이)는 정면도로 못 읽음(Y=0 가정), Z 는 10T 바닥판 윗면(116.4)에 얹힘으로 가정. 세로 650 / 깊이 726 배정 근거는 SECTION A-A 의 PL25 수직쌍 길이 601~631(≈650). [CEO확인]')

# ── 항번8 -004 (2EA) : 좌측면도 PL30 수평쌍 2단
for y0 in (4674.14, 4959.64):
    add(8, '2110DN-0031-004', 'ZUP_0', [0.0, 327569.09 - LY, y0 - FZ], 'medium',
        '좌측면도: PL30 평행선쌍 간격 30.0 / 수평 / 도면x 327569.1~327796.2 → Y 311.5~538.6, Z %.1f. 2EA 가 상·하 2단으로 실측됨.'
        % (y0 - FZ),
        'X(폭방향) 미실측 — 프레임 좌측 외면 정렬 가정. 방향은 긴 변 344.46 을 Y(깊이)로 둠: 프레임 외폭이 정면도 실측 250 이라 344.46 이 X 로 갈 수 없기 때문(치수 모순 배제). [CEO확인]')

# ── 항번9 -011 : 좌측면도 PL30 쌍 + SECTION C-C PL30 쌍 (2뷰 교차) — 2026-08-17 각도 수정
add(9, '2110DN-0031-011', 'YNORM_13', [0.0, 327614.81 - LY, 4787.53 - FZ], 'medium',
    '[2026-08-17 각도 재검·수정] 이전 판은 연직(90°) 직각판으로 봤으나 실제는 X-Y 평면 내 12.99° '
    '틀어진 판이다. 근거 3가지가 서로 닫힌다: (1) 좌측면도 면선 2줄 (327614.8,4787.5)-(327614.8,4875.7) / '
    '(327644.0,...) 은 정확히 90.00°(연직) 이지만 간격이 29.23 으로 공칭 PL30 보다 0.77 작다 '
    '— 30·cos(12.99°)=29.232 와 0.002mm 일치(비스듬한 판의 투영 축소). '
    '(2) SECTION C-C 에서 같은 PL30 판이 102.99°(연직 대비 12.99°)로 참두께 30.00 을 보인다. '
    '(3) C-C 의 끝단(두께면) 선분이 12.99° 방향으로 길이 정확히 30.0 — 두께면을 정면으로 본 값. '
    '회전축은 Z: 후보 4개 중 좌측면도 투영에서 면선각 90.00°+간격 29.232 를 동시에 재현하는 것은 '
    'Rz(12.99)@Rx(90) 뿐이다(나머지 3개는 모두 30.000).',
    'X 미실측. 각도 12.99° 는 2뷰 실측으로 확정했으나 위치 X 는 여전히 미실측. [CEO확인]')

# ── 항번15 -012 (2EA) : 우측면도 PL6 수직쌍
add(15, '2110DN-0031-012', 'YNORM', [0.0, 0.0, 4920.92 - FZ], 'low',
    '우측면도: PL6 평행선쌍 간격 6.0 / 수직(90°) / 구간 y4920.9~5037.9 → Z 545.7~662.7. 90x90 거셋.',
    'X·Y 미실측(1뷰). 2EA 중 1개만 배치. [CEO확인]')

# ── 항번6 -006 : SECTION B-B 사재. 길이 613.0 = 부재 613 치수 정확 일치(치수일치 실측)
add(6, '2110DN-0031-006', 'XNORM_57', [0.0, 330321.79 - BY, 6311.98 - BZ], 'medium',
    'SECTION B-B: PL20 평행선쌍 간격 20.0 / 각도 123°(수평 대비 57° 경사) / 길이 613.0 = 부재 613 치수와 정확 일치. 경사 사재 확정.',
    'SECTION B-B 좌표는 공통 10T 바닥판(좌우 끝 400.0 일치)으로 좌측면도에 정합함 = 2뷰 교차. X(폭방향)만 미실측. [CEO확인]')

# ── 항번5 -010 / 항번7 -008 : SECTION B-B PL20 수직쌍 (동일 형상 198x83x20)
add(5, '2110DN-0031-010', 'XNORM', [0.0, 330655.65 - BY, 6226.98 - BZ], 'medium',
    'SECTION B-B: PL20 평행선쌍 간격 20.0 / 수직 / 구간길이 83.0 = 부재 83 치수 일치.',
    '바닥판 정합 후 Z=231.4 = 항번1(-001) 윗면 231.4 와 일치(부재가 맞닿음, 독립 검증됨). -008 과 형상 동일(198x83x20)이라 -010/-008 라벨은 지시선 근접으로만 구분 — 서로 바뀌었을 수 있음. X 미실측. [CEO확인]')
add(7, '2110DN-0031-008', 'XNORM', [0.0, 330655.65 - BY, 6111.98 - BZ], 'medium',
    'SECTION B-B: PL20 평행선쌍 간격 20.0 / 수직 / 구간길이 85.0(≈83).',
    '바닥판 정합 후 Z=116.4 = 10T 바닥판 윗면과 일치(부재가 맞닿음, 독립 검증됨). -010 과 형상 동일해 라벨이 서로 바뀌었을 수 있음. X 미실측. [CEO확인]')

# ── 항번4 -005 : 실측 매칭 실패 — 대칭 추정
add(4, '2110DN-0031-005', 'XNORM_57', [0.0, 330321.79 - BY, 6311.98 - BZ], 'low',
    '치수일치 스캔(전 시트)에서 607/196/25 조합의 평행선쌍을 못 찾음. 풍선4 정면도 지시선은 -003 판 위에 떨어지고, SECTION B-B 지시선 끝에는 대응 형상 없음. '
    '[2026-08-17 각도 재검] 다시 전 시트를 훑었지만 결과는 같다 — PL25 간격에 길이 607 또는 196 인 평행선쌍이 0건. '
    '즉 이 부재의 57° 는 실측값이 아니라 -006 의 값을 그대로 물려받은 것이며 각도 근거가 없다. '
    '근거 없는 값을 다른 근거 없는 값으로 바꾸지 않기 위해 각도를 건드리지 않고 그대로 두었다.',
    '각도 57° 는 -006 에서 물려받은 미검증값 — [CEO확인] 필수. BOM 상 -005 는 1EA 라 -006(1EA)의 좌우 대칭짝이 아니다. SECTION A-A 도 스캔했으나 PL25 수직쌍 길이가 591/601 로 -005 의 607 과 불일치(그쪽은 항번3 -003 PL25 로 보임). 위치 근거 없음 — 임의 좌표를 넣지 않고 -006 과 같은 기준점에 놓기만 함. [CEO확인] 필수')

# ── 항번14 -009 : PL12 쌍 실측 실패, 지시선 끝점만 확보
add(14, '2110DN-0031-009', 'YNORM', [0.0, 327719.39 - LY, 5197.64 - FZ], 'low',
    '좌측면도 풍선14 지시선 끝점 (327719.4, 5197.6) 만 확보. PL12 평행선쌍 실측 실패(다른 선과 중첩).',
    '지시선 끝점을 부착점으로 쓴 근사 배치 — [CEO확인] 필수')

meta = {
    '도번': '2110DN-0031', '품명': 'CENTER FRAME',
    '출처': '발주도면 확인 — 2110DN-0031 1/2 시트(조립도: 좌측면도/정면도/우측면도 + SECTION A-A,B-B,C-C + 항번 풍선), 2/2 시트(단품 절단도), 아이소메트릭+BOM 시트',
    '좌표계': 'X=폭(정면도 도면x-328801.51), Y=깊이(좌측면도 도면x-327257.6), Z=높이(입면도 도면y-4375.2). 단위 mm. 도면 1:1 확인(항번 풍선 반지름 18.8 = 시트틀만 약 5.9배 확대된 표준 도면).',
    '뷰정렬_교차확인': '(1) 정면도 y 4375.2~5361.6 / 좌측면도 4390.2~5342.5 / 우측면도 4522.9~5338.4 — 상·하단 일치로 3각법 Z 공유 실측 확인. (2) SECTION B-B 는 공통 10T 바닥판(좌측면도 x327328.1~327728.1 / B-B x330305.7~330705.7, 둘 다 길이 400.0, 간격 10.0)으로 좌측면도에 정합 — 오프셋 Y -2977.6 / Z -1620.4',
    'BOM_15항목': '3D 배치 대상 11개(단품 OBJ 보유, 2EA 3건 포함 13개 인스턴스). 참고 — 항번3 2110DN-0031-003 PL25 2EA(대형 측판 — 2026-08-15 다른 세션이 단품 OBJ 생성 완료, 이번 조립에 포함됨), 항번10 RB702-0286A BOSS, 항번11 RB702-0290A BOSS, 항번12 RB702-0272B PL14, 항번13 2110DN-0059 STEP ASSY(사급/서브앗세이)',
    '한계': '이 조립도는 정투상 3면도가 아니라 부분 입면도 + 단면도(A-A/B-B/C-C) 조합이라, 폭방향 X 를 읽을 수 있는 뷰가 정면도뿐이다. 그래서 어느 부재도 3축 전부를 2뷰 이상에서 읽지 못했고, 11개 인스턴스 전부 [CEO확인] 이다. Y·Z 는 정합 후 잘 맞는다.',
    '독립검증': '(1) 조립 전체 Y 폭 708.0 ≤ 좌측면도 실측 깊이 910.4 (정합 전 1027.5 로 초과하던 것이 해소). (2) 항번7(-008) 바닥 Z 116.4 = 10T 바닥판 윗면. (3) 항번5(-010) 바닥 Z 231.4 = 항번1(-001) 윗면 231.4 — 정면도에서 따로 잰 값과 정확히 맞음(정합에 쓰지 않은 독립 부재라 교차검증으로 성립). (4) [2026-08-17 정정] 조립 X 폭은 0~253.27 로 정면도 실측 프레임 외폭 250 을 3.27 넘는다. 이전 판이 「250 과 일치」 라고 적은 것은 사실이 아니었다(그때도 253.0 이었다). 원인은 항번9(-011)·항번3(-003) 등의 X 좌표가 애초에 미실측이라 「좌측 외면 정렬」 로 가정 배치됐기 때문이며, 각도 재검과는 무관하다(재검으로 253.0→253.27 로 0.27 늘었을 뿐). X 방향은 여전히 전 부재 [CEO확인] 대상이다.',
    'parts': P,
}
os.makedirs(OUT, exist_ok=True)
p = os.path.join(OUT, '2110DN-0031_assembly.json')
json.dump(meta, open(p, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(p, len(P), '배치')
for x in P:
    print('%2d %-18s %-9s rot%s pos%s bbox%s %s'
          % (x['item'], x['pn'], x['orient'], x['rot_deg_xyz'], x['pos_mm'],
             x['bbox_global'], x['confidence']))
