diff --git a/README.md b/README.md
index 58e96f1..ca67b83 100644
--- a/README.md
+++ b/README.md
@@ -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` 그리드와 내부 마진을 줘 점선 링이 살짝 잘리던 문제도 함께 줄였습니다.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index a33f8b7..37952ed 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -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 프레임이 요청 크기 그대로 로드됨
diff --git a/src/AxCopilot/Assets/diamond_pixel.svg b/src/AxCopilot/Assets/diamond_pixel.svg
index 48d7a81..0da1f27 100644
--- a/src/AxCopilot/Assets/diamond_pixel.svg
+++ b/src/AxCopilot/Assets/diamond_pixel.svg
@@ -1,13 +1,27 @@
diff --git a/src/AxCopilot/Assets/icon.ico b/src/AxCopilot/Assets/icon.ico
index 273db74..5cffa14 100644
Binary files a/src/AxCopilot/Assets/icon.ico and b/src/AxCopilot/Assets/icon.ico differ
diff --git a/tools/IconGenerator/Program.cs b/tools/IconGenerator/Program.cs
index 3688a0f..36dcd0a 100644
--- a/tools/IconGenerator/Program.cs
+++ b/tools/IconGenerator/Program.cs
@@ -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();
-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();
-
-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 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 pngs, IReadOnlyList 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,
+ };
+ }
}