using System; using System.Numerics; using Windows.Foundation; using Windows.UI; namespace Sonex.Client.Controls.SceneCanvas; public sealed class SceneCanvasLineObject : SceneCanvasObject { private const double CoordinatePrecision = 0.01d; private Vector2 _startOffsetMeters; private Vector2 _endOffsetMeters; private double _thicknessMeters; public SceneCanvasLineObject( Vector2 startPointMeters, Vector2 endPointMeters, double thicknessMeters, Color strokeColor) { StrokeColor = strokeColor; ThicknessMeters = thicknessMeters; CanMove = true; CanResize = false; CanRotate = false; SetPoints(startPointMeters, endPointMeters); } public Color StrokeColor { get; set; } public double ThicknessMeters { get => _thicknessMeters; set => _thicknessMeters = Math.Max(CoordinatePrecision, RoundTo2(value)); } public Vector2 StartPointMeters { get => new( (float)RoundTo2(BoundsMeters.X + _startOffsetMeters.X), (float)RoundTo2(BoundsMeters.Y + _startOffsetMeters.Y)); set => SetPoints(value, EndPointMeters); } public Vector2 EndPointMeters { get => new( (float)RoundTo2(BoundsMeters.X + _endOffsetMeters.X), (float)RoundTo2(BoundsMeters.Y + _endOffsetMeters.Y)); set => SetPoints(StartPointMeters, value); } public double StartPointZMeters { get => RoundTo2(PositionZMeters); set => PositionZMeters = RoundTo2(value); } public double EndPointZMeters { get => RoundTo2(PositionZMeters + HeightMeters); set => HeightMeters = RoundTo2(value - PositionZMeters); } public override Rect SpatialBoundsMeters { get { var start = StartPointMeters; var end = EndPointMeters; var minX = Math.Min(start.X, end.X); var minY = Math.Min(start.Y, end.Y); var maxX = Math.Max(start.X, end.X); var maxY = Math.Max(start.Y, end.Y); var halfThickness = ThicknessMeters * 0.5d; return new Rect( minX - halfThickness, minY - halfThickness, Math.Max(0d, (maxX - minX) + (halfThickness * 2d)), Math.Max(0d, (maxY - minY) + (halfThickness * 2d))); } } public override bool HitTest(Vector2 worldPointMeters, float toleranceMeters = 0.05f) { var start = StartPointMeters; var end = EndPointMeters; var threshold = (float)Math.Max(0.0001d, (ThicknessMeters * 0.5d) + toleranceMeters); return DistancePointToSegment(worldPointMeters, start, end) <= threshold; } public override void OnDraw(SceneCanvasDrawContext context) { var strokeWidth = (float)Math.Max(0.0001d, ThicknessMeters); context.DrawingSession.DrawLine(StartPointMeters, EndPointMeters, StrokeColor, strokeWidth); } public override SceneCanvasObject Clone() { var clone = new SceneCanvasLineObject(StartPointMeters, EndPointMeters, ThicknessMeters, StrokeColor); CopyBasePropertiesTo(clone); return clone; } public void SetPoints(Vector2 startPointMeters, Vector2 endPointMeters) { var startX = RoundTo2(startPointMeters.X); var startY = RoundTo2(startPointMeters.Y); var endX = RoundTo2(endPointMeters.X); var endY = RoundTo2(endPointMeters.Y); var minX = Math.Min(startX, endX); var minY = Math.Min(startY, endY); var maxX = Math.Max(startX, endX); var maxY = Math.Max(startY, endY); minX = RoundTo2(minX); minY = RoundTo2(minY); maxX = RoundTo2(maxX); maxY = RoundTo2(maxY); BoundsMeters = new Rect(minX, minY, RoundTo2(Math.Max(0d, maxX - minX)), RoundTo2(Math.Max(0d, maxY - minY))); _startOffsetMeters = new Vector2((float)RoundTo2(startX - minX), (float)RoundTo2(startY - minY)); _endOffsetMeters = new Vector2((float)RoundTo2(endX - minX), (float)RoundTo2(endY - minY)); } private static float DistancePointToSegment(Vector2 point, Vector2 segmentStart, Vector2 segmentEnd) { var segment = segmentEnd - segmentStart; var lengthSquared = segment.LengthSquared(); if (lengthSquared < 0.0000001f) { return Vector2.Distance(point, segmentStart); } var toPoint = point - segmentStart; var t = Vector2.Dot(toPoint, segment) / lengthSquared; t = Math.Clamp(t, 0f, 1f); var closest = segmentStart + (segment * t); return Vector2.Distance(point, closest); } private static double RoundTo2(double value) { return Math.Round(value, 2, MidpointRounding.AwayFromZero); } }