Skip to content

Commit 9e8e675

Browse files
authored
Merge pull request #1231 from Scriptwonder/feat/editor-logo-setup-polish
feat(editor): embed Ocean brand mark in Editor UI + setup polish
2 parents 8618f83 + 42d9faa commit 9e8e675

20 files changed

Lines changed: 731 additions & 65 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using MCPForUnity.Editor.Helpers;
4+
5+
namespace MCPForUnity.Editor.Dependencies
6+
{
7+
/// <summary>
8+
/// Runs the official uv installer for the current platform. UI-agnostic: build the command,
9+
/// run it (off the main thread), and report the outcome. The caller owns confirmation and
10+
/// re-checking dependencies afterwards.
11+
/// Installer reference: https://docs.astral.sh/uv/getting-started/installation/
12+
/// </summary>
13+
public static class UvInstaller
14+
{
15+
public readonly struct UvInstallResult
16+
{
17+
public readonly bool Success;
18+
public readonly string Output;
19+
20+
public UvInstallResult(bool success, string output)
21+
{
22+
Success = success;
23+
Output = output ?? string.Empty;
24+
}
25+
}
26+
27+
/// <summary>One-click install is available on Windows, macOS and Linux.</summary>
28+
public static bool IsSupported =>
29+
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ||
30+
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||
31+
RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
32+
33+
/// <summary>
34+
/// Build the installer invocation for the current platform. Pure and testable — the
35+
/// returned command is exactly what <see cref="Run"/> executes and what the confirm
36+
/// dialog shows the user.
37+
/// </summary>
38+
public static (string file, string arguments) BuildInstallCommand()
39+
{
40+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
41+
{
42+
return ("powershell",
43+
"-NoProfile -ExecutionPolicy ByPass -c \"irm https://astral.sh/uv/install.ps1 | iex\"");
44+
}
45+
46+
// macOS and Linux share the POSIX shell installer.
47+
return ("/bin/sh", "-c \"curl -LsSf https://astral.sh/uv/install.sh | sh\"");
48+
}
49+
50+
/// <summary>Human-readable one-line description of the command that will run.</summary>
51+
public static string DescribeCommand()
52+
{
53+
var (file, arguments) = BuildInstallCommand();
54+
return $"{file} {arguments}";
55+
}
56+
57+
/// <summary>
58+
/// Run the installer and return the outcome. Blocks until the installer exits or the
59+
/// timeout elapses, so call it off the main thread (e.g. via Task.Run).
60+
/// </summary>
61+
public static UvInstallResult Run(int timeoutMs = 180000)
62+
{
63+
try
64+
{
65+
var (file, arguments) = BuildInstallCommand();
66+
bool ok = ExecPath.TryRun(file, arguments, null, out string stdout, out string stderr, timeoutMs);
67+
return new UvInstallResult(ok, Combine(stdout, stderr));
68+
}
69+
catch (Exception ex)
70+
{
71+
return new UvInstallResult(false, ex.Message);
72+
}
73+
}
74+
75+
private static string Combine(string stdout, string stderr)
76+
{
77+
string outText = (stdout ?? string.Empty).Trim();
78+
string errText = (stderr ?? string.Empty).Trim();
79+
string combined =
80+
string.IsNullOrEmpty(errText) ? outText :
81+
string.IsNullOrEmpty(outText) ? errText :
82+
outText + "\n" + errText;
83+
84+
// Keep dialogs readable — echo only the tail of long installer output.
85+
const int max = 1500;
86+
if (combined.Length > max)
87+
{
88+
combined = "…" + combined.Substring(combined.Length - max);
89+
}
90+
return combined;
91+
}
92+
}
93+
}

‎MCPForUnity/Editor/Dependencies/UvInstaller.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎MCPForUnity/Editor/Helpers/ExecPath.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ internal static string ResolveClaude()
3434
"/opt/homebrew/bin/claude",
3535
"/usr/local/bin/claude",
3636
Path.Combine(home, ".local", "bin", "claude"),
37+
Path.Combine(home, ".claude", "local", "claude"), // `claude migrate-installer` location
3738
};
3839
foreach (string c in candidates) { if (File.Exists(c)) return c; }
3940
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
@@ -59,6 +60,7 @@ internal static string ResolveClaude()
5960
Path.Combine(localAppData, "Programs", "claude", "claude.exe"),
6061
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
6162
Path.Combine(home, ".local", "bin", "claude.exe"),
63+
Path.Combine(home, ".claude", "local", "claude.exe"), // `claude migrate-installer` location
6264
// npm global locations (.cmd preferred for non-interactive processes)
6365
Path.Combine(appData, "npm", "claude.cmd"),
6466
Path.Combine(localAppData, "npm", "claude.cmd"),
@@ -81,6 +83,7 @@ internal static string ResolveClaude()
8183
"/usr/local/bin/claude",
8284
"/usr/bin/claude",
8385
Path.Combine(home, ".local", "bin", "claude"),
86+
Path.Combine(home, ".claude", "local", "claude"), // `claude migrate-installer` location
8487
};
8588
foreach (string c in candidates) { if (File.Exists(c)) return c; }
8689
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude

‎MCPForUnity/Editor/Services/PathResolverService.cs‎

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -107,60 +107,10 @@ public string GetClaudeCliPath()
107107
return null;
108108
}
109109

110-
// No override - use platform-specific discovery
111-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
112-
{
113-
string[] candidates = new[]
114-
{
115-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "claude", "claude.exe"),
116-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
117-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude.exe"),
118-
};
119-
120-
foreach (var c in candidates)
121-
{
122-
if (File.Exists(c)) return c;
123-
}
124-
125-
#if UNITY_EDITOR_WIN
126-
// Fall back to PATH search (handles non-standard install locations and npm shims)
127-
foreach (var name in new[] { "claude.exe", "claude.cmd", "claude.ps1" })
128-
{
129-
string fromPath = ExecPath.FindInPathWindows(name);
130-
if (!string.IsNullOrEmpty(fromPath)) return fromPath;
131-
}
132-
#endif
133-
}
134-
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
135-
{
136-
string[] candidates = new[]
137-
{
138-
"/opt/homebrew/bin/claude",
139-
"/usr/local/bin/claude",
140-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
141-
};
142-
143-
foreach (var c in candidates)
144-
{
145-
if (File.Exists(c)) return c;
146-
}
147-
}
148-
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
149-
{
150-
string[] candidates = new[]
151-
{
152-
"/usr/bin/claude",
153-
"/usr/local/bin/claude",
154-
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
155-
};
156-
157-
foreach (var c in candidates)
158-
{
159-
if (File.Exists(c)) return c;
160-
}
161-
}
162-
163-
return null;
110+
// No override: delegate to the shared discovery in ExecPath, which covers the
111+
// native-installer, npm, NVM and PATH-scan locations for every platform. Kept in
112+
// one place so both call sites (this and ExecPath's own callers) stay in sync.
113+
return ExecPath.ResolveClaude();
164114
}
165115

166116
public bool IsPythonDetected()

‎MCPForUnity/Editor/Windows/Components/Branding.meta‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
using UnityEngine;
2+
using UnityEngine.UIElements;
3+
4+
namespace MCPForUnity.Editor.Windows.Components.Branding
5+
{
6+
/// <summary>
7+
/// The MCP for Unity "Ocean split-cube" brand mark.
8+
///
9+
/// On Unity 2022.1+ it is drawn as resolution-independent vector geometry via UI Toolkit's
10+
/// Painter2D — crisp at any DPI, theme-independent (fixed brand colors matching the source
11+
/// SVG), and with no package dependency. Painter2D does not exist on the 2021.3 floor, so
12+
/// there we fall back to the raster brand icon (package-icon.png) that ships with the package.
13+
///
14+
/// Geometry is transcribed from website/static/img/logo-mark.svg (viewBox "30 30 140 140"),
15+
/// which remains the human-facing source of truth. Size the element via USS (width/height);
16+
/// the drawing scales to the resolved content box.
17+
/// </summary>
18+
public sealed class OceanMark : VisualElement
19+
{
20+
public OceanMark()
21+
{
22+
#if UNITY_2022_1_OR_NEWER
23+
generateVisualContent += OnGenerateVisualContent;
24+
RegisterCallback<GeometryChangedEvent>(_ => MarkDirtyRepaint());
25+
#else
26+
ApplyRasterFallback();
27+
#endif
28+
pickingMode = PickingMode.Ignore; // purely decorative
29+
}
30+
31+
#if UNITY_2022_1_OR_NEWER
32+
// The source SVG viewBox spans 30..170 on both axes (a 140-unit square).
33+
private const float SvgOrigin = 30f;
34+
private const float SvgSize = 140f;
35+
36+
// Brand palette (hex values lifted directly from logo-mark.svg).
37+
private static readonly Color LeftFill = FromHex(0x2563EB); // blue cube faces
38+
private static readonly Color LeftStroke = FromHex(0x60A5FA); // blue cube outline
39+
private static readonly Color RightFill = FromHex(0x0D9488); // teal cube faces
40+
private static readonly Color RightStroke = FromHex(0x2DD4BF);// teal cube outline
41+
private static readonly Color Bridge = FromHex(0x22D3EE); // cyan bridge
42+
43+
private void OnGenerateVisualContent(MeshGenerationContext mgc)
44+
{
45+
Rect rect = contentRect;
46+
float box = Mathf.Min(rect.width, rect.height);
47+
if (box <= 1f) return; // not laid out yet / too small to draw
48+
49+
float scale = box / SvgSize;
50+
Vector2 P(float x, float y) => MapSvgPoint(x, y, rect);
51+
float S(float w) => w * scale;
52+
53+
Painter2D p = mgc.painter2D;
54+
p.lineJoin = LineJoin.Round;
55+
p.lineCap = LineCap.Round;
56+
57+
// --- Left cube (blue) ---
58+
FillPoly(p, WithAlpha(LeftFill, 0.22f), P(94, 40), P(42, 70), P(94, 100));
59+
FillPoly(p, WithAlpha(LeftFill, 0.12f), P(42, 70), P(42, 130), P(94, 160), P(94, 100));
60+
StrokePath(p, LeftStroke, S(3f), true, P(94, 40), P(42, 70), P(42, 130), P(94, 160));
61+
StrokePath(p, WithAlpha(LeftStroke, 0.65f), S(1.8f), false, P(42, 70), P(94, 100));
62+
63+
// --- Right cube (teal) ---
64+
FillPoly(p, WithAlpha(RightFill, 0.22f), P(106, 40), P(158, 70), P(106, 100));
65+
FillPoly(p, WithAlpha(RightFill, 0.12f), P(158, 70), P(158, 130), P(106, 160), P(106, 100));
66+
StrokePath(p, RightStroke, S(3f), true, P(106, 40), P(158, 70), P(158, 130), P(106, 160));
67+
StrokePath(p, WithAlpha(RightStroke, 0.65f), S(1.8f), false, P(158, 70), P(106, 100));
68+
69+
// --- Bridge rungs (cyan) ---
70+
StrokePath(p, Bridge, S(3f), false, P(94, 70), P(106, 70));
71+
StrokePath(p, Bridge, S(3f), false, P(94, 100), P(106, 100));
72+
StrokePath(p, Bridge, S(3f), false, P(94, 130), P(106, 130));
73+
74+
// --- Bridge dots (cyan) ---
75+
float dot = S(2.2f);
76+
FillDot(p, Bridge, P(94, 70), dot);
77+
FillDot(p, Bridge, P(106, 70), dot);
78+
FillDot(p, Bridge, P(94, 100), dot);
79+
FillDot(p, Bridge, P(106, 100), dot);
80+
FillDot(p, Bridge, P(94, 130), dot);
81+
FillDot(p, Bridge, P(106, 130), dot);
82+
}
83+
84+
private static void FillPoly(Painter2D p, Color color, params Vector2[] pts)
85+
{
86+
p.fillColor = color;
87+
p.BeginPath();
88+
p.MoveTo(pts[0]);
89+
for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
90+
p.ClosePath();
91+
p.Fill();
92+
}
93+
94+
private static void StrokePath(Painter2D p, Color color, float width, bool closed, params Vector2[] pts)
95+
{
96+
p.strokeColor = color;
97+
p.lineWidth = width;
98+
p.BeginPath();
99+
p.MoveTo(pts[0]);
100+
for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
101+
if (closed) p.ClosePath();
102+
p.Stroke();
103+
}
104+
105+
private static void FillDot(Painter2D p, Color color, Vector2 center, float radius)
106+
{
107+
if (radius <= 0.01f) return;
108+
p.fillColor = color;
109+
p.BeginPath();
110+
p.Arc(center, radius, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
111+
p.Fill();
112+
}
113+
114+
private static Color WithAlpha(Color c, float a)
115+
{
116+
c.a = a;
117+
return c;
118+
}
119+
120+
/// <summary>Map a point from SVG viewBox space into the element's content rect (square, centered).</summary>
121+
private static Vector2 MapSvgPoint(float svgX, float svgY, Rect content)
122+
{
123+
float box = Mathf.Min(content.width, content.height);
124+
float scale = box / SvgSize;
125+
float offsetX = (content.width - box) * 0.5f;
126+
float offsetY = (content.height - box) * 0.5f;
127+
return new Vector2(
128+
offsetX + (svgX - SvgOrigin) * scale,
129+
offsetY + (svgY - SvgOrigin) * scale);
130+
}
131+
132+
/// <summary>Convert a 0xRRGGBB literal into an opaque Color.</summary>
133+
private static Color FromHex(int rgb) => new Color(
134+
((rgb >> 16) & 0xFF) / 255f,
135+
((rgb >> 8) & 0xFF) / 255f,
136+
(rgb & 0xFF) / 255f,
137+
1f);
138+
#else
139+
// Painter2D is unavailable before Unity 2022.1 — show the raster brand icon instead.
140+
private void ApplyRasterFallback()
141+
{
142+
Texture2D tex = LoadBrandTexture();
143+
if (tex == null) return;
144+
style.backgroundImage = new StyleBackground(tex);
145+
style.unityBackgroundScaleMode = ScaleMode.ScaleToFit;
146+
}
147+
148+
private static Texture2D LoadBrandTexture()
149+
{
150+
try
151+
{
152+
string root = MCPForUnity.Editor.Helpers.AssetPathUtility.GetMcpPackageRootPath();
153+
return UnityEditor.AssetDatabase.LoadAssetAtPath<Texture2D>($"{root}/package-icon.png");
154+
}
155+
catch (System.Exception ex)
156+
{
157+
MCPForUnity.Editor.Helpers.McpLog.Warn($"OceanMark: failed to load brand icon fallback: {ex.Message}");
158+
return null;
159+
}
160+
}
161+
#endif
162+
}
163+
}

‎MCPForUnity/Editor/Windows/Components/Branding/OceanMark.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)