# -*- coding: utf-8 -*- # 2026-10-02 PD 지시(자율 반복) — 형태 길잡이 모음을 만든다. # a6 검토(에이전트가 직접 봄): 글자만(T)은 21종 중 4종만 합격, 실사풍 초안(D)은 물건 여러 개 · 인물이 섞임, "깨끗한 윤곽을 빌린 것(S)"은 8장 모두 형태가 읽히고 여백도 맞았다. # → 종류마다 깨끗한 윤곽(물건 하나 · 투명 배경)을 모아 두고, 그 윤곽선만 ControlNet 으로 넘겨 목표 풍으로 새로 그린다. 색 · 질감은 넘어가지 않는다. # 길잡이의 출처 셋: ① FR 장비 아이콘 원본 ② Layer Lab GUI Pro-FantasyRPG 아이콘(PD 보유 에셋) ③ 이 스크립트가 직접 그리는 단순 도형(원본이 없는 종류: 지팡이 · 화살통 · 뿔피리 · 오브 · 토템 · 투척 · 활) # 합격한 생성물도 다시 길잡이로 쓸 수 있다(add_from). # python icon_shapes.py → moba_anfg/shapes/<종류>__<출처>.png (1024 · 어두운 바탕 · 여백 통일) + shapes_sheet.png import glob, io, math, os, sys from PIL import Image, ImageDraw HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.join(HERE, "moba_anfg", "shapes") FR = r"C:\BurningTimes\FirstRising\Assets\GameAssets\UI\EquipmentItemIcon" LL = r"C:\Users\sw\Downloads\Layer Lab\GUI Pro-FantasyRPG\ResourcesData\Sprites\Component" LL_ITEM = os.path.join(LL, "Icon_ItemIcons", "NoShadow", "Original") SIZE, FIT, BG = 1024, 0.70, (16, 16, 20) # a7 검토(에이전트가 직접 봄) 반영: # · Layer Lab 길잡이는 뭉툭한 비례 · 굵은 외곽선이 결과에 남아 "캐주얼"이 샌다 → 투구 · 책 · 사슬 목걸이만 남기고 뺀다. # · 벨트는 fr1351(정면 띠 + 버클)만 허리띠로 읽혔다. 단검은 화면 끝에서 잘렸다 → 가는 무기는 더 작게 놓는다. # · 직접 그린 도형은 밋밋했다(활 · 뿔 · 오브 · 화살통 · 토템) → 장식 선을 더해 다시 그렸다(draw*2). 도끼 · 반지 · 방패 도형을 새로 그렸다. # 종류 → FR 원본 번호 FR_IDS = { # a8 검토: fr1015(망치)는 가시 철퇴로, fr1043 · fr1071(단검)은 너무 작거나 삼지창으로, fr1029(활)는 0장 → 뺐다(직접 그린 도형으로 대신한다) "blunt": [1057], # a12: fr1435(검)는 0/3(흰 납작면 · 잘림) → 직접 그린 검으로 대신 "necklace": [1099, 1113, 1127, 1141], "gloves": [1169, 1183, 1197, 1211], "armor": [1239, 1253, 1267, 1281], "belt": [1351], "boots": [1379, 1393, 1407, 1421], } # 종류 → Layer Lab 아이템 아이콘 이름(…icon_<이름>.png) LL_NAMES = { # a8 검토: Layer Lab 투구 · 목걸이 · book_0 은 0장(캐주얼 윤곽 · 책이 판으로 나옴) → book_1 만 남긴다 "spellbook": ["book_1"], } # 가는 무기는 대각선으로 놓여 화면 끝에 닿기 쉽다 — 종류별로 더 작게 놓는다(기본 0.70) FIT_BY_KIND = {"dagger": 0.74, "sword": 0.76, "staff": 0.74, "bow": 0.72, "throwing": 0.68, "blunt": 0.72, "hammer": 0.72, "axe": 0.74} FG, LINE = (150, 150, 158), (70, 70, 78) # 도형의 면 · 안쪽 선(선화 추출이 테두리와 안쪽 선을 잡는다) def place(im, fit=FIT): """투명 그림을 물체 테두리로 잘라 1024 어두운 바탕 가운데에 같은 여백으로 놓는다""" im = im.convert("RGBA") box = im.getchannel("A").getbbox() if box: im = im.crop(box) scale = SIZE * fit / float(max(im.size)) im = im.resize((max(1, int(im.width * scale)), max(1, int(im.height * scale))), Image.LANCZOS) rgb = Image.new("RGB", (SIZE, SIZE), BG) rgb.paste(im, ((SIZE - im.width) // 2, (SIZE - im.height) // 2), im) return rgb def canvas(): im = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) return im, ImageDraw.Draw(im) def draw_staff(kind): """지팡이: 긴 자루 + 머리 장식(0 = 마름모 수정 · 1 = 둥근 보석을 감싼 갈고리)""" im, d = canvas() d.rounded_rectangle([490, 250, 534, 1000], 14, fill=FG, outline=LINE, width=6) for y in (420, 760, 940): d.rectangle([482, y, 542, y + 22], fill=FG, outline=LINE, width=5) if kind == 0: d.polygon([(512, 20), (600, 150), (512, 280), (424, 150)], fill=FG, outline=LINE) d.line([(512, 20), (512, 280)], fill=LINE, width=6); d.line([(424, 150), (600, 150)], fill=LINE, width=6) d.polygon([(430, 250), (512, 300), (594, 250), (560, 330), (464, 330)], fill=FG, outline=LINE) else: d.ellipse([432, 60, 592, 220], fill=FG, outline=LINE, width=8) d.arc([380, 10, 644, 300], 110, 250, fill=FG, width=34); d.arc([380, 10, 644, 300], 290, 70, fill=FG, width=34) d.polygon([(450, 250), (574, 250), (540, 320), (484, 320)], fill=FG, outline=LINE) return im.rotate(-35, resample=Image.BICUBIC, expand=True) def draw_quiver(): """화살통: 원통 + 위로 나온 화살 깃 + 어깨끈""" im, d = canvas() for k, x in enumerate((400, 470, 540, 610)): top = 120 + (k % 2) * 50 d.rectangle([x - 7, top, x + 7, 420], fill=FG, outline=LINE, width=3) d.polygon([(x - 34, top + 10), (x, top - 40), (x + 34, top + 10), (x + 34, top + 130), (x, top + 90), (x - 34, top + 130)], fill=FG, outline=LINE) d.rounded_rectangle([340, 380, 680, 980], 60, fill=FG, outline=LINE, width=8) d.rectangle([330, 380, 690, 450], fill=FG, outline=LINE, width=8) d.rectangle([340, 820, 680, 870], fill=FG, outline=LINE, width=6) d.arc([180, 300, 560, 900], 100, 260, fill=FG, width=30) return im.rotate(-22, resample=Image.BICUBIC, expand=True) def draw_horn(): """뿔피리: 굽으면서 가늘어지는 뿔 + 테 둘""" im, d = canvas() cx, cy, R = 380, 300, 520 outer, inner = [], [] for k in range(0, 41): t = k / 40.0 a = math.radians(5 + 100 * t) w = 150 * (1 - t) + 14 # 입구 쪽이 굵고 끝이 가늘다 outer.append((cx + (R + w / 2) * math.cos(a), cy + (R + w / 2) * math.sin(a))) inner.append((cx + (R - w / 2) * math.cos(a), cy + (R - w / 2) * math.sin(a))) d.polygon(outer + inner[::-1], fill=FG, outline=LINE) for t in (0.06, 0.45): a = math.radians(5 + 100 * t); w = 150 * (1 - t) + 40 d.line([(cx + (R + w / 2) * math.cos(a), cy + (R + w / 2) * math.sin(a)), (cx + (R - w / 2) * math.cos(a), cy + (R - w / 2) * math.sin(a))], fill=LINE, width=26) return im def draw_orb(): """오브: 구슬 + 받침""" im, d = canvas() d.polygon([(400, 640), (624, 640), (580, 800), (444, 800)], fill=FG, outline=LINE) d.rounded_rectangle([340, 800, 684, 880], 24, fill=FG, outline=LINE, width=8) d.arc([300, 380, 724, 760], 20, 160, fill=FG, width=40) d.ellipse([292, 140, 732, 580], fill=FG, outline=LINE, width=10) d.arc([350, 190, 600, 440], 180, 270, fill=LINE, width=8) return im def draw_totem(): """토템: 층층이 쌓은 나무 기둥 + 옆 날개 + 받침""" im, d = canvas() d.polygon([(250, 300), (400, 250), (400, 380)], fill=FG, outline=LINE); d.polygon([(774, 300), (624, 250), (624, 380)], fill=FG, outline=LINE) for k, y in enumerate((100, 330, 560)): d.rounded_rectangle([390, y, 634, y + 220], 26, fill=FG, outline=LINE, width=8) d.ellipse([440, y + 60, 500, y + 120], outline=LINE, width=8); d.ellipse([524, y + 60, 584, y + 120], outline=LINE, width=8) d.rectangle([470, y + 150, 554, y + 180], outline=LINE, width=8) d.rounded_rectangle([340, 780, 684, 880], 20, fill=FG, outline=LINE, width=8) return im def draw_throwing(kind): """투척: 0 = 쿠나이(잎 모양 날 + 자루 + 고리) · 1 = 네 날 표창""" im, d = canvas() if kind == 0: d.polygon([(512, 60), (640, 420), (512, 560), (384, 420)], fill=FG, outline=LINE) d.line([(512, 60), (512, 560)], fill=LINE, width=6) d.rectangle([486, 560, 538, 800], fill=FG, outline=LINE, width=6) d.ellipse([442, 790, 582, 930], outline=FG, width=34) return im.rotate(-40, resample=Image.BICUBIC, expand=True) for a in (0, 90, 180, 270): r = math.radians(a) tip = (512 + 420 * math.cos(r), 512 + 420 * math.sin(r)) l = (512 + 120 * math.cos(r + 1.0), 512 + 120 * math.sin(r + 1.0)); rr = (512 + 120 * math.cos(r - 1.0), 512 + 120 * math.sin(r - 1.0)) d.polygon([tip, l, rr], fill=FG, outline=LINE) d.ellipse([412, 412, 612, 612], fill=FG, outline=LINE, width=10); d.ellipse([472, 472, 552, 552], fill=(0, 0, 0, 0), outline=LINE, width=8) return im.rotate(20, resample=Image.BICUBIC, expand=True) def draw_bow(): """활: 굽은 몸체 + 곧은 시위 + 가운데 손잡이""" im, d = canvas() d.arc([200, 80, 900, 944], 100, 260, fill=FG, width=46) top = (550 + 350 * math.cos(math.radians(260)), 512 + 432 * math.sin(math.radians(260))) bot = (550 + 350 * math.cos(math.radians(100)), 512 + 432 * math.sin(math.radians(100))) d.line([top, bot], fill=FG, width=8) d.rounded_rectangle([170, 440, 250, 584], 20, fill=FG, outline=LINE, width=8) return im.rotate(-30, resample=Image.BICUBIC, expand=True) def arc_points(cx, cy, rx, ry, a0, a1, n=40): """타원 호 위의 점들(도형의 굽은 변을 만들 때 쓴다)""" return [(cx + rx * math.cos(math.radians(a0 + (a1 - a0) * k / float(n))), cy + ry * math.sin(math.radians(a0 + (a1 - a0) * k / float(n)))) for k in range(n + 1)] def draw_axe(): """도끼: 자루가 날을 관통 + 속이 찬 부채꼴 날(바깥이 넓은 호) + 반대쪽 작은 뿔 + 자루 끝 마개. (a9 검토: 처음 도형은 날 안쪽이 파여 낫으로 읽혔고 날과 자루가 한 점으로만 닿았다 → 날을 자루에 넓게 붙였다.)""" im, d = canvas() blade = [(520, 150)] + arc_points(520, 320, 330, 290, -62, 62) + [(520, 490)] d.polygon(blade, fill=FG, outline=LINE) d.line(arc_points(520, 320, 270, 235, -55, 55), fill=LINE, width=7) # 날 안쪽의 연마선 d.polygon([(500, 230), (360, 260), (330, 320), (360, 380), (500, 410)], fill=FG, outline=LINE) # 반대쪽 뿔 d.rounded_rectangle([482, 90, 542, 990], 16, fill=FG, outline=LINE, width=6) d.rounded_rectangle([466, 200, 558, 440], 14, outline=LINE, width=8) # 날을 무는 쇠테 d.polygon([(482, 90), (512, 30), (542, 90)], fill=FG, outline=LINE) for y in (640, 720, 800): d.line([(482, y), (542, y)], fill=LINE, width=6) d.rounded_rectangle([470, 930, 554, 1000], 14, fill=FG, outline=LINE, width=6) return im.rotate(-35, resample=Image.BICUBIC, expand=True) def draw_bow2(): """활(다시): 굵은 리커브 몸체(끝이 바깥으로 말림) + 손잡이 + 끝 장식 + 시위""" im, d = canvas() outer = arc_points(620, 512, 400, 430, 115, 245) inner = arc_points(650, 512, 370, 400, 245, 115) d.polygon(outer + inner, fill=FG, outline=LINE) d.line(arc_points(635, 512, 385, 415, 120, 240), fill=LINE, width=5) top, bot = outer[-1], outer[0] d.polygon([top, (top[0] + 90, top[1] - 40), (top[0] + 60, top[1] + 30)], fill=FG, outline=LINE) d.polygon([bot, (bot[0] + 90, bot[1] + 40), (bot[0] + 60, bot[1] - 30)], fill=FG, outline=LINE) d.line([(top[0] + 70, top[1] - 10), (bot[0] + 70, bot[1] + 10)], fill=FG, width=9) d.rounded_rectangle([208, 420, 290, 604], 22, fill=FG, outline=LINE, width=8) for y in (450, 512, 574): d.line([(208, y), (290, y)], fill=LINE, width=6) return im.rotate(-25, resample=Image.BICUBIC, expand=True) def draw_horn2(): """뿔피리(다시): 넓은 나팔 입구 + 테 둘 + 끈""" im, d = canvas() cx, cy, R = 300, 250, 520 outer, inner = [], [] for k in range(0, 41): t = k / 40.0 a = math.radians(0 + 95 * t) w = 260 * (1 - t) ** 1.3 + 26 outer.append((cx + (R + w / 2) * math.cos(a), cy + (R + w / 2) * math.sin(a))) inner.append((cx + (R - w / 2) * math.cos(a), cy + (R - w / 2) * math.sin(a))) d.polygon(outer + inner[::-1], fill=FG, outline=LINE) d.ellipse([cx + R - 150, cy - 46, cx + R + 150, cy + 46], fill=FG, outline=LINE, width=9) # 나팔 입구 for t in (0.14, 0.52, 0.8): a = math.radians(95 * t); w = 260 * (1 - t) ** 1.3 + 50 d.line([(cx + (R + w / 2) * math.cos(a), cy + (R + w / 2) * math.sin(a)), (cx + (R - w / 2) * math.cos(a), cy + (R - w / 2) * math.sin(a))], fill=LINE, width=30) d.arc([330, 330, 760, 900], 190, 300, fill=FG, width=12) return im def draw_orb2(): """오브(다시): 구슬 + 비스듬히 두른 금속 테 + 아래 작은 받침 발톱(받침대 기둥은 뺐다 — 트로피로 읽혔다)""" im, d = canvas() d.ellipse([232, 190, 792, 750], fill=FG, outline=LINE, width=12) d.arc([300, 250, 560, 510], 180, 280, fill=LINE, width=9) d.arc([170, 380, 854, 620], 0, 180, fill=LINE, width=34) d.arc([170, 380, 854, 620], 0, 180, fill=FG, width=20) for x in (360, 512, 664): d.polygon([(x - 46, 730), (x + 46, 730), (x, 860)], fill=FG, outline=LINE) return im def draw_quiver2(): """화살통(다시): 세운 원통 + 위로 나온 화살 셋(깃이 또렷) + 띠 + 어깨끈""" im, d = canvas() for k, x in enumerate((430, 512, 594)): top = 90 + (k % 2) * 60 d.rectangle([x - 8, top + 60, x + 8, 430], fill=FG, outline=LINE, width=3) d.polygon([(x - 44, top + 40), (x - 8, top), (x - 8, top + 190), (x - 44, top + 230)], fill=FG, outline=LINE) d.polygon([(x + 44, top + 40), (x + 8, top), (x + 8, top + 190), (x + 44, top + 230)], fill=FG, outline=LINE) d.polygon([(360, 400), (664, 400), (630, 960), (394, 960)], fill=FG, outline=LINE) d.ellipse([350, 370, 674, 440], fill=FG, outline=LINE, width=8) d.line([(372, 560), (652, 560)], fill=LINE, width=22); d.line([(386, 820), (638, 820)], fill=LINE, width=22) d.ellipse([462, 640, 562, 740], outline=LINE, width=8) d.arc([560, 380, 860, 940], 270, 90, fill=FG, width=26) return im.rotate(-15, resample=Image.BICUBIC, expand=True) def draw_ring(): """반지: 얇은 고리(비스듬히 본 타원) + 위쪽 보석과 물림쇠""" im, d = canvas() d.ellipse([222, 330, 802, 910], outline=FG, width=64) d.ellipse([222, 330, 802, 910], outline=LINE, width=6); d.ellipse([286, 394, 738, 846], outline=LINE, width=6) d.polygon([(400, 330), (624, 330), (580, 420), (444, 420)], fill=FG, outline=LINE) d.polygon([(512, 110), (640, 240), (580, 350), (444, 350), (384, 240)], fill=FG, outline=LINE) d.line([(384, 240), (640, 240)], fill=LINE, width=6); d.line([(512, 110), (444, 350)], fill=LINE, width=5); d.line([(512, 110), (580, 350)], fill=LINE, width=5) return im.rotate(-18, resample=Image.BICUBIC, expand=True) def draw_totem2(): """토템(다시): 세 단으로 쌓은 기둥의 외곽 + 단마다 띠 + 꼭대기 뿔 + 받침(얼굴 구멍은 뺐다 — 만화 얼굴로 읽혔다)""" im, d = canvas() d.polygon([(420, 110), (360, 20), (470, 80)], fill=FG, outline=LINE); d.polygon([(604, 110), (664, 20), (554, 80)], fill=FG, outline=LINE) for k, (y, w) in enumerate(((90, 250), (330, 290), (590, 250))): d.rounded_rectangle([512 - w // 2, y, 512 + w // 2, y + 230], 30, fill=FG, outline=LINE, width=8) d.line([(512 - w // 2 + 10, y + 180), (512 + w // 2 - 10, y + 180)], fill=LINE, width=14) d.polygon([(512, y + 40), (560, y + 100), (512, y + 160), (464, y + 100)], outline=LINE) d.polygon([(367, 400), (240, 360), (367, 500)], fill=FG, outline=LINE); d.polygon([(657, 400), (784, 360), (657, 500)], fill=FG, outline=LINE) d.rounded_rectangle([350, 820, 674, 910], 20, fill=FG, outline=LINE, width=8) return im def draw_shield(): """방패: 연 모양 몸체 + 안쪽 테 + 가운데 돌기 + 세로 · 가로 띠""" im, d = canvas() body = [(512, 80), (800, 170), (780, 560), (512, 950), (244, 560), (224, 170)] d.polygon(body, fill=FG, outline=LINE) inner = [(512, 150), (735, 220), (718, 540), (512, 850), (306, 540), (289, 220)] d.line(inner + [inner[0]], fill=LINE, width=10) d.line([(512, 150), (512, 850)], fill=LINE, width=8); d.line([(295, 400), (729, 400)], fill=LINE, width=8) d.ellipse([432, 320, 592, 480], fill=FG, outline=LINE, width=10) return im.rotate(-12, resample=Image.BICUBIC, expand=True) def draw_dagger(): """단검: 짧고 넓은 날 + 가로 날밑 + 짧은 손잡이 + 자루 끝 구슬""" im, d = canvas() d.polygon([(512, 60), (590, 220), (575, 600), (449, 600), (434, 220)], fill=FG, outline=LINE) d.line([(512, 60), (512, 600)], fill=LINE, width=6) d.polygon([(330, 590), (694, 590), (720, 630), (694, 670), (330, 670), (304, 630)], fill=FG, outline=LINE) d.rounded_rectangle([474, 670, 550, 880], 14, fill=FG, outline=LINE, width=6) for y in (720, 770, 820): d.line([(474, y), (550, y)], fill=LINE, width=6) d.ellipse([456, 866, 568, 978], fill=FG, outline=LINE, width=8) return im.rotate(-40, resample=Image.BICUBIC, expand=True) def draw_hammer(): """망치: 직사각 덩어리 머리(양쪽 테) + 곧은 자루 + 자루 끝 마개 — 가시가 없어 철퇴와 구분된다""" im, d = canvas() d.rounded_rectangle([484, 300, 540, 990], 16, fill=FG, outline=LINE, width=6) d.rounded_rectangle([250, 80, 774, 360], 30, fill=FG, outline=LINE, width=10) d.rectangle([250, 80, 330, 360], outline=LINE, width=10); d.rectangle([694, 80, 774, 360], outline=LINE, width=10) d.ellipse([452, 160, 572, 280], outline=LINE, width=9) d.rounded_rectangle([462, 350, 562, 430], 12, fill=FG, outline=LINE, width=7) for y in (620, 700, 780): d.line([(484, y), (540, y)], fill=LINE, width=6) d.rounded_rectangle([470, 930, 554, 1000], 14, fill=FG, outline=LINE, width=6) return im.rotate(-35, resample=Image.BICUBIC, expand=True) def draw_totem3(): """토템(셋째): 통나무 기둥 하나에 새긴 얼굴 둘(눈 · 입 윤곽) + 머리 깃 + 옆 날개 + 받침 — 블록 쌓기로 읽히지 않게 기둥을 한 덩어리로 그린다""" im, d = canvas() d.polygon([(380, 330), (220, 250), (250, 400), (380, 430)], fill=FG, outline=LINE) d.polygon([(644, 330), (804, 250), (774, 400), (644, 430)], fill=FG, outline=LINE) for x in (440, 512, 584): d.polygon([(x - 30, 130), (x, 20 if x == 512 else 50), (x + 30, 130)], fill=FG, outline=LINE) d.rounded_rectangle([380, 120, 644, 860], 40, fill=FG, outline=LINE, width=10) for y in (170, 510): d.polygon([(420, y + 40), (490, y + 60), (480, y + 110), (420, y + 100)], outline=LINE) # 왼눈 d.polygon([(604, y + 40), (534, y + 60), (544, y + 110), (604, y + 100)], outline=LINE) # 오른눈 d.polygon([(512, y + 100), (540, y + 190), (484, y + 190)], outline=LINE) # 코 d.rounded_rectangle([430, y + 220, 594, y + 280], 12, outline=LINE, width=8) # 입 for x in (470, 512, 554): d.line([(x, y + 220), (x, y + 280)], fill=LINE, width=6) d.line([(380, 490), (644, 490)], fill=LINE, width=16) d.rounded_rectangle([330, 850, 694, 940], 22, fill=FG, outline=LINE, width=8) return im def draw_helmet(kind): """투구: 둥근 머리 + 얼굴 가리개(눈 틈) + 목 보호대. 0 = 꼭대기 볏 · 1 = 양옆 뿔""" im, d = canvas() if kind == 1: # 뿔: 투구 옆면(밑동이 본체 안으로 겹친다)에서 솟아 위로 휘며 뾰족해진다 d.polygon([(330, 420), (280, 540), (150, 430), (110, 250), (150, 90), (200, 250), (260, 360)], fill=FG, outline=LINE) d.polygon([(694, 420), (744, 540), (874, 430), (914, 250), (874, 90), (824, 250), (764, 360)], fill=FG, outline=LINE) d.polygon(arc_points(512, 470, 270, 330, 180, 360) + [(782, 760), (680, 900), (344, 900), (242, 760)], fill=FG, outline=LINE) d.line(arc_points(512, 470, 270, 330, 180, 360), fill=LINE, width=10) d.polygon([(300, 470), (724, 470), (700, 560), (540, 560), (512, 620), (484, 560), (324, 560)], fill=(0, 0, 0, 0), outline=LINE) # 눈 틈 d.line([(512, 150), (512, 470)], fill=LINE, width=10) d.line([(512, 620), (512, 900)], fill=LINE, width=8) d.line([(242, 760), (782, 760)], fill=LINE, width=8) if kind == 0: d.polygon(arc_points(512, 300, 60, 260, 180, 360) + [(572, 300), (452, 300)], fill=FG, outline=LINE) return im def draw_sword2(): """검(직접 그림): 넓은 날 + 가운데 홈(풀러) + 날개형 날밑 + 감은 손잡이 + 마름모 자루 끝. (a12 검토: fr1435 윤곽은 너무 가늘어 흰 납작면 · 잘림이 났다 → 날을 넓히고 안쪽 선을 넣어 명암을 올릴 자리를 만든다.)""" im, d = canvas() d.polygon([(512, 30), (578, 170), (570, 640), (454, 640), (446, 170)], fill=FG, outline=LINE) d.line([(512, 60), (512, 640)], fill=LINE, width=7) d.line([(486, 190), (486, 640)], fill=LINE, width=4); d.line([(538, 190), (538, 640)], fill=LINE, width=4) d.polygon([(290, 650), (350, 600), (454, 626), (570, 626), (674, 600), (734, 650), (700, 712), (600, 694), (424, 694), (324, 712)], fill=FG, outline=LINE) d.ellipse([478, 630, 546, 698], outline=LINE, width=7) d.rounded_rectangle([480, 694, 544, 880], 12, fill=FG, outline=LINE, width=6) for y in (730, 770, 810, 850): d.line([(480, y), (544, y - 18)], fill=LINE, width=5) d.polygon([(512, 840), (576, 916), (512, 992), (448, 916)], fill=FG, outline=LINE) # 자루 끝(손잡이와 겹친다 — a13: 떨어져 보였다) return im.rotate(-40, resample=Image.BICUBIC, expand=True) def draw_dagger2(): """단검(다시): 잎 모양의 짧은 날 + 홈 + 굽은 작은 날밑 + 손잡이 + 둥근 자루 끝. (a12 검토: 처음 도형은 날밑이 날만큼 넓어 '뭉툭한 십자 덩어리'가 됐다 → 날을 길게, 날밑을 좁게.)""" im, d = canvas() d.polygon([(512, 90), (592, 300), (566, 640), (458, 640), (432, 300)], fill=FG, outline=LINE) d.line([(512, 120), (512, 640)], fill=LINE, width=7) d.line([(478, 320), (486, 640)], fill=LINE, width=4); d.line([(546, 320), (538, 640)], fill=LINE, width=4) d.polygon([(370, 610), (420, 640), (604, 640), (654, 610), (650, 690), (590, 700), (434, 700), (374, 690)], fill=FG, outline=LINE) d.rounded_rectangle([478, 700, 546, 870], 12, fill=FG, outline=LINE, width=6) for y in (735, 775, 815): d.line([(478, y), (546, y)], fill=LINE, width=5) d.ellipse([458, 856, 566, 964], fill=FG, outline=LINE, width=8) return im.rotate(-40, resample=Image.BICUBIC, expand=True) def draw_axe3(): """도끼(셋째): 자루에 넓게 붙은 속 찬 수염 도끼 머리(각진 다각형 + 안쪽 연마선) + 반대쪽 뿔 + 꼭대기 창끝. (a12 검토: 부채꼴 호가 초승달 조각으로 떨어져 나왔다 → 호를 쓰지 않고 자루와 면으로 맞닿는 각진 머리로 그린다.)""" im, d = canvas() d.rounded_rectangle([480, 110, 544, 990], 16, fill=FG, outline=LINE, width=6) d.polygon([(544, 170), (700, 110), (830, 210), (870, 350), (830, 500), (720, 600), (640, 470), (544, 450)], fill=FG, outline=LINE) d.line([(690, 160), (790, 240), (822, 350), (790, 470), (716, 540)], fill=LINE, width=7) d.polygon([(480, 240), (360, 300), (480, 380)], fill=FG, outline=LINE) d.rounded_rectangle([462, 160, 562, 460], 14, outline=LINE, width=8) d.polygon([(480, 110), (512, 30), (544, 110)], fill=FG, outline=LINE) for y in (640, 720, 800): d.line([(480, y), (544, y)], fill=LINE, width=6) d.rounded_rectangle([468, 930, 556, 1000], 14, fill=FG, outline=LINE, width=6) return im.rotate(-35, resample=Image.BICUBIC, expand=True) def draw_axe4(): """도끼(넷째): 속이 찬 반달 날 한 덩어리가 자루와 넓게 겹친다. 안쪽 윤곽은 날 끝 쪽의 연마선 한 줄뿐. (a13 검토: 셋째 도형도 안쪽 윤곽이 고리로 읽혀 '속 빈 초승달'이 됐다 → 쇠테 · 겹선을 모두 없앴다.)""" im, d = canvas() d.rounded_rectangle([478, 110, 546, 990], 16, fill=FG, outline=LINE, width=6) blade = [(500, 150)] + arc_points(500, 330, 360, 250, -80, 80, 30) + [(500, 510)] d.polygon(blade, fill=FG, outline=LINE) d.line(arc_points(500, 330, 315, 210, -62, 62, 30), fill=LINE, width=6) d.polygon([(478, 250), (380, 330), (478, 410)], fill=FG, outline=LINE) d.polygon([(478, 110), (512, 30), (546, 110)], fill=FG, outline=LINE) for y in (660, 740, 820): d.line([(478, y), (546, y)], fill=LINE, width=6) d.rounded_rectangle([466, 930, 558, 1000], 14, fill=FG, outline=LINE, width=6) return im.rotate(-35, resample=Image.BICUBIC, expand=True) def draw_dagger3(): """단검(셋째): 넓고 짧은 날(길이:폭 약 3:1) + 날 길이의 60% 에서 닫히는 가는 홈 + 작은 날밑 + 손잡이 + 자루 끝(손잡이와 겹침). (a13 검토: 둘째 도형은 끝까지 그은 중심선이 날을 둘로 가르는 경계로 읽혀 두 갈래 날이 됐다.)""" im, d = canvas() d.polygon([(512, 110), (606, 330), (586, 650), (438, 650), (418, 330)], fill=FG, outline=LINE) d.ellipse([498, 330, 526, 640], outline=LINE, width=5) d.polygon([(360, 630), (438, 650), (586, 650), (664, 630), (650, 706), (374, 706)], fill=FG, outline=LINE) d.rounded_rectangle([476, 700, 548, 880], 12, fill=FG, outline=LINE, width=6) for y in (740, 785, 830): d.line([(476, y), (548, y)], fill=LINE, width=5) d.ellipse([454, 850, 570, 966], fill=FG, outline=LINE, width=8) return im.rotate(-40, resample=Image.BICUBIC, expand=True) def draw_bow3(): """활(셋째): 가운데가 굵고 양 끝으로 가늘어지는 활대 + 바깥으로 젖힌 끝 고리 + 감은 손잡이 + 손잡이 위아래 마름모 장식 + 가는 시위 한 줄. (a13 검토: 형태는 처음으로 온전했으나 굵기가 일정한 '매끈한 관'이라 밋밋했다. 시위가 굵으면 막대로 그려진다.)""" im, d = canvas() outer, inner = [], [] for k in range(0, 61): t = k / 60.0 ang = math.radians(118 + 124 * t) # 활대의 중심선(왼쪽으로 볼록한 호) w = 22 + 46 * (1 - abs(2 * t - 1)) ** 0.8 # 가운데 68 → 끝 22 cx, cy = 640 + 400 * math.cos(ang), 512 + 440 * math.sin(ang) nx, ny = math.cos(ang), math.sin(ang) outer.append((cx + nx * w / 2, cy + ny * w / 2)); inner.append((cx - nx * w / 2, cy - ny * w / 2)) d.polygon(outer + inner[::-1], fill=FG, outline=LINE) top = (640 + 400 * math.cos(math.radians(242)), 512 + 440 * math.sin(math.radians(242))) bot = (640 + 400 * math.cos(math.radians(118)), 512 + 440 * math.sin(math.radians(118))) for (x, y), sgn in ((top, -1), (bot, 1)): d.polygon([(x - 20, y), (x + 70, y + sgn * 70), (x + 96, y + sgn * 40), (x + 30, y - sgn * 24)], fill=FG, outline=LINE) # 젖힌 끝 d.line([(top[0] + 78, top[1] - 52), (bot[0] + 78, bot[1] + 52)], fill=FG, width=5) # 시위 d.rounded_rectangle([198, 430, 290, 594], 20, fill=FG, outline=LINE, width=7) for y in (462, 496, 530, 564): d.line([(198, y), (290, y - 14)], fill=LINE, width=5) for y in (360, 664): d.polygon([(250, y - 46), (292, y), (250, y + 46), (208, y)], fill=FG, outline=LINE) return im.rotate(-25, resample=Image.BICUBIC, expand=True) def draw_hammer2(): """망치(다시): 직사각 머리 + 양 끝의 두꺼운 띠와 모서리 장식 + 가운데 문장 + 자루 목의 쇠테 + 감은 자루. (a13 검토: 처음 도형은 '단순한 상자 머리'로 장식이 부족했다.)""" im, d = canvas() d.rounded_rectangle([482, 300, 542, 990], 16, fill=FG, outline=LINE, width=6) d.rounded_rectangle([250, 90, 774, 360], 26, fill=FG, outline=LINE, width=10) for x0, x1 in ((230, 330), (694, 794)): d.rounded_rectangle([x0, 70, x1, 380], 18, fill=FG, outline=LINE, width=9) d.line([(x0 + 50, 70), (x0 + 50, 380)], fill=LINE, width=5) d.polygon([(512, 140), (590, 225), (512, 310), (434, 225)], outline=LINE) d.line([(512, 140), (512, 310)], fill=LINE, width=5); d.line([(434, 225), (590, 225)], fill=LINE, width=5) d.polygon([(482, 90), (512, 36), (542, 90)], fill=FG, outline=LINE) d.rounded_rectangle([458, 350, 566, 440], 12, fill=FG, outline=LINE, width=7) for y in (560, 610, 660, 710, 760, 810): d.line([(482, y), (542, y - 18)], fill=LINE, width=5) d.rounded_rectangle([466, 920, 558, 1000], 14, fill=FG, outline=LINE, width=6) return im.rotate(-35, resample=Image.BICUBIC, expand=True) DRAWN = { "staff": [("draw0", lambda: draw_staff(0)), ("draw1", lambda: draw_staff(1))], "quiver": [("draw2", draw_quiver2)], "horn": [("draw2", draw_horn2)], "orb": [("draw2", draw_orb2)], "totem": [("draw3", draw_totem3)], "throwing": [("draw0", lambda: draw_throwing(0)), ("draw1", lambda: draw_throwing(1))], "bow": [("draw3", draw_bow3)], "axe": [("draw4", draw_axe4)], "ring": [("draw0", draw_ring)], "shield": [("draw0", draw_shield)], "dagger": [("draw3", draw_dagger3)], "hammer": [("draw2", draw_hammer2)], "helmet": [("draw0", lambda: draw_helmet(0)), ("draw1", lambda: draw_helmet(1))], "sword": [("draw2", draw_sword2)], } def add_from(kind, path, label): """합격한 생성물(투명 PNG)을 길잡이 모음에 더한다 — 다른 색 · 장식으로 다시 그릴 때의 형태가 된다. active.txt 에도 올린다.""" os.makedirs(OUT, exist_ok=True) name = "%s__%s.png" % (kind, label) place(Image.open(path), FIT_BY_KIND.get(kind, FIT)).save(os.path.join(OUT, name)) act = os.path.join(OUT, "active.txt") have = io.open(act, encoding="utf-8").read().split() if os.path.exists(act) else [] if name not in have: io.open(act, "a", encoding="utf-8").write(name + "\n") def main(): os.makedirs(OUT, exist_ok=True) active = [] # 이번 정의로 쓰는 길잡이(예전 정의로 만든 파일은 지우지 않고 폴더에 남긴다 — 생성 스크립트는 active.txt 만 본다) for kind, ids in FR_IDS.items(): for gid in ids: name = "%s__fr%d.png" % (kind, gid) place(Image.open(os.path.join(FR, "%d.png" % gid)), FIT_BY_KIND.get(kind, FIT)).save(os.path.join(OUT, name)); active.append(name) for kind, names in LL_NAMES.items(): for nm in names: hits = glob.glob(os.path.join(LL_ITEM, "*icon_%s.png" % nm)) if not hits: print("Layer Lab 에 없음:", nm); continue name = "%s__ll_%s.png" % (kind, nm) place(Image.open(hits[0]), FIT_BY_KIND.get(kind, FIT)).save(os.path.join(OUT, name)); active.append(name) for kind, items in DRAWN.items(): for label, fn in items: name = "%s__%s.png" % (kind, label) place(fn(), FIT_BY_KIND.get(kind, FIT)).save(os.path.join(OUT, name)); active.append(name) # add_from 으로 더한 합격본 길잡이(gen*)는 계속 쓴다 active += [os.path.basename(f) for f in glob.glob(os.path.join(OUT, "*__gen*.png"))] active = sorted(set(active)) io.open(os.path.join(OUT, "active.txt"), "w", encoding="utf-8").write("\n".join(active) + "\n") cell, pad, cols = 160, 4, 10 rows = (len(active) + cols - 1) // cols sheet = Image.new("RGB", (cols * (cell + pad) + pad, rows * (cell + pad) + pad), (40, 40, 44)) d = ImageDraw.Draw(sheet) for i, name in enumerate(active): x, y = pad + (i % cols) * (cell + pad), pad + (i // cols) * (cell + pad) sheet.paste(Image.open(os.path.join(OUT, name)).resize((cell, cell), Image.LANCZOS), (x, y)) d.text((x + 3, y + 2), name[:-4], fill=(240, 220, 160)) sheet.save(os.path.join(OUT, "shapes_sheet.png")) print("shapes %d (%d 종류) -> %s" % (len(active), len(set(n.split("__")[0] for n in active)), OUT)) if __name__ == "__main__": # python icon_shapes.py → 길잡이 모음을 다시 만든다 # python icon_shapes.py add <종류> <투명 PNG 경로> <이름> → 그림 한 장의 윤곽을 길잡이로 등록(이름은 gen 으로 시작해야 다시 만들 때 남는다) if len(sys.argv) > 4 and sys.argv[1] == "add": add_from(sys.argv[2], sys.argv[3], sys.argv[4]); print("등록", sys.argv[2], sys.argv[4]) else: main()