figma_to_html_agent 추가 + MCP/Claude 설정

figma_to_html_agent/:
- Figma MCP 기반 블록 추출 에이전트 (CLAUDE.md, PLAN.md, PROCESS.md 등)
- block-tests/: Figma→HTML 변환 결과물 (bim-3roles-cards 등)
- templates_staging/: Jinja2 템플릿 + meta.yaml + example.yaml
- figma-analysis/, figma-assets/: Figma 분석 데이터 + 에셋
- scripts/: gradient_math.py 등 유틸리티

설정:
- .mcp.json: Figma MCP 서버 연결 설정
- .claude/settings.json: Claude Code 프로젝트 설정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:00:31 +09:00
parent 360cd8e44c
commit 51548fdc41
467 changed files with 25280 additions and 10 deletions

View File

@@ -0,0 +1,239 @@
"""
SVG `<linearGradient>` → CSS `linear-gradient(...)` 수학 변환.
수학 원리: ../MATH.md §2
사용법 (모듈):
from scripts.gradient_math import svg_to_css
css = svg_to_css(
W=350, H=350,
x1=110.833, y1=18.2292, x2=219.479, y2=175,
stops=[(0, '#FDC69E'), (1, '#E0782C')]
)
# → "linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%)"
사용법 (CLI):
python scripts/gradient_math.py \\
--w 350 --h 350 \\
--x1 110.833 --y1 18.2292 --x2 219.479 --y2 175 \\
--stops "0:#FDC69E,1:#E0782C"
"""
import math
from typing import List, Tuple
def svg_to_css(
W: float,
H: float,
x1: float,
y1: float,
x2: float,
y2: float,
stops: List[Tuple[float, str]],
) -> str:
"""
SVG linearGradient (userSpaceOnUse) → CSS linear-gradient 문자열.
Args:
W, H: CSS 박스 크기 (background-origin이 적용되는 영역)
x1, y1: SVG 그라데이션 벡터 시작점 (이미 viewBox padding 보정된 값)
x2, y2: SVG 그라데이션 벡터 끝점
stops: [(offset, color), ...]
offset: 0.0 ~ 1.0 (SVG stop offset)
color: CSS 색상 문자열 (#RGB, #RRGGBB, rgba(), 등)
Returns:
"linear-gradient(Ndeg, color P%, color Q%, ...)"
수식:
dx, dy = x2-x1, y2-y1
L_svg = √(dx² + dy²)
css_angle= degrees(atan2(dy, dx)) + 90 (SVG y-down → CSS 12-o'clock CW)
L_css = |W·sin α| + |H·cos α| (α = radians(css_angle))
t_center = ((W/2 - x1)·dx + (H/2 - y1)·dy) / L_svg²
half = (L_css / 2) / L_svg
t0, t1 = t_center ∓ half (CSS 0%, 100% in SVG t-space)
pct = (offset - t0) / (t1 - t0) × 100
참조: ../MATH.md §2
"""
if not stops:
raise ValueError("stops 비어있음")
dx = x2 - x1
dy = y2 - y1
L_svg_sq = dx * dx + dy * dy
if L_svg_sq == 0:
raise ValueError(f"그라데이션 벡터 길이 0: ({x1},{y1})→({x2},{y2})")
L_svg = math.sqrt(L_svg_sq)
# SVG 벡터 각도 → CSS 각도 (12시 방향 0°, +CW)
svg_angle_deg = math.degrees(math.atan2(dy, dx))
css_angle = (svg_angle_deg + 90) % 360
# CSS 그라데이션 선 길이
a = math.radians(css_angle)
L_css = abs(W * math.sin(a)) + abs(H * math.cos(a))
# 박스 중심의 t 파라미터 (SVG 벡터 위에서 0=시작, 1=끝)
t_center = ((W / 2 - x1) * dx + (H / 2 - y1) * dy) / L_svg_sq
# CSS 0% / 100% ↔ SVG t-space
half = (L_css / 2) / L_svg
t0 = t_center - half
t1 = t_center + half
span = t1 - t0
if span == 0:
raise ValueError("L_css = 0 (퇴화 케이스)")
# 각 stop을 CSS percent로
parts = [f"{css_angle:.2f}deg"]
for offset, color in stops:
pct = (offset - t0) / span * 100
parts.append(f"{color} {pct:.2f}%")
return "linear-gradient(" + ", ".join(parts) + ")"
def svg_to_css_remap(
css_W: float,
css_H: float,
viewbox_padding: float,
x1: float,
y1: float,
x2: float,
y2: float,
stops: List[Tuple[float, str]],
) -> str:
"""
viewBox padding이 있는 SVG (drop-shadow 여백 등)에서 CSS box로 좌표 remap 후 변환.
예: SVG viewBox 310×310, 실제 fill 영역 280×280 (각 변 15 padding)
→ svg_to_css_remap(css_W=280, css_H=280, viewbox_padding=15, ...)
참조: ../MATH.md §5
"""
return svg_to_css(
W=css_W,
H=css_H,
x1=x1 - viewbox_padding,
y1=y1 - viewbox_padding,
x2=x2 - viewbox_padding,
y2=y2 - viewbox_padding,
stops=stops,
)
def parse_stops(stops_str: str) -> List[Tuple[float, str]]:
"""
"0:#FDC69E,1:#E0782C" → [(0.0, '#FDC69E'), (1.0, '#E0782C')]
"""
out = []
for piece in stops_str.split(","):
piece = piece.strip()
if not piece:
continue
offset_s, color = piece.split(":", 1)
out.append((float(offset_s), color.strip()))
return out
def _cli():
import argparse
p = argparse.ArgumentParser(
description="SVG linearGradient → CSS linear-gradient (수학적 변환)"
)
p.add_argument("--w", type=float, required=True, help="CSS 박스 너비")
p.add_argument("--h", type=float, required=True, help="CSS 박스 높이")
p.add_argument("--x1", type=float, required=True)
p.add_argument("--y1", type=float, required=True)
p.add_argument("--x2", type=float, required=True)
p.add_argument("--y2", type=float, required=True)
p.add_argument(
"--stops",
required=True,
help='"offset:color,offset:color" (예: "0:#FDC69E,1:#E0782C")',
)
p.add_argument(
"--viewbox-padding",
type=float,
default=0,
help="viewBox padding (drop-shadow 여백 등)",
)
args = p.parse_args()
stops = parse_stops(args.stops)
if args.viewbox_padding:
css = svg_to_css_remap(
css_W=args.w,
css_H=args.h,
viewbox_padding=args.viewbox_padding,
x1=args.x1,
y1=args.y1,
x2=args.x2,
y2=args.y2,
stops=stops,
)
else:
css = svg_to_css(
W=args.w,
H=args.h,
x1=args.x1,
y1=args.y1,
x2=args.x2,
y2=args.y2,
stops=stops,
)
print(css)
# ─── Self-test ─────────────────────────────────────────────
def _test():
"""1171281211 frame의 12개 그라데이션이 예상 값과 일치하는지 회귀 테스트"""
cases = [
# (label, args, expected_substring)
(
"big-safety outer",
dict(W=350, H=350, x1=110.833, y1=18.2292, x2=219.479, y2=175,
stops=[(0, "#FDC69E"), (1, "#E0782C")]),
"145.28deg",
),
(
"big-productivity outer",
dict(W=350, H=350, x1=300.084, y1=48.7416, x2=57.5503, y2=350,
stops=[(0, "#D5AA89"), (1, "#737373")]),
"218.84deg",
),
(
"big-trust outer",
dict(W=350, H=350, x1=56.9631, y1=11.1577, x2=272.483, y2=329.446,
stops=[(0, "#FFFFFF"), (1, "#253E1F")]),
"145.90deg",
),
]
failed = 0
for label, args, expected in cases:
css = svg_to_css(**args)
ok = expected in css
mark = "" if ok else ""
print(f" {mark} {label}: {css}")
if not ok:
failed += 1
print(f" expected substring: {expected}")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return failed == 0
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "--test":
ok = _test()
sys.exit(0 if ok else 1)
else:
_cli()