Skip to content
Draft
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ We run identical math operations across vmath, GLSL, [nim-glm](https://github.co

❌ **Orthogonal vector note**: Jolt Physics provides `GetNormalizedPerpendicular()`. vmath's `orthogonal()` returns a perpendicular vector without normalizing it. GLSL and nim-GLM have no direct equivalent, while gl-matrix only computes one internally as part of `quat.rotationTo()`.

# 3.0.0 to 3.1.0 vmath changes:

Version `3.1.0` adds explicit floating-point classification functions: `isNan`, `isInf`, and `isFinite`.

* **Breaking change:** `isNan(x)` now returns true only for NaN, matching standard `std/math.isNaN` behavior. It previously also returned true for positive and negative infinity. Replace old checks with `not isFinite(x)` when both NaN and infinity should be rejected.
* **`isInf(x)` added:** Returns true for positive or negative infinity.
* **`isFinite(x)` added:** Returns true for usable finite values, including zero and subnormal values.

# 2.x.x to 3.0.0 vmath breaking changes:

Version `3.0.0` changed rotation to be CCW (counter-clockwise) and updated the quaternion conventions to match GLSL, GLM, gl-matrix. Added a multi-library conformance suite.
Expand Down
12 changes: 10 additions & 2 deletions src/vmath.nim
Original file line number Diff line number Diff line change
Expand Up @@ -635,8 +635,16 @@ proc toDegrees*(deg: SomeInteger): float32 =
deg.float32.toDegrees

proc isNan*(x: SomeFloat): bool =
## Returns true if number is a NaN.
x != 0.0 and (x != x or x * 0.5 == x)
## Returns true if x is NaN.
x != x

proc isInf*(x: SomeFloat): bool =
## Returns true if x is positive or negative infinity.
x == Inf or x == -Inf

proc isFinite*(x: SomeFloat): bool =
## Returns true if x is finite, including zero and subnormal values.
x == x and x != Inf and x != -Inf

proc `zmod`*(a, b: float32): float32 =
## Float point mod.
Expand Down
88 changes: 84 additions & 4 deletions tests/bench_isNan.nim
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ proc isNaN3*(x: SomeFloat): bool =
## Returns true if number is a NaN.
x != 0.0 and (x != x or x * 0.5 == x)

proc isInfClassify(x: SomeFloat): bool =
classify(x.float64) in {fcInf, fcNegInf}

proc isFiniteClassify(x: SomeFloat): bool =
classify(x.float64) notin {fcNan, fcInf, fcNegInf}

assert isNaNSlow(0.3) == false
assert isNaNSlow(0.0) == false
assert isNaNSlow(0.3/0.0) == true
Expand All @@ -29,14 +35,16 @@ assert isNaNRyan(5.0e-324) == false

assert isNan(float32(0.3)) == false
assert isNan(float32(0.0)) == false
assert isNan(float32(0.3/0.0)) == true
assert isNan(float32(-0.3/0.0)) == true
assert isNan(float32(0.3/0.0)) == false
assert isNan(float32(-0.3/0.0)) == false
assert isNan(float32(0.0/0.0)) == true
assert isNan(float32(5.0e-324)) == false

assert isNan(float64(0.3)) == false
assert isNan(float64(0.0)) == false
assert isNan(float64(0.3/0.0)) == true
assert isNan(float64(-0.3/0.0)) == true
assert isNan(float64(0.3/0.0)) == false
assert isNan(float64(-0.3/0.0)) == false
assert isNan(float64(0.0/0.0)) == true
assert isNan(float64(5.0e-324)) == false

assert isNan3(float32(0.3)) == false
Expand All @@ -51,8 +59,24 @@ assert isNan3(float64(0.3/0.0)) == true
assert isNan3(float64(-0.3/0.0)) == true
assert isNan3(float64(5.0e-324)) == false

for x in [NaN.float32, Inf.float32, NegInf.float32, 0.0'f32,
1.0e-45'f32, 1.0'f32]:
assert isInf(x) == isInfClassify(x)
assert isFinite(x) == isFiniteClassify(x)

for x in [NaN.float64, Inf.float64, NegInf.float64, 0.0'f64,
5.0e-324'f64, 1.0'f64]:
assert isInf(x) == isInfClassify(x)
assert isFinite(x) == isFiniteClassify(x)

const num = 1_00_000

let
values32 = [0.0'f32, -0.0'f32, 1.0'f32, -1.0'f32, Inf.float32,
NegInf.float32, NaN.float32, 1.0e-45'f32]
values64 = [0.0'f64, -0.0'f64, 1.0'f64, -1.0'f64, Inf.float64,
NegInf.float64, NaN.float64, 5.0e-324'f64]

timeIt "isNaNSlow float32":
var x: float32
var n = 0
Expand Down Expand Up @@ -124,3 +148,59 @@ timeIt "isNan3 float64":
n += 1
x = float64(i) / float64(i)
keep n

timeIt "isInf direct float32":
var n = 0
for i in 0 .. num:
if values32[i and 7].isInf:
n += 1
keep n

timeIt "isInf classify float32":
var n = 0
for i in 0 .. num:
if values32[i and 7].isInfClassify:
n += 1
keep n

timeIt "isInf direct float64":
var n = 0
for i in 0 .. num:
if values64[i and 7].isInf:
n += 1
keep n

timeIt "isInf classify float64":
var n = 0
for i in 0 .. num:
if values64[i and 7].isInfClassify:
n += 1
keep n

timeIt "isFinite direct float32":
var n = 0
for i in 0 .. num:
if values32[i and 7].isFinite:
n += 1
keep n

timeIt "isFinite classify float32":
var n = 0
for i in 0 .. num:
if values32[i and 7].isFiniteClassify:
n += 1
keep n

timeIt "isFinite direct float64":
var n = 0
for i in 0 .. num:
if values64[i and 7].isFinite:
n += 1
keep n

timeIt "isFinite classify float64":
var n = 0
for i in 0 .. num:
if values64[i and 7].isFiniteClassify:
n += 1
keep n
42 changes: 38 additions & 4 deletions tests/tests.nim
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,44 @@ suite "scalar utilities":
check angleBetween(0.2, 0.1) ~= -0.1

test "isNan":
check vmath.isNan(float32(0.3)) == false
check vmath.isNan(float32(0.0)) == false
check vmath.isNan(float32(0.3/0.0)) == true
check vmath.isNan(float64(0.3/0.0)) == true
check not vmath.isNan(0.3'f32)
check not vmath.isNan(0.0'f32)
check vmath.isNan(NaN.float32)
check vmath.isNan(NaN.float64)
check not vmath.isNan(Inf.float32)
check not vmath.isNan(NegInf.float64)

test "isInf":
check vmath.isInf(Inf.float32)
check vmath.isInf(NegInf.float64)
check not vmath.isInf(NaN.float32)
check not vmath.isInf(0.0'f64)

test "isFinite":
check vmath.isFinite(0.0'f32)
check vmath.isFinite(0.3'f64)
check vmath.isFinite(1.0e-45'f32) # Subnormal float32.
check vmath.isFinite(5.0e-324'f64) # Subnormal float64.
check not vmath.isFinite(NaN.float32)
check not vmath.isFinite(Inf.float64)
check not vmath.isFinite(NegInf.float32)

test "float classification matches std/math":
for x in [NaN.float32, Inf.float32, NegInf.float32, -0.0'f32,
0.0'f32, 1.0e-45'f32, -1.0e-45'f32, 1.0'f32, -1.0'f32]:
let standardClass = math.classify(x.float64)
check vmath.isNan(x) == math.isNaN(x)
check vmath.isInf(x) == (standardClass in {fcInf, fcNegInf})
check vmath.isFinite(x) ==
(standardClass notin {fcNan, fcInf, fcNegInf})

for x in [NaN.float64, Inf.float64, NegInf.float64, -0.0'f64,
0.0'f64, 5.0e-324'f64, -5.0e-324'f64, 1.0'f64, -1.0'f64]:
let standardClass = math.classify(x)
check vmath.isNan(x) == math.isNaN(x)
check vmath.isInf(x) == (standardClass in {fcInf, fcNegInf})
check vmath.isFinite(x) ==
(standardClass notin {fcNan, fcInf, fcNegInf})

suite "vector memory layout":
test "vec2 cast to array":
Expand Down
2 changes: 1 addition & 1 deletion vmath.nimble
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "3.0.0"
version = "3.1.0"
author = "Andre von Houck"
description = "Your single stop for vector math routines for 2d and 3d graphics."
license = "MIT"
Expand Down