diff --git a/src/Quantity.zig b/src/Quantity.zig index 7ff27f1..4e30ec9 100644 --- a/src/Quantity.zig +++ b/src/Quantity.zig @@ -1,6 +1,7 @@ const std = @import("std"); const hlp = @import("helper.zig"); const Scales = @import("Scales.zig"); +const UnitScale = Scales.UnitScale; const Dimensions = @import("Dimensions.zig"); const Dimension = Dimensions.Dimension; @@ -63,9 +64,6 @@ pub fn Quantity( return .{ .data = @splat(v) }; } - /// Backward-compat alias used by Vector tests (`initDefault`). - pub const initDefault = splat; - pub const zero: Self = splat(0); pub const one: Self = splat(1); @@ -185,7 +183,7 @@ pub fn Quantity( const rhs_q = rhs(r); const RhsType = @TypeOf(rhs_q); const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const RhsNorm = Quantity(T, N, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const RhsNorm = Quantity(T, RhsType.Len, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data; const rr_base = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm); const rr: Vec = broadcastToVec(RhsNorm, rr_base); @@ -203,7 +201,7 @@ pub fn Quantity( const rhs_q = rhs(r); const RhsType = @TypeOf(rhs_q); const SelfNorm = Quantity(T, N, dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); - const RhsNorm = Quantity(T, N, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); + const RhsNorm = Quantity(T, RhsType.Len, RhsType.dims.argsOpt(), hlp.finerScales(Self, RhsType).argsOpt()); const l: Vec = if (comptime Self == SelfNorm) self.data else self.to(SelfNorm).data; const rr_base = if (comptime RhsType == RhsNorm) rhs_q else rhs_q.to(RhsNorm); const rr: Vec = broadcastToVec(RhsNorm, rr_base); @@ -285,17 +283,27 @@ pub fn Quantity( // Conversion // --------------------------------------------------------------- - /// Convert to a compatible quantity type. Dimension mismatch is a compile error. + /// Convert to a compatible quantity type. Dimension mismatch is a compile error. + /// Dest can have the same Len as this quantity, or Len == 1 (in which case it + /// will be automatically recast to this quantity's Len). /// The scale ratio is computed entirely at comptime; the only runtime cost is /// a SIMD multiply-by-splat (or element-wise cast for cross-numeric-type conversions). - pub inline fn to(self: Self, comptime Dest: type) Dest { - if (comptime !dims.eql(Dest.dims)) - @compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ Dest.dims.str()); - if (comptime Self == Dest) return self; - comptime std.debug.assert(Dest.Len == N); + pub inline fn to( + self: Self, + comptime Dest: type, + ) Quantity(Dest.ValueType, N, Dest.dims.argsOpt(), Dest.scales.argsOpt()) { + const ActualDest = Quantity(Dest.ValueType, N, Dest.dims.argsOpt(), Dest.scales.argsOpt()); - const DestT = Dest.ValueType; - const ratio = comptime (scales.getFactor(dims) / Dest.scales.getFactor(Dest.dims)); + if (comptime !dims.eql(ActualDest.dims)) + @compileError("Dimension mismatch in to: " ++ dims.str() ++ " vs " ++ ActualDest.dims.str()); + + if (comptime Self == ActualDest) return self; + + // Allow Dest to be exactly matching Len or a Scalar (Len == 1) + comptime std.debug.assert(Dest.Len == N or Dest.Len == 1); + + const DestT = ActualDest.ValueType; + const ratio = comptime (scales.getFactor(dims) / ActualDest.scales.getFactor(ActualDest.dims)); const DestVec = @Vector(N, DestT); // ── Same numeric type path ── @@ -947,17 +955,305 @@ test "Vector initiate" { try std.testing.expect(m.data[0] == 1); } -// test "Format VectorX" { -// const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); -// const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }); -// -// const accel = MeterPerSecondSq.Vec3.initDefault(9.81); -// const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } }; -// -// var buf: [64]u8 = undefined; -// var res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); -// try std.testing.expectEqualStrings("(9.81, 9.81, 9.81)m.ns⁻²", res); -// -// res = try std.fmt.bufPrint(&buf, "{d:.2}", .{momentum}); -// try std.testing.expectEqualStrings("(43.00, 0.00, 11.00)m.kg.s⁻¹", res); -// } +test "Vector format" { + const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); + const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }); + + const accel = MeterPerSecondSq.Vec3.splat(9.81); + const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } }; + + var buf: [64]u8 = undefined; + var res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); + try std.testing.expectEqualStrings("(9.81, 9.81, 9.81)m.ns⁻²", res); + + res = try std.fmt.bufPrint(&buf, "{d:.2}", .{momentum}); + try std.testing.expectEqualStrings("(43.00, 0.00, 11.00)m.kg.s⁻¹", res); +} + +test "Vector Vec3 Init and Basic Arithmetic" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const Vec3M = Meter.Vec3; + + // Test zero, one, initDefault + const v_zero = Vec3M.zero; + try std.testing.expectEqual(0, v_zero.data[0]); + try std.testing.expectEqual(0, v_zero.data[1]); + try std.testing.expectEqual(0, v_zero.data[2]); + + const v_one = Vec3M.one; + try std.testing.expectEqual(1, v_one.data[0]); + try std.testing.expectEqual(1, v_one.data[1]); + try std.testing.expectEqual(1, v_one.data[2]); + + const v_def = Vec3M.splat(5); + try std.testing.expectEqual(5, v_def.data[0]); + try std.testing.expectEqual(5, v_def.data[1]); + try std.testing.expectEqual(5, v_def.data[2]); + + // Test add and sub + const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; + const v2 = Vec3M{ .data = .{ 2, 4, 6 } }; + + const added = v1.add(v2); + try std.testing.expectEqual(12, added.data[0]); + try std.testing.expectEqual(24, added.data[1]); + try std.testing.expectEqual(36, added.data[2]); + + const subbed = v1.sub(v2); + try std.testing.expectEqual(8, subbed.data[0]); + try std.testing.expectEqual(16, subbed.data[1]); + try std.testing.expectEqual(24, subbed.data[2]); + + // Test negate + const neg = v1.negate(); + try std.testing.expectEqual(-10, neg.data[0]); + try std.testing.expectEqual(-20, neg.data[1]); + try std.testing.expectEqual(-30, neg.data[2]); +} + +test "Vector Kinematics (Scalar Mul/Div)" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const Second = Scalar(i32, .{ .T = 1 }, .{}); + const Vec3M = Meter.Vec3; + + const pos = Vec3M{ .data = .{ 100, 200, 300 } }; + const time = Second.splat(10); + + // Vector divided by scalar (Velocity = Position / Time) + const vel = pos.divScalar(time); + try std.testing.expectEqual(10, vel.data[0]); + try std.testing.expectEqual(20, vel.data[1]); + try std.testing.expectEqual(30, vel.data[2]); + try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L)); + try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T)); + + // Vector multiplied by scalar (Position = Velocity * Time) + const new_pos = vel.mulScalar(time); + try std.testing.expectEqual(100, new_pos.data[0]); + try std.testing.expectEqual(200, new_pos.data[1]); + try std.testing.expectEqual(300, new_pos.data[2]); + try std.testing.expectEqual(1, @TypeOf(new_pos).dims.get(.L)); + try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T)); +} + +test "Vector Element-wise Math and Scaling" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const Vec3M = Meter.Vec3; + + const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; + const v2 = Vec3M{ .data = .{ 2, 5, 10 } }; + + // Element-wise division + const div = v1.div(v2); + try std.testing.expectEqual(5, div.data[0]); + try std.testing.expectEqual(4, div.data[1]); + try std.testing.expectEqual(3, div.data[2]); + try std.testing.expectEqual(0, @TypeOf(div).dims.get(.L)); // M / M = Dimensionless +} + +test "Vector Conversions" { + const KiloMeter = Scalar(i32, .{ .L = 1 }, .{ .L = .k }); + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + + const v_km = KiloMeter.Vec3{ .data = .{ 1, 2, 3 } }; + const v_m = v_km.to(Meter); + + try std.testing.expectEqual(1000, v_m.data[0]); + try std.testing.expectEqual(2000, v_m.data[1]); + try std.testing.expectEqual(3000, v_m.data[2]); + + // Type checking the result + try std.testing.expectEqual(1, @TypeOf(v_m).dims.get(.L)); + try std.testing.expectEqual(UnitScale.none, @TypeOf(v_m).scales.get(.L)); +} + +test "Vector Length" { + const MeterInt = Scalar(i32, .{ .L = 1 }, .{}); + const MeterFloat = Scalar(f32, .{ .L = 1 }, .{}); + + // Integer length + // 3-4-5 triangle on XY plane + const v_int = MeterInt.Vec3{ .data = .{ 3, 4, 0 } }; + try std.testing.expectEqual(25, v_int.lengthSqr()); + try std.testing.expectEqual(5, v_int.length()); + + // Float length + const v_float = MeterFloat.Vec3{ .data = .{ 3.0, 4.0, 0.0 } }; + try std.testing.expectApproxEqAbs(@as(f32, 25.0), v_float.lengthSqr(), 1e-4); + try std.testing.expectApproxEqAbs(@as(f32, 5.0), v_float.length(), 1e-4); +} + +test "Vector Comparisons" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); + + const v1 = Meter.Vec3{ .data = .{ 1000.0, 500.0, 0.0 } }; + const v2 = KiloMeter.Vec3{ .data = .{ 1.0, 0.5, 0.0 } }; + const v3 = KiloMeter.Vec3{ .data = .{ 1.0, 0.6, 0.0 } }; + + // 1. Equality (Whole vector) + try std.testing.expect(v1.eqAll(v2)); + try std.testing.expect(v1.neAll(v3)); + + // 2. Element-wise Ordered Comparison + const higher = v3.gt(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m + try std.testing.expectEqual(false, higher[0]); // 1km == 1000m + try std.testing.expectEqual(true, higher[1]); // 0.6km > 500m + try std.testing.expectEqual(false, higher[2]); // 0 == 0 + + // 3. Element-wise Equal Comparison + const equal = v3.eq(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m + try std.testing.expectEqual(true, equal[0]); // 1km == 1000m + try std.testing.expectEqual(false, equal[1]); // 0.6km > 500m + try std.testing.expectEqual(true, equal[2]); // 0 == 0 + + // 3. Less than or equal + const low_eq = v1.lte(v3); + try std.testing.expect(low_eq[0] and low_eq[1] and low_eq[2]); +} + +test "Vector vs Scalar Comparisons" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); + + const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } }; + const threshold = KiloMeter.splat(1); // 1km (1000m) + + // Check which axes exceed the 1km threshold + const exceeded = positions.gtScalar(threshold); + + try std.testing.expectEqual(false, exceeded[0]); // 500m > 1km is false + try std.testing.expectEqual(true, exceeded[1]); // 1200m > 1km is true + try std.testing.expectEqual(true, exceeded[2]); // 3000m > 1km is true + + // Check for equality (broadcasted) + const exact_match = positions.eqScalar(Meter.splat(500)); + try std.testing.expect(exact_match[0] == true); + try std.testing.expect(exact_match[1] == false); +} + +test "Vector Dot and Cross Products" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{}); + + const pos = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } }; + const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } }; + + // 1. Dot Product (Work = F dot d) + const work = force.dot(pos); + try std.testing.expectEqual(50.0, work.value()); + // Dimensions should be M¹L²T⁻² (Energy/Joules) + try std.testing.expectEqual(1, @TypeOf(work).dims.get(.M)); + try std.testing.expectEqual(2, @TypeOf(work).dims.get(.L)); + try std.testing.expectEqual(-2, @TypeOf(work).dims.get(.T)); + + // 2. Cross Product (Torque = r cross F) + const torque = pos.cross(force); + try std.testing.expectEqual(0.0, torque.data[0]); + try std.testing.expectEqual(0.0, torque.data[1]); + try std.testing.expectEqual(50.0, torque.data[2]); + // Torque dimensions are same as Energy but as a Vector + try std.testing.expectEqual(2, @TypeOf(torque).dims.get(.L)); +} + +test "Vector Abs, Pow, Sqrt and Product" { + const Meter = Scalar(f32, .{ .L = 1 }, .{}); + + const v1 = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } }; + + // 1. Abs + const v_abs = v1.abs(); + try std.testing.expectEqual(2.0, v_abs.data[0]); + try std.testing.expectEqual(4.0, v_abs.data[2]); + + // 2. Product (L1 * L1 * L1 = L3) + const vol = v_abs.product(); + try std.testing.expectEqual(24.0, vol.value()); + try std.testing.expectEqual(3, @TypeOf(vol).dims.get(.L)); + + // 3. Pow (Scalar exponent: (L1)^2 = L2) + const area_vec = v_abs.pow(2); + try std.testing.expectEqual(4.0, area_vec.data[0]); + try std.testing.expectEqual(16.0, area_vec.data[2]); + try std.testing.expectEqual(2, @TypeOf(area_vec).dims.get(.L)); + + // 4. Sqrt + const sqrted = area_vec.sqrt(); + try std.testing.expectEqual(2, sqrted.data[0]); + try std.testing.expectEqual(4, sqrted.data[2]); + try std.testing.expectEqual(1, @TypeOf(sqrted).dims.get(.L)); +} + +test "Vector mulScalar comptime_int" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const v = Meter.Vec3{ .data = .{ 1, 2, 3 } }; + + const scaled = v.mulScalar(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 "Vector mulScalar comptime_float" { + const MeterF = Scalar(f32, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 1.0, 2.0, 4.0 } }; + + const scaled = v.mulScalar(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 "Vector mulScalar 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.mulScalar(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 "Vector divScalar comptime_int" { + const Meter = Scalar(i32, .{ .L = 1 }, .{}); + const v = Meter.Vec3{ .data = .{ 10, 20, 30 } }; + + const halved = v.divScalar(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 "Vector divScalar comptime_float" { + const MeterF = Scalar(f64, .{ .L = 1 }, .{}); + const v = MeterF.Vec3{ .data = .{ 9.0, 6.0, 3.0 } }; + + const r = v.divScalar(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 "Vector 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/Vector.zig b/src/Vector.zig deleted file mode 100644 index fa726c4..0000000 --- a/src/Vector.zig +++ /dev/null @@ -1,831 +0,0 @@ -const std = @import("std"); -const hlp = @import("helper.zig"); - -const Scalar = @import("Quantity.zig").Scalar; -const Scales = @import("Scales.zig"); -const UnitScale = Scales.UnitScale; -const Dimensions = @import("Dimensions.zig"); -const Dimension = Dimensions.Dimension; - -/// A fixed-size array of `len` elements sharing the same dimension and scale as scalar type `Q`. -pub fn Vector(comptime len: usize, comptime Q: type) type { - const T = Q.ValueType; - - return struct { - data: [len]T, - - const Self = @This(); - pub const ScalarType = Q; - pub const ValueType = T; - pub const dims: Dimensions = Q.dims; - pub const scales = Q.scales; - - pub const zero = initDefault(0); - pub const one = initDefault(1); - - pub fn initDefault(v: T) Self { - var data: [len]T = undefined; - inline for (&data) |*item| item.* = v; - 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 mul / div 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, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).add(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - - /// Element-wise subtraction. Dimensions must match; scales resolve to the finer of the two. - pub inline fn sub(self: Self, rhs: anytype) Vector(len, Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).sub(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - - /// Element-wise division. Dimension exponents are subtracted per component. - pub inline fn div( - self: Self, - rhs: anytype, - ) Vector(len, Scalar( - T, - dims.sub(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.sub(Tr.dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).div(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - - /// Element-wise multiplication. Dimension exponents are summed per component. - pub inline fn mul( - self: Self, - rhs: anytype, - ) Vector(len, Scalar( - T, - dims.add(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - const Tr = @TypeOf(rhs); - var res: Vector(len, Scalar( - T, - dims.add(Tr.dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| { - const q = (Q{ .value = v }).mul(Tr.ScalarType{ .value = rhs.data[i] }); - res.data[i] = q.value; - } - return res; - } - - // ------------------------------------------------------------------- - // 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 divScalar( - self: Self, - scalar: anytype, - ) Vector(len, Scalar( - T, - 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(SN.dims).argsOpt(), - hlp.finerScales(Self, SN).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = (Q{ .value = v }).div(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 mulScalar( - self: Self, - scalar: anytype, - ) Vector(len, Scalar( - T, - 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(SN.dims).argsOpt(), - hlp.finerScales(Self, SN).argsOpt(), - )) = undefined; - inline for (self.data, 0..) |v, i| - res.data[i] = (Q{ .value = v }).mul(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( - T, - dims.add(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - ) { - const Tr = @TypeOf(rhs); - - var sum: T = 0; - inline for (self.data, 0..) |v, i| { - const q_lhs = Q{ .value = v }; - const q_rhs = Tr.ScalarType{ .value = rhs.data[i] }; - sum += q_lhs.mul(q_rhs).value; - } - return .{ .value = sum }; - } - - /// 3D Cross product. Dimensions are summed. - /// Only valid for vectors of length 3. - pub inline fn cross(self: Self, rhs: anytype) Vector(3, Scalar( - T, - dims.add(@TypeOf(rhs).dims).argsOpt(), - hlp.finerScales(Self, @TypeOf(rhs)).argsOpt(), - )) { - if (comptime len != 3) - @compileError("Cross product is only defined for Vector(3, ...)"); - - const Tr = @TypeOf(rhs); - const ResScalar = Scalar(T, dims.add(Tr.dims).argsOpt(), hlp.finerScales(Self, Tr).argsOpt()); - const ResVec = Vector(3, ResScalar); - - // Calculation: [y1*z2 - z1*y2, z1*x2 - x1*z2, x1*y2 - y1*x2] - const s1 = Q{ .value = self.data[0] }; - const s2 = Q{ .value = self.data[1] }; - const s3 = Q{ .value = self.data[2] }; - - const o1 = Tr.ScalarType{ .value = rhs.data[0] }; - const o2 = Tr.ScalarType{ .value = rhs.data[1] }; - const o3 = Tr.ScalarType{ .value = rhs.data[2] }; - - return ResVec{ - .data = .{ - s2.mul(o3).sub(s3.mul(o2)).value, - s3.mul(o1).sub(s1.mul(o3)).value, - s1.mul(o2).sub(s2.mul(o1)).value, - }, - }; - } - - // ------------------------------------------------------------------- - // 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; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.abs().value; - } - return res; - } - - /// Returns a vector where each component is the absolute value of the original. - pub inline fn sqrt(self: Self) Self { - var res: Self = undefined; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.sqrt().value; - } - return res; - } - - /// Multiplies all components of the vector together. - /// Resulting dimensions are (Original Dims * len). - pub inline fn product(self: Self) Scalar( - T, - dims.scale(len).argsOpt(), - scales.argsOpt(), - ) { - var res_val: T = 1; - if (comptime hlp.isInt(T)) { - inline for (self.data) |v| - res_val = res_val *| v; - } else inline for (self.data) |v| - res_val *= v; - return .{ .value = res_val }; - } - - /// Raises every component to a compile-time integer power. - /// Dimensions are scaled by the exponent. - pub inline fn pow(self: Self, comptime exp: comptime_int) Vector( - len, - Scalar( - T, - dims.scale(exp).argsOpt(), - scales.argsOpt(), - ), - ) { - const ResScalar = Scalar(T, dims.scale(exp).argsOpt(), scales.argsOpt()); - var res: Vector(len, ResScalar) = undefined; - inline for (self.data, 0..) |v, i| { - const q = Q{ .value = v }; - res.data[i] = q.pow(exp).value; - } - 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); - if (comptime !dims.eql(Tr.dims)) - @compileError("Dimension mismatch in eq: " ++ dims.str() ++ " vs " ++ Tr.dims.str()); - - inline for (self.data, 0..) |v, i| { - const lhs_q = Q{ .value = v }; - const rhs_q = Tr.ScalarType{ .value = rhs.data[i] }; - if (!lhs_q.eq(rhs_q)) return false; - } - return true; - } - - /// Returns true if any component differs after scale resolution. - pub inline fn neAll(self: Self, rhs: anytype) bool { - return !self.eqAll(rhs); - } - - /// Element-wise "Equal". Returns an array of booleans. - pub inline fn eq(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).eq(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Not Equal". Returns an array of booleans. - pub inline fn ne(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).ne(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Greater Than". Returns an array of booleans. - pub inline fn gt(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gt(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Greater Than or Equal". Returns an array of booleans. - pub inline fn gte(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gte(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Less Than". Returns an array of booleans. - pub inline fn lt(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lt(Tr.ScalarType{ .value = rhs.data[i] }); - return res; - } - - /// Element-wise "Less Than or Equal". Returns an array of booleans. - pub inline fn lte(self: Self, rhs: anytype) [len]bool { - const Tr = @TypeOf(rhs); - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lte(Tr.ScalarType{ .value = rhs.data[i] }); - 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 { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).eq(scalar); - return res; - } - - /// Compares every element in the vector to a single scalar for inequality. - /// Returns an array of booleans [len]bool. Dimensions must match; scales are auto-resolved. - pub inline fn neScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).ne(scalar); - return res; - } - - /// Checks if each element in the vector is strictly greater than the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn gtScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gt(scalar); - return res; - } - - /// Checks if each element in the vector is greater than or equal to the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn gteScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).gte(scalar); - return res; - } - - /// Checks if each element in the vector is strictly less than the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn ltScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lt(scalar); - return res; - } - - /// Checks if each element in the vector is less than or equal to the given scalar. - /// Returns an array of booleans [len]bool. - pub inline fn lteScalar(self: Self, scalar: anytype) [len]bool { - var res: [len]bool = undefined; - inline for (self.data, 0..) |v, i| - res[i] = (Q{ .value = v }).lte(scalar); - return res; - } - - // ------------------------------------------------------------------- - // Formatting - // ------------------------------------------------------------------- - - pub fn formatNumber( - self: Self, - writer: *std.Io.Writer, - options: std.fmt.Number, - ) !void { - try writer.writeAll("("); - for (self.data, 0..) |v, i| { - if (i > 0) try writer.writeAll(", "); - switch (@typeInfo(T)) { - .float, .comptime_float => try writer.printFloat(v, options), - .int, .comptime_int => try writer.printInt(v, 10, .lower, .{ - .width = options.width, - .alignment = options.alignment, - .fill = options.fill, - .precision = options.precision, - }), - else => unreachable, - } - } - try writer.writeAll(")"); - var first = true; - inline for (std.enums.values(Dimension)) |bu| { - const v = dims.get(bu); - if (comptime v == 0) continue; - if (!first) - try writer.writeAll("."); - - first = false; - - const uscale = scales.get(bu); - if (bu == .T and (uscale == .min or uscale == .hour or uscale == .year)) - try writer.print("{s}", .{uscale.str()}) - else - try writer.print("{s}{s}", .{ uscale.str(), bu.unit() }); - - if (v != 1) - try hlp.printSuperscript(writer, v); - } - } - }; -} - -test "Format VectorX" { - const MeterPerSecondSq = Scalar(f32, .{ .L = 1, .T = -2 }, .{ .T = .n }); - const KgMeterPerSecond = Scalar(f32, .{ .M = 1, .L = 1, .T = -1 }, .{ .M = .k }); - - const accel = MeterPerSecondSq.Vec3.initDefault(9.81); - const momentum = KgMeterPerSecond.Vec3{ .data = .{ 43, 0, 11 } }; - - var buf: [64]u8 = undefined; - var res = try std.fmt.bufPrint(&buf, "{d}", .{accel}); - try std.testing.expectEqualStrings("(9.81, 9.81, 9.81)m.ns⁻²", res); - - res = try std.fmt.bufPrint(&buf, "{d:.2}", .{momentum}); - try std.testing.expectEqualStrings("(43.00, 0.00, 11.00)m.kg.s⁻¹", res); -} - -test "VecX Init and Basic Arithmetic" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const Vec3M = Meter.Vec3; - - // Test zero, one, initDefault - const v_zero = Vec3M.zero; - try std.testing.expectEqual(0, v_zero.data[0]); - try std.testing.expectEqual(0, v_zero.data[1]); - try std.testing.expectEqual(0, v_zero.data[2]); - - const v_one = Vec3M.one; - try std.testing.expectEqual(1, v_one.data[0]); - try std.testing.expectEqual(1, v_one.data[1]); - try std.testing.expectEqual(1, v_one.data[2]); - - const v_def = Vec3M.initDefault(5); - try std.testing.expectEqual(5, v_def.data[0]); - try std.testing.expectEqual(5, v_def.data[1]); - try std.testing.expectEqual(5, v_def.data[2]); - - // Test add and sub - const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; - const v2 = Vec3M{ .data = .{ 2, 4, 6 } }; - - const added = v1.add(v2); - try std.testing.expectEqual(12, added.data[0]); - try std.testing.expectEqual(24, added.data[1]); - try std.testing.expectEqual(36, added.data[2]); - - const subbed = v1.sub(v2); - try std.testing.expectEqual(8, subbed.data[0]); - try std.testing.expectEqual(16, subbed.data[1]); - try std.testing.expectEqual(24, subbed.data[2]); - - // Test negate - const neg = v1.negate(); - try std.testing.expectEqual(-10, neg.data[0]); - try std.testing.expectEqual(-20, neg.data[1]); - try std.testing.expectEqual(-30, neg.data[2]); -} - -test "VecX Kinematics (Scalar Mul/Div)" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const Second = Scalar(i32, .{ .T = 1 }, .{}); - const Vec3M = Meter.Vec3; - - const pos = Vec3M{ .data = .{ 100, 200, 300 } }; - const time = Second{ .value = 10 }; - - // Vector divided by scalar (Velocity = Position / Time) - const vel = pos.divScalar(time); - try std.testing.expectEqual(10, vel.data[0]); - try std.testing.expectEqual(20, vel.data[1]); - try std.testing.expectEqual(30, vel.data[2]); - try std.testing.expectEqual(1, @TypeOf(vel).dims.get(.L)); - try std.testing.expectEqual(-1, @TypeOf(vel).dims.get(.T)); - - // Vector multiplied by scalar (Position = Velocity * Time) - const new_pos = vel.mulScalar(time); - try std.testing.expectEqual(100, new_pos.data[0]); - try std.testing.expectEqual(200, new_pos.data[1]); - try std.testing.expectEqual(300, new_pos.data[2]); - try std.testing.expectEqual(1, @TypeOf(new_pos).dims.get(.L)); - try std.testing.expectEqual(0, @TypeOf(new_pos).dims.get(.T)); -} - -test "VecX Element-wise Math and Scaling" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const Vec3M = Meter.Vec3; - - const v1 = Vec3M{ .data = .{ 10, 20, 30 } }; - const v2 = Vec3M{ .data = .{ 2, 5, 10 } }; - - // Element-wise division - const div = v1.div(v2); - try std.testing.expectEqual(5, div.data[0]); - try std.testing.expectEqual(4, div.data[1]); - try std.testing.expectEqual(3, div.data[2]); - try std.testing.expectEqual(0, @TypeOf(div).dims.get(.L)); // M / M = Dimensionless -} - -test "VecX Conversions" { - const KiloMeter = Scalar(i32, .{ .L = 1 }, .{ .L = .k }); - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - - const v_km = KiloMeter.Vec3{ .data = .{ 1, 2, 3 } }; - const v_m = v_km.to(Meter); - - try std.testing.expectEqual(1000, v_m.data[0]); - try std.testing.expectEqual(2000, v_m.data[1]); - try std.testing.expectEqual(3000, v_m.data[2]); - - // Type checking the result - try std.testing.expectEqual(1, @TypeOf(v_m).dims.get(.L)); - try std.testing.expectEqual(UnitScale.none, @TypeOf(v_m).scales.get(.L)); -} - -test "VecX Length" { - const MeterInt = Scalar(i32, .{ .L = 1 }, .{}); - const MeterFloat = Scalar(f32, .{ .L = 1 }, .{}); - - // Integer length - // 3-4-5 triangle on XY plane - const v_int = MeterInt.Vec3{ .data = .{ 3, 4, 0 } }; - try std.testing.expectEqual(25, v_int.lengthSqr()); - try std.testing.expectEqual(5, v_int.length()); - - // Float length - const v_float = MeterFloat.Vec3{ .data = .{ 3.0, 4.0, 0.0 } }; - try std.testing.expectApproxEqAbs(@as(f32, 25.0), v_float.lengthSqr(), 1e-4); - try std.testing.expectApproxEqAbs(@as(f32, 5.0), v_float.length(), 1e-4); -} - -test "Vector Comparisons" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); - - const v1 = Meter.Vec3{ .data = .{ 1000.0, 500.0, 0.0 } }; - const v2 = KiloMeter.Vec3{ .data = .{ 1.0, 0.5, 0.0 } }; - const v3 = KiloMeter.Vec3{ .data = .{ 1.0, 0.6, 0.0 } }; - - // 1. Equality (Whole vector) - try std.testing.expect(v1.eqAll(v2)); - try std.testing.expect(v1.neAll(v3)); - - // 2. Element-wise Ordered Comparison - const higher = v3.gt(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m - try std.testing.expectEqual(false, higher[0]); // 1km == 1000m - try std.testing.expectEqual(true, higher[1]); // 0.6km > 500m - try std.testing.expectEqual(false, higher[2]); // 0 == 0 - - // 3. Element-wise Equal Comparison - const equal = v3.eq(v1); // compares 1km, 0.6km, 0km vs 1000m, 500m, 0m - try std.testing.expectEqual(true, equal[0]); // 1km == 1000m - try std.testing.expectEqual(false, equal[1]); // 0.6km > 500m - try std.testing.expectEqual(true, equal[2]); // 0 == 0 - - // 3. Less than or equal - const low_eq = v1.lte(v3); - try std.testing.expect(low_eq[0] and low_eq[1] and low_eq[2]); -} - -test "Vector vs Scalar Comparisons" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - const KiloMeter = Scalar(f32, .{ .L = 1 }, .{ .L = .k }); - - const positions = Meter.Vec3{ .data = .{ 500.0, 1200.0, 3000.0 } }; - const threshold = KiloMeter{ .value = 1.0 }; // 1km (1000m) - - // Check which axes exceed the 1km threshold - const exceeded = positions.gtScalar(threshold); - - try std.testing.expectEqual(false, exceeded[0]); // 500m > 1km is false - try std.testing.expectEqual(true, exceeded[1]); // 1200m > 1km is true - try std.testing.expectEqual(true, exceeded[2]); // 3000m > 1km is true - - // Check for equality (broadcasted) - const exact_match = positions.eqScalar(Meter{ .value = 500.0 }); - try std.testing.expect(exact_match[0] == true); - try std.testing.expect(exact_match[1] == false); -} - -test "Vector Dot and Cross Products" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - const Newton = Scalar(f32, .{ .M = 1, .L = 1, .T = -2 }, .{}); - - const pos = Meter.Vec3{ .data = .{ 10.0, 0.0, 0.0 } }; - const force = Newton.Vec3{ .data = .{ 5.0, 5.0, 0.0 } }; - - // 1. Dot Product (Work = F dot d) - const work = force.dot(pos); - try std.testing.expectEqual(50.0, work.value); - // Dimensions should be M¹L²T⁻² (Energy/Joules) - try std.testing.expectEqual(1, @TypeOf(work).dims.get(.M)); - try std.testing.expectEqual(2, @TypeOf(work).dims.get(.L)); - try std.testing.expectEqual(-2, @TypeOf(work).dims.get(.T)); - - // 2. Cross Product (Torque = r cross F) - const torque = pos.cross(force); - try std.testing.expectEqual(0.0, torque.data[0]); - try std.testing.expectEqual(0.0, torque.data[1]); - try std.testing.expectEqual(50.0, torque.data[2]); - // Torque dimensions are same as Energy but as a Vector - try std.testing.expectEqual(2, @TypeOf(torque).dims.get(.L)); -} - -test "Vector Abs, Pow, Sqrt and Product" { - const Meter = Scalar(f32, .{ .L = 1 }, .{}); - - const v1 = Meter.Vec3{ .data = .{ -2.0, 3.0, -4.0 } }; - - // 1. Abs - const v_abs = v1.abs(); - try std.testing.expectEqual(2.0, v_abs.data[0]); - try std.testing.expectEqual(4.0, v_abs.data[2]); - - // 2. Product (L1 * L1 * L1 = L3) - const vol = v_abs.product(); - try std.testing.expectEqual(24.0, vol.value); - try std.testing.expectEqual(3, @TypeOf(vol).dims.get(.L)); - - // 3. Pow (Scalar exponent: (L1)^2 = L2) - const area_vec = v_abs.pow(2); - try std.testing.expectEqual(4.0, area_vec.data[0]); - try std.testing.expectEqual(16.0, area_vec.data[2]); - try std.testing.expectEqual(2, @TypeOf(area_vec).dims.get(.L)); - - // 4. Sqrt - const sqrted = area_vec.sqrt(); - try std.testing.expectEqual(2, sqrted.data[0]); - try std.testing.expectEqual(4, sqrted.data[2]); - try std.testing.expectEqual(2, @TypeOf(sqrted).dims.get(.L)); -} - -test "mulScalar comptime_int" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const v = Meter.Vec3{ .data = .{ 1, 2, 3 } }; - - const scaled = v.mulScalar(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 "mulScalar comptime_float" { - const MeterF = Scalar(f32, .{ .L = 1 }, .{}); - const v = MeterF.Vec3{ .data = .{ 1.0, 2.0, 4.0 } }; - - const scaled = v.mulScalar(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 "mulScalar 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.mulScalar(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 "divScalar comptime_int" { - const Meter = Scalar(i32, .{ .L = 1 }, .{}); - const v = Meter.Vec3{ .data = .{ 10, 20, 30 } }; - - const halved = v.divScalar(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 "divScalar comptime_float" { - const MeterF = Scalar(f64, .{ .L = 1 }, .{}); - const v = MeterF.Vec3{ .data = .{ 9.0, 6.0, 3.0 } }; - - const r = v.divScalar(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/main.zig b/src/main.zig index f56c009..6867c7f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,7 @@ const std = @import("std"); -pub const Vector = @import("Vector.zig").Vector; +pub const Vector = @import("Quantity.zig").Vector; +pub const Scalar = @import("Quantity.zig").Scalar; pub const Dimensions = @import("Dimensions.zig"); pub const Scales = @import("Scales.zig"); pub const Base = @import("Base.zig");