diff --git a/Tools/prefab_tree.py b/Tools/prefab_tree.py new file mode 100644 index 00000000..09e77e7e --- /dev/null +++ b/Tools/prefab_tree.py @@ -0,0 +1,86 @@ +# 팩 데모 프리팹을 레퍼런스로 화면을 옮길 때 수치 추출(PD 2026-11-09 화면별 레퍼런스 재제작) — 예: -I Tools/prefab_tree.py "<팩>/Prefabs/Prefabs_DemoScene_Panels/Collection_List.prefab" "<팩>/ResourcesData" Content 4 +# Unity 프리팹 YAML → 계층(이름 · 앵커 · 위치 · 크기 · 이미지 스프라이트/색 · TMP 글자/크기/색) 출력 — 인수: 프리팹 · 스프라이트 루트 · [시작 이름] · [최대 깊이] +import os +import re +import sys + +path, sproot = sys.argv[1], sys.argv[2] +start = sys.argv[3] if len(sys.argv) > 3 else None +maxd = int(sys.argv[4]) if len(sys.argv) > 4 else 99 +txt = open(path, encoding='utf-8', errors='ignore').read() +docs = {} +for m in re.finditer(r'--- !u!(\d+) &(-?\d+)[^\n]*\n(.*?)(?=\n--- !u!|\Z)', txt, re.S): + docs[m.group(2)] = (m.group(1), m.group(3)) +guid2name = {} +for root, _, files in os.walk(sproot): + for f in files: + if f.endswith('.meta'): + g = re.search(r'guid: (\w+)', open(os.path.join(root, f), encoding='utf-8', errors='ignore').read()) + if g: + guid2name[g.group(1)] = f[:-5] + + +def field(body, key): + m = re.search(r'\n\s*' + key + r': (.*)', '\n' + body) + return m.group(1).strip() if m else '' + + +go_comps = {} +go_name = {} +for fid, (t, body) in docs.items(): + if t == '1': + go_name[fid] = field(body, 'm_Name') + go_comps[fid] = re.findall(r'component: \{fileID: (-?\d+)\}', body) +rt_of = {} +for g, comps in go_comps.items(): + for c in comps: + if c in docs and docs[c][0] == '224': + rt_of[g] = c +go_of_rt = {v: k for k, v in rt_of.items()} + + +def v2(s): + m = re.findall(r'[-\d.]+', s) + return '(' + ','.join(str(round(float(x), 1)) for x in m) + ')' if m else '' + + +def desc(g): + rt = docs[rt_of[g]][1] + out = '%s a%s-%s p%s pos%s size%s' % (go_name[g], v2(field(rt, 'm_AnchorMin')), v2(field(rt, 'm_AnchorMax')), v2(field(rt, 'm_Pivot')), v2(field(rt, 'm_AnchoredPosition')), v2(field(rt, 'm_SizeDelta'))) + for c in go_comps[g]: + if c not in docs or docs[c][0] != '114': + continue + b = docs[c][1] + if not re.search(r'm_Enabled: 1', b): + continue + sp = re.search(r'm_Sprite: \{fileID: -?\d+, guid: (\w+)', b) + col = re.search(r'm_Color: \{r: ([\d.]+), g: ([\d.]+), b: ([\d.]+), a: ([\d.]+)\}', b) + if sp: + hexc = '#%02X%02X%02X/%.2f' % tuple([int(float(x) * 255 + 0.5) for x in col.groups()[:3]] + [float(col.group(4))]) if col else '' + ty = field(b, 'm_Type') + ppu = field(b, 'm_PixelsPerUnitMultiplier') + out += ' | IMG ' + guid2name.get(sp.group(1), sp.group(1)[:8]) + ' ' + hexc + ' t' + ty + (' ppu' + ppu if ppu and ppu != '1' else '') + tx = re.search(r'm_text: (.*)', b) + if tx: + fc = re.search(r'm_fontColor: \{r: ([\d.]+), g: ([\d.]+), b: ([\d.]+), a: ([\d.]+)\}', b) + hexc = '#%02X%02X%02X' % tuple(int(float(x) * 255 + 0.5) for x in fc.groups()[:3]) if fc else '' + out += ' | TXT "%s" size%s %s align%s' % (tx.group(1)[:30], field(b, 'm_fontSize'), hexc, field(b, 'm_HorizontalAlignment') or field(b, 'm_textAlignment')) + if 'm_EffectDistance' in b: + out += ' | FX ' + v2(field(b, 'm_EffectDistance')) + return out + + +def walk(g, d): + if d > maxd: + return + print(' ' * d + desc(g)) + rt = docs[rt_of[g]][1] + for c in re.findall(r'- \{fileID: (-?\d+)\}', rt.split('m_Children:')[1].split('m_Father')[0]) if 'm_Children:' in rt else []: + if c in go_of_rt: + walk(go_of_rt[c], d + 1) + + +roots = [g for g in go_name if g in rt_of and (start and go_name[g] == start)] +if not roots: + roots = [g for g in go_name if g in rt_of and field(docs[rt_of[g]][1], 'm_Father') == '{fileID: 0}'] +walk(roots[0], 0)