# obj2sketchup.py - E500PRO 부재 OBJ -> 스케치업용 DAE(Collada) 일괄 변환
# 사용: python obj2sketchup.py
# 출력: fab_models/sketchup/<도번>.dae  +  500PRO_전체부재.dae (격자 배열 종합본)
# 단위: 원본 OBJ 좌표 = mm. DAE <asset><unit meter="0.001" name="millimeter"> 로 명시.
import json, os, re, math, sys
import numpy as np
import trimesh

import product          # 제품 고유값 모음 — 기본값 500pro
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
FAB = product.FAB               # 제품별 OBJ 폴더 (500pro=fab_models, 400q=fab_models\400q)
OUT = os.path.join(FAB, 'sketchup')
DOWON = product.PN
GAP = 150.0  # 부재 간 여백(mm)


def patch_asset(path):
    """pycollada 기본 unit(meter)/up_axis 를 mm / Z_UP 으로 교정."""
    with open(path, 'r', encoding='utf-8') as f:
        x = f.read()
    x = re.sub(r'<unit[^/]*/>', '<unit meter="0.001" name="millimeter"/>', x, count=1)
    x = re.sub(r'<up_axis>[^<]*</up_axis>', '<up_axis>Z_UP</up_axis>', x, count=1)
    if '<unit ' not in x:  # unit 태그가 아예 없으면 asset 안에 삽입
        x = x.replace('<asset>', '<asset><unit meter="0.001" name="millimeter"/>', 1)
    with open(path, 'w', encoding='utf-8') as f:
        f.write(x)


def export_multi_dae(scene, path):
    """부재별 이름(도번)을 가진 다중 geometry/node 로 Collada 저장 (pycollada 직접 사용).
    trimesh 는 Scene->dae 내보내기를 지원하지 않으므로 여기서 직접 구성한다."""
    import collada
    mesh_doc = collada.Collada()
    mesh_doc.assetInfo.unitmeter = 0.001
    mesh_doc.assetInfo.unitname = 'millimeter'
    mesh_doc.assetInfo.upaxis = collada.asset.UP_AXIS.Z_UP
    effect = collada.material.Effect('eff0', [], 'phong', diffuse=(0.6, 0.6, 0.6),
                                     specular=(0.2, 0.2, 0.2))
    mat = collada.material.Material('mat0', 'steel', effect)
    mesh_doc.effects.append(effect)
    mesh_doc.materials.append(mat)
    nodes = []
    for name, g in scene.geometry.items():
        gid = re.sub(r'[^A-Za-z0-9_.-]', '_', str(name))
        vert = np.asarray(g.vertices, dtype=np.float32).ravel()
        norm = np.asarray(g.vertex_normals, dtype=np.float32).ravel()
        vs = collada.source.FloatSource(gid + '-v', vert, ('X', 'Y', 'Z'))
        ns = collada.source.FloatSource(gid + '-n', norm, ('X', 'Y', 'Z'))
        geom = collada.geometry.Geometry(mesh_doc, gid, gid, [vs, ns])
        il = collada.source.InputList()
        il.addInput(0, 'VERTEX', '#' + gid + '-v')
        il.addInput(1, 'NORMAL', '#' + gid + '-n')
        idx = np.asarray(g.faces, dtype=np.int32).ravel()
        idx = np.column_stack([idx, idx]).ravel()
        geom.primitives.append(geom.createTriangleSet(idx, il, 'ref0'))
        mesh_doc.geometries.append(geom)
        matnode = collada.scene.MaterialNode('ref0', mat, inputs=[])
        nodes.append(collada.scene.Node(
            gid, children=[collada.scene.GeometryNode(geom, [matnode])]))
    myscene = collada.scene.Scene('scene0', nodes)
    mesh_doc.scenes.append(myscene)
    mesh_doc.scene = myscene
    mesh_doc.write(path)


def select_items():
    items = json.load(open(product.MANIFEST, encoding='utf-8'))['items']
    sel = []
    for it in items:
        blob = (it['file'] + ' ' + it.get('name', '')).lower()
        if product.OBJ_PREFIX.rstrip('_') not in blob:
            continue
        if re.search(r'_구버전|_격리', it['file']):
            continue
        sel.append(it)
    return sel


def main():
    os.makedirs(OUT, exist_ok=True)
    sel = select_items()
    ok, fail, parts = [], [], []
    for it in sel:
        src = os.path.join(ROOT, it['file'].replace('/', os.sep))
        m = DOWON.search(it.get('name', ''))
        pid = m.group(0) if m else os.path.splitext(os.path.basename(src))[0]
        try:
            mesh = trimesh.load(src, force='mesh', process=False)
            if mesh.is_empty:
                raise ValueError('empty mesh')
            mesh.metadata['name'] = pid
            dst = os.path.join(OUT, pid + '.dae')
            mesh.export(dst, file_type='dae')
            patch_asset(dst)
            ext = np.sort(mesh.bounding_box.extents)
            ref = np.sort(np.array(it.get('size_mm') or [0, 0, 0], dtype=float))
            match = bool(np.all(np.abs(ext - ref) <= 0.5)) if ref.any() else None
            ok.append((pid, len(mesh.vertices), ext, ref, match, dst))
            parts.append((pid, mesh))
        except Exception as e:
            fail.append((pid, src, repr(e)))

    # ---- 종합 파일: 격자 배열(변환을 정점에 직접 반영) ----
    if parts:
        n = len(parts)
        cols = int(math.ceil(math.sqrt(n)))
        cw = max(p[1].bounding_box.extents[0] for p in parts) + GAP
        ch = max(p[1].bounding_box.extents[1] for p in parts) + GAP
        scene = trimesh.Scene()
        for i, (pid, mesh) in enumerate(parts):
            g = mesh.copy()
            g.apply_translation(-g.bounds[0])  # 각 부재 원점 정렬
            T = trimesh.transformations.translation_matrix(
                [(i % cols) * cw, (i // cols) * ch, 0.0])
            g.apply_transform(T)
            g.metadata['name'] = pid
            scene.add_geometry(g, geom_name=pid, node_name=pid)
        comb = os.path.join(OUT, product.SKP_NAME + '.dae')
        export_multi_dae(scene, comb)
        patch_asset(comb)
    else:
        comb = None

    # ---- 리포트 ----
    matched = sum(1 for r in ok if r[4])
    print('변환 성공: %d / 실패: %d' % (len(ok), len(fail)))
    print('manifest size_mm 일치: %d / %d' % (matched, len(ok)))
    for r in ok:
        if not r[4]:
            print('  [치수불일치] %s  bbox=%s  manifest=%s' % (r[0], r[2], r[3]))
    for f in fail:
        print('  [실패] %s %s' % (f[0], f[2]))
    if comb:
        print('종합 파일:', comb, '(부재 %d개)' % len(parts))
    return 0 if not fail else 1


if __name__ == '__main__':
    sys.exit(main())
