Skip to content

Commit c8d343c

Browse files
committed
perf(geometry): simplify exact planar containment and distance predicates
1 parent eec40bf commit c8d343c

8 files changed

Lines changed: 295 additions & 51 deletions

File tree

src/FixedMathSharp/Geometry/Primitives/Segments/FixedSegment2d.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public readonly bool IsDistanceAtLeast(
142142
if (TryGetUniqueIntersection(other, out _, out _))
143143
return false;
144144

145-
return WidePlanarProjection.AreSegmentEndpointDistancesAtLeast(
145+
return WideFiniteAxisIntersection.AreSegmentEndpointDistancesAtLeast(
146146
this,
147147
other,
148148
minimumDistance);

src/FixedMathSharp/Geometry/Wide/Common/WidePlanarProjection.cs

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -52,53 +52,6 @@ internal RationalDistance(
5252

5353
#endregion
5454

55-
internal static bool AreSegmentEndpointDistancesAtLeast(
56-
FixedSegment2d first,
57-
FixedSegment2d second,
58-
Fixed64 minimumDistance)
59-
{
60-
Signed192 denominator = Signed192.Signed(1L);
61-
RationalPoint firstStart = GetRawPoint(first.Start);
62-
RationalPoint firstEnd = GetRawPoint(first.End);
63-
RationalPoint secondStart = GetRawPoint(second.Start);
64-
RationalPoint secondEnd = GetRawPoint(second.End);
65-
Signed192 minimumRaw = Signed192.Raw(minimumDistance);
66-
67-
return CompareDistanceToRaw(
68-
GetSegmentDistance(
69-
first.Start,
70-
denominator,
71-
secondStart,
72-
secondEnd),
73-
minimumRaw) >= 0
74-
&& CompareDistanceToRaw(
75-
GetSegmentDistance(
76-
first.End,
77-
denominator,
78-
secondStart,
79-
secondEnd),
80-
minimumRaw) >= 0
81-
&& CompareDistanceToRaw(
82-
GetSegmentDistance(
83-
second.Start,
84-
denominator,
85-
firstStart,
86-
firstEnd),
87-
minimumRaw) >= 0
88-
&& CompareDistanceToRaw(
89-
GetSegmentDistance(
90-
second.End,
91-
denominator,
92-
firstStart,
93-
firstEnd),
94-
minimumRaw) >= 0;
95-
}
96-
97-
private static RationalPoint GetRawPoint(Vector2d point) =>
98-
new(
99-
Signed320.ExtendValue(Signed192.Raw(point.X)),
100-
Signed320.ExtendValue(Signed192.Raw(point.Y)));
101-
10255
internal static bool TryGetSphereRelation(
10356
Vector2d circleCenter,
10457
Fixed64 circleRadius,

src/FixedMathSharp/Geometry/Wide/Convex/WideConvex2dRelations.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,9 @@ internal static bool ContainsPoint(
495495
Fixed64 convexRotation,
496496
ReadOnlySpan<Vector2d> convexVertexOffsets)
497497
{
498+
if (convexRotation == Fixed64.Zero)
499+
return ContainsPointUnrotated(point, convexOrigin, convexVertexOffsets);
500+
498501
RotationFrame2d convexFrame = new(convexRotation);
499502
// Widen before subtracting: the point-to-origin displacement can exceed
500503
// the scalar domain. Keep it at the rotated vertices' product scale.
@@ -545,6 +548,45 @@ internal static bool ContainsPoint(
545548
return true;
546549
}
547550

551+
private static bool ContainsPointUnrotated(
552+
Vector2d point,
553+
Vector2d convexOrigin,
554+
ReadOnlySpan<Vector2d> convexVertexOffsets)
555+
{
556+
// At zero rotation the general determinant is this raw determinant times
557+
// 2^64, so its sign is unchanged. Widen before every subtraction: edges
558+
// need 65 signed bits and point - origin - vertex can need 66.
559+
Signed192 relativePointX = WideArithmetic.Difference(point.X, convexOrigin.X);
560+
Signed192 relativePointY = WideArithmetic.Difference(point.Y, convexOrigin.Y);
561+
Vector2d start = convexVertexOffsets[0];
562+
bool hasPositive = false;
563+
bool hasNegative = false;
564+
for (int i = 0; i < convexVertexOffsets.Length; i++)
565+
{
566+
Vector2d end = convexVertexOffsets[
567+
i + 1 == convexVertexOffsets.Length ? 0 : i + 1];
568+
Signed192 edgeX = WideArithmetic.Difference(end.X, start.X);
569+
Signed192 edgeY = WideArithmetic.Difference(end.Y, start.Y);
570+
Signed192 pointX = WideArithmetic.SubtractSigned192(relativePointX, Signed192.Raw(start.X));
571+
Signed192 pointY = WideArithmetic.SubtractSigned192(relativePointY, Signed192.Raw(start.Y));
572+
int orientation = WideArithmetic.MultiplySubtract(
573+
edgeX,
574+
pointY,
575+
edgeY,
576+
pointX).Sign;
577+
if (orientation > 0)
578+
hasPositive = true;
579+
else if (orientation < 0)
580+
hasNegative = true;
581+
if (hasPositive && hasNegative)
582+
return false;
583+
584+
start = end;
585+
}
586+
587+
return true;
588+
}
589+
548590
internal static bool TryProjectAnchorOntoFeature(
549591
in FixedPointAnchor2d source,
550592
Vector2d targetOrigin,

src/FixedMathSharp/Geometry/Wide/FiniteAxis/WideFiniteAxisIntersection.Containment.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,43 @@ namespace FixedMathSharp.Geometry;
1414
/// </content>
1515
internal static partial class WideFiniteAxisIntersection
1616
{
17+
internal static bool AreSegmentEndpointDistancesAtLeast(
18+
FixedSegment2d first,
19+
FixedSegment2d second,
20+
Fixed64 minimumDistance)
21+
{
22+
if (second.Start == second.End)
23+
{
24+
FixedSegment2d temporary = first;
25+
first = second;
26+
second = temporary;
27+
}
28+
29+
Signed192 squaredRadius = GetSquaredRadius(Signed192.Raw(minimumDistance));
30+
bool firstSeparated = IsPointSegmentDistanceAtLeast(first.Start, second, squaredRadius);
31+
// For a point, this is the complete minimum distance. Distances to the
32+
// other segment's endpoints cannot be smaller, and repeating the point adds nothing.
33+
if (!firstSeparated || first.Start == first.End)
34+
return firstSeparated;
35+
36+
return IsPointSegmentDistanceAtLeast(first.End, second, squaredRadius)
37+
&& IsPointSegmentDistanceAtLeast(second.Start, first, squaredRadius)
38+
&& IsPointSegmentDistanceAtLeast(second.End, first, squaredRadius);
39+
}
40+
41+
private static bool IsPointSegmentDistanceAtLeast(
42+
Vector2d point,
43+
FixedSegment2d segment,
44+
Signed192 squaredRadius) =>
45+
// Negating strict capsule containment keeps equality without rounding a distance.
46+
!IsCapsulePointContained(
47+
GetDot(point, segment.Start, point, segment.Start),
48+
GetDot(point, segment.End, point, segment.End),
49+
GetDot(point, segment.Start, segment.End, segment.Start),
50+
GetDot(segment.End, segment.Start, segment.End, segment.Start),
51+
squaredRadius,
52+
strict: true);
53+
1754
internal static bool ContainsPointInCenteredFiniteCylinder(
1855
Vector3d point,
1956
Vector3d center,

src/FixedMathSharp/Geometry/Wide/FiniteAxis/WideFiniteAxisIntersection.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,10 @@ internal static bool TryGetCapsuleInterval(
5858
Signed192 expandedRadius = GetExpandedRadius(radius, radiusExpansion);
5959
Signed192 squaredRadius = GetSquaredRadius(expandedRadius);
6060
Signed192 axisLengthSquared = GetDot(axis.End, axis.Start, axis.End, axis.Start);
61-
Signed192 queryLengthSquared = GetDot(query.End, query.Start, query.End, query.Start);
6261
Signed192 startDistanceSquared = GetDot(query.Start, axis.Start, query.Start, axis.Start);
6362
Signed192 endDistanceSquared = GetDot(query.End, axis.Start, query.End, axis.Start);
64-
Signed192 directionsDot = GetDot(query.End, query.Start, axis.End, axis.Start);
6563
Signed192 startAxisProjection = GetDot(query.Start, axis.Start, axis.End, axis.Start);
6664
Signed192 endAxisProjection = GetDot(query.End, axis.Start, axis.End, axis.Start);
67-
Signed192 startDirectionProjection = GetDot(query.Start, axis.Start, query.End, query.Start);
6865

6966
if (axisLengthSquared.IsZero)
7067
{
@@ -93,6 +90,18 @@ internal static bool TryGetCapsuleInterval(
9390
axisLengthSquared,
9491
squaredRadius,
9592
strict: true);
93+
// A capsule is convex: these already-certified endpoints enclose the
94+
// complete query, so neither the finite-axis nor cap solves add evidence.
95+
if (startContained && endContainedStrict)
96+
{
97+
entry = Fixed64.Zero;
98+
exit = Fixed64.One;
99+
return true;
100+
}
101+
102+
Signed192 queryLengthSquared = GetDot(query.End, query.Start, query.End, query.Start);
103+
Signed192 directionsDot = GetDot(query.End, query.Start, axis.End, axis.Start);
104+
Signed192 startDirectionProjection = GetDot(query.Start, axis.Start, query.End, query.Start);
96105
bool found = TryGetFiniteAxisInterval(
97106
queryLengthSquared,
98107
axisLengthSquared,

tests/FixedMathSharp.Tests/Geometry/Primitives/FiniteAxisIntersection.Capsule.Tests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,49 @@ namespace FixedMathSharp.Tests.Bounds;
77

88
public sealed partial class FiniteAxisIntersectionTests
99
{
10+
[Theory]
11+
[InlineData(false, false)]
12+
[InlineData(false, true)]
13+
[InlineData(true, false)]
14+
[InlineData(true, true)]
15+
public void EndpointCapsule2d_ContainedSegmentsRetainTheFullDomain(bool fullDomain, bool point)
16+
{
17+
FixedSegment2d axis = new(
18+
new Vector2d(fullDomain ? Fixed64.MinValue : new Fixed64(-4), Fixed64.Zero),
19+
new Vector2d(fullDomain ? Fixed64.MaxValue : new Fixed64(4), Fixed64.Zero));
20+
FixedSegment2d query = point
21+
? new FixedSegment2d(new Vector2d(Fixed64.Zero, Fixed64.Half),
22+
new Vector2d(Fixed64.Zero, Fixed64.Half))
23+
: fullDomain ? axis : new FixedSegment2d(
24+
new Vector2d(new Fixed64(-4) - Fixed64.Half, Fixed64.Zero),
25+
new Vector2d(new Fixed64(4) + Fixed64.Half, Fixed64.Zero));
26+
27+
Assert.True(query.TryGetCapsuleIntersectionInterval(axis, Fixed64.Half, Fixed64.Half,
28+
out Fixed64 entry, out Fixed64 exit, out bool startContained, out bool endContainedStrict));
29+
Assert.Equal(Fixed64.Zero, entry);
30+
Assert.Equal(Fixed64.One, exit);
31+
Assert.True(startContained);
32+
Assert.True(endContainedStrict);
33+
}
34+
35+
[Theory]
36+
[InlineData(1, 0, 0L, 4294967296L, true, true)]
37+
[InlineData(0, 1, 0L, 4294967296L, true, false)]
38+
[InlineData(2, 0, 2147483648L, 4294967296L, false, true)]
39+
[InlineData(0, 2, 0L, 2147483648L, true, false)]
40+
public void EndpointCapsule2d_BoundaryAndPartialQueriesRetainExactContainmentFlags(
41+
int startY, int endY, long entryRaw, long exitRaw, bool expectedStart, bool expectedEndStrict)
42+
{
43+
FixedSegment2d axis = new(new Vector2d(-1, 0), new Vector2d(1, 0));
44+
FixedSegment2d query = new(new Vector2d(0, startY), new Vector2d(0, endY));
45+
Assert.True(query.TryGetCapsuleIntersectionInterval(axis, Fixed64.One, Fixed64.Zero,
46+
out Fixed64 entry, out Fixed64 exit, out bool startContained, out bool endContainedStrict));
47+
Assert.Equal(Fixed64.FromRaw(entryRaw), entry);
48+
Assert.Equal(Fixed64.FromRaw(exitRaw), exit);
49+
Assert.Equal(expectedStart, startContained);
50+
Assert.Equal(expectedEndStrict, endContainedStrict);
51+
}
52+
1053
[Fact]
1154
public void CenteredCapsule_RepresentableAxisMatchesEndpointContract()
1255
{

tests/FixedMathSharp.Tests/Geometry/Primitives/FixedConvex2dRelations.Tests.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,85 @@ public void ContainsPoint_PreservesRotatedEdgesWiderThanTheScalarDomain(bool clo
217217
new(Fixed64.MinValue, Fixed64.Zero), new(Fixed64.MaxValue, Fixed64.Zero), Fixed64.PiOver4, rectangle));
218218
}
219219

220+
[Theory]
221+
[InlineData(false)]
222+
[InlineData(true)]
223+
public void ContainsPoint_ZeroRotationPreservesFullDomainDiagonal(bool clockwise)
224+
{
225+
Vector2d[] triangle =
226+
{
227+
new(Fixed64.MinValue, Fixed64.MinValue),
228+
new(Fixed64.MaxValue, Fixed64.MinValue),
229+
new(Fixed64.MinValue, Fixed64.MaxValue)
230+
};
231+
if (clockwise)
232+
Array.Reverse(triangle);
233+
234+
// The diagonal is x + y = -1 raw unit, not zero: MinValue + MaxValue = -1 raw.
235+
AssertZeroRotationContainment(true, triangle[0], Vector2d.Zero, triangle);
236+
AssertZeroRotationContainment(true,
237+
new(Fixed64.Zero, -Fixed64.MinIncrement), Vector2d.Zero, triangle);
238+
AssertZeroRotationContainment(false, Vector2d.Zero, Vector2d.Zero, triangle);
239+
}
240+
241+
[Theory]
242+
[InlineData(false, false)]
243+
[InlineData(false, true)]
244+
[InlineData(true, false)]
245+
[InlineData(true, true)]
246+
public void ContainsPoint_ZeroRotationDoesNotNarrowOppositeExtremeDisplacements(
247+
bool clockwise,
248+
bool positiveOrigin)
249+
{
250+
Vector2d[] rectangle =
251+
{
252+
new(Fixed64.MinValue, Fixed64.MinValue),
253+
new(Fixed64.MaxValue, Fixed64.MinValue),
254+
new(Fixed64.MaxValue, Fixed64.MaxValue),
255+
new(Fixed64.MinValue, Fixed64.MaxValue)
256+
};
257+
if (clockwise)
258+
Array.Reverse(rectangle);
259+
Fixed64 originCoordinate = positiveOrigin ? Fixed64.MaxValue : Fixed64.MinValue;
260+
Fixed64 pointCoordinate = positiveOrigin ? Fixed64.MinValue : Fixed64.MaxValue;
261+
Vector2d origin = new(originCoordinate, originCoordinate);
262+
263+
AssertZeroRotationContainment(true, origin, origin, rectangle);
264+
// Each displacement exceeds the raw scalar range; clamping would put it on an included face.
265+
AssertZeroRotationContainment(false, new(pointCoordinate, originCoordinate), origin, rectangle);
266+
AssertZeroRotationContainment(false, new(originCoordinate, pointCoordinate), origin, rectangle);
267+
}
268+
269+
[Theory]
270+
[InlineData(false)]
271+
[InlineData(true)]
272+
public void ContainsPoint_ZeroRotationPreservesCollinearAndRepeatedEdges(bool clockwise)
273+
{
274+
Vector2d[] rectangle =
275+
{
276+
new(0, 0), new(1, 0), new(2, 0), new(2, 0), new(2, 2), new(0, 2)
277+
};
278+
if (clockwise)
279+
Array.Reverse(rectangle);
280+
Vector2d origin = new(40, 50);
281+
282+
AssertZeroRotationContainment(true, new(41, 51), origin, rectangle);
283+
AssertZeroRotationContainment(true, new(40, 51), origin, rectangle);
284+
// Only the last-to-first edge rejects this point, in either authored winding.
285+
AssertZeroRotationContainment(false,
286+
new((Fixed64)40 - Fixed64.MinIncrement, (Fixed64)51), origin, rectangle);
287+
}
288+
289+
private static void AssertZeroRotationContainment(
290+
bool expected,
291+
Vector2d point,
292+
Vector2d origin,
293+
Vector2d[] offsets)
294+
{
295+
Assert.Equal(expected, FixedConvex2dRelations.ContainsPoint(point, origin, offsets));
296+
Assert.Equal(expected, FixedConvex2dRelations.ContainsPoint(point, origin, Fixed64.Zero, offsets));
297+
}
298+
220299
[Fact]
221300
public void SupportOffset_PreservesTheFirstAuthoredFeatureOnAnExactTie()
222301
{

0 commit comments

Comments
 (0)