Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CSMath.Tests/Matrix4Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Xunit;

namespace CSMath.Tests;

public class Matrix4Tests
{
[Fact]
public void GetArbitraryAxisReturnsIdentityForTheZAxis()
{
Assert.Equal(Matrix4.Identity, Matrix4.GetArbitraryAxis(XYZ.AxisZ));
}

[Fact]
public void GetArbitraryAxisMirrorsXAndZForANegatedZAxis()
{
Matrix4 m = Matrix4.GetArbitraryAxis(-XYZ.AxisZ);

AssertUtils.AreEqual(new XYZ(-1, 2, -3), m * new XYZ(1, 2, 3));
}

[Fact]
public void GetArbitraryAxisHandlesANormalThatIsNearlyButNotExactlyTheZAxis()
{
//What a real drawing stores: a normal that means "+Z" but carries the dust of whatever
//arithmetic produced it. The arbitrary axis algorithm switches to the Y reference below a
//1/64 threshold precisely for this case - written as integer division that threshold is
//zero, the branch never runs, and the cross product with Z is then so close to the zero
//vector that normalising it yields a direction unrelated to the entity.
XYZ almostZ = new XYZ(-3.707652143685886E-13, 8.818399612355268E-14, 1);

Matrix4 m = Matrix4.GetArbitraryAxis(almostZ);

XYZ point = new XYZ(3952258.943164733, -9072802.879860947, 0);

AssertUtils.AreEqual(point, m * point);
}

[Fact]
public void GetArbitraryAxisUsesTheYReferenceBelowTheThreshold()
{
//Just inside the threshold the X axis comes from Y x N, just outside it from Z x N. Both
//have to produce a right handed frame whose Z is the normal itself.
foreach (double tilt in new[] { 1.0 / 128.0, 1.0 / 32.0 })
{
XYZ normal = new XYZ(tilt, 0, 1).Normalize();

Matrix4 m = Matrix4.GetArbitraryAxis(normal);

AssertUtils.AreEqual(normal, m * XYZ.AxisZ);
}
}
}
15 changes: 12 additions & 3 deletions CSMath/MathHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;

namespace CSMath;

Expand Down Expand Up @@ -339,12 +339,21 @@ public static double NormalizeAngleRadians(double angle)
/// Convert a value from radian to degree.
/// </summary>
/// <param name="value">Value in radians</param>
/// <param name="absolute">Calculates the negative values in a 0-360 range.</param>
/// <param name="absolute">
/// Normalises the result into a 0-360 range. Pass <see langword="false"/> to keep the converted
/// value as it is, in the -360 to 360 range the caller gave it in.
/// </param>
/// <returns>The degree value.</returns>
/// <remarks>
/// The parameter used to be accepted and ignored, so the result was always normalised. That
/// loses a full sweep: an angle pair of -PI to PI, which is how a circular hatch boundary is
/// stored, came back as 180 and 180 - the same number twice, with the 360 degrees between them
/// gone. Measured on real drawings, twenty-five boundaries in four of them.
/// </remarks>
public static double RadToDeg(double value, bool absolute = true)
{
var result = value * RadToDegFactor;
return NormalizeAngle(result);
return absolute ? NormalizeAngle(result) : result;
}

/// <summary>
Expand Down
6 changes: 5 additions & 1 deletion CSMath/Matrix4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,11 @@ public static Matrix4 GetArbitraryAxis(XYZ zaxis)
}

XYZ xaxis;
if (Math.Abs(zaxis.X) < (1 / 64) && Math.Abs(zaxis.Y) < (1 / 64))
//1/64 is integer division and is therefore zero, which made this branch unreachable and sent
//every normal that is near +Z - but not exactly it, which is what a real drawing stores -
//down the Wz path, where the cross product is near zero and normalises to a direction that
//has nothing to do with the entity.
if (Math.Abs(zaxis.X) < (1.0 / 64.0) && Math.Abs(zaxis.Y) < (1.0 / 64.0))
{
xaxis = XYZ.Cross(XYZ.AxisY, zaxis);
}
Expand Down
67 changes: 66 additions & 1 deletion CSUtilities.Tests/IO/StreamIOTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,42 @@ namespace CSUtilities.Tests.IO;

public class StreamIOTests : IDisposable
{
private sealed class PartialReadMemoryStream : MemoryStream
{
public PartialReadMemoryStream(byte[] data) : base(data) { }

public override int Read(byte[] buffer, int offset, int count)
{
return base.Read(buffer, offset, Math.Min(count, 2));
}

public override Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken)
{
return base.ReadAsync(buffer, offset, Math.Min(count, 2), cancellationToken);
}
}

private sealed class NonSeekableStream : Stream
{
private readonly Stream _inner;

public NonSeekableStream(byte[] data)
{
this._inner = new MemoryStream(data);
}

public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count) => this._inner.Read(buffer, offset, Math.Min(count, 2));
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

private readonly MemoryStream _memoryStream;
private readonly StreamIO _streamIO;

Expand Down Expand Up @@ -41,6 +77,35 @@ public void WriteAndReadBytes_WorksCorrectly()
Assert.Equal(data, _streamIO.ReadBytes(data.Length));
}

[Fact]
public void ConstructorBuffersANonSeekablePartialReadStream()
{
byte[] data = { 1, 2, 3, 4, 5, 6 };
using NonSeekableStream source = new NonSeekableStream(data);
using StreamIO stream = new StreamIO(source);

Assert.True(stream.Stream.CanSeek);
Assert.Equal(data, stream.ReadBytes(data.Length));
}

[Fact]
public void ReadBytesCompletesPartialReads()
{
byte[] data = { 1, 2, 3, 4, 5, 6 };
using StreamIO stream = new StreamIO(new PartialReadMemoryStream(data));

Assert.Equal(data, stream.ReadBytes(data.Length));
}

[Fact]
public async Task ReadBytesAsyncCompletesPartialReads()
{
byte[] data = { 1, 2, 3, 4, 5, 6 };
using StreamIO stream = new StreamIO(new PartialReadMemoryStream(data));

Assert.Equal(data, await stream.ReadBytesAsync(data.Length));
}

[Fact]
public void WriteAndReadString_WorksCorrectly()
{
Expand Down Expand Up @@ -206,4 +271,4 @@ public void ReadBytes_ThrowsOnEndOfStream()
{
Assert.Throws<EndOfStreamException>(() => _streamIO.ReadBytes(1));
}
}
}
108 changes: 76 additions & 32 deletions CSUtilities/IO/StreamIO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,36 @@ public StreamIO(string filename, FileMode mode, FileAccess access)
/// <param name="resetPosition"></param>
public StreamIO(Stream stream, bool createCopy, bool resetPosition)
{
long position = stream.Position;
if (stream == null)
{
throw new ArgumentNullException(nameof(stream));
}

//Check if supports seeking
//Readers require seeking. A non-seekable input therefore has to be buffered from its
//current position; querying Position or Length first would itself throw.
if (!stream.CanSeek || createCopy)
{
stream.Position = 0;
//Create a copy of the stream to allow seeking
byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
_stream = (Stream)new MemoryStream(buffer);
stream.Position = position;
MemoryStream copy = new MemoryStream();
if (stream.CanSeek)
{
long position = stream.Position;
try
{
stream.Position = 0;
stream.CopyTo(copy);
}
finally
{
stream.Position = position;
}
}
else
{
stream.CopyTo(copy);
}

copy.Position = 0;
_stream = copy;
}
else
{
Expand Down Expand Up @@ -116,14 +135,15 @@ public byte[] GetBytes(int offset, int length)
//Set the position to the begining
this.Position = offset;

byte[] buffer = this.ReadBytes(length);
//if (this.m_stream.Read(buffer, offset, length) < length)
// throw new EndOfStreamException();

//Reset the position
this.Position = save;

return buffer;
try
{
return this.ReadBytes(length);
}
finally
{
//A failed read must not leave this utility at the requested offset.
this.Position = save;
}
}

public async Task<byte[]> GetBytesAsync(int offset, int length)
Expand All @@ -136,14 +156,14 @@ public async Task<byte[]> GetBytesAsync(int offset, int length)
//Set the position to the begining
this.Position = offset;

byte[] buffer = await this.ReadBytesAsync(length);
//if (this.m_stream.Read(buffer, offset, length) < length)
// throw new EndOfStreamException();

//Reset the position
this.Position = save;

return buffer;
try
{
return await this.ReadBytesAsync(length);
}
finally
{
this.Position = save;
}
}

/// <summary>
Expand All @@ -163,9 +183,15 @@ public byte LookByte()
/// <returns></returns>
public byte[] LookBytes(int count)
{
byte[] bs = this.ReadBytes(count);
this.Position -= count;
return bs;
long position = this.Position;
try
{
return this.ReadBytes(count);
}
finally
{
this.Position = position;
}
}

/// <summary>
Expand Down Expand Up @@ -214,8 +240,17 @@ public virtual byte[] ReadBytes(int length)

byte[] buffer = new byte[length];

if (this._stream.Read(buffer, 0, length) < length)
throw new EndOfStreamException();
int offset = 0;
while (offset < length)
{
int read = this._stream.Read(buffer, offset, length - offset);
if (read == 0)
{
throw new EndOfStreamException();
}

offset += read;
}

return buffer;
}
Expand All @@ -227,8 +262,17 @@ public virtual async Task<byte[]> ReadBytesAsync(int length, CancellationToken c

byte[] buffer = new byte[length];

if (await this._stream.ReadAsync(buffer, 0, length, cancellationToken) < length)
throw new EndOfStreamException();
int offset = 0;
while (offset < length)
{
int read = await this._stream.ReadAsync(buffer, offset, length - offset, cancellationToken);
if (read == 0)
{
throw new EndOfStreamException();
}

offset += read;
}

return buffer;
}
Expand Down Expand Up @@ -520,4 +564,4 @@ public void Dispose()
{
_stream.Dispose();
}
}
}