Skip to content

Commit e400999

Browse files
committed
feat(geometry): certify exact segment contacts
1 parent 80e019a commit e400999

4 files changed

Lines changed: 142 additions & 0 deletions

File tree

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ public FixedBoundBox Bounds
121121
[MethodImpl(MethodImplOptions.AggressiveInlining)]
122122
public Vector3d ClosestPoint(Vector3d point) => Vector3d.ClosestPointOnLineSegment(point, Start, End);
123123

124+
/// <summary>
125+
/// Determines whether the supplied point lies exactly on this finite segment.
126+
/// </summary>
127+
/// <remarks>
128+
/// Collinearity and finite bounds use the complete fixed-point raw domain and
129+
/// do not compare a rounded closest-point projection.
130+
/// </remarks>
131+
public readonly bool Contains(Vector3d point)
132+
{
133+
Signed192 lengthSquared = GetDifferenceDot(End, Start, End, Start);
134+
return PointOnSegment(point, this, lengthSquared);
135+
}
136+
124137
/// <summary>
125138
/// Computes the squared distance from the supplied point to this finite segment.
126139
/// </summary>

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,37 @@ public readonly bool TryGetUniqueIntersection(
398398
return TryGetUniqueIntersection(other, out thisParameter, out _);
399399
}
400400

401+
/// <summary>
402+
/// Attempts to enclose the unique intersection parameter on this segment.
403+
/// </summary>
404+
/// <remarks>
405+
/// The exact rational intersection is enclosed by expanding the nearest
406+
/// representable parameter by one raw unit and clamping to [0, 1]. Collinear
407+
/// positive-length overlaps retain the unique-intersection failure behavior.
408+
/// </remarks>
409+
public readonly bool TryGetUniqueIntersectionParameterEnclosure(
410+
FixedSegment2d other,
411+
out Fixed64 nearestParameter,
412+
out Fixed64 lowerParameter,
413+
out Fixed64 upperParameter)
414+
{
415+
if (!TryGetUniqueIntersection(other, out nearestParameter))
416+
{
417+
nearestParameter = default;
418+
lowerParameter = default;
419+
upperParameter = default;
420+
return false;
421+
}
422+
423+
lowerParameter = nearestParameter > Fixed64.Zero
424+
? Fixed64.FromRaw(nearestParameter.m_rawValue - 1L)
425+
: Fixed64.Zero;
426+
upperParameter = nearestParameter < Fixed64.One
427+
? Fixed64.FromRaw(nearestParameter.m_rawValue + 1L)
428+
: Fixed64.One;
429+
return true;
430+
}
431+
401432
/// <summary>
402433
/// Returns the closest finite points on this segment and another segment.
403434
/// </summary>

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Numerics;
23
using System.Text.Json;
34
using FixedMathSharp.Geometry;
@@ -8,6 +9,51 @@ namespace FixedMathSharp.Tests.Bounds;
89

910
public class FixedSegmentTests
1011
{
12+
[Fact]
13+
public void Contains_UsesExactFiniteCollinearityInsteadOfRoundedProjection()
14+
{
15+
const long scale = 65_536L;
16+
var segment = new FixedSegment(
17+
Vector3d.Zero,
18+
new Vector3d(
19+
Fixed64.FromRaw(3L * scale),
20+
Fixed64.FromRaw(2L * scale),
21+
Fixed64.Zero));
22+
var exactInterior = new Vector3d(
23+
Fixed64.FromRaw(3L * scale / 2L),
24+
Fixed64.FromRaw(scale),
25+
Fixed64.Zero);
26+
27+
Assert.True(segment.Contains(segment.Start));
28+
Assert.True(segment.Contains(exactInterior));
29+
Assert.True(segment.Contains(segment.End));
30+
Assert.False(segment.Contains(new Vector3d(
31+
Fixed64.FromRaw(1L),
32+
Fixed64.Zero,
33+
Fixed64.Zero)));
34+
Assert.False(segment.Contains(new Vector3d(
35+
Fixed64.FromRaw(1L),
36+
Fixed64.FromRaw(1L),
37+
Fixed64.Zero)));
38+
Assert.False(segment.Contains(new Vector3d(
39+
Fixed64.FromRaw(3L * scale + 3L),
40+
Fixed64.FromRaw(2L * scale + 2L),
41+
Fixed64.Zero)));
42+
43+
var point = new FixedSegment(exactInterior, exactInterior);
44+
Assert.True(point.Contains(exactInterior));
45+
Assert.False(point.Contains(segment.Start));
46+
47+
long before = GC.GetAllocatedBytesForCurrentThread();
48+
bool contains = false;
49+
for (int i = 0; i < 256; i++)
50+
contains = segment.Contains(exactInterior);
51+
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
52+
53+
Assert.True(contains);
54+
Assert.Equal(0, allocated);
55+
}
56+
1157
[Fact]
1258
public void Constructor_AssignsEndpointsAndDerivedValues()
1359
{

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Numerics;
23
using System.Text.Json;
34
using FixedMathSharp.Geometry;
@@ -8,6 +9,57 @@ namespace FixedMathSharp.Tests.Bounds;
89

910
public class FixedSegment2dTests
1011
{
12+
[Fact]
13+
public void UniqueIntersectionEnclosure_ContainsNonrepresentableCrossingAndClampsEndpoints()
14+
{
15+
var horizontal = new FixedSegment2d(new Vector2d(0, 0), new Vector2d(3, 0));
16+
var interior = new FixedSegment2d(new Vector2d(1, -1), new Vector2d(1, 1));
17+
Assert.True(horizontal.TryGetUniqueIntersection(interior, out Fixed64 nearest));
18+
19+
Assert.True(horizontal.TryGetUniqueIntersectionParameterEnclosure(
20+
interior,
21+
out Fixed64 enclosedNearest,
22+
out Fixed64 lower,
23+
out Fixed64 upper));
24+
Assert.Equal(nearest, enclosedNearest);
25+
Assert.Equal(Fixed64.FromRaw(nearest.m_rawValue - 1L), lower);
26+
Assert.Equal(Fixed64.FromRaw(nearest.m_rawValue + 1L), upper);
27+
Assert.True(lower.m_rawValue * 3L <= Fixed64.One.m_rawValue);
28+
Assert.True(upper.m_rawValue * 3L >= Fixed64.One.m_rawValue);
29+
30+
var endpoint = new FixedSegment2d(new Vector2d(0, -1), new Vector2d(0, 1));
31+
Assert.True(horizontal.TryGetUniqueIntersectionParameterEnclosure(
32+
endpoint,
33+
out enclosedNearest,
34+
out lower,
35+
out upper));
36+
Assert.Equal(Fixed64.Zero, enclosedNearest);
37+
Assert.Equal(Fixed64.Zero, lower);
38+
Assert.Equal(Fixed64.MinIncrement, upper);
39+
40+
var disjoint = new FixedSegment2d(new Vector2d(4, -1), new Vector2d(4, 1));
41+
Assert.False(horizontal.TryGetUniqueIntersectionParameterEnclosure(
42+
disjoint,
43+
out _,
44+
out _,
45+
out _));
46+
47+
long before = GC.GetAllocatedBytesForCurrentThread();
48+
bool intersects = false;
49+
for (int i = 0; i < 256; i++)
50+
{
51+
intersects = horizontal.TryGetUniqueIntersectionParameterEnclosure(
52+
interior,
53+
out _,
54+
out _,
55+
out _);
56+
}
57+
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
58+
59+
Assert.True(intersects);
60+
Assert.Equal(0, allocated);
61+
}
62+
1163
[Fact]
1264
public void Constructor_AssignsEndpointsAndDerivedValues()
1365
{

0 commit comments

Comments
 (0)