From e6c70acd778dd60a211b228ba5361f40b1e8cef0 Mon Sep 17 00:00:00 2001 From: cinnaboot Date: Thu, 29 Jan 2026 13:00:28 -0500 Subject: [PATCH] Implement dynamic camera zoom with percentage-based speed and distance clamping --- docs/planning/dynamic-camera-zoom-plan.md | 185 ++++++++++++++++++++++ src/renderer.cpp | 63 +++++++- 2 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 docs/planning/dynamic-camera-zoom-plan.md diff --git a/docs/planning/dynamic-camera-zoom-plan.md b/docs/planning/dynamic-camera-zoom-plan.md new file mode 100644 index 0000000..0254cd8 --- /dev/null +++ b/docs/planning/dynamic-camera-zoom-plan.md @@ -0,0 +1,185 @@ +# Dynamic Camera Zoom Implementation Plan + +## Overview +Replace fixed zoom behavior with adaptive zoom speed and minimum distance based on current camera distance and target characteristics. + +## Goals +- Zoom speed adapts to current distance (faster when far, slower when close) +- Minimum camera distance based on target's physical size (radius) +- Prevent zooming through body surfaces +- Maintain smooth, controllable zoom with delta clamping + +## Changes Required + +### 1. Add New Constants (src/renderer.cpp) +Add after existing camera constants: + +```cpp +// Dynamic zoom settings +#define ZOOM_PERCENTAGE 0.07f // 7% of current distance per keypress +#define ZOOM_MIN_DELTA 0.05f // Minimum zoom delta (prevents too-slow zoom) +#define ZOOM_MAX_DELTA 5.0f // Maximum zoom delta (prevents too-fast zoom) + +// Minimum distance settings (in render units) +#define MIN_DISTANCE_BODY_RADIUS_MULT 2.75f // 2.75x body radius minimum +#define MIN_DISTANCE_SPACECRAFT_DEFAULT 0.5f // Fallback for orphaned spacecraft +#define MIN_DISTANCE_SPACECRAFT_PARENT_RADIUS_MULT 0.1f // Scale parent radius for spacecraft +``` + +### 2. Add Zoom Direction Enum (src/renderer.cpp) +Local enum for improved readability: + +```cpp +enum ZoomDirection { + ZOOM_IN = -1.0f, + ZOOM_OUT = 1.0f +}; +``` + +### 3. Modify `zoom_camera()` Function +**Location:** src/renderer.cpp:223 + +**Changes:** +- Update signature: `static void zoom_camera(RenderState* render_state, SimulationState* sim, ZoomDirection direction)` +- Replace fixed `CAMERA_ZOOM_DELTA` with dynamic calculation +- Add minimum distance based on target radius + +**Implementation:** +```cpp +static void zoom_camera(RenderState* render_state, SimulationState* sim, ZoomDirection direction) { + Vector3 to_target = Vector3Subtract(render_state->camera.target, render_state->camera.position); + Vector3 direction_vec = Vector3Normalize(to_target); + float camera_distance = Vector3Length(to_target); + + // Calculate dynamic zoom delta based on current distance + float raw_delta = camera_distance * ZOOM_PERCENTAGE * (float)direction; + + // Clamp delta to reasonable bounds + float distance_delta = raw_delta; + if (fabsf(distance_delta) < ZOOM_MIN_DELTA) { + distance_delta = ZOOM_MIN_DELTA * (float)direction; + } + if (fabsf(distance_delta) > ZOOM_MAX_DELTA) { + distance_delta = ZOOM_MAX_DELTA * (float)direction; + } + + // Get target-specific minimum distance + float min_dist = get_target_min_distance(render_state, sim); + + // Check if zoom would exceed minimum distance + if (direction == ZOOM_IN && camera_distance - distance_delta <= min_dist) { + distance_delta = camera_distance - min_dist - 0.001f; + } + + render_state->camera.position = Vector3Add(render_state->camera.position, Vector3Scale(direction_vec, distance_delta)); + + if (render_state->camera_target_enabled) { + render_state->camera_offset = Vector3Subtract( + render_state->camera.position, + render_state->camera.target + ); + } +} +``` + +### 4. Add `get_target_min_distance()` Helper Function +**Location:** Add new static function before `zoom_camera()` + +```cpp +static float get_target_min_distance(RenderState* render_state, SimulationState* sim) { + float target_radius = 0.0f; + + if (render_state->selected_body_index >= 0) { + CelestialBody* body = &sim->bodies[render_state->selected_body_index]; + target_radius = scale_radius(body->radius, render_state->distance_scale); + } else if (render_state->selected_craft_index >= 0) { + Spacecraft* craft = &sim->spacecraft[render_state->selected_craft_index]; + if (craft->parent_index >= 0 && craft->parent_index < sim->body_count) { + CelestialBody* parent = &sim->bodies[craft->parent_index]; + target_radius = scale_radius(parent->radius, render_state->distance_scale); + target_radius *= MIN_DISTANCE_SPACECRAFT_PARENT_RADIUS_MULT; + } else { + // Spacecraft without parent (shouldn't happen in valid configs) + return MIN_DISTANCE_SPACECRAFT_DEFAULT; + } + } else { + // No target selected, use existing fallback + return CAMERA_MIN_DISTANCE; + } + + return target_radius * MIN_DISTANCE_BODY_RADIUS_MULT; +} +``` + +### 5. Update `update_camera()` Calls +**Location:** src/renderer.cpp:277-282 + +**Change from:** +```cpp +if (IsKeyDown(KEY_UP)) { + zoom_camera(render_state, CAMERA_ZOOM_DELTA); +} +if (IsKeyDown(KEY_DOWN)) { + zoom_camera(render_state, -CAMERA_ZOOM_DELTA); +} +``` + +**Change to:** +```cpp +if (IsKeyDown(KEY_UP)) { + zoom_camera(render_state, sim, ZOOM_IN); +} +if (IsKeyDown(KEY_DOWN)) { + zoom_camera(render_state, sim, ZOOM_OUT); +} +``` + +### 6. Remove/Update Old Constants +**Location:** src/renderer.cpp:8 + +**Remove:** `#define CAMERA_ZOOM_DELTA 2.0f` (replaced by dynamic calculation) +**Keep:** `#define CAMERA_MIN_DISTANCE 0.1f` (as fallback when no target) + +## Technical Details + +### Dynamic Zoom Calculation +1. Calculate raw delta: `current_distance * ZOOM_PERCENTAGE * direction` +2. Clamp delta between `ZOOM_MIN_DELTA` and `ZOOM_MAX_DELTA` +3. Ensure sign matches zoom direction + +### Minimum Distance Logic +1. For bodies: `body_radius * MIN_DISTANCE_BODY_RADIUS_MULT` +2. For spacecraft: `parent_radius * MIN_DISTANCE_SPACECRAFT_PARENT_RADIUS_MULT * MIN_DISTANCE_BODY_RADIUS_MULT` +3. For no target: `CAMERA_MIN_DISTANCE` (existing fallback) + +### Sign Convention +- `ZOOM_IN (-1.0)`: Move camera closer to target (decrease distance) +- `ZOOM_OUT (+1.0)`: Move camera farther from target (increase distance) + +### Edge Cases +- Spacecraft without parent: Uses `MIN_DISTANCE_SPACECRAFT_DEFAULT` +- No target selected: Uses existing `CAMERA_MIN_DISTANCE` +- Very close zoom: Delta clamping prevents too-slow movement + +## Implementation Order +1. Add new constants and enum to renderer.cpp +2. Implement `get_target_min_distance()` helper +3. Modify `zoom_camera()` function signature and implementation +4. Update `update_camera()` calls to `zoom_camera()` +5. Remove old `CAMERA_ZOOM_DELTA` constant +6. Build and test + +## Testing Checklist +- [ ] Zoom in/out works smoothly with new percentage-based behavior +- [ ] Zoom speed adapts based on current distance (faster when far, slower when close) +- [ ] Cannot zoom through body surface (minimum distance enforced) +- [ ] Spacecraft zoom uses parent body radius (scaled down) +- [ ] Zoom delta clamping prevents too-fast or too-slow zoom +- [ ] No target selected still works (uses existing fallbacks) +- [ ] Build succeeds without errors + +## Future Enhancements (Not in scope) +- Use parent's SOI radius instead of radius for more physically meaningful bounds +- Consider orbital characteristics (semi-major axis) for context-aware minimums +- Add configurable zoom speed presets (slow/normal/fast) +- Smooth interpolation when zooming rapidly diff --git a/src/renderer.cpp b/src/renderer.cpp index 4378052..b9b82b2 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -5,7 +5,6 @@ // Camera control constants #define CAMERA_ORBIT_ANGLE_SPEED 0.02f -#define CAMERA_ZOOM_DELTA 2.0f #define CAMERA_MIN_DISTANCE 0.1f #define CAMERA_INITIAL_HEIGHT_FACTOR 0.3f #define CAMERA_INITIAL_FOV 45.0f @@ -19,6 +18,22 @@ // Initial camera distance fallback #define INITIAL_DISTANCE_RADIUS_MULTIPLIER 100.0f +// Dynamic zoom settings +#define ZOOM_PERCENTAGE 0.07f +#define ZOOM_MIN_DELTA 0.05f +#define ZOOM_MAX_DELTA 5.0f + +// Minimum distance settings (in render units) +#define MIN_DISTANCE_BODY_RADIUS_MULT 2.75f +#define MIN_DISTANCE_SPACECRAFT_DEFAULT 0.5f +#define MIN_DISTANCE_SPACECRAFT_PARENT_RADIUS_MULT 0.1f + +// Zoom direction enumeration for improved readability +enum ZoomDirection { + ZOOM_IN = 1, + ZOOM_OUT = -1 +}; + // TODO: Consider extracting other hardcoded constants: // - Reference grid parameters (lines 600-610) // - Spacecraft rendering (screen size, color) @@ -220,12 +235,50 @@ static void rotate_camera_orbitally(RenderState* render_state, float angle) { } } -static void zoom_camera(RenderState* render_state, float distance_delta) { +static float get_target_min_distance(RenderState* render_state, SimulationState* sim) { + float target_radius = 0.0f; + + if (render_state->selected_body_index >= 0) { + CelestialBody* body = &sim->bodies[render_state->selected_body_index]; + target_radius = scale_radius(body->radius, render_state->distance_scale); + } else if (render_state->selected_craft_index >= 0) { + Spacecraft* craft = &sim->spacecraft[render_state->selected_craft_index]; + if (craft->parent_index >= 0 && craft->parent_index < sim->body_count) { + CelestialBody* parent = &sim->bodies[craft->parent_index]; + target_radius = scale_radius(parent->radius, render_state->distance_scale); + target_radius *= MIN_DISTANCE_SPACECRAFT_PARENT_RADIUS_MULT; + } else { + return MIN_DISTANCE_SPACECRAFT_DEFAULT; + } + } else { + return CAMERA_MIN_DISTANCE; + } + + return target_radius * MIN_DISTANCE_BODY_RADIUS_MULT; +} + +static void zoom_camera(RenderState* render_state, SimulationState* sim, ZoomDirection zoom_dir) { Vector3 to_target = Vector3Subtract(render_state->camera.target, render_state->camera.position); Vector3 direction = Vector3Normalize(to_target); float camera_distance = Vector3Length(to_target); - if (camera_distance - distance_delta <= CAMERA_MIN_DISTANCE) return; + float raw_delta = camera_distance * ZOOM_PERCENTAGE * (float)zoom_dir; + float distance_delta = raw_delta; + + if (fabsf(distance_delta) < ZOOM_MIN_DELTA) { + distance_delta = ZOOM_MIN_DELTA * (float)zoom_dir; + } + if (fabsf(distance_delta) > ZOOM_MAX_DELTA) { + distance_delta = ZOOM_MAX_DELTA * (float)zoom_dir; + } + + float min_dist = get_target_min_distance(render_state, sim); + + if (zoom_dir == ZOOM_IN) { + if (camera_distance - distance_delta <= min_dist) { + distance_delta = camera_distance - min_dist - 0.001f; + } + } render_state->camera.position = Vector3Add(render_state->camera.position, Vector3Scale(direction, distance_delta)); @@ -275,10 +328,10 @@ void update_camera(RenderState* render_state, SimulationState* sim) { } if (IsKeyDown(KEY_UP)) { - zoom_camera(render_state, CAMERA_ZOOM_DELTA); + zoom_camera(render_state, sim, ZOOM_IN); } if (IsKeyDown(KEY_DOWN)) { - zoom_camera(render_state, -CAMERA_ZOOM_DELTA); + zoom_camera(render_state, sim, ZOOM_OUT); } update_last_target(render_state);