| | | 1 | | //======================================================================= |
| | | 2 | | // TriangleCircularGeometry.cs |
| | | 3 | | //======================================================================= |
| | | 4 | | // MIT License, Copyright (c) 2024-present David Oravsky (mrdav30) |
| | | 5 | | // See LICENSE file in the project root for full license information. |
| | | 6 | | //======================================================================= |
| | | 7 | | |
| | | 8 | | using System; |
| | | 9 | | using static FixedMathSharp.Geometry.CylinderContactAlgebra; |
| | | 10 | | |
| | | 11 | | namespace FixedMathSharp.Geometry; |
| | | 12 | | |
| | | 13 | | /// <summary>Exact triangle coordinates and circular extents in a canonical +Y frame.</summary> |
| | | 14 | | internal readonly struct TriangleCircularGeometry |
| | | 15 | | { |
| | | 16 | | internal readonly WideRationalBasis3d WorldBasis; |
| | | 17 | | internal readonly WideDirection3d A, B, C, FaceNormal; |
| | | 18 | | internal readonly Signed576 HalfHeight, Radius; |
| | | 19 | | internal readonly Signed320 RawScale; |
| | | 20 | | internal readonly bool IsAffine; |
| | 52596 | 21 | | internal int ParameterWords => IsAffine ? 80 : CylinderContactAlgebra.Words; |
| | 38962 | 22 | | internal int ValueWords => IsAffine ? 112 : CircularRimContactAlgebra.ValueWords; |
| | | 23 | | internal readonly Signed192 EdgeScale; |
| | | 24 | | internal readonly int ValueShift; |
| | | 25 | | private readonly WideDirection3d firstEdge, secondEdge; |
| | | 26 | | private readonly WideDirection3d materialOffset; |
| | | 27 | | |
| | | 28 | | internal TriangleCircularGeometry(FixedTriangle triangle, in ScaledRigidFrame3d sourceFrame, |
| | | 29 | | in CylinderHullSupport hull, int a, int b, int c) |
| | | 30 | | { |
| | 8729 | 31 | | materialOffset = default; |
| | 17458 | 32 | | IsAffine = hull.IsAffine; WorldBasis = hull.WorldBasis; |
| | 26187 | 33 | | A = hull.Vertex(a); B = hull.Vertex(b); C = hull.Vertex(c); |
| | 26187 | 34 | | RawScale = hull.RawScale; HalfHeight = hull.HalfHeight; Radius = hull.Radius; |
| | 17458 | 35 | | firstEdge = Subtract(B, A); secondEdge = Subtract(C, B); |
| | 17458 | 36 | | EdgeScale = Signed192.Signed(1); ValueShift = hull.ValueShift; |
| | 8729 | 37 | | WideDirection3d localNormal = WideDirection3d.Cross( |
| | 8729 | 38 | | WideDirection3d.GetLocalEdge(sourceFrame, triangle.A, triangle.B), |
| | 8729 | 39 | | WideDirection3d.GetLocalEdge(sourceFrame, triangle.A, triangle.C)); |
| | 8729 | 40 | | FaceNormal = localNormal.Transform(hull.Frame.Basis); |
| | 8729 | 41 | | } |
| | | 42 | | |
| | | 43 | | internal TriangleCircularGeometry(FixedTriangle triangle, Vector3d origin, FixedQuaternion rotation, |
| | | 44 | | Vector3d cylinderCenter, FixedQuaternion cylinderRotation, Signed192 height, Fixed64 radius) |
| | | 45 | | { |
| | 584 | 46 | | materialOffset = default; |
| | 584 | 47 | | IsAffine = false; |
| | 584 | 48 | | WorldBasis = new WideRationalBasis3d(cylinderRotation); |
| | 584 | 49 | | var frame = new CylinderPolytopeFrame(cylinderCenter, cylinderRotation, |
| | 584 | 50 | | height, radius, origin, rotation); |
| | 1752 | 51 | | WideAxis3 a = frame.Transform(triangle.A), b = frame.Transform(triangle.B), c = frame.Transform(triangle.C); |
| | 584 | 52 | | Span<Signed320> coordinates = stackalloc Signed320[12] |
| | 584 | 53 | | { |
| | 584 | 54 | | Signed320.ExtendValue(WideArithmetic.AddSigned192(frame.Denominator, frame.Denominator)), |
| | 584 | 55 | | a.X, a.Y, a.Z, b.X, b.Y, b.Z, c.X, c.Y, c.Z, |
| | 584 | 56 | | frame.Cap, WideArithmetic.MultiplySigned192(frame.Radius, frame.Denominator) |
| | 584 | 57 | | }; |
| | 584 | 58 | | WideArithmetic.ReduceCommonScale(coordinates); |
| | 584 | 59 | | RawScale = coordinates[0]; |
| | 584 | 60 | | A = new WideDirection3d(coordinates[1], coordinates[2], coordinates[3]); |
| | 584 | 61 | | B = new WideDirection3d(coordinates[4], coordinates[5], coordinates[6]); |
| | 584 | 62 | | C = new WideDirection3d(coordinates[7], coordinates[8], coordinates[9]); |
| | | 63 | | // IsNormalized bounds each exact quaternion norm denominator by |
| | | 64 | | // Q^2+(EpsilonRaw+2)*Q: four rounded squares add at most two raw |
| | | 65 | | // units of error. Thus their product D<2^129. Rational bases are |
| | | 66 | | // exactly orthogonal, so an unreduced edge 2D*R*(Braw-Araw) has norm |
| | | 67 | | // <2*2^129*sqrt(3)*2^64<2^195 (and conservatively <2^196). |
| | | 68 | | // Translation cancels; common reduction can only shrink this bound. |
| | 1168 | 69 | | firstEdge = Subtract(B, A); secondEdge = Subtract(C, B); |
| | 584 | 70 | | EdgeScale = Signed192.Signed(1); |
| | 1168 | 71 | | HalfHeight = Signed576.ExtendValue(coordinates[10]); Radius = Signed576.ExtendValue(coordinates[11]); |
| | 584 | 72 | | triangle.GetExactNormal(out Signed192 nx, out Signed192 ny, out Signed192 nz, out _); |
| | 584 | 73 | | FaceNormal = new WideDirection3d(WideRigidProjection.TransformLocalAxis(frame.Basis, nx, ny, nz)); |
| | | 74 | | // Each quaternion denominator is <2^65. Relative transformed full-domain |
| | | 75 | | // points, including doubled coordinates and translated origin differences, |
| | | 76 | | // are <2^197. Edge/cap differences are <2^198. Squared residual values |
| | | 77 | | // are below 2^(2*(bits+4)); the common reduction only lowers these bounds. |
| | 584 | 78 | | ValueShift = GetValueShift(coordinates[1..], 4); |
| | 584 | 79 | | } |
| | | 80 | | |
| | | 81 | | internal TriangleCircularGeometry(FixedTriangle triangle, in ScaledRigidFrame3d sourceFrame, |
| | | 82 | | Vector3d cylinderCenter, FixedQuaternion cylinderRotation, Signed192 height, Fixed64 radius) |
| | | 83 | | { |
| | | 84 | | // Entry points dispatch rigid frames before constructing affine geometry. |
| | 486 | 85 | | if (height.Sign < 0 || WideArithmetic.SubtractSigned192(height, Signed192.Raw((Fixed64)8192)).Sign > 0) |
| | 2 | 86 | | throw new ArgumentOutOfRangeException(nameof(height)); |
| | 484 | 87 | | if (radius < Fixed64.Zero || radius > (Fixed64)4096) |
| | 2 | 88 | | throw new ArgumentOutOfRangeException(nameof(radius)); |
| | 482 | 89 | | materialOffset = default; |
| | 482 | 90 | | IsAffine = true; |
| | 482 | 91 | | WorldBasis = new WideRationalBasis3d(cylinderRotation); |
| | 482 | 92 | | var frame = new CylinderPolytopeFrame(sourceFrame, cylinderCenter, cylinderRotation, height, radius); |
| | 1446 | 93 | | WideAxis3 a = frame.Transform(triangle.A), b = frame.Transform(triangle.B), c = frame.Transform(triangle.C); |
| | 482 | 94 | | Span<Signed320> coordinates = stackalloc Signed320[12] |
| | 482 | 95 | | { |
| | 482 | 96 | | WideArithmetic.AddSigned320(frame.CoordinateDenominator, frame.CoordinateDenominator), |
| | 482 | 97 | | a.X, a.Y, a.Z, b.X, b.Y, b.Z, c.X, c.Y, c.Z, |
| | 482 | 98 | | frame.Cap, Signed320.NarrowValue(WideArithmetic.MultiplySigned320(frame.CoordinateDenominator, frame.Radius) |
| | 482 | 99 | | }; |
| | 482 | 100 | | WideArithmetic.ReduceCommonScale(coordinates); |
| | 482 | 101 | | RawScale = coordinates[0]; |
| | 482 | 102 | | A = new WideDirection3d(coordinates[1], coordinates[2], coordinates[3]); |
| | 482 | 103 | | B = new WideDirection3d(coordinates[4], coordinates[5], coordinates[6]); |
| | 482 | 104 | | C = new WideDirection3d(coordinates[7], coordinates[8], coordinates[9]); |
| | 964 | 105 | | HalfHeight = Signed576.ExtendValue(coordinates[10]); Radius = Signed576.ExtendValue(coordinates[11]); |
| | 964 | 106 | | firstEdge = Subtract(B, A); secondEdge = Subtract(C, B); |
| | 482 | 107 | | EdgeScale = Signed192.Signed(1); |
| | 482 | 108 | | WideDirection3d localNormal = WideDirection3d.Cross( |
| | 482 | 109 | | WideDirection3d.GetLocalEdge(sourceFrame, triangle.A, triangle.B), |
| | 482 | 110 | | WideDirection3d.GetLocalEdge(sourceFrame, triangle.A, triangle.C)); |
| | 482 | 111 | | FaceNormal = localNormal.Transform(frame.Basis); |
| | 482 | 112 | | ValueShift = GetValueShift(coordinates[1..], 4); |
| | 482 | 113 | | } |
| | | 114 | | |
| | | 115 | | private TriangleCircularGeometry(in TriangleCircularGeometry source, Signed192 multiplier, in WideDirection3d coreOf |
| | | 116 | | int valueShift = 0) |
| | | 117 | | { |
| | 3823 | 118 | | materialOffset = Multiply(source.materialOffset, multiplier); |
| | 3823 | 119 | | IsAffine = source.IsAffine; |
| | 7646 | 120 | | WorldBasis = source.WorldBasis; FaceNormal = source.FaceNormal; |
| | 3823 | 121 | | A = Multiply(source.A, multiplier); |
| | 3823 | 122 | | B = Multiply(source.B, multiplier); |
| | 3823 | 123 | | C = Multiply(source.C, multiplier); |
| | 7646 | 124 | | HalfHeight = Multiply(source.HalfHeight, multiplier); Radius = Multiply(source.Radius, multiplier); |
| | 3823 | 125 | | RawScale = Signed320.NarrowValue(WideArithmetic.MultiplySigned320(source.RawScale, multiplier)); |
| | 3823 | 126 | | EdgeScale = Signed192.NarrowProven(WideArithmetic.MultiplySigned192(source.EdgeScale, multiplier)); |
| | 7646 | 127 | | firstEdge = source.firstEdge; secondEdge = source.secondEdge; |
| | 3823 | 128 | | Span<Signed576> bounds = stackalloc Signed576[13] |
| | 3823 | 129 | | { |
| | 3823 | 130 | | A.X, A.Y, A.Z, B.X, B.Y, B.Z, C.X, C.Y, C.Z, |
| | 3823 | 131 | | HalfHeight, Radius, coreOffset.X, coreOffset.Z |
| | 3823 | 132 | | }; |
| | | 133 | | // Include the actual endpoint offset: a long core can dominate a |
| | | 134 | | // small centered triangle even when its common denominator is small. |
| | | 135 | | // Two additions cover endpoint/cap translation; the extra margin |
| | | 136 | | // bounds the squared support residual in both regions by 2^ValueShift. |
| | 3823 | 137 | | ValueShift = Math.Max(valueShift, GetValueShift(bounds, 5)); |
| | 3823 | 138 | | } |
| | | 139 | | |
| | | 140 | | internal TriangleCircularGeometry WithCoreChart(in TriangleCircularGeometry chart) => |
| | 3416 | 141 | | new(this, chart.EdgeScale, default, chart.ValueShift); |
| | | 142 | | |
| | | 143 | | internal TriangleCircularGeometry WithValueShift(int valueShift) |
| | | 144 | | { |
| | 121 | 145 | | return new TriangleCircularGeometry(this, Signed192.Signed(1), default, valueShift); |
| | | 146 | | } |
| | | 147 | | |
| | | 148 | | internal WideDirection3d TransformHullVertex(in WideDirection3d vertex) |
| | | 149 | | { |
| | 356476 | 150 | | WideDirection3d point = EdgeScale.Equals(Signed192.Signed(1)) ? vertex : Multiply(vertex, EdgeScale); |
| | 356476 | 151 | | return materialOffset.IsZero ? point : Add(point, materialOffset); |
| | | 152 | | } |
| | | 153 | | |
| | | 154 | | internal int GetSupportMask(ReadOnlySpan<ulong> direction, ReadOnlySpan<int> signs) |
| | | 155 | | { |
| | 58928 | 156 | | Span<ulong> projection = stackalloc ulong[Words]; |
| | 117856 | 157 | | int best = 0, mask = 1; |
| | 353568 | 158 | | for (int i = 1; i < 3; i++) |
| | | 159 | | { |
| | 117856 | 160 | | Dot(Subtract(Vertex(i), Vertex(best)), direction, signs, projection, out int sign); |
| | 173418 | 161 | | if (sign > 0) { best = i; mask = 1 << i; } |
| | 135355 | 162 | | else if (sign == 0) mask |= 1 << i; |
| | | 163 | | } |
| | 58928 | 164 | | return mask; |
| | | 165 | | } |
| | | 166 | | |
| | | 167 | | private TriangleCircularGeometry(in TriangleCircularGeometry source, in WideDirection3d offset) |
| | | 168 | | { |
| | 3520 | 169 | | this = source; |
| | 3520 | 170 | | materialOffset = Add(source.materialOffset, offset); |
| | 10560 | 171 | | A = Add(source.A, offset); B = Add(source.B, offset); C = Add(source.C, offset); |
| | 3520 | 172 | | } |
| | | 173 | | |
| | | 174 | | // Affine contacts round once in the physical circular frame. Rounding |
| | | 175 | | // authored coordinates first would amplify one raw unit by the source scale. |
| | | 176 | | internal Signed576 WitnessCoordinate(FixedTriangle triangle, int vertex, int component) |
| | | 177 | | { |
| | 4893 | 178 | | if (IsAffine) |
| | 1212 | 179 | | return WideArithmetic.SubtractSigned576(Component(Vertex(vertex), component), Component(materialOffset, comp |
| | 3681 | 180 | | Vector3d point = vertex == 0 ? triangle.A : vertex == 1 ? triangle.B : triangle.C; |
| | 3681 | 181 | | return Signed576.ExtendValue(Signed192.Raw(component == 0 ? point.X : component == 1 ? point.Y : point.Z)); |
| | | 182 | | } |
| | | 183 | | |
| | | 184 | | internal Vector3d WitnessVertex(FixedTriangle triangle, int vertex) |
| | | 185 | | { |
| | 33 | 186 | | if (!IsAffine) return vertex == 0 ? triangle.A : vertex == 1 ? triangle.B : triangle.C; |
| | 3 | 187 | | Signed320 denominator = RawScale; |
| | 3 | 188 | | return new Vector3d( |
| | 3 | 189 | | RoundWitnessRatio(WitnessCoordinate(triangle, vertex, 0), denominator), |
| | 3 | 190 | | RoundWitnessRatio(WitnessCoordinate(triangle, vertex, 1), denominator), |
| | 3 | 191 | | RoundWitnessRatio(WitnessCoordinate(triangle, vertex, 2), denominator)); |
| | | 192 | | } |
| | | 193 | | |
| | | 194 | | private static Fixed64 RoundWitnessRatio(Signed576 numerator, Signed320 denominator) |
| | | 195 | | { |
| | 18 | 196 | | Span<ulong> n = stackalloc ulong[9], d = stackalloc ulong[5]; |
| | 9 | 197 | | WideArithmetic.GetMagnitude(numerator, n); |
| | 9 | 198 | | WideArithmetic.GetMagnitude(denominator, out d[4], out d[3], out d[2], out d[1], out d[0]); |
| | 9 | 199 | | bool represented = Fixed64.TryGetSignedRawRatio(n, d, numerator.Sign < 0, out Fixed64 result); |
| | | 200 | | System.Diagnostics.Debug.Assert(represented); |
| | 9 | 201 | | return result; |
| | | 202 | | } |
| | | 203 | | |
| | | 204 | | internal TriangleCircularGeometry WithCore(Vector2d axis, Fixed64 length, out WideDirection3d coreOffset) |
| | | 205 | | { |
| | | 206 | | // The reduced cylinder scale need not retain its original factor two. |
| | | 207 | | // One common 2Q multiplier represents L*axis/(2Q) at either endpoint, |
| | | 208 | | // including odd raw lengths, without forming a rounded endpoint. |
| | | 209 | | // Affine callers provide admitted finite cores (length <=8192 and |
| | | 210 | | // unit-axis components); the scalar path retains its full-domain input. |
| | 286 | 211 | | Signed192 multiplier = Signed192.Raw(Fixed64.Two); |
| | 286 | 212 | | Signed576 lengthScale = WideArithmetic.MultiplySigned320(RawScale, Signed192.Raw(length)); |
| | 286 | 213 | | coreOffset = new WideDirection3d(Multiply(lengthScale, Signed192.Raw(axis.X)), default, |
| | 286 | 214 | | Multiply(lengthScale, Signed192.Raw(axis.Y))); |
| | 286 | 215 | | if (IsAffine) |
| | | 216 | | { |
| | | 217 | | // Cancel the same factor from 2Q and both endpoint offsets before |
| | | 218 | | // enlarging coordinates. All points, extents, RawScale and EdgeScale |
| | | 219 | | // then use the reduced multiplier, so no endpoint is rounded. |
| | | 220 | | // Width proof for the admitted affine frame: before either gcd, |
| | | 221 | | // coordinates need at most 306 bits; 2Q adds 33. The largest |
| | | 222 | | // projected squared gap needs at most 2608 bits: edge differences |
| | | 223 | | // need 307, endpoint/cap sums 341, e^2*v-(e.v)*e at most 959, |
| | | 224 | | // support dots at most 1302, then squaring/addition fits 2608. |
| | | 225 | | // If the earlier |
| | | 226 | | // coordinate gcd is G, g=log2(G), that degree-eight expression loses |
| | | 227 | | // 8g bits. Its original RawScale contains 2Q^2, hence the endpoint |
| | | 228 | | // offsets retain at least max(0,65-g) factors of two. This gcd removes |
| | | 229 | | // f>=min(33,max(0,65-g)) more; the degree-four dependence on scaled |
| | | 230 | | // points loses 4f bits. Thus 2608-8g-4f<=2476, below the existing |
| | | 231 | | // 2560-bit analytic bank. No rotation-denominator cancellation is |
| | | 232 | | // assumed: a translated core need not share such a factor. |
| | 95 | 233 | | Span<Signed576> factors = stackalloc Signed576[3] |
| | 95 | 234 | | { coreOffset.X, coreOffset.Z, Signed576.ExtendValue(multiplier) }; |
| | 95 | 235 | | WideArithmetic.ReduceCommonScale(factors); |
| | 95 | 236 | | coreOffset = new(factors[0], default, factors[1]); |
| | 95 | 237 | | multiplier = Signed192.NarrowProven(Signed320.NarrowValue(factors[2])); |
| | | 238 | | } |
| | | 239 | | // Both endpoint regions retain one actual-bound-based value scale. |
| | 286 | 240 | | return new TriangleCircularGeometry(this, multiplier, coreOffset); |
| | | 241 | | } |
| | | 242 | | |
| | | 243 | | internal TriangleCircularGeometry AtCoreRegion(in WideDirection3d coreOffset, int sign) => |
| | 3520 | 244 | | new(this, sign > 0 ? coreOffset : -coreOffset); |
| | | 245 | | |
| | 5681 | 246 | | internal WideDirection3d EdgeFromTo(int from, int to) => (from + 1) % 3 == to ? Edge(from) : -Edge(to); |
| | | 247 | | |
| | | 248 | | internal static Signed576 Multiply(Signed576 value, Signed192 multiplier) => |
| | 250213 | 249 | | WideArithmetic.MultiplySigned576(value, multiplier); |
| | | 250 | | |
| | 80665 | 251 | | private static WideDirection3d Multiply(in WideDirection3d value, Signed192 multiplier) => new( |
| | 80665 | 252 | | Multiply(value.X, multiplier), Multiply(value.Y, multiplier), Multiply(value.Z, multiplier)); |
| | | 253 | | |
| | 66123 | 254 | | private static WideDirection3d Add(in WideDirection3d first, in WideDirection3d second) => new( |
| | 66123 | 255 | | WideArithmetic.AddSigned576(first.X, second.X), WideArithmetic.AddSigned576(first.Y, second.Y), |
| | 66123 | 256 | | WideArithmetic.AddSigned576(first.Z, second.Z)); |
| | | 257 | | |
| | | 258 | | internal static int GetValueShift(ReadOnlySpan<Signed576> coordinates, int carryBits) |
| | | 259 | | { |
| | 9278 | 260 | | int bits = 0; |
| | 9278 | 261 | | Span<ulong> magnitude = stackalloc ulong[9]; |
| | 174678 | 262 | | for (int index = 0; index < coordinates.Length; index++) |
| | | 263 | | { |
| | 78061 | 264 | | WideArithmetic.GetMagnitude(coordinates[index], magnitude); |
| | 78061 | 265 | | bits = Math.Max(bits, WideArithmetic.GetMagnitudeBitLength(magnitude)); |
| | | 266 | | } |
| | 9278 | 267 | | return 2 * (bits + carryBits); |
| | | 268 | | } |
| | | 269 | | |
| | | 270 | | internal static int GetValueShift(ReadOnlySpan<Signed320> coordinates, int carryBits) |
| | | 271 | | { |
| | 1066 | 272 | | int bits = 0; |
| | 1066 | 273 | | Span<ulong> magnitude = stackalloc ulong[5]; |
| | 25584 | 274 | | for (int index = 0; index < coordinates.Length; index++) |
| | | 275 | | { |
| | 11726 | 276 | | WideArithmetic.GetMagnitude(coordinates[index], out magnitude[4], out magnitude[3], |
| | 11726 | 277 | | out magnitude[2], out magnitude[1], out magnitude[0]); |
| | 11726 | 278 | | bits = Math.Max(bits, WideArithmetic.GetMagnitudeBitLength(magnitude)); |
| | | 279 | | } |
| | 1066 | 280 | | return 2 * (bits + carryBits); |
| | | 281 | | } |
| | | 282 | | |
| | | 283 | | /// <summary> |
| | | 284 | | /// After nonnegative cap and face support tests, certifies that their |
| | | 285 | | /// retained minimum is global without enumerating the remaining features. |
| | | 286 | | /// </summary> |
| | | 287 | | internal bool HasFaceMinimumCertificate() |
| | | 288 | | { |
| | 449 | 289 | | if (FaceNormal.X.IsZero && FaceNormal.Z.IsZero) |
| | | 290 | | { |
| | 71 | 291 | | Signed576 axial = A.Y.Sign < 0 ? WideArithmetic.Negate(A.Y) : A.Y; |
| | 71 | 292 | | Signed576 depth = WideArithmetic.SubtractSigned576(HalfHeight, axial); |
| | 71 | 293 | | if (WideArithmetic.SubtractSigned576(depth, Radius).Sign > 0) |
| | 11 | 294 | | return false; |
| | | 295 | | } |
| | 378 | 296 | | else if (!FaceNormal.Y.IsZero || WideArithmetic.SubtractSigned576(Radius, HalfHeight).Sign > 0) |
| | 222 | 297 | | return false; |
| | | 298 | | |
| | | 299 | | // Let q be the origin's orthogonal projection onto the triangle plane. |
| | | 300 | | // A horizontal face has depth d=H-|q.Y|<=R; a vertical face has |
| | | 301 | | // d=R-|q.radial|<=R<=H. The caller has proved d>=0. If q is in the |
| | | 302 | | // triangle, the cylinder contains a radius-d ball centered at q. |
| | | 303 | | // Thus triangle-cylinder contains the origin-centered radius-d ball, |
| | | 304 | | // while the tested face support attains d: no other feature is smaller. |
| | 432 | 305 | | WideDirection3d e = Edge(0), f = -Edge(2); |
| | 648 | 306 | | Signed832 ee = e.SquaredLength, ff = f.SquaredLength, ef = WideDirection3d.Dot(e, f); |
| | 216 | 307 | | Signed832 d1 = WideArithmetic.SubtractSigned832(default, WideDirection3d.Dot(A, e)); |
| | 216 | 308 | | Signed832 d2 = WideArithmetic.SubtractSigned832(default, WideDirection3d.Dot(A, f)); |
| | | 309 | | // Retained edges are <196 bits; scaled offsets are <232. Thus Gram |
| | | 310 | | // dots are <394, offset dots <430, and mixed minors <825. The actual |
| | | 311 | | // barycentric denominator is EdgeScale*det (not det); alpha stays |
| | | 312 | | // <827 bits, within Signed832 even in a positive-core region. |
| | 216 | 313 | | if (IsAffine) |
| | 21 | 314 | | return HasNonnegativeGramWeights(ee, ff, ef, d1, d2); |
| | 195 | 315 | | Signed832 determinant = ProductDifference(Signed576.NarrowValue(ee), Signed576.NarrowValue(ff), |
| | 195 | 316 | | Signed576.NarrowValue(ef), Signed576.NarrowValue(ef)); |
| | 195 | 317 | | Signed832 beta = ProductDifference(Signed576.NarrowValue(ff), Signed576.NarrowValue(d1), |
| | 195 | 318 | | Signed576.NarrowValue(ef), Signed576.NarrowValue(d2)); |
| | 195 | 319 | | Signed832 gamma = ProductDifference(Signed576.NarrowValue(ee), Signed576.NarrowValue(d2), |
| | 195 | 320 | | Signed576.NarrowValue(ef), Signed576.NarrowValue(d1)); |
| | 195 | 321 | | Signed832 alpha = WideArithmetic.SubtractSigned832( |
| | 195 | 322 | | WideArithmetic.SubtractSigned832(WideArithmetic.MultiplySigned832(determinant, EdgeScale), beta), gamma); |
| | 195 | 323 | | return alpha.Sign >= 0 && beta.Sign >= 0 && gamma.Sign >= 0; |
| | | 324 | | } |
| | | 325 | | |
| | | 326 | | private bool HasNonnegativeGramWeights(Signed832 ee, Signed832 ff, Signed832 ef, Signed832 d1, Signed832 d2) |
| | | 327 | | { |
| | 42 | 328 | | Span<ulong> values = stackalloc ulong[20 * Words]; Span<int> signs = stackalloc int[20]; |
| | 42 | 329 | | ContactQuadratic a = ContactQuadratic.At(values, signs, 0), b = ContactQuadratic.At(values, signs, 1), |
| | 42 | 330 | | c = ContactQuadratic.At(values, signs, 2), d = ContactQuadratic.At(values, signs, 3), |
| | 42 | 331 | | e = ContactQuadratic.At(values, signs, 4), determinant = ContactQuadratic.At(values, signs, 5), |
| | 42 | 332 | | beta = ContactQuadratic.At(values, signs, 6), gamma = ContactQuadratic.At(values, signs, 7), |
| | 42 | 333 | | alpha = ContactQuadratic.At(values, signs, 8), term = ContactQuadratic.At(values, signs, 9); |
| | 105 | 334 | | a.Set(ee); b.Set(ff); c.Set(ef); d.Set(d1); e.Set(d2); |
| | 21 | 335 | | ContactQuadratic.Multiply(a, b, ReadOnlySpan<ulong>.Empty, determinant); |
| | 42 | 336 | | ContactQuadratic.Multiply(c, c, ReadOnlySpan<ulong>.Empty, term); determinant.Add(term, -1); |
| | 21 | 337 | | ContactQuadratic.Multiply(b, d, ReadOnlySpan<ulong>.Empty, beta); |
| | 42 | 338 | | ContactQuadratic.Multiply(c, e, ReadOnlySpan<ulong>.Empty, term); beta.Add(term, -1); |
| | 21 | 339 | | ContactQuadratic.Multiply(a, e, ReadOnlySpan<ulong>.Empty, gamma); |
| | 42 | 340 | | ContactQuadratic.Multiply(c, d, ReadOnlySpan<ulong>.Empty, term); gamma.Add(term, -1); |
| | 21 | 341 | | ContactQuadratic.Scale(determinant, Signed576.ExtendValue(Signed320.ExtendValue(EdgeScale)), alpha); |
| | 42 | 342 | | alpha.Add(beta, -1); alpha.Add(gamma, -1); |
| | 21 | 343 | | return alpha.Signs[0] >= 0 && beta.Signs[0] >= 0 && gamma.Signs[0] >= 0; |
| | | 344 | | } |
| | | 345 | | |
| | | 346 | | internal bool HasFaceDiskMinimumCertificate(in WideDirection3d coreAxis, ConvexContactCandidate best) |
| | | 347 | | { |
| | 116 | 348 | | if (!WideDirection3d.Cross(FaceNormal, coreAxis).IsZero) |
| | 65 | 349 | | return false; |
| | | 350 | | // The caller has tested +/-face with d>=0 and proved that its shape |
| | | 351 | | // contains the normal-axis segment [-s,s], with d<=s-|a| for plane |
| | | 352 | | // u.x=a. A face-plane disk(q,d), q=a*u, minus that segment contains |
| | | 353 | | // the origin ball(d). Its three inward edge clearances therefore |
| | | 354 | | // certify the retained d globally, without changing its earlier tie. |
| | 51 | 355 | | Span<ulong> values = stackalloc ulong[ConvexContactCandidate.Slots * Words]; |
| | 51 | 356 | | Span<int> signs = stackalloc int[ConvexContactCandidate.Slots]; |
| | 51 | 357 | | Span<ulong> work = stackalloc ulong[6 * Words]; |
| | 153 | 358 | | Span<ulong> zero = Slot(work, 1), scale = Slot(work, 3), squaredScale = Slot(work, 4); |
| | 153 | 359 | | values.Clear(); signs.Clear(); zero.Clear(); |
| | 51 | 360 | | Import(Signed576.ExtendValue(RawScale), scale); |
| | 51 | 361 | | WideArithmetic.MultiplyMagnitudes(scale, scale, squaredScale); |
| | 338 | 362 | | for (int edge = 0; edge < 3; edge++) |
| | | 363 | | { |
| | | 364 | | // Use the parallel raw core axis, not the much wider transformed |
| | | 365 | | // face normal. Parallelism cross<295 bits; tangent<231, offset |
| | | 366 | | // dot<465 and tangent norm²<464 fit the existing fixed carriers. |
| | | 367 | | // Scale²*norm²<792 and numerator²<930 fit forty-word candidates. |
| | 131 | 368 | | WideDirection3d tangent = WideDirection3d.Cross(coreAxis, Edge(edge)); |
| | 131 | 369 | | int inward = WideDirection3d.Dot(EdgeFromTo(edge, (edge + 2) % 3), tangent).Sign; |
| | 131 | 370 | | Signed832 projection = WideDirection3d.Dot(Vertex(edge), tangent); |
| | 131 | 371 | | if (!HasFaceDiskClearance(projection, -inward * projection.Sign, tangent, |
| | 131 | 372 | | best, work, values, signs)) |
| | 13 | 373 | | return false; |
| | | 374 | | } |
| | 38 | 375 | | return true; |
| | | 376 | | } |
| | | 377 | | |
| | | 378 | | // Shared by single-triangle disks and complete patch tubes. Work slot 1 |
| | | 379 | | // is zero and slot 4 is the squared coordinate scale, prepared once. |
| | | 380 | | internal static bool HasFaceDiskClearance(Signed832 projection, int projectionSign, |
| | | 381 | | in WideDirection3d tangent, ConvexContactCandidate best, Span<ulong> work, |
| | | 382 | | Span<ulong> values, Span<int> signs) |
| | | 383 | | { |
| | 131 | 384 | | Import(projection, Slot(work, 0)); |
| | 131 | 385 | | Import(tangent.SquaredLength, Slot(work, 2)); |
| | 131 | 386 | | WideArithmetic.MultiplyMagnitudes(Slot(work, 2), Slot(work, 4), Slot(work, 5)); |
| | 131 | 387 | | int gapSign = BuildRadialCandidate(Slot(work, 0), projectionSign, |
| | 131 | 388 | | Slot(work, 1), Slot(work, 5), values, signs); |
| | 131 | 389 | | return WideConvexPrismRelations.CompareConvexContactCandidates( |
| | 131 | 390 | | new ConvexContactCandidate(values, signs, gapSign), best) >= 0; |
| | | 391 | | } |
| | | 392 | | |
| | | 393 | | private static Signed832 ProductDifference(Signed576 a, Signed576 b, Signed576 c, Signed576 d) => |
| | 585 | 394 | | WideArithmetic.SubtractSigned832(WideArithmetic.MultiplySigned576ToSigned832(a, b), |
| | 585 | 395 | | WideArithmetic.MultiplySigned576ToSigned832(c, d)); |
| | | 396 | | |
| | | 397 | | /// <summary>Rounds a rational local normal's support gap, cancelling principal-axis scale exactly.</summary> |
| | | 398 | | internal void GetAnalyticDepth(ConvexContactCandidate candidate, ReadOnlySpan<int> directionSigns, int mask, |
| | | 399 | | out Fixed64 depth, out bool clamped) |
| | | 400 | | { |
| | 748 | 401 | | int components = (directionSigns[0] == 0 ? 0 : 1) |
| | 748 | 402 | | + (directionSigns[1] == 0 ? 0 : 1) + (directionSigns[2] == 0 ? 0 : 1); |
| | 748 | 403 | | if (components != 1) |
| | | 404 | | { |
| | 338 | 405 | | WideConvexPrismRelations.GetRoundedConvexContactCandidateDepth(candidate, Fixed64.Zero, out depth, out clamp |
| | 338 | 406 | | return; |
| | | 407 | | } |
| | | 408 | | // A single local component cancels its magnitude from the normalized |
| | | 409 | | // support gap. Every vertex in the selected mask has this coordinate. |
| | 410 | 410 | | int axis = directionSigns[0] != 0 ? 0 : directionSigns[1] != 0 ? 1 : 2; |
| | 410 | 411 | | int vertex = (mask & 1) != 0 ? 0 : (mask & 2) != 0 ? 1 : 2; |
| | 410 | 412 | | Signed576 coordinate = Component(Vertex(vertex), axis); |
| | 410 | 413 | | Signed576 numerator = WideArithmetic.AddSigned576(axis == 1 ? HalfHeight : Radius, |
| | 410 | 414 | | directionSigns[axis] < 0 ? WideArithmetic.Negate(coordinate) : coordinate); |
| | | 415 | | // Shifted numerator and exact clamp threshold fit 233 bits. Test exact |
| | | 416 | | // overflow before nearest-even rounding, including Max+fractions. |
| | 410 | 417 | | Signed576 maximum = WideArithmetic.MultiplySigned320(RawScale, Signed192.Raw(Fixed64.MaxValue)); |
| | 410 | 418 | | clamped = WideArithmetic.SubtractSigned576(numerator, maximum).Sign > 0; |
| | 410 | 419 | | if (clamped) |
| | | 420 | | { |
| | 3 | 421 | | depth = Fixed64.MaxValue; |
| | 3 | 422 | | return; |
| | | 423 | | } |
| | 407 | 424 | | bool represented = Fixed64.TryGetSignedRawRatio(numerator, Signed576.ExtendValue(RawScale), out depth); |
| | | 425 | | System.Diagnostics.Debug.Assert(represented); |
| | 407 | 426 | | } |
| | | 427 | | |
| | 613917 | 428 | | internal WideDirection3d Vertex(int index) => index == 0 ? A : index == 1 ? B : C; |
| | 23803 | 429 | | internal WideDirection3d Edge(int index) => index == 0 ? firstEdge : index == 1 ? secondEdge : -Add(firstEdge, secon |
| | 32967 | 430 | | internal WideDirection3d CapOffset(int vertex, int cap) => new(Vertex(vertex).X, |
| | 32967 | 431 | | WideArithmetic.AddSigned576(Vertex(vertex).Y, cap > 0 ? HalfHeight : WideArithmetic.Negate(HalfHeight)), |
| | 32967 | 432 | | Vertex(vertex).Z); |
| | 564199 | 433 | | internal static WideDirection3d Subtract(in WideDirection3d first, in WideDirection3d second) => new( |
| | 564199 | 434 | | WideArithmetic.SubtractSigned576(first.X, second.X), |
| | 564199 | 435 | | WideArithmetic.SubtractSigned576(first.Y, second.Y), |
| | 564199 | 436 | | WideArithmetic.SubtractSigned576(first.Z, second.Z)); |
| | | 437 | | internal static FixedPointAnchor GetSupport(Vector3d center, FixedQuaternion rotation, |
| | | 438 | | Signed192 height, Vector3d radialPoint, int cap) |
| | | 439 | | { |
| | 819 | 440 | | ulong floor = height.Low >> 1; |
| | 819 | 441 | | Fixed64 halfHeight = Fixed64.FromRaw((long)(floor + ((height.Low & 1UL) != 0 && (floor & 1UL) != 0 ? 1UL : 0UL)) |
| | 819 | 442 | | Vector3d axial = new(Fixed64.Zero, cap >= 0 ? -halfHeight : halfHeight, Fixed64.Zero); |
| | | 443 | | // Public cylinder heights fit one Fixed64 and can retain an odd raw |
| | | 444 | | // half-height. Slab heights may span 64 unsigned bits but are even, |
| | | 445 | | // because their authored half-thickness was representable and exact. |
| | 819 | 446 | | FixedPointAnchorTerm3d term = height.Low <= long.MaxValue |
| | 819 | 447 | | ? FixedPointAnchorTerm3d.CreateCenteredAxisSupport(Vector3d.Up, |
| | 819 | 448 | | Fixed64.FromRaw(cap >= 0 ? -(long)height.Low : (long)height.Low), |
| | 819 | 449 | | Vector3d.Zero, Fixed64.Zero, axial, Vector3d.Zero) |
| | 819 | 450 | | : default; |
| | 819 | 451 | | return new FixedPointAnchor(center, rotation, axial, radialPoint, term); |
| | | 452 | | } |
| | | 453 | | } |