AX Copilot 아이콘 점유율과 트레이 DPI 프레임을 키운다
작업 표시줄과 트레이에서 AX Copilot 아이콘이 다른 앱보다 작게 보이던 원인은 icon.ico 내부 여백이 커서 실제 도형 점유율이 낮았기 때문이다. 현재 4다이아몬드 계열 형태는 유지한 채 내부 여백을 줄이고 캔버스를 더 넓게 쓰는 새 멀티사이즈 아이콘으로 자산을 재생성했다. 아이콘 생성 경로도 함께 정리했다. tools/IconGenerator는 현재 AX 아이콘 스타일을 기본으로 생성하고 16 20 24 32 40 48 64 128 256 프레임을 포함하도록 바꿨다. src/AxCopilot/Assets/diamond_pixel.svg도 같은 비율로 맞춰 소스 SVG와 실제 ico 자산이 덜 어긋나게 정리했다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_icon_size\ -p:IntermediateOutputPath=obj\verify_icon_size\ / 경고 0 오류 0 검증: System.Drawing.Icon 확인 결과 16 20 24 32 프레임이 요청 크기 그대로 로드됨
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# AX Commander
|
||||
|
||||
- 업데이트: 2026-04-15 21:19 (KST)
|
||||
- AX Copilot 앱 아이콘을 더 크게 보이도록 다시 생성했습니다. `src/AxCopilot/Assets/icon.ico`의 실제 도형 점유율이 작아 작업 표시줄과 트레이에서 다른 앱보다 작게 보였는데, 같은 4다이아몬드 계열 형태를 유지한 채 내부 여백을 줄이고 캔버스를 더 넓게 쓰도록 멀티사이즈 아이콘을 다시 만들었습니다.
|
||||
- 트레이 DPI 대응도 함께 보강했습니다. `tools/IconGenerator/Program.cs`가 현재 앱 아이콘 스타일을 기본으로 생성하고 `16/20/24/32/40/48/64/128/256` 프레임을 포함하도록 바뀌어, `src/AxCopilot/App.xaml.cs`의 `LoadAppIcon()`이 고DPI 트레이 크기에서도 더 알맞은 프레임을 읽게 됩니다.
|
||||
- `src/AxCopilot/Assets/diamond_pixel.svg`도 같은 레이아웃으로 정리해 소스 자산과 실제 `icon.ico`가 서로 덜 어긋나게 맞췄습니다.
|
||||
- 검증:
|
||||
- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_icon_size\\ -p:IntermediateOutputPath=obj\\verify_icon_size\\` 경고 0 / 오류 0
|
||||
- `System.Drawing.Icon` 확인: 16/20/24/32 프레임 모두 요청 크기 그대로 로드
|
||||
|
||||
- 업데이트: 2026-04-15 21:11 (KST)
|
||||
- AX Agent 좌측 대화 목록 선택 카드의 배경이 실제로 보이지 않던 회귀를 바로잡았습니다. `src/AxCopilot/Views/ChatWindow.xaml`에서 `ConversationItemTemplate` 루트 `Border`의 `Background`/`BorderBrush` 로컬값을 제거하고 스타일 기본값으로 내린 뒤, 선택 트리거가 `ItemSelectedBackground`를 정상 적용하도록 고쳤습니다.
|
||||
- 같은 파일에서 `ConversationItemsControl`의 컨테이너를 가로 `Stretch`로 맞춰 선택 배경이 제목 주변만이 아니라 행 전체 둥근 카드로 깔리게 했고, idle 심볼은 `16x16` 그리드와 내부 마진을 줘 점선 링이 살짝 잘리던 문제도 함께 줄였습니다.
|
||||
|
||||
@@ -1577,3 +1577,10 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
||||
- idle 심볼은 `16x16` 영역과 내부 마진을 주도록 수정해 점선 링이 가장자리에서 약간 잘리던 문제를 줄였습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_conversation_list_selection_fix\\ -p:IntermediateOutputPath=obj\\verify_conversation_list_selection_fix\\` 경고 0 / 오류 0
|
||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ConversationItemViewModelTests" -p:OutputPath=bin\\verify_conversation_list_selection_fix_tests\\ -p:IntermediateOutputPath=obj\\verify_conversation_list_selection_fix_tests\\` 통과 3
|
||||
|
||||
업데이트: 2026-04-15 21:19 (KST)
|
||||
- AX Copilot 앱 아이콘이 작업 표시줄과 트레이에서 작게 보이던 문제를 자산 기준으로 조정했습니다. 기존 `src/AxCopilot/Assets/icon.ico`는 내부 여백이 커서 32px 기준 실사용 영역이 작았고, 트레이에서도 같은 아이콘이 더 축소돼 보였습니다.
|
||||
- `tools/IconGenerator/Program.cs`를 현재 AX 4다이아몬드 아이콘 스타일 기준 생성기로 정리하고, `16/20/24/32/40/48/64/128/256` 프레임을 포함하는 멀티사이즈 `ico`를 만들도록 바꿨습니다. 이 변경으로 `src/AxCopilot/App.xaml.cs`의 `LoadAppIcon()`이 DPI별 트레이 크기에서도 더 맞는 프레임을 읽을 수 있습니다.
|
||||
- `src/AxCopilot/Assets/icon.ico`는 내부 도형 점유율을 키운 새 아이콘으로 재생성했고, `src/AxCopilot/Assets/diamond_pixel.svg`도 같은 비율의 소스 자산으로 맞췄습니다.
|
||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_icon_size\\ -p:IntermediateOutputPath=obj\\verify_icon_size\\` 경고 0 / 오류 0
|
||||
- 검증: `System.Drawing.Icon` 확인 결과 16/20/24/32 프레임이 요청 크기 그대로 로드됨
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<!-- 다이아몬드 픽셀 아이콘: 상=파랑, 하=빨강, 좌=녹, 우=녹 -->
|
||||
<defs>
|
||||
<linearGradient id="blueTile" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#748EFF"/>
|
||||
<stop offset="1" stop-color="#5B70F7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="greenTileTop" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#7CF088"/>
|
||||
<stop offset="1" stop-color="#42D663"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="greenTileBottom" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#64DB8B"/>
|
||||
<stop offset="1" stop-color="#36C678"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="redTile" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#FF6C84"/>
|
||||
<stop offset="1" stop-color="#F24769"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g transform="translate(256,256) rotate(45)">
|
||||
<!-- 좌상→상: Blue -->
|
||||
<rect x="-105" y="-105" width="95" height="95" rx="10" fill="#4488FF"/>
|
||||
<!-- 우상→우: Green -->
|
||||
<rect x="10" y="-105" width="95" height="95" rx="10" fill="#44DD66"/>
|
||||
<!-- 좌하→좌: Green -->
|
||||
<rect x="-105" y="10" width="95" height="95" rx="10" fill="#44DD66"/>
|
||||
<!-- 우하→하: Red -->
|
||||
<rect x="10" y="10" width="95" height="95" rx="10" fill="#FF4466"/>
|
||||
<rect x="-140" y="-140" width="132" height="132" rx="20" fill="url(#blueTile)"/>
|
||||
<rect x="8" y="-140" width="132" height="132" rx="20" fill="url(#greenTileTop)"/>
|
||||
<rect x="-140" y="8" width="132" height="132" rx="20" fill="url(#greenTileBottom)"/>
|
||||
<rect x="8" y="8" width="132" height="132" rx="20" fill="url(#redTile)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 669 B After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 22 KiB |
@@ -2,30 +2,115 @@ using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
|
||||
// 다이아몬드 픽셀 아이콘 생성기 v4
|
||||
// 보석 다이아몬드 컷 실루엣 (flat top → wide girdle → bottom point)
|
||||
// 내부 facet 선 + RGBG 4색 채움
|
||||
// 참고: Samsung Diamond Pixel 구조 (흰색선 다이아몬드 도형)
|
||||
var options = IconGeneratorOptions.Parse(args);
|
||||
var pngFrames = new List<byte[]>();
|
||||
|
||||
var outputPath = args.Length > 0 ? args[0]
|
||||
: Path.Combine(AppContext.BaseDirectory, "icon.ico");
|
||||
|
||||
int[] sizes = [16, 24, 32, 48, 64, 128, 256];
|
||||
var pngList = new List<byte[]>();
|
||||
|
||||
foreach (var sz in sizes)
|
||||
foreach (var size in options.Sizes)
|
||||
{
|
||||
using var bmp = DrawGemDiamond(sz);
|
||||
using var ms = new MemoryStream();
|
||||
bmp.Save(ms, ImageFormat.Png);
|
||||
pngList.Add(ms.ToArray());
|
||||
Console.WriteLine($" {sz}x{sz} OK");
|
||||
using var bitmap = options.Style switch
|
||||
{
|
||||
IconStyle.LegacyGem => DrawLegacyGemDiamond(size),
|
||||
_ => DrawAxDiamondPixel(size),
|
||||
};
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
bitmap.Save(stream, ImageFormat.Png);
|
||||
pngFrames.Add(stream.ToArray());
|
||||
Console.WriteLine($" {size}x{size} OK");
|
||||
|
||||
if (options.PreviewSize == size && !string.IsNullOrWhiteSpace(options.PreviewPath))
|
||||
bitmap.Save(options.PreviewPath!, ImageFormat.Png);
|
||||
}
|
||||
|
||||
CreateIco(pngList, sizes, outputPath);
|
||||
Console.WriteLine($"Icon saved: {outputPath}");
|
||||
CreateIco(pngFrames, options.Sizes, options.OutputPath);
|
||||
Console.WriteLine($"Icon saved: {options.OutputPath}");
|
||||
|
||||
static Bitmap DrawGemDiamond(int size)
|
||||
static Bitmap DrawAxDiamondPixel(int size)
|
||||
{
|
||||
var bitmap = new Bitmap(size, size, PixelFormat.Format32bppArgb);
|
||||
using var g = Graphics.FromImage(bitmap);
|
||||
g.Clear(Color.Transparent);
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
g.CompositingQuality = CompositingQuality.HighQuality;
|
||||
|
||||
var canvas = size;
|
||||
var groupSpan = canvas * 0.55f;
|
||||
var tileSize = canvas * 0.245f;
|
||||
var gap = Math.Max(1f, groupSpan - tileSize * 2f);
|
||||
var left = (canvas - groupSpan) / 2f;
|
||||
var top = left;
|
||||
var radius = Math.Max(2f, tileSize * 0.16f);
|
||||
var center = new PointF(canvas / 2f, canvas / 2f);
|
||||
|
||||
var tiles = new[]
|
||||
{
|
||||
new IconTile(
|
||||
new RectangleF(left, top, tileSize, tileSize),
|
||||
Color.FromArgb(0x74, 0x8E, 0xFF),
|
||||
Color.FromArgb(0x5B, 0x70, 0xF7)),
|
||||
new IconTile(
|
||||
new RectangleF(left + tileSize + gap, top, tileSize, tileSize),
|
||||
Color.FromArgb(0x7C, 0xF0, 0x88),
|
||||
Color.FromArgb(0x42, 0xD6, 0x63)),
|
||||
new IconTile(
|
||||
new RectangleF(left, top + tileSize + gap, tileSize, tileSize),
|
||||
Color.FromArgb(0x64, 0xDB, 0x8B),
|
||||
Color.FromArgb(0x36, 0xC6, 0x78)),
|
||||
new IconTile(
|
||||
new RectangleF(left + tileSize + gap, top + tileSize + gap, tileSize, tileSize),
|
||||
Color.FromArgb(0xFF, 0x6C, 0x84),
|
||||
Color.FromArgb(0xF2, 0x47, 0x69)),
|
||||
};
|
||||
|
||||
var shadowOffset = Math.Max(0.45f, canvas * 0.02f);
|
||||
using var shadowBrush = new SolidBrush(Color.FromArgb((int)(255 * 0.16f), 0x2A, 0x34, 0x56));
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
using var shadowPath = CreateRoundedRectangle(new RectangleF(
|
||||
tile.Bounds.X + shadowOffset,
|
||||
tile.Bounds.Y + shadowOffset,
|
||||
tile.Bounds.Width,
|
||||
tile.Bounds.Height), radius);
|
||||
using var matrix = new Matrix();
|
||||
matrix.RotateAt(45f, center);
|
||||
shadowPath.Transform(matrix);
|
||||
g.FillPath(shadowBrush, shadowPath);
|
||||
}
|
||||
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
using var path = CreateRoundedRectangle(tile.Bounds, radius);
|
||||
using var matrix = new Matrix();
|
||||
matrix.RotateAt(45f, center);
|
||||
path.Transform(matrix);
|
||||
|
||||
using var brush = new LinearGradientBrush(tile.Bounds, tile.Highlight, tile.Base, 45f, true);
|
||||
g.FillPath(brush, path);
|
||||
|
||||
using var outline = new Pen(Color.FromArgb((int)(255 * 0.26f), 255, 255, 255), Math.Max(0.7f, canvas / 60f))
|
||||
{
|
||||
LineJoin = LineJoin.Round
|
||||
};
|
||||
g.DrawPath(outline, path);
|
||||
}
|
||||
|
||||
using var shineBrush = new SolidBrush(Color.FromArgb((int)(255 * 0.18f), 255, 255, 255));
|
||||
var shineSize = Math.Max(1.2f, canvas * 0.07f);
|
||||
foreach (var point in new[]
|
||||
{
|
||||
new PointF(canvas * 0.30f, canvas * 0.28f),
|
||||
new PointF(canvas * 0.67f, canvas * 0.30f),
|
||||
new PointF(canvas * 0.34f, canvas * 0.66f),
|
||||
})
|
||||
{
|
||||
g.FillEllipse(shineBrush, point.X, point.Y, shineSize, shineSize);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
static Bitmap DrawLegacyGemDiamond(int size)
|
||||
{
|
||||
var bmp = new Bitmap(size, size, PixelFormat.Format32bppArgb);
|
||||
using var g = Graphics.FromImage(bmp);
|
||||
@@ -33,173 +118,211 @@ static Bitmap DrawGemDiamond(int size)
|
||||
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
g.Clear(Color.Transparent);
|
||||
|
||||
float s = size;
|
||||
float cx = s / 2f;
|
||||
var s = size;
|
||||
var cx = s / 2f;
|
||||
var tableL = s * 0.22f;
|
||||
var tableR = s * 0.78f;
|
||||
var tableY = s * 0.18f;
|
||||
var girdleL = s * 0.06f;
|
||||
var girdleR = s * 0.94f;
|
||||
var girdleY = s * 0.40f;
|
||||
var culetX = cx;
|
||||
var culetY = s * 0.92f;
|
||||
|
||||
// ── 보석 다이아몬드 외곽 좌표 ──
|
||||
// Table (평평한 윗면)
|
||||
float tableL = s * 0.22f;
|
||||
float tableR = s * 0.78f;
|
||||
float tableY = s * 0.18f;
|
||||
PointF tl = new(tableL, tableY);
|
||||
PointF tr = new(tableR, tableY);
|
||||
PointF gl = new(girdleL, girdleY);
|
||||
PointF gr = new(girdleR, girdleY);
|
||||
PointF bt = new(culetX, culetY);
|
||||
PointF cm = new(cx, girdleY);
|
||||
PointF ct = new(cx, tableY);
|
||||
|
||||
// Girdle (가장 넓은 부분)
|
||||
float girdleL = s * 0.06f;
|
||||
float girdleR = s * 0.94f;
|
||||
float girdleY = s * 0.40f;
|
||||
|
||||
// Culet (바닥 뾰족한 점)
|
||||
float culetX = cx;
|
||||
float culetY = s * 0.92f;
|
||||
|
||||
// 주요 꼭짓점
|
||||
PointF TL = new(tableL, tableY); // 테이블 좌
|
||||
PointF TR = new(tableR, tableY); // 테이블 우
|
||||
PointF GL = new(girdleL, girdleY); // 거들 좌
|
||||
PointF GR = new(girdleR, girdleY); // 거들 우
|
||||
PointF BT = new(culetX, culetY); // 바닥 점
|
||||
|
||||
// Crown 내부 포인트 (table → girdle 사이 facet)
|
||||
PointF CM = new(cx, girdleY); // 거들 중앙
|
||||
PointF CT = new(cx, tableY); // 테이블 중앙
|
||||
|
||||
// Crown facet 분할점
|
||||
PointF CL = new(s * 0.14f, girdleY); // 거들 좌측 근처
|
||||
PointF CR = new(s * 0.86f, girdleY); // 거들 우측 근처
|
||||
|
||||
// Pavilion facet 내부 교차점들
|
||||
float pavMidY = s * 0.62f;
|
||||
PointF PL = new(s * 0.28f, pavMidY); // 파빌리온 좌 중간
|
||||
PointF PR = new(s * 0.72f, pavMidY); // 파빌리온 우 중간
|
||||
PointF PM = new(cx, pavMidY); // 파빌리온 중앙
|
||||
|
||||
// ── 색상 ──
|
||||
var blue = Color.FromArgb(50, 110, 230);
|
||||
var blue = Color.FromArgb(50, 110, 230);
|
||||
var blueBright = Color.FromArgb(80, 150, 255);
|
||||
var green = Color.FromArgb(60, 200, 80);
|
||||
var green = Color.FromArgb(60, 200, 80);
|
||||
var greenBright = Color.FromArgb(100, 235, 110);
|
||||
var red = Color.FromArgb(230, 50, 65);
|
||||
var red = Color.FromArgb(230, 50, 65);
|
||||
var redBright = Color.FromArgb(255, 90, 100);
|
||||
var greenDk = Color.FromArgb(45, 170, 75);
|
||||
var greenDk = Color.FromArgb(45, 170, 75);
|
||||
var greenDkBr = Color.FromArgb(80, 210, 100);
|
||||
|
||||
// ── Crown (상단부) 채우기 ──
|
||||
FillPolygon(g, new[] { tl, gl, cm, ct }, blueBright, blue);
|
||||
FillPolygon(g, new[] { tr, ct, cm, gr }, greenBright, green);
|
||||
FillPolygon(g, new[] { gl, cm, bt }, redBright, red);
|
||||
FillPolygon(g, new[] { cm, gr, bt }, greenDkBr, greenDk);
|
||||
|
||||
// Crown 좌: Blue
|
||||
FillPoly(g, new[]{TL, GL, CM, CT}, blueBright, blue);
|
||||
// Crown 우: Green
|
||||
FillPoly(g, new[]{TR, CT, CM, GR}, greenBright, green);
|
||||
|
||||
// ── Pavilion (하단부) 채우기 ──
|
||||
|
||||
// Pavilion 좌: Red
|
||||
FillPoly(g, new[]{GL, CM, BT}, redBright, red);
|
||||
// Pavilion 우: Green (darker)
|
||||
FillPoly(g, new[]{CM, GR, BT}, greenDkBr, greenDk);
|
||||
|
||||
// ── Facet 내부 색상 변화 (깊이감) ──
|
||||
// Crown 좌측 어두운 삼각형
|
||||
using var crownShadow = new SolidBrush(Color.FromArgb(25, 0, 0, 0));
|
||||
g.FillPolygon(crownShadow, new[]{TL, GL, new PointF(cx * 0.7f, girdleY * 0.85f)});
|
||||
g.FillPolygon(crownShadow, new[] { tl, gl, new PointF(cx * 0.7f, girdleY * 0.85f) });
|
||||
|
||||
// Pavilion 중앙 밝은 삼각형 (반사)
|
||||
using var pavHighlight = new SolidBrush(Color.FromArgb(20, 255, 255, 255));
|
||||
g.FillPolygon(pavHighlight, new[]{CM, BT, new PointF(cx - s*0.08f, pavMidY)});
|
||||
g.FillPolygon(pavHighlight, new[] { cm, bt, new PointF(cx - s * 0.08f, s * 0.62f) });
|
||||
|
||||
// ── Facet 선 (흰색) ──
|
||||
float lw = Math.Max(0.8f, s / 140f);
|
||||
using var facetPen = new Pen(Color.FromArgb(180, 255, 255, 255), lw)
|
||||
using var facetPen = new Pen(Color.FromArgb(180, 255, 255, 255), Math.Max(0.8f, s / 140f))
|
||||
{
|
||||
LineJoin = LineJoin.Round,
|
||||
StartCap = LineCap.Round,
|
||||
EndCap = LineCap.Round
|
||||
};
|
||||
|
||||
// Crown facet lines
|
||||
// 테이블 윗변
|
||||
g.DrawLine(facetPen, TL, TR);
|
||||
// 테이블 → 거들 대각선
|
||||
g.DrawLine(facetPen, TL, GL);
|
||||
g.DrawLine(facetPen, TR, GR);
|
||||
// 세로 중심선 (table → girdle)
|
||||
g.DrawLine(facetPen, CT, CM);
|
||||
// Crown 크로스 facet
|
||||
g.DrawLine(facetPen, TL, CM);
|
||||
g.DrawLine(facetPen, TR, CM);
|
||||
// 추가 Crown facet (table 모서리 → 거들 중간)
|
||||
g.DrawLine(facetPen, TL, new PointF(girdleL + (cx - girdleL) * 0.5f, girdleY));
|
||||
g.DrawLine(facetPen, TR, new PointF(cx + (girdleR - cx) * 0.5f, girdleY));
|
||||
g.DrawLine(facetPen, tl, tr);
|
||||
g.DrawLine(facetPen, tl, gl);
|
||||
g.DrawLine(facetPen, tr, gr);
|
||||
g.DrawLine(facetPen, ct, cm);
|
||||
g.DrawLine(facetPen, tl, cm);
|
||||
g.DrawLine(facetPen, tr, cm);
|
||||
g.DrawLine(facetPen, tl, new PointF(girdleL + (cx - girdleL) * 0.5f, girdleY));
|
||||
g.DrawLine(facetPen, tr, new PointF(cx + (girdleR - cx) * 0.5f, girdleY));
|
||||
g.DrawLine(facetPen, gl, gr);
|
||||
g.DrawLine(facetPen, gl, bt);
|
||||
g.DrawLine(facetPen, gr, bt);
|
||||
g.DrawLine(facetPen, cm, bt);
|
||||
|
||||
// Girdle 수평선
|
||||
g.DrawLine(facetPen, GL, GR);
|
||||
|
||||
// Pavilion facet lines
|
||||
// 거들 → 바닥 점
|
||||
g.DrawLine(facetPen, GL, BT);
|
||||
g.DrawLine(facetPen, GR, BT);
|
||||
// 중심 → 바닥
|
||||
g.DrawLine(facetPen, CM, BT);
|
||||
// Pavilion 크로스 facets
|
||||
float crossY = girdleY + (culetY - girdleY) * 0.45f;
|
||||
var crossY = girdleY + (culetY - girdleY) * 0.45f;
|
||||
PointF crossL = new(girdleL + (culetX - girdleL) * 0.45f, crossY);
|
||||
PointF crossR = new(girdleR - (girdleR - culetX) * 0.45f, crossY);
|
||||
g.DrawLine(facetPen, GL, crossR);
|
||||
g.DrawLine(facetPen, GR, crossL);
|
||||
// 거들 중간점에서 바닥으로
|
||||
g.DrawLine(facetPen, new PointF(girdleL + (cx - girdleL) * 0.5f, girdleY), BT);
|
||||
g.DrawLine(facetPen, new PointF(cx + (girdleR - cx) * 0.5f, girdleY), BT);
|
||||
g.DrawLine(facetPen, gl, crossR);
|
||||
g.DrawLine(facetPen, gr, crossL);
|
||||
g.DrawLine(facetPen, new PointF(girdleL + (cx - girdleL) * 0.5f, girdleY), bt);
|
||||
g.DrawLine(facetPen, new PointF(cx + (girdleR - cx) * 0.5f, girdleY), bt);
|
||||
|
||||
// ── 외곽선 (두꺼운 흰색) ──
|
||||
float olw = Math.Max(1.2f, s / 100f);
|
||||
using var outlinePen = new Pen(Color.FromArgb(220, 255, 255, 255), olw)
|
||||
using var outlinePen = new Pen(Color.FromArgb(220, 255, 255, 255), Math.Max(1.2f, s / 100f))
|
||||
{
|
||||
LineJoin = LineJoin.Round
|
||||
};
|
||||
g.DrawPolygon(outlinePen, new[]{TL, TR, GR, BT, GL});
|
||||
g.DrawPolygon(outlinePen, new[] { tl, tr, gr, bt, gl });
|
||||
|
||||
// ── 상단 하이라이트 (테이블 면 빛 반사) ──
|
||||
using var tableHL = new LinearGradientBrush(
|
||||
TL, new PointF(cx, girdleY),
|
||||
using var tableHighlight = new LinearGradientBrush(
|
||||
tl,
|
||||
new PointF(cx, girdleY),
|
||||
Color.FromArgb(45, 255, 255, 255),
|
||||
Color.Transparent);
|
||||
g.FillPolygon(tableHL, new[]{TL, TR, CT});
|
||||
g.FillPolygon(tableHighlight, new[] { tl, tr, ct });
|
||||
|
||||
return bmp;
|
||||
}
|
||||
|
||||
static void FillPoly(Graphics g, PointF[] pts, Color c1, Color c2)
|
||||
static GraphicsPath CreateRoundedRectangle(RectangleF bounds, float radius)
|
||||
{
|
||||
float minX = pts.Min(p => p.X), maxX = pts.Max(p => p.X);
|
||||
float minY = pts.Min(p => p.Y), maxY = pts.Max(p => p.Y);
|
||||
var rect = new RectangleF(minX, minY, Math.Max(1, maxX - minX), Math.Max(1, maxY - minY));
|
||||
try
|
||||
var path = new GraphicsPath();
|
||||
var diameter = radius * 2f;
|
||||
|
||||
if (radius <= 0.01f)
|
||||
{
|
||||
using var brush = new LinearGradientBrush(rect, c1, c2, LinearGradientMode.Vertical);
|
||||
g.FillPolygon(brush, pts);
|
||||
}
|
||||
catch
|
||||
{
|
||||
using var brush = new SolidBrush(c1);
|
||||
g.FillPolygon(brush, pts);
|
||||
path.AddRectangle(bounds);
|
||||
return path;
|
||||
}
|
||||
|
||||
path.AddArc(bounds.Left, bounds.Top, diameter, diameter, 180, 90);
|
||||
path.AddArc(bounds.Right - diameter, bounds.Top, diameter, diameter, 270, 90);
|
||||
path.AddArc(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter, 0, 90);
|
||||
path.AddArc(bounds.Left, bounds.Bottom - diameter, diameter, diameter, 90, 90);
|
||||
path.CloseFigure();
|
||||
return path;
|
||||
}
|
||||
|
||||
static void CreateIco(List<byte[]> pngs, int[] sizes, string path)
|
||||
static void FillPolygon(Graphics g, PointF[] points, Color topColor, Color bottomColor)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using var bw = new BinaryWriter(ms);
|
||||
bw.Write((short)0);
|
||||
bw.Write((short)1);
|
||||
bw.Write((short)pngs.Count);
|
||||
int offset = 6 + 16 * pngs.Count;
|
||||
for (int i = 0; i < pngs.Count; i++)
|
||||
var minX = points.Min(p => p.X);
|
||||
var maxX = points.Max(p => p.X);
|
||||
var minY = points.Min(p => p.Y);
|
||||
var maxY = points.Max(p => p.Y);
|
||||
var rect = new RectangleF(minX, minY, Math.Max(1, maxX - minX), Math.Max(1, maxY - minY));
|
||||
|
||||
using var brush = new LinearGradientBrush(rect, topColor, bottomColor, LinearGradientMode.Vertical);
|
||||
g.FillPolygon(brush, points);
|
||||
}
|
||||
|
||||
static void CreateIco(IReadOnlyList<byte[]> pngs, IReadOnlyList<int> sizes, string outputPath)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var writer = new BinaryWriter(memoryStream);
|
||||
|
||||
writer.Write((short)0);
|
||||
writer.Write((short)1);
|
||||
writer.Write((short)pngs.Count);
|
||||
|
||||
var offset = 6 + 16 * pngs.Count;
|
||||
for (var i = 0; i < pngs.Count; i++)
|
||||
{
|
||||
byte dim = (byte)(sizes[i] >= 256 ? 0 : sizes[i]);
|
||||
bw.Write(dim); bw.Write(dim);
|
||||
bw.Write((byte)0); bw.Write((byte)0);
|
||||
bw.Write((short)1); bw.Write((short)32);
|
||||
bw.Write(pngs[i].Length); bw.Write(offset);
|
||||
var dimension = (byte)(sizes[i] >= 256 ? 0 : sizes[i]);
|
||||
writer.Write(dimension);
|
||||
writer.Write(dimension);
|
||||
writer.Write((byte)0);
|
||||
writer.Write((byte)0);
|
||||
writer.Write((short)1);
|
||||
writer.Write((short)32);
|
||||
writer.Write(pngs[i].Length);
|
||||
writer.Write(offset);
|
||||
offset += pngs[i].Length;
|
||||
}
|
||||
foreach (var png in pngs) bw.Write(png);
|
||||
File.WriteAllBytes(path, ms.ToArray());
|
||||
|
||||
foreach (var png in pngs)
|
||||
writer.Write(png);
|
||||
|
||||
File.WriteAllBytes(outputPath, memoryStream.ToArray());
|
||||
}
|
||||
|
||||
file sealed record IconTile(RectangleF Bounds, Color Highlight, Color Base);
|
||||
|
||||
file enum IconStyle
|
||||
{
|
||||
AxDiamond,
|
||||
LegacyGem,
|
||||
}
|
||||
|
||||
file sealed class IconGeneratorOptions
|
||||
{
|
||||
public string OutputPath { get; init; } = "";
|
||||
public string? PreviewPath { get; init; }
|
||||
public int PreviewSize { get; init; } = 256;
|
||||
public int[] Sizes { get; init; } = [16, 20, 24, 32, 40, 48, 64, 128, 256];
|
||||
public IconStyle Style { get; init; } = IconStyle.AxDiamond;
|
||||
|
||||
public static IconGeneratorOptions Parse(string[] args)
|
||||
{
|
||||
string? outputPath = null;
|
||||
string? previewPath = null;
|
||||
var previewSize = 256;
|
||||
var style = IconStyle.AxDiamond;
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (arg.StartsWith("--preview=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
previewPath = arg["--preview=".Length..].Trim('"');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.StartsWith("--preview-size=", StringComparison.OrdinalIgnoreCase)
|
||||
&& int.TryParse(arg["--preview-size=".Length..], out var parsedPreviewSize))
|
||||
{
|
||||
previewSize = parsedPreviewSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.StartsWith("--style=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var styleValue = arg["--style=".Length..].Trim().ToLowerInvariant();
|
||||
style = styleValue switch
|
||||
{
|
||||
"legacy-gem" or "gem" => IconStyle.LegacyGem,
|
||||
_ => IconStyle.AxDiamond,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!arg.StartsWith("--", StringComparison.Ordinal))
|
||||
outputPath ??= arg.Trim('"');
|
||||
}
|
||||
|
||||
outputPath ??= Path.Combine(AppContext.BaseDirectory, "icon.ico");
|
||||
|
||||
return new IconGeneratorOptions
|
||||
{
|
||||
OutputPath = outputPath,
|
||||
PreviewPath = previewPath,
|
||||
PreviewSize = previewSize,
|
||||
Style = style,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user