|
| 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 | +} |
0 commit comments