diff --git a/src/Scalar.zig b/src/Scalar.zig index 21de0e1..f2c78a4 100644 --- a/src/Scalar.zig +++ b/src/Scalar.zig @@ -7,43 +7,6 @@ const UnitScale = Scales.UnitScale; const Dimensions = @import("Dimensions.zig"); const Dimension = Dimensions.Dimension; -// --------------------------------------------------------------------------- -// RHS normalisation helpers -// --------------------------------------------------------------------------- - -/// Returns true if `T` is a `Scalar_` type (has `dims`, `scales`, and `value`). -pub fn isScalarType(comptime T: type) bool { - return @typeInfo(T) == .@"struct" and - @hasDecl(T, "dims") and - @hasDecl(T, "scales") and - @hasField(T, "value"); -} - -/// Resolve the Scalar type that `rhs` will be treated as. -/// -/// Accepted rhs types: -/// - Any `Scalar_` type → returned as-is -/// - `comptime_int` / `comptime_float` → dimensionless `Scalar_(BaseT, {}, {})` -/// - `BaseT` (the scalar's value type) → dimensionless `Scalar_(BaseT, {}, {})` -/// -/// Everything else is a compile error, including other int/float types. -pub fn rhsScalarType(comptime BaseT: type, comptime RhsT: type) type { - if (comptime isScalarType(RhsT)) return RhsT; - if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == BaseT) - return Scalar(BaseT, .{}, .{}); - @compileError( - "rhs must be a Scalar, " ++ @typeName(BaseT) ++ - ", comptime_int, or comptime_float; got " ++ @typeName(RhsT), - ); -} - -/// Convert `rhs` to its normalised Scalar form (see `rhsScalarType`). -pub inline fn toRhsScalar(comptime BaseT: type, rhs: anytype) rhsScalarType(BaseT, @TypeOf(rhs)) { - if (comptime isScalarType(@TypeOf(rhs))) return rhs; - const DimLess = Scalar(BaseT, .{}, .{}); - return DimLess{ .value = @as(BaseT, rhs) }; -} - // --------------------------------------------------------------------------- /// A dimensioned scalar value. `T` is the numeric type, `d` the dimension exponents, `s` the SI scales. @@ -69,12 +32,12 @@ pub fn Scalar(comptime T: type, comptime d_opt: Dimensions.ArgOpts, comptime s_o /// Scalar type that `rhs` normalises to (bare numbers → dimensionless). inline fn RhsT(comptime Rhs: type) type { - return rhsScalarType(T, Rhs); + return hlp.rhsScalarType(T, Rhs); } /// Normalise `rhs` (bare number or Scalar) into a proper Scalar value. inline fn rhs(r: anytype) RhsT(@TypeOf(r)) { - return toRhsScalar(T, r); + return hlp.toRhsScalar(T, r); } // --------------------------------------------------------------- diff --git a/src/Vector.zig b/src/Vector.zig index 660a800..333b46f 100644 --- a/src/Vector.zig +++ b/src/Vector.zig @@ -29,6 +29,33 @@ pub fn Vector(comptime len: usize, comptime Q: type) type { return .{ .data = data }; } + // ------------------------------------------------------------------- + // Internal: scalar-rhs normalisation (mirrors Scalar.zig) + // ------------------------------------------------------------------- + + /// Resolved Scalar type for a scalar operand (bare number or Scalar). + /// Passing another Vector here is a compile error. + inline fn ScalarRhsT(comptime Rhs: type) type { + if (comptime switch (@typeInfo(Rhs)) { + .@"struct", .@"enum", .@"union", .@"opaque" => @hasDecl(Rhs, "ScalarType"), + else => false, + }) + @compileError( + "Expected a Scalar or bare number; got a Vector. " ++ + "Use mulBy / divBy for element-wise vector operations.", + ); + return hlp.rhsScalarType(T, Rhs); + } + + /// Normalise a scalar rhs (bare number → dimensionless Scalar). + inline fn scalarRhs(r: anytype) ScalarRhsT(@TypeOf(r)) { + return hlp.toRhsScalar(T, r); + } + + // ------------------------------------------------------------------- + // Vector–Vector operations (rhs must be a Vector of the same length) + // ------------------------------------------------------------------- + /// Element-wise addition. Dimensions must match; scales resolve to the finer of the two. pub inline fn add(self: Self, rhs: anytype) Vector(len, Scalar( T, @@ -110,48 +137,59 @@ pub fn Vector(comptime len: usize, comptime Q: type) type { return res; } - /// Divide every component by a single scalar. Dimensions are subtracted (e.g. position / time → velocity). + // ------------------------------------------------------------------- + // Vector–Scalar operations + // scalar may be: Scalar, T, comptime_int, comptime_float + // ------------------------------------------------------------------- + + /// Divide every component by a single scalar. Dimensions are subtracted. + /// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. pub inline fn divByScalar( self: Self, scalar: anytype, ) Vector(len, Scalar( T, - dims.sub(@TypeOf(scalar).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(scalar)).argsOpt(), + dims.sub(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(), + hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(), )) { + const s_norm = scalarRhs(scalar); + const SN = @TypeOf(s_norm); var res: Vector(len, Scalar( T, - dims.sub(@TypeOf(scalar).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(scalar)).argsOpt(), + dims.sub(SN.dims).argsOpt(), + hlp.finerScales(Self, SN).argsOpt(), )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.divBy(scalar).value; - } + inline for (self.data, 0..) |v, i| + res.data[i] = (Q{ .value = v }).divBy(s_norm).value; return res; } /// Multiply every component by a single scalar. Dimensions are summed. + /// `scalar` may be a Scalar, `T`, `comptime_int`, or `comptime_float`. pub inline fn mulByScalar( self: Self, scalar: anytype, ) Vector(len, Scalar( T, - dims.add(@TypeOf(scalar).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(scalar)).argsOpt(), + dims.add(ScalarRhsT(@TypeOf(scalar)).dims).argsOpt(), + hlp.finerScales(Self, ScalarRhsT(@TypeOf(scalar))).argsOpt(), )) { + const s_norm = scalarRhs(scalar); + const SN = @TypeOf(s_norm); var res: Vector(len, Scalar( T, - dims.add(@TypeOf(scalar).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(scalar)).argsOpt(), + dims.add(SN.dims).argsOpt(), + hlp.finerScales(Self, SN).argsOpt(), )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.mulBy(scalar).value; - } + inline for (self.data, 0..) |v, i| + res.data[i] = (Q{ .value = v }).mulBy(s_norm).value; return res; } + // ------------------------------------------------------------------- + // Dot / Cross + // ------------------------------------------------------------------- + /// Standard dot product. Dimensions are summed (e.g., Force * Distance = Energy). /// Returns a Scalar type with the combined dimensions and finest scale. pub inline fn dot(self: Self, rhs: anytype) Scalar( @@ -202,6 +240,10 @@ pub fn Vector(comptime len: usize, comptime Q: type) type { }; } + // ------------------------------------------------------------------- + // Unary + // ------------------------------------------------------------------- + /// Returns a vector where each component is the absolute value of the original. pub inline fn abs(self: Self) Self { var res: Self = undefined; @@ -254,6 +296,55 @@ pub fn Vector(comptime len: usize, comptime Q: type) type { return res; } + /// Negate all components. Dimensions are preserved. + pub fn negate(self: Self) Self { + var res: Self = undefined; + inline for (self.data, 0..) |v, i| + res.data[i] = -v; + return res; + } + + // ------------------------------------------------------------------- + // Conversion + // ------------------------------------------------------------------- + + /// Convert all components to a compatible scalar type. Compile error on dimension mismatch. + pub inline fn to(self: Self, comptime DestQ: type) Vector(len, DestQ) { + var res: Vector(len, DestQ) = undefined; + inline for (self.data, 0..) |v, i| + res.data[i] = (Q{ .value = v }).to(DestQ).value; + return res; + } + + // ------------------------------------------------------------------- + // Length + // ------------------------------------------------------------------- + + /// Sum of squared components. Cheaper than `length` — use for comparisons. + pub inline fn lengthSqr(self: Self) T { + var sum: T = 0; + inline for (self.data) |v| + sum += v * v; + return sum; + } + + /// Euclidean length. Integer types use integer sqrt (truncated). + pub inline fn length(self: Self) T { + const len_sq = self.lengthSqr(); + + if (comptime @typeInfo(T) == .int) { + const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); + const u_len_sq = @as(UnsignedT, @intCast(len_sq)); + return @as(T, @intCast(std.math.sqrt(u_len_sq))); + } else { + return @sqrt(len_sq); + } + } + + // ------------------------------------------------------------------- + // Vector–Vector comparisons + // ------------------------------------------------------------------- + /// Returns true only if all components are equal after scale resolution. pub inline fn eqAll(self: Self, rhs: anytype) bool { const Tr = @TypeOf(rhs); @@ -327,6 +418,11 @@ pub fn Vector(comptime len: usize, comptime Q: type) type { return res; } + // ------------------------------------------------------------------- + // Vector–Scalar comparisons + // scalar may be: Scalar, T, comptime_int, comptime_float + // ------------------------------------------------------------------- + /// Compares every element in the vector to a single scalar for equality. /// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved. pub inline fn eqScalar(self: Self, scalar: anytype) [len]bool { @@ -381,42 +477,9 @@ pub fn Vector(comptime len: usize, comptime Q: type) type { return res; } - /// Negate all components. Dimensions are preserved. - pub fn negate(self: Self) Self { - var res: Self = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = -v; - return res; - } - - /// Convert all components to a compatible scalar type. Compile error on dimension mismatch. - pub inline fn to(self: Self, comptime DestQ: type) Vector(len, DestQ) { - var res: Vector(len, DestQ) = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = (Q{ .value = v }).to(DestQ).value; - return res; - } - - /// Sum of squared components. Cheaper than `length` — use for comparisons. - pub inline fn lengthSqr(self: Self) T { - var sum: T = 0; - inline for (self.data) |v| - sum += v * v; - return sum; - } - - /// Euclidean length. Integer types use integer sqrt (truncated). - pub inline fn length(self: Self) T { - const len_sq = self.lengthSqr(); - - if (comptime @typeInfo(T) == .int) { - const UnsignedT = @Int(.unsigned, @typeInfo(T).int.bits); - const u_len_sq = @as(UnsignedT, @intCast(len_sq)); - return @as(T, @intCast(std.math.sqrt(u_len_sq))); - } else { - return @sqrt(len_sq); - } - } + // ------------------------------------------------------------------- + // Formatting + // ------------------------------------------------------------------- pub fn formatNumber( self: Self, @@ -688,3 +751,77 @@ test "Vector Abs, Pow, Sqrt and Product" { try std.testing.expectEqual(4, sqrted.data[2]); try std.testing.expectEqual(2, @TypeOf(sqrted).dims.get(.L)); } + +test "mulByScalar comptime_int" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const v = Meter.Vec3{ .data = .{ 1, 2, 3 } }; + + const scaled = v.mulByScalar(10); // comptime_int → dimensionless + try std.testing.expectEqual(10, scaled.data[0]); + try std.testing.expectEqual(20, scaled.data[1]); + try std.testing.expectEqual(30, scaled.data[2]); + // Dimensions unchanged: L¹ × dimensionless = L¹ + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); + try std.testing.expectEqual(0, @TypeOf(scaled).dims.get(.T)); +} + +test "mulByScalar comptime_float" { + const MeterF = Scalar(f32, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 1.0, 2.0, 4.0 } }; + + const scaled = v.mulByScalar(0.5); // comptime_float → dimensionless + try std.testing.expectApproxEqAbs(0.5, scaled.data[0], 1e-6); + try std.testing.expectApproxEqAbs(1.0, scaled.data[1], 1e-6); + try std.testing.expectApproxEqAbs(2.0, scaled.data[2], 1e-6); + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); +} + +test "mulByScalar T (value type)" { + const MeterF = Scalar(f32, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 3.0, 6.0, 9.0 } }; + const factor: f32 = 2.0; + + const scaled = v.mulByScalar(factor); + try std.testing.expectApproxEqAbs(6.0, scaled.data[0], 1e-6); + try std.testing.expectApproxEqAbs(12.0, scaled.data[1], 1e-6); + try std.testing.expectApproxEqAbs(18.0, scaled.data[2], 1e-6); + try std.testing.expectEqual(1, @TypeOf(scaled).dims.get(.L)); +} + +test "divByScalar comptime_int" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const v = Meter.Vec3{ .data = .{ 10, 20, 30 } }; + + const halved = v.divByScalar(2); // comptime_int → dimensionless divisor + try std.testing.expectEqual(5, halved.data[0]); + try std.testing.expectEqual(10, halved.data[1]); + try std.testing.expectEqual(15, halved.data[2]); + try std.testing.expectEqual(1, @TypeOf(halved).dims.get(.L)); +} + +test "divByScalar comptime_float" { + const MeterF = Scalar(f64, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 9.0, 6.0, 3.0 } }; + + const r = v.divByScalar(3.0); + try std.testing.expectApproxEqAbs(3.0, r.data[0], 1e-9); + try std.testing.expectApproxEqAbs(2.0, r.data[1], 1e-9); + try std.testing.expectApproxEqAbs(1.0, r.data[2], 1e-9); + try std.testing.expectEqual(1, @TypeOf(r).dims.get(.L)); +} + +test "eqScalar / gtScalar with comptime_int on dimensionless vector" { + // Bare numbers are dimensionless, so comparisons only work when vector is dimensionless too. + const DimLess = Scalar(i32, .{}, .{}); + const v = DimLess.Vec3{ .data = .{ 1, 2, 3 } }; + + const eq_res = v.eqScalar(2); + try std.testing.expectEqual(false, eq_res[0]); + try std.testing.expectEqual(true, eq_res[1]); + try std.testing.expectEqual(false, eq_res[2]); + + const gt_res = v.gtScalar(1); + try std.testing.expectEqual(false, gt_res[0]); + try std.testing.expectEqual(true, gt_res[1]); + try std.testing.expectEqual(true, gt_res[2]); +} diff --git a/src/helper.zig b/src/helper.zig index 71a0372..30c89fc 100644 --- a/src/helper.zig +++ b/src/helper.zig @@ -57,3 +57,42 @@ pub fn finerScales(comptime T1: type, comptime T2: type) Scales { } comptime return out; } + +// --------------------------------------------------------------------------- +// RHS normalisation helpers +// --------------------------------------------------------------------------- + +const Scalar = @import("Scalar.zig").Scalar; + +/// Returns true if `T` is a `Scalar_` type (has `dims`, `scales`, and `value`). +pub fn isScalarType(comptime T: type) bool { + return @typeInfo(T) == .@"struct" and + @hasDecl(T, "dims") and + @hasDecl(T, "scales") and + @hasField(T, "value"); +} + +/// Resolve the Scalar type that `rhs` will be treated as. +/// +/// Accepted rhs types: +/// - Any `Scalar_` type → returned as-is +/// - `comptime_int` / `comptime_float` → dimensionless `Scalar_(BaseT, {}, {})` +/// - `BaseT` (the scalar's value type) → dimensionless `Scalar_(BaseT, {}, {})` +/// +/// Everything else is a compile error, including other int/float types. +pub fn rhsScalarType(comptime BaseT: type, comptime RhsT: type) type { + if (comptime isScalarType(RhsT)) return RhsT; + if (comptime RhsT == comptime_int or RhsT == comptime_float or RhsT == BaseT) + return Scalar(BaseT, .{}, .{}); + @compileError( + "rhs must be a Scalar, " ++ @typeName(BaseT) ++ + ", comptime_int, or comptime_float; got " ++ @typeName(RhsT), + ); +} + +/// Convert `rhs` to its normalised Scalar form (see `rhsScalarType`). +pub inline fn toRhsScalar(comptime BaseT: type, rhs: anytype) rhsScalarType(BaseT, @TypeOf(rhs)) { + if (comptime isScalarType(@TypeOf(rhs))) return rhs; + const DimLess = Scalar(BaseT, .{}, .{}); + return DimLess{ .value = @as(BaseT, rhs) }; +}