A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using osu.Framework.Graphics.Primitives;
6using osu.Framework.Graphics.Textures;
7using osuTK;
8using osu.Framework.Graphics.Shaders;
9using osu.Framework.Allocation;
10using System.Collections.Generic;
11using osu.Framework.Caching;
12using osu.Framework.Extensions.EnumExtensions;
13using osuTK.Graphics;
14using osuTK.Graphics.ES30;
15
16namespace osu.Framework.Graphics.Lines
17{
18 public partial class Path : Drawable, IBufferedDrawable
19 {
20 public IShader RoundedTextureShader { get; private set; }
21 public IShader TextureShader { get; private set; }
22 private IShader pathShader;
23
24 public Path()
25 {
26 AutoSizeAxes = Axes.Both;
27 }
28
29 [BackgroundDependencyLoader]
30 private void load(ShaderManager shaders)
31 {
32 RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
33 TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
34 pathShader = shaders.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE);
35 }
36
37 private readonly List<Vector2> vertices = new List<Vector2>();
38
39 public IReadOnlyList<Vector2> Vertices
40 {
41 get => vertices;
42 set
43 {
44 vertices.Clear();
45 vertices.AddRange(value);
46
47 vertexBoundsCache.Invalidate();
48 segmentsCache.Invalidate();
49
50 Invalidate(Invalidation.DrawSize);
51 }
52 }
53
54 private float pathRadius = 10f;
55
56 /// <summary>
57 /// How wide this path is on each side of the line.
58 /// </summary>
59 /// <remarks>
60 /// The actual width of the path is twice the PathRadius.
61 /// </remarks>
62 public virtual float PathRadius
63 {
64 get => pathRadius;
65 set
66 {
67 if (pathRadius == value) return;
68
69 pathRadius = value;
70
71 vertexBoundsCache.Invalidate();
72 segmentsCache.Invalidate();
73
74 Invalidate(Invalidation.DrawSize);
75 }
76 }
77
78 public override Axes RelativeSizeAxes
79 {
80 get => base.RelativeSizeAxes;
81 set
82 {
83 if ((AutoSizeAxes & value) != 0)
84 throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time.");
85
86 base.RelativeSizeAxes = value;
87 }
88 }
89
90 private Axes autoSizeAxes;
91
92 /// <summary>
93 /// Controls which <see cref="Axes"/> are automatically sized w.r.t. the bounds of the vertices.
94 /// It is not allowed to manually set <see cref="Size"/> (or <see cref="Width"/> / <see cref="Height"/>)
95 /// on any <see cref="Axes"/> which are automatically sized.
96 /// </summary>
97 public virtual Axes AutoSizeAxes
98 {
99 get => autoSizeAxes;
100 set
101 {
102 if (value == autoSizeAxes)
103 return;
104
105 if ((RelativeSizeAxes & value) != 0)
106 throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time.");
107
108 autoSizeAxes = value;
109 OnSizingChanged();
110 }
111 }
112
113 public override float Width
114 {
115 get
116 {
117 if (AutoSizeAxes.HasFlagFast(Axes.X))
118 return base.Width = vertexBounds.Width;
119
120 return base.Width;
121 }
122 set
123 {
124 if ((AutoSizeAxes & Axes.X) != 0)
125 throw new InvalidOperationException($"The width of a {nameof(Path)} with {nameof(AutoSizeAxes)} can not be set manually.");
126
127 base.Width = value;
128 }
129 }
130
131 public override float Height
132 {
133 get
134 {
135 if (AutoSizeAxes.HasFlagFast(Axes.Y))
136 return base.Height = vertexBounds.Height;
137
138 return base.Height;
139 }
140 set
141 {
142 if ((AutoSizeAxes & Axes.Y) != 0)
143 throw new InvalidOperationException($"The height of a {nameof(Path)} with {nameof(AutoSizeAxes)} can not be set manually.");
144
145 base.Height = value;
146 }
147 }
148
149 public override Vector2 Size
150 {
151 get
152 {
153 if (AutoSizeAxes != Axes.None)
154 return base.Size = vertexBounds.Size;
155
156 return base.Size;
157 }
158 set
159 {
160 if ((AutoSizeAxes & Axes.Both) != 0)
161 throw new InvalidOperationException($"The Size of a {nameof(Path)} with {nameof(AutoSizeAxes)} can not be set manually.");
162
163 base.Size = value;
164 }
165 }
166
167 private readonly Cached<RectangleF> vertexBoundsCache = new Cached<RectangleF>();
168
169 private RectangleF vertexBounds
170 {
171 get
172 {
173 if (vertexBoundsCache.IsValid)
174 return vertexBoundsCache.Value;
175
176 if (vertices.Count > 0)
177 {
178 float minX = 0;
179 float minY = 0;
180 float maxX = 0;
181 float maxY = 0;
182
183 foreach (var v in vertices)
184 {
185 minX = Math.Min(minX, v.X - PathRadius);
186 minY = Math.Min(minY, v.Y - PathRadius);
187 maxX = Math.Max(maxX, v.X + PathRadius);
188 maxY = Math.Max(maxY, v.Y + PathRadius);
189 }
190
191 return vertexBoundsCache.Value = new RectangleF(minX, minY, maxX - minX, maxY - minY);
192 }
193
194 return vertexBoundsCache.Value = new RectangleF(0, 0, 0, 0);
195 }
196 }
197
198 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
199 {
200 var localPos = ToLocalSpace(screenSpacePos);
201 var pathRadiusSquared = PathRadius * PathRadius;
202
203 foreach (var t in segments)
204 {
205 if (t.DistanceSquaredToPoint(localPos) <= pathRadiusSquared)
206 return true;
207 }
208
209 return false;
210 }
211
212 public Vector2 PositionInBoundingBox(Vector2 pos) => pos - vertexBounds.TopLeft;
213
214 public void ClearVertices()
215 {
216 if (vertices.Count == 0)
217 return;
218
219 vertices.Clear();
220
221 vertexBoundsCache.Invalidate();
222 segmentsCache.Invalidate();
223
224 Invalidate(Invalidation.DrawSize);
225 }
226
227 public void AddVertex(Vector2 pos)
228 {
229 vertices.Add(pos);
230
231 vertexBoundsCache.Invalidate();
232 segmentsCache.Invalidate();
233
234 Invalidate(Invalidation.DrawSize);
235 }
236
237 private readonly List<Line> segmentsBacking = new List<Line>();
238 private readonly Cached segmentsCache = new Cached();
239 private List<Line> segments => segmentsCache.IsValid ? segmentsBacking : generateSegments();
240
241 private List<Line> generateSegments()
242 {
243 segmentsBacking.Clear();
244
245 if (vertices.Count > 1)
246 {
247 Vector2 offset = vertexBounds.TopLeft;
248 for (int i = 0; i < vertices.Count - 1; ++i)
249 segmentsBacking.Add(new Line(vertices[i] - offset, vertices[i + 1] - offset));
250 }
251
252 segmentsCache.Validate();
253 return segmentsBacking;
254 }
255
256 private Texture texture;
257
258 protected Texture Texture
259 {
260 get => texture ?? Texture.WhitePixel;
261 set
262 {
263 if (texture == value)
264 return;
265
266 texture?.Dispose();
267 texture = value;
268
269 Invalidate(Invalidation.DrawNode);
270 }
271 }
272
273 public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo;
274
275 public Vector2 FrameBufferScale { get; } = Vector2.One;
276
277 // The path should not receive the true colour to avoid colour doubling when the frame-buffer is rendered to the back-buffer.
278 public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending);
279
280 public Color4 BackgroundColour => new Color4(0, 0, 0, 0);
281
282 private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(new[] { RenderbufferInternalFormat.DepthComponent16 });
283
284 protected override DrawNode CreateDrawNode() => new BufferedDrawNode(this, new PathDrawNode(this), sharedData);
285
286 protected override void Dispose(bool isDisposing)
287 {
288 base.Dispose(isDisposing);
289
290 texture?.Dispose();
291 texture = null;
292
293 sharedData.Dispose();
294 }
295 }
296}