From a32eb160438ca564c7224f19578fb0ada28a316f Mon Sep 17 00:00:00 2001 From: Gabe DiFiore Date: Thu, 20 Aug 2026 19:00:26 -0400 Subject: [PATCH 1/2] Fix reviewed physics, API, and build issues --- CMakeLists.txt | 14 +++++- README.md | 3 ++ build.sh | 11 +++-- docs/aerodynamic_model.md | 4 +- docs/how.md | 6 ++- docs/roll_model.md | 6 ++- docs/terrain.md | 5 ++- docs/wasm.md | 4 +- include/DefaultRollModel.hpp | 76 ++++++++++++++++++++++++-------- include/launch_data.hpp | 4 +- include/terrain_interface.hpp | 7 ++- src/FlightSimulator.cpp | 17 ++++++- test/test_default_roll_model.cpp | 44 ++++++++++++++++-- test/test_flight_simulator.cpp | 6 ++- test/test_roll_phase.cpp | 3 ++ wasm/bindings.cpp | 16 ++++--- 16 files changed, 174 insertions(+), 52 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index afefd44..d93cda7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,12 @@ project(golf VERSION 4.6.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Keep the library consumable from offline or vendored builds. Tests fetch +# GoogleTest, so they are opt-in rather than a configure-time dependency for +# every library consumer. +option(BUILD_TESTING "Build the libgolf test suite" OFF) +option(GOLF_BUILD_EXAMPLES "Build libgolf examples and calibration runner" ON) + # Version information set(GOLF_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) set(GOLF_VERSION_MINOR ${PROJECT_VERSION_MINOR}) @@ -83,7 +89,7 @@ if(ENABLE_COVERAGE AND NOT MSVC) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") endif() -if(NOT EMSCRIPTEN) +if(BUILD_TESTING AND NOT EMSCRIPTEN) # Include Google Test include(FetchContent) FetchContent_Declare( @@ -121,6 +127,9 @@ if(NOT EMSCRIPTEN) add_executable(libgolf_tests ${TEST_SOURCES}) target_link_libraries(libgolf_tests ${PROJECT_NAME} GTest::gtest_main) +endif() + +if(GOLF_BUILD_EXAMPLES AND NOT EMSCRIPTEN) # Examples add_executable(calculate_ball_landing examples/calculate_ball_landing.cpp) target_link_libraries(calculate_ball_landing ${PROJECT_NAME}) @@ -144,6 +153,9 @@ if(NOT EMSCRIPTEN) add_executable(calibration_sim_runner tools/calibration/sim_runner.cpp) target_link_libraries(calibration_sim_runner ${PROJECT_NAME}) +endif() + +if(BUILD_TESTING AND NOT EMSCRIPTEN) # Discover tests include(GoogleTest) gtest_discover_tests(libgolf_tests) diff --git a/README.md b/README.md index 81e4a95..e9d52b5 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,9 @@ chmod +x build.sh ./build.sh ``` +The default build is dependency-free. To fetch GoogleTest and run the test +suite, use `./build.sh --test` (or configure CMake with `-DBUILD_TESTING=ON`). + ## Using libgolf in your project After installing (`cmake --install build`), consume it from another CMake diff --git a/build.sh b/build.sh index b965457..c598800 100755 --- a/build.sh +++ b/build.sh @@ -24,8 +24,13 @@ fi mkdir -p build cd build -# Generate build files using CMake -cmake .. +# Generate build files using CMake. The normal library/example build has no +# network dependency; --test explicitly enables the GoogleTest-based suite. +if [ "$RUN_TESTS" -eq 1 ]; then + cmake .. -DBUILD_TESTING=ON +else + cmake .. +fi # Build the project cmake --build . @@ -37,4 +42,4 @@ fi # Optionally install (uncomment if you want to install system-wide) # echo "Installing to system directories (password may be required)" -# sudo cmake --install . \ No newline at end of file +# sudo cmake --install . diff --git a/docs/aerodynamic_model.md b/docs/aerodynamic_model.md index c601fdf..e5ee481 100644 --- a/docs/aerodynamic_model.md +++ b/docs/aerodynamic_model.md @@ -32,7 +32,7 @@ The fields fall into four groups: **Kinematic state** — `velocity`, `windVelocity`, `spinVector`, `position`, and `currentTime` are snapshots of the ball and its surrounding wind at the current timestep. The default model uses only the first three; `position` and `currentTime` are there so models with altitude-, location-, or time-dependent behaviour can reach for them. -**Ball geometry** — `ballRadius` is populated from `physics_constants::STD_BALL_RADIUS_FT`. The library does not currently support non-standard ball sizes; changing the constant is the only hook. +**Ball geometry** — `ballRadius` is derived from the `BallProperties` supplied to `FlightSimulator`; omitting that argument uses the standard golf-ball dimensions. **Lumped atmosphere** — `c0` and `re100` are precomputed by `ShotPhysicsContext` from temperature, pressure, and humidity. They're a compact encoding for lumped-parameter force laws of the form `F = c0 · Cd(Re, S) · vw · v_rel`. The default model consumes them directly; custom models that prefer the same form can too. @@ -83,7 +83,7 @@ F_magnus = c0 * (Cl / |omega|) * vw * (omega × v_rel) **Drag** (piecewise-linear through the drag crisis): ``` -Re <= RE_THRESHOLD_LOW (0.5): Cd = CD_LOW (0.500) +Re <= RE_THRESHOLD_LOW (0.5): Cd = CD_LOW (0.500) + CD_SPIN * S RE_THRESHOLD_LOW < Re < RE_THRESHOLD_HIGH (1.0): linear + CD_SPIN * S Re >= RE_THRESHOLD_HIGH (1.0): Cd = CD_HIGH (0.200) + CD_SPIN * S ``` diff --git a/docs/how.md b/docs/how.md index a9d5195..530c45a 100644 --- a/docs/how.md +++ b/docs/how.md @@ -17,6 +17,9 @@ cmake -B build cmake --build build ``` +Tests are opt-in because they fetch GoogleTest. Configure with +`cmake -B build -DBUILD_TESTING=ON` before building when you want to run them. + Include the main header in your source files: ```c++ @@ -126,7 +129,8 @@ printf("Bearing: %.1f degrees\n", result.bearing); ``` `LandingResult` contains: -- `xF`, `yF`, `zF` — final position in yards +- `xF`, `yF` — final lateral/downrange displacement from launch in yards +- `zF` — final height above the terrain at rest in yards (normally `0`) - `distance` — total distance in yards - `bearing` — direction in degrees - `timeOfFlight` — total simulation time in seconds diff --git a/docs/roll_model.md b/docs/roll_model.md index 4352517..407c42a 100644 --- a/docs/roll_model.md +++ b/docs/roll_model.md @@ -63,7 +63,8 @@ The built-in implementation lives in `include/DefaultRollModel.hpp` and is used Algorithm: ``` -a = -g·sin(θ) along slope - μ·g·cos(θ) opposing motion (Coulomb friction) +if |g_tangent| <= μ_static·N and the ball is nearly stationary: rest +else: a = g_tangent - μ_dynamic·N opposing (or initiating) motion v' = v + a·dt v_xy := 0 if sign flipped across the step and |v_old_xy| > ε (prevents reversal) p' = p + v'·dt @@ -72,7 +73,8 @@ atRest = |v'_horizontal| < STOPPING_VELOCITY ``` Where: -- `μ = surface.frictionDynamic`. +- `surface.frictionStatic` determines whether a near-stationary ball holds on + a slope; `surface.frictionDynamic` supplies Coulomb friction once it rolls. - `θ` derived from `surfaceNormal[2]`. Surfaces with `cos(θ) > FLAT_SURFACE_THRESHOLD` skip slope decomposition. - `STOPPING_VELOCITY = 0.1 ft/s` and `SPIN_DECAY_RATE = 2.0 rad/s²` live as `static constexpr` members on `DefaultRollModel`. diff --git a/docs/terrain.md b/docs/terrain.md index fecc3ee..d254002 100644 --- a/docs/terrain.md +++ b/docs/terrain.md @@ -201,7 +201,10 @@ During the roll phase, two forces act on the ball: 1. **Gravity component along slope**: Accelerates ball downhill or decelerates uphill 2. **Rolling friction**: Opposes motion in all directions -On flat surfaces, rolling friction alone causes deceleration. On slopes, the ball will accelerate if the gravity component exceeds friction. +On flat surfaces, rolling friction alone causes deceleration. A nearly stationary +ball holds while static friction can balance the downhill gravity component; it +starts rolling once that threshold is exceeded. Rolling friction then opposes +its motion. **Spin decay during roll:** Linear decay model where ground friction applies constant torque opposing spin. This differs from aerial phase which uses exponential decay due to aerodynamic damping. diff --git a/docs/wasm.md b/docs/wasm.md index cd570bd..87d4824 100644 --- a/docs/wasm.md +++ b/docs/wasm.md @@ -69,7 +69,7 @@ const result = libgolf.runShot( temp: 70.0, // °F elevation: 0.0, // ft vWind: 10.0, // mph - phiWind: 90.0, // 0 = cross-L, 90 = head, 180 = tail, 270 = cross-R + phiWind: 90.0, // 0 = tail, 90 = crosswind to the right, 180 = head, 270 = crosswind to the left hWind: 0.0, relHumidity: 50.0, // % pressure: 29.92, // inHg @@ -90,7 +90,7 @@ const result = libgolf.runShot( | Field | Type | Description | |------|------|-------------| -| `trajectory` | `VectorFloat` | Flat `[x, y, z, x, y, z, ...]` array of positions in **yards** | +| `trajectory` | `VectorFloat` | Flat `[x, y, z, x, y, z, ...]` array in **yards**; x/y are relative to launch and z is above ground | | `carryIndex` | `number` | Index (in points, not floats) of first ground contact | | `carryYards` | `number` | Downrange distance at first ground contact | | `totalYards` | `number` | Downrange distance at rest (carry + roll) | diff --git a/include/DefaultRollModel.hpp b/include/DefaultRollModel.hpp index c37a91a..9952ff2 100644 --- a/include/DefaultRollModel.hpp +++ b/include/DefaultRollModel.hpp @@ -38,11 +38,25 @@ class DefaultRollModel : public RollModel const GroundSurface &surface) const override { const float dt = state.dt; - - const Vector3D accel = computeAcceleration(state.velocity, state.surfaceNormal, surface); - const float oldVelX = state.velocity[0]; const float oldVelY = state.velocity[1]; + const float oldHorizontal = std::sqrt(oldVelX * oldVelX + oldVelY * oldVelY); + + // A ball that is nearly stationary should remain at rest only when + // static friction can balance gravity along the surface. Conversely, a + // steep enough slope must start a stationary ball rolling. + if (oldHorizontal < STOPPING_VELOCITY && + staticFrictionHolds(state.surfaceNormal, surface)) + { + return RollResult{ + state.position, + {0.0F, 0.0F, 0.0F}, + state.spinVector, + true + }; + } + + const Vector3D accel = computeAcceleration(state.velocity, state.surfaceNormal, surface); Vector3D newVel = state.velocity + accel * dt; @@ -80,7 +94,6 @@ class DefaultRollModel : public RollModel newSpin = {0.0F, 0.0F, 0.0F}; } - const float oldHorizontal = std::sqrt(oldVelX * oldVelX + oldVelY * oldVelY); const float vHorizontal = std::sqrt(newVel[0] * newVel[0] + newVel[1] * newVel[1]); const bool atRest = vHorizontal < STOPPING_VELOCITY && vHorizontal <= oldHorizontal; @@ -89,6 +102,24 @@ class DefaultRollModel : public RollModel } private: + static Vector3D gravityTangent(const Vector3D &surfaceNormal) + { + const Vector3D gravity = {0.0F, 0.0F, -physics_constants::GRAVITY_FT_PER_S2}; + const float gravityDotNormal = math_utils::dot(gravity, surfaceNormal); + return gravity - surfaceNormal * gravityDotNormal; + } + + static bool staticFrictionHolds(const Vector3D &surfaceNormal, + const GroundSurface &surface) + { + const Vector3D tangentGravity = gravityTangent(surfaceNormal); + const float tangentMagnitude = math_utils::magnitude(tangentGravity); + const float normalForce = std::abs( + math_utils::dot(Vector3D{0.0F, 0.0F, -physics_constants::GRAVITY_FT_PER_S2}, + surfaceNormal)); + return tangentMagnitude <= surface.frictionStatic * normalForce; + } + static Vector3D computeAcceleration(const Vector3D &velocity, const Vector3D &surfaceNormal, const GroundSurface &surface) @@ -97,13 +128,6 @@ class DefaultRollModel : public RollModel const float vHorizontal = std::sqrt(velocity[0] * velocity[0] + velocity[1] * velocity[1]); - // Stationary: friction direction undefined, gravity along slope is - // balanced by static friction at rest. - if (vHorizontal < physics_constants::MIN_SPEED) - { - return acceleration; - } - const float cosTheta = surfaceNormal[2]; // Near-flat: skip slope decomposition. @@ -116,17 +140,31 @@ class DefaultRollModel : public RollModel return acceleration; } - const Vector3D gravity = {0.0F, 0.0F, -physics_constants::GRAVITY_FT_PER_S2}; + acceleration = gravityTangent(surfaceNormal); - const float gravityDotNormal = math_utils::dot(gravity, surfaceNormal); - const Vector3D gravityNormal = surfaceNormal * gravityDotNormal; - acceleration = gravity - gravityNormal; - - const float normalForce = std::abs(gravityDotNormal); + const float normalForce = std::abs( + math_utils::dot(Vector3D{0.0F, 0.0F, -physics_constants::GRAVITY_FT_PER_S2}, + surfaceNormal)); const float frictionDeceleration = surface.frictionDynamic * normalForce; - acceleration[0] -= frictionDeceleration * (velocity[0] / vHorizontal); - acceleration[1] -= frictionDeceleration * (velocity[1] / vHorizontal); + if (vHorizontal >= physics_constants::MIN_SPEED) + { + acceleration[0] -= frictionDeceleration * (velocity[0] / vHorizontal); + acceleration[1] -= frictionDeceleration * (velocity[1] / vHorizontal); + } + else + { + // Static friction has already failed, so kinetic friction opposes the + // impending downhill motion. + const float tangentHorizontal = + std::sqrt(acceleration[0] * acceleration[0] + + acceleration[1] * acceleration[1]); + if (tangentHorizontal >= physics_constants::MIN_SPEED) + { + acceleration[0] -= frictionDeceleration * acceleration[0] / tangentHorizontal; + acceleration[1] -= frictionDeceleration * acceleration[1] / tangentHorizontal; + } + } return acceleration; } diff --git a/include/launch_data.hpp b/include/launch_data.hpp index c258ed0..25ff62f 100644 --- a/include/launch_data.hpp +++ b/include/launch_data.hpp @@ -83,7 +83,7 @@ struct LaunchData float startY = 0.0f; /** - * @brief Starting height above ground (feet). + * @brief Starting height above the terrain at (startX, startY) (feet). * * Typically 0.0 for ground-level shots. Set to tee height for teed-up shots. */ @@ -119,7 +119,7 @@ struct LandingResult float yF; /** - * @brief Final height above ground (yards). + * @brief Final height above the terrain at the resting position (yards). * * Typically 0.0 when ball comes to rest on ground. */ diff --git a/include/terrain_interface.hpp b/include/terrain_interface.hpp index 3172a26..1750fc6 100644 --- a/include/terrain_interface.hpp +++ b/include/terrain_interface.hpp @@ -35,11 +35,10 @@ * * // Use custom terrain in flight simulation * auto terrain = std::make_shared(groundSurface); - * FlightSimulator sim(physicsVars, ball, atmos, groundSurface, terrain); + * FlightSimulator sim(launch, atmos, terrain); * - * // Note: When a custom terrain is provided, the groundSurface parameter - * // serves as a fallback for backward compatibility. The flight simulator - * // will query terrain properties from the TerrainInterface implementation. + * // The simulator queries all ground data from TerrainInterface; there is no + * // separate fallback GroundSurface when using this constructor. * @endcode * * @copyright Copyright (c) 2025, Gabriel DiFiore diff --git a/src/FlightSimulator.cpp b/src/FlightSimulator.cpp index 78fd9ef..b91bb68 100644 --- a/src/FlightSimulator.cpp +++ b/src/FlightSimulator.cpp @@ -63,7 +63,15 @@ FlightSimulator::FlightSimulator( void FlightSimulator::initializeFromLaunch(const LaunchData &launch) { const float v0_fps = launch.ballSpeedMph * physics_constants::MPH_TO_FT_PER_S; - startPosition_ = Vector3D{launch.startX, launch.startY, launch.startZ}; + // LaunchData::startZ is a tee height above the local terrain, whereas the + // simulation state always stores absolute world coordinates. + const float launchTerrainHeight = + terrainStorage_->getHeight(launch.startX, launch.startY); + startPosition_ = Vector3D{ + launch.startX, + launch.startY, + launchTerrainHeight + launch.startZ + }; const Vector3D &startPos = startPosition_; state = BallState::fromLaunchParameters( @@ -153,7 +161,12 @@ LandingResult FlightSimulator::getLandingResult() const LandingResult result; result.xF = relative[0] / physics_constants::YARDS_TO_FEET; result.yF = relative[1] / physics_constants::YARDS_TO_FEET; - result.zF = relative[2] / physics_constants::YARDS_TO_FEET; + // Unlike x/y, zF is a clearance above the terrain at the resting point. + // This remains zero for a ball at rest on an elevated green. + const float finalTerrainHeight = + terrainStorage_->getHeight(state.position[0], state.position[1]); + result.zF = (state.position[2] - finalTerrainHeight) / + physics_constants::YARDS_TO_FEET; result.timeOfFlight = state.currentTime; result.bearing = std::atan2(relative[0], relative[1]) * 180.0F / physics_constants::PI; diff --git a/test/test_default_roll_model.cpp b/test/test_default_roll_model.cpp index 29256e4..1ec63b2 100644 --- a/test/test_default_roll_model.cpp +++ b/test/test_default_roll_model.cpp @@ -146,9 +146,12 @@ TEST(DefaultRollModelTest, SignFlipClampsToZero) TEST(DefaultRollModelTest, NearZeroSlopeStartGetsAccelerated) { - DefaultRollModel model; - GroundSurface surface; - surface.frictionDynamic = 0.15F; + DefaultRollModel model; + GroundSurface surface; + // A 10° slope exceeds this static-friction limit, so the ball must begin + // moving even though it starts almost at rest. + surface.frictionStatic = 0.1F; + surface.frictionDynamic = 0.15F; const float angle = 10.0F * physics_constants::DEG_TO_RAD; Vector3D normal{0.0F, std::sin(angle), std::cos(angle)}; @@ -160,7 +163,40 @@ TEST(DefaultRollModelTest, NearZeroSlopeStartGetsAccelerated) // Velocity below STOPPING_VELOCITY: sign-flip clamp must NOT kick in, // so gravity along slope can grow it. EXPECT_GT(result.newVelocity[1], 0.02F); - EXPECT_FALSE(result.atRest); + EXPECT_FALSE(result.atRest); +} + +TEST(DefaultRollModelTest, StaticFrictionHoldsOnShallowSlope) +{ + DefaultRollModel model; + GroundSurface surface; + surface.frictionStatic = 0.5F; + surface.frictionDynamic = 0.15F; + + const float angle = 10.0F * physics_constants::DEG_TO_RAD; + const Vector3D normal{0.0F, std::sin(angle), std::cos(angle)}; + + auto result = model.step(makeState({0.0F, 0.02F, 0.0F}, normal), surface); + + EXPECT_NEAR(result.newVelocity[0], 0.0F, 1e-6F); + EXPECT_NEAR(result.newVelocity[1], 0.0F, 1e-6F); + EXPECT_TRUE(result.atRest); +} + +TEST(DefaultRollModelTest, SteepSlopeBreaksStaticFrictionFromRest) +{ + DefaultRollModel model; + GroundSurface surface; + surface.frictionStatic = 0.1F; + surface.frictionDynamic = 0.05F; + + const float angle = 30.0F * physics_constants::DEG_TO_RAD; + const Vector3D normal{0.0F, std::sin(angle), std::cos(angle)}; + + auto result = model.step(makeState({0.0F, 0.0F, 0.0F}, normal), surface); + + EXPECT_GT(result.newVelocity[1], 0.0F); + EXPECT_FALSE(result.atRest); } TEST(DefaultRollModelTest, SpinDecaysButPreservesAxis) diff --git a/test/test_flight_simulator.cpp b/test/test_flight_simulator.cpp index fc6f139..b16d266 100644 --- a/test/test_flight_simulator.cpp +++ b/test/test_flight_simulator.cpp @@ -320,9 +320,9 @@ TEST_F(FlightSimulatorTest, HandlesNonZeroGroundHeight) elevatedGround.frictionDynamic = 0.2F; elevatedGround.firmness = 0.8F; - // Start ball at the elevated ground height + // startZ is a height above the terrain, so zero starts on this elevated tee. LaunchData elevatedBall = ball; - elevatedBall.startZ = elevatedGround.height; + elevatedBall.startZ = 0.0F; FlightSimulator sim(elevatedBall, atmos, elevatedGround); sim.run(0.01F); @@ -339,6 +339,8 @@ TEST_F(FlightSimulatorTest, HandlesNonZeroGroundHeight) finalState.velocity[2] * finalState.velocity[2] ); EXPECT_LT(finalSpeed, 1.0F) << "Ball should be stopped or nearly stopped"; + EXPECT_NEAR(sim.getLandingResult().zF, 0.0F, 0.01F) + << "LandingResult zF is height above the final terrain"; } TEST_F(FlightSimulatorTest, SpinDecaysAcrossAllPhases) diff --git a/test/test_roll_phase.cpp b/test/test_roll_phase.cpp index 7f01ffc..af68059 100644 --- a/test/test_roll_phase.cpp +++ b/test/test_roll_phase.cpp @@ -301,6 +301,9 @@ TEST_F(RollPhaseTest, SpinDecaysToZeroFromNegative) TEST_F(RollPhaseTest, BallCanStartRollingFromNearZeroVelocityOnSlope) { // Create sloped terrain (10 degree downslope) + // tan(10°) exceeds this static-friction coefficient, so the ball should + // begin rolling rather than remain held on the slope. + ground.frictionStatic = 0.1F; ground.frictionDynamic = 0.15F; // Create a simple sloped terrain for testing diff --git a/wasm/bindings.cpp b/wasm/bindings.cpp index 3f17173..40b74c1 100644 --- a/wasm/bindings.cpp +++ b/wasm/bindings.cpp @@ -24,7 +24,8 @@ using namespace emscripten; /** * Result of a shot simulation, returned to JavaScript as a plain object. * - * trajectory: Flat [x, y, z, x, y, z, ...] array of positions in YARDS + * trajectory: Flat [x, y, z, x, y, z, ...] array of shot-local positions in + * YARDS. x/y are relative to the launch point; z is above flat ground. * x = lateral (right = positive) * y = downrange (forward = positive) * z = height (up = positive) @@ -36,7 +37,7 @@ struct ShotResult { std::vector trajectory; int carryIndex = 0; // index of first ground contact (point count, not float count) - float carryYards = 0.0F; // downrange distance at first ground contact + float carryYards = 0.0F; // downrange displacement at first ground contact float totalYards = 0.0F; // downrange distance at rest float apexYards = 0.0F; // peak height above ground float offlineYards = 0.0F; // lateral position at rest (right = +) @@ -64,9 +65,9 @@ ShotResult runShot(const LaunchData &launch, for (size_t i = 0; i < trajectory.size(); ++i) { const auto &p = trajectory[i].position; - out.trajectory.push_back(p[0] / FT_PER_YD); - out.trajectory.push_back(p[1] / FT_PER_YD); - out.trajectory.push_back(p[2] / FT_PER_YD); + out.trajectory.push_back((p[0] - launch.startX) / FT_PER_YD); + out.trajectory.push_back((p[1] - launch.startY) / FT_PER_YD); + out.trajectory.push_back((p[2] - ground.height) / FT_PER_YD); if (i == 0 || p[2] > apexFt) { apexFt = p[2]; @@ -88,8 +89,9 @@ ShotResult runShot(const LaunchData &launch, } out.carryIndex = carryIdx; - out.carryYards = trajectory.empty() ? 0.0F : trajectory[carryIdx].position[1] / FT_PER_YD; - out.apexYards = std::max(0.0F, apexFt) / FT_PER_YD; + out.carryYards = trajectory.empty() ? 0.0F : + (trajectory[carryIdx].position[1] - launch.startY) / FT_PER_YD; + out.apexYards = std::max(0.0F, apexFt - ground.height) / FT_PER_YD; out.totalYards = landing.yF; // landing fields already in yards out.offlineYards = landing.xF; out.timeOfFlight = landing.timeOfFlight; From 59b99539db91eb0fb83f8071c1857f61b949cde3 Mon Sep 17 00:00:00 2001 From: Gabe DiFiore Date: Thu, 20 Aug 2026 19:03:18 -0400 Subject: [PATCH 2/2] Enable tests in CI coverage builds --- .github/workflows/ci-multi-platform.yml | 4 ++-- .github/workflows/code-coverage.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-multi-platform.yml b/.github/workflows/ci-multi-platform.yml index a714eeb..b668d99 100644 --- a/.github/workflows/ci-multi-platform.yml +++ b/.github/workflows/ci-multi-platform.yml @@ -64,11 +64,11 @@ jobs: env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} + run: cmake -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DBUILD_TESTING=ON - name: Configure CMake (Windows) if: runner.os == 'Windows' - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} + run: cmake -B build -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} -DBUILD_TESTING=ON - name: Build run: cmake --build build --config ${{ env.BUILD_TYPE }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index c3b4d90..891df84 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -25,7 +25,8 @@ jobs: run: | cmake -B build \ -DCMAKE_BUILD_TYPE=Debug \ - -DENABLE_COVERAGE=ON + -DENABLE_COVERAGE=ON \ + -DBUILD_TESTING=ON - name: Build run: cmake --build build