// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Textures; using osuTK; using osu.Framework.Graphics.Shaders; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Caching; using osu.Framework.Extensions.EnumExtensions; using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Lines { public partial class Path : Drawable, IBufferedDrawable { public IShader RoundedTextureShader { get; private set; } public IShader TextureShader { get; private set; } private IShader pathShader; public Path() { AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(ShaderManager shaders) { RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); pathShader = shaders.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE); } private readonly List vertices = new List(); public IReadOnlyList Vertices { get => vertices; set { vertices.Clear(); vertices.AddRange(value); vertexBoundsCache.Invalidate(); segmentsCache.Invalidate(); Invalidate(Invalidation.DrawSize); } } private float pathRadius = 10f; /// /// How wide this path is on each side of the line. /// /// /// The actual width of the path is twice the PathRadius. /// public virtual float PathRadius { get => pathRadius; set { if (pathRadius == value) return; pathRadius = value; vertexBoundsCache.Invalidate(); segmentsCache.Invalidate(); Invalidate(Invalidation.DrawSize); } } public override Axes RelativeSizeAxes { get => base.RelativeSizeAxes; set { if ((AutoSizeAxes & value) != 0) throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time."); base.RelativeSizeAxes = value; } } private Axes autoSizeAxes; /// /// Controls which are automatically sized w.r.t. the bounds of the vertices. /// It is not allowed to manually set (or / ) /// on any which are automatically sized. /// public virtual Axes AutoSizeAxes { get => autoSizeAxes; set { if (value == autoSizeAxes) return; if ((RelativeSizeAxes & value) != 0) throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time."); autoSizeAxes = value; OnSizingChanged(); } } public override float Width { get { if (AutoSizeAxes.HasFlagFast(Axes.X)) return base.Width = vertexBounds.Width; return base.Width; } set { if ((AutoSizeAxes & Axes.X) != 0) throw new InvalidOperationException($"The width of a {nameof(Path)} with {nameof(AutoSizeAxes)} can not be set manually."); base.Width = value; } } public override float Height { get { if (AutoSizeAxes.HasFlagFast(Axes.Y)) return base.Height = vertexBounds.Height; return base.Height; } set { if ((AutoSizeAxes & Axes.Y) != 0) throw new InvalidOperationException($"The height of a {nameof(Path)} with {nameof(AutoSizeAxes)} can not be set manually."); base.Height = value; } } public override Vector2 Size { get { if (AutoSizeAxes != Axes.None) return base.Size = vertexBounds.Size; return base.Size; } set { if ((AutoSizeAxes & Axes.Both) != 0) throw new InvalidOperationException($"The Size of a {nameof(Path)} with {nameof(AutoSizeAxes)} can not be set manually."); base.Size = value; } } private readonly Cached vertexBoundsCache = new Cached(); private RectangleF vertexBounds { get { if (vertexBoundsCache.IsValid) return vertexBoundsCache.Value; if (vertices.Count > 0) { float minX = 0; float minY = 0; float maxX = 0; float maxY = 0; foreach (var v in vertices) { minX = Math.Min(minX, v.X - PathRadius); minY = Math.Min(minY, v.Y - PathRadius); maxX = Math.Max(maxX, v.X + PathRadius); maxY = Math.Max(maxY, v.Y + PathRadius); } return vertexBoundsCache.Value = new RectangleF(minX, minY, maxX - minX, maxY - minY); } return vertexBoundsCache.Value = new RectangleF(0, 0, 0, 0); } } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { var localPos = ToLocalSpace(screenSpacePos); var pathRadiusSquared = PathRadius * PathRadius; foreach (var t in segments) { if (t.DistanceSquaredToPoint(localPos) <= pathRadiusSquared) return true; } return false; } public Vector2 PositionInBoundingBox(Vector2 pos) => pos - vertexBounds.TopLeft; public void ClearVertices() { if (vertices.Count == 0) return; vertices.Clear(); vertexBoundsCache.Invalidate(); segmentsCache.Invalidate(); Invalidate(Invalidation.DrawSize); } public void AddVertex(Vector2 pos) { vertices.Add(pos); vertexBoundsCache.Invalidate(); segmentsCache.Invalidate(); Invalidate(Invalidation.DrawSize); } private readonly List segmentsBacking = new List(); private readonly Cached segmentsCache = new Cached(); private List segments => segmentsCache.IsValid ? segmentsBacking : generateSegments(); private List generateSegments() { segmentsBacking.Clear(); if (vertices.Count > 1) { Vector2 offset = vertexBounds.TopLeft; for (int i = 0; i < vertices.Count - 1; ++i) segmentsBacking.Add(new Line(vertices[i] - offset, vertices[i + 1] - offset)); } segmentsCache.Validate(); return segmentsBacking; } private Texture texture; protected Texture Texture { get => texture ?? Texture.WhitePixel; set { if (texture == value) return; texture?.Dispose(); texture = value; Invalidate(Invalidation.DrawNode); } } public DrawColourInfo? FrameBufferDrawColour => base.DrawColourInfo; public Vector2 FrameBufferScale { get; } = Vector2.One; // The path should not receive the true colour to avoid colour doubling when the frame-buffer is rendered to the back-buffer. public override DrawColourInfo DrawColourInfo => new DrawColourInfo(Color4.White, base.DrawColourInfo.Blending); public Color4 BackgroundColour => new Color4(0, 0, 0, 0); private readonly BufferedDrawNodeSharedData sharedData = new BufferedDrawNodeSharedData(new[] { RenderbufferInternalFormat.DepthComponent16 }); protected override DrawNode CreateDrawNode() => new BufferedDrawNode(this, new PathDrawNode(this), sharedData); protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); texture?.Dispose(); texture = null; sharedData.Dispose(); } } }