/// <summary> /// Converts a given point list into a set of path segments. /// </summary> /// <param name="points">The point list.</param> /// <param name="endPoint">Any extra endpoint to consider as part of the points. This will NOT be returned.</param> /// <param name="first">Whether this is the first segment in the set. If <c>true</c> the first of the returned segments will contain a zero point.</param> /// <param name="offset">The positional offset to apply to the control points.</param> /// <returns>The set of points contained by <paramref name="points"/> as one or more segments of the path, prepended by an extra zero point if <paramref name="first"/> is <c>true</c>.</returns> private IEnumerable <Memory <PathControlPoint> > convertPoints(ReadOnlyMemory <string> points, string endPoint, bool first, Vector2 offset) { PathType type = convertPathType(points.Span[0]); int readOffset = first ? 1 : 0; // First control point is zero for the first segment. int readablePoints = points.Length - 1; // Total points readable from the base point span. int endPointLength = endPoint != null ? 1 : 0; // Extra length if an endpoint is given that lies outside the base point span. var vertices = new PathControlPoint[readOffset + readablePoints + endPointLength]; // Fill any non-read points. for (int i = 0; i < readOffset; i++) { vertices[i] = new PathControlPoint(); } // Parse into control points. for (int i = 1; i < points.Length; i++) { readPoint(points.Span[i], offset, out vertices[readOffset + i - 1]); } // If an endpoint is given, add it to the end. if (endPoint != null) { readPoint(endPoint, offset, out vertices[^ 1]);
private PathControlPoint addControlPoint(Vector2 position) { position -= HitObject.Position; int insertionIndex = 0; float minDistance = float.MaxValue; for (int i = 0; i < controlPoints.Count - 1; i++) { float dist = new Line(controlPoints[i].Position, controlPoints[i + 1].Position).DistanceToPoint(position); if (dist < minDistance) { insertionIndex = i + 1; minDistance = dist; } } var pathControlPoint = new PathControlPoint { Position = position }; // Move the control points from the insertion index onwards to make room for the insertion controlPoints.Insert(insertionIndex, pathControlPoint); HitObject.SnapTo(snapProvider); return(pathControlPoint); }
/// <summary> /// Selects the <see cref="PathControlPointPiece"/> corresponding to the given <paramref name="pathControlPoint"/>, /// and deselects all other <see cref="PathControlPointPiece"/>s. /// </summary> public void SetSelectionTo(PathControlPoint pathControlPoint) { foreach (var p in Pieces) { p.IsSelected.Value = p.ControlPoint == pathControlPoint; } }
private void updateCursor() { if (canPlaceNewControlPoint(out _)) { // The cursor does not overlap a previous control point, so it can be added if not already existing. if (cursor == null) { HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } }); // The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier). currentSegmentLength++; updatePathType(); } // Update the cursor position. cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position; } else if (cursor != null) { // The cursor overlaps a previous control point, so it's removed. HitObject.Path.ControlPoints.Remove(cursor); cursor = null; // The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear). currentSegmentLength--; updatePathType(); } }
public SliderPlacementBlueprint() : base(new Objects.Slider()) { RelativeSizeAxes = Axes.Both; HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear)); currentSegmentLength = 1; }
protected override void OnMouseUp(MouseUpEvent e) { if (placementControlPoint != null) { placementControlPoint = null; changeHandler?.EndChange(); } }
protected override bool OnDoubleClick(DoubleClickEvent e) { // Todo: This should all not occur on double click, but rather if the previous control point is hovered. segmentStart = HitObject.Path.ControlPoints[HitObject.Path.ControlPoints.Count - 1]; segmentStart.Type.Value = PathType.Linear; currentSegmentLength = 1; return(true); }
/// <summary> /// Whether a new control point can be placed at the current mouse position. /// </summary> /// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param> /// <returns>Whether a new control point can be placed at the current position.</returns> private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint) { // We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point. var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last); lastPoint = last; return(lastPiece?.IsHovered != true); }
private void dragStarted(PathControlPoint controlPoint) { dragStartPositions = slider.Path.ControlPoints.Select(point => point.Position).ToArray(); dragPathTypes = slider.Path.ControlPoints.Select(point => point.Type).ToArray(); draggedControlPointIndex = slider.Path.ControlPoints.IndexOf(controlPoint); selectedControlPoints = new HashSet <PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint)); Debug.Assert(draggedControlPointIndex >= 0); changeHandler?.BeginChange(); }
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) { this.slider = slider; ControlPoint = controlPoint; // we don't want to run the path type update on construction as it may inadvertently change the slider. cachePoints(slider); slider.Path.Version.BindValueChanged(_ => { cachePoints(slider); updatePathType(); }); controlPoint.Changed += updateMarkerDisplay; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { marker = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Children = new[] { new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(20), }, markerRing = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(28), Masking = true, BorderThickness = 2, BorderColour = Color4.White, Alpha = 0, Child = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0, AlwaysPresent = true } } } } }; }
private void ensureCursor() { if (cursor == null) { HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } }); currentSegmentLength++; updatePathType(); } }
protected PathControlPoint(PathControlPoint obj) { if (obj == null) { throw new System.ArgumentNullException("obj"); } _path = obj._path; _brush = obj.Brush; _pen = obj.Pen; _size = obj.Size; }
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) { this.slider = slider; ControlPoint = controlPoint; slider.Path.Version.BindValueChanged(_ => { PointsInSegment = slider.Path.PointsInSegment(ControlPoint); updatePathType(); }, runOnceImmediately: true); controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { marker = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Children = new[] { new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(20), }, markerRing = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(28), Masking = true, BorderThickness = 2, BorderColour = Color4.White, Alpha = 0, Child = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0, AlwaysPresent = true } } } } }; }
public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint) { this.slider = slider; ControlPoint = controlPoint; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; InternalChild = path = new SmoothPath { Anchor = Anchor.Centre, PathRadius = 1 }; }
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) { this.slider = slider; ControlPoint = controlPoint; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { path = new SmoothPath { Anchor = Anchor.Centre, PathRadius = 1 }, marker = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Children = new[] { new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(10), }, markerRing = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(14), Masking = true, BorderThickness = 2, BorderColour = Color4.White, Alpha = 0, Child = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0, AlwaysPresent = true } } } } }; }
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) { this.slider = slider; this.controlPointIndex = controlPointIndex; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; ControlPoint = slider.Path.ControlPoints[controlPointIndex]; InternalChild = path = new SmoothPath { Anchor = Anchor.Centre, PathRadius = 1 }; }
protected override bool OnClick(ClickEvent e) { switch (state) { case PlacementState.Initial: beginCurve(); break; case PlacementState.Body: switch (e.Button) { case MouseButton.Left: ensureCursor(); // Detatch the cursor cursor = null; break; } break; } return(true); }
protected override bool OnMouseDown(MouseDownEvent e) { if (e.Button != MouseButton.Left) { return(base.OnMouseDown(e)); } switch (state) { case SliderPlacementState.Initial: beginCurve(); break; case SliderPlacementState.Body: if (canPlaceNewControlPoint(out var lastPoint)) { // Place a new point by detatching the current cursor. updateCursor(); cursor = null; } else { // Transform the last point into a new segment. Debug.Assert(lastPoint != null); segmentStart = lastPoint; segmentStart.Type.Value = PathType.Linear; currentSegmentLength = 1; } break; } return(true); }
private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { PathType?lastType = null; for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { PathControlPoint point = pathData.Path.ControlPoints[i]; if (point.Type != null) { // We've reached a new (explicit) segment! // Explicit segments have a new format in which the type is injected into the middle of the control point string. // To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point. // One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PerfectCurve; // Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable. // Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder. if (i > 1) { // We need to use the absolute control point position to determine equality, otherwise floating point issues may arise. Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position; Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position; if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y) { needsExplicitSegment = true; } } if (needsExplicitSegment) { switch (point.Type) { case PathType.Bezier: writer.Write("B|"); break; case PathType.Catmull: writer.Write("C|"); break; case PathType.PerfectCurve: writer.Write("P|"); break; case PathType.Linear: writer.Write("L|"); break; } lastType = point.Type; } else { // New segment with the same type - duplicate the control point writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|")); } } if (i != 0) { writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}")); writer.Write(i != pathData.Path.ControlPoints.Count - 1 ? "|" : ","); } } var curveData = pathData as IHasPathWithRepeats; writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},")); writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},")); if (curveData != null) { for (int i = 0; i < curveData.NodeSamples.Count; i++) { writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); } for (int i = 0; i < curveData.NodeSamples.Count; i++) { writer.Write(getSampleBank(curveData.NodeSamples[i], true)); writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); } } }
protected override bool OnDoubleClick(DoubleClickEvent e) { // Todo: This should all not occur on double click, but rather if the previous control point is hovered. segmentStart = HitObject.Path.ControlPoints[^ 1];
private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { PathType?lastType = null; for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { PathControlPoint point = pathData.Path.ControlPoints[i]; if (point.Type.Value != null) { if (point.Type.Value != lastType) { switch (point.Type.Value) { case PathType.Bezier: writer.Write("B|"); break; case PathType.Catmull: writer.Write("C|"); break; case PathType.PerfectCurve: writer.Write("P|"); break; case PathType.Linear: writer.Write("L|"); break; } lastType = point.Type.Value; } else { // New segment with the same type - duplicate the control point writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}|")); } } if (i != 0) { writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}")); writer.Write(i != pathData.Path.ControlPoints.Count - 1 ? "|" : ","); } } var curveData = pathData as IHasPathWithRepeats; writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},")); writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},")); if (curveData != null) { for (int i = 0; i < curveData.NodeSamples.Count; i++) { writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); } for (int i = 0; i < curveData.NodeSamples.Count; i++) { writer.Write(getSampleBank(curveData.NodeSamples[i], true)); writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); } } }