From 90e6cfea28f226fc6cea8c5aa96241e62d5f0466 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Wed, 27 May 2026 23:21:00 -0400 Subject: [PATCH] chore: add macOS trackpad scroll and pinch camera zoom. Map InputEventPanGesture and InputEventMagnifyGesture to the existing discrete zoom bands; document in client README. Unrelated to NEO-93. Co-authored-by: Cursor --- client/README.md | 2 +- client/scripts/isometric_follow_camera.gd | 40 +++++++++++++++++++- client/test/isometric_follow_camera_test.gd | 41 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/client/README.md b/client/README.md index 431dbce..ec6fe40 100644 --- a/client/README.md +++ b/client/README.md @@ -284,7 +284,7 @@ On **Linux** (including GitHub Actions), the path must be **`gdUnit4`** with a c **Scope:** Unit tests cover **`player.gd`**, **`player_locomotion_wasd.gd`** (pure WASD seam helpers), **`position_authority_client.gd`**, **`inventory_client.gd`**, **`item_definitions_client.gd`** (NEO-72), **`skill_progression_client.gd`**, **`gig_progression_client.gd`** (NEO-86), **`prototype_interactable_picker.gd`**, extended **`interaction_request_client_test.gd`**, **`gather_feedback_refresh_test.gd`** (NEO-73), **`craft_client.gd`**, **`recipe_definitions_client.gd`**, **`craft_feedback_refresh_test.gd`** (NEO-74), **`prototype_economy_hud_section.gd`** (NEO-75), **`combat_targets_client.gd`**, **`combat_feedback_refresh_test.gd`** (NEO-85), **`gig_feedback_refresh_test.gd`** (NEO-86), **`isometric_follow_camera.gd`** (eye math + effective zoom distance), **`camera_state.gd`**, and **`zoom_band_config.gd`**. **`main.gd`** and scene wiring are **not** automated here—use manual checks above. -**Camera zoom:** Input actions **`camera_zoom_in`** / **`camera_zoom_out`** (mouse wheel, **`=`** / **`-`**, keypad **+** / **−**) are defined in **`project.godot`**. On layouts where **`+`** requires **Shift**, remap or add a binding under **Project → Project Settings → Input Map** if needed. +**Camera zoom:** Input actions **`camera_zoom_in`** / **`camera_zoom_out`** (mouse wheel, **`=`** / **`-`**, keypad **+** / **−**) are defined in **`project.godot`**. **`isometric_follow_camera.gd`** also maps **macOS trackpad** two-finger scroll ([`InputEventPanGesture`](https://docs.godotengine.org/en/stable/classes/class_inputeventpangesture.html)) and pinch ([`InputEventMagnifyGesture`](https://docs.godotengine.org/en/stable/classes/class_inputeventmagnifygesture.html)) to the same discrete zoom bands. On layouts where **`+`** requires **Shift**, remap or add a binding under **Project → Project Settings → Input Map** if needed. **Dev (NEO-18):** **`dev_toggle_occluder_obstacle`** (default **Ctrl+Shift+K**) toggles the prototype `Obstacle` visibility/collision in **`main.gd`**. **F9** / **F10** are used by the embedded debugger; use this action instead when the game is running in the editor. diff --git a/client/scripts/isometric_follow_camera.gd b/client/scripts/isometric_follow_camera.gd index 3f511da..e4f4166 100644 --- a/client/scripts/isometric_follow_camera.gd +++ b/client/scripts/isometric_follow_camera.gd @@ -14,7 +14,11 @@ extends Node3D ## [signal zoom_band_changed] when schema exists; optional occlusion/perf counters per module doc. signal zoom_band_changed(new_index: int, distance: float) -const CameraStateScript := preload("res://scripts/camera_state.gd") +## Minimum |delta.y| before a pan gesture advances one zoom band (macOS trackpad). +const PAN_ZOOM_DELTA_THRESHOLD: float = 0.05 + +## Magnify factor must exceed 1.0 ± this before one zoom band step (trackpad pinch). +const MAGNIFY_ZOOM_FACTOR_EPSILON: float = 0.02 const ZoomBandConfigScript := preload("res://scripts/zoom_band_config.gd") const OcclusionPolicyScript := preload("res://scripts/occlusion_policy.gd") @@ -128,12 +132,46 @@ func _exit_tree() -> void: func _unhandled_input(event: InputEvent) -> void: if event.is_echo(): return + var pan_step: int = zoom_step_from_pan_gesture(event) + if pan_step != 0: + _apply_zoom_step(pan_step) + return + var magnify_step: int = zoom_step_from_magnify_gesture(event) + if magnify_step != 0: + _apply_zoom_step(magnify_step) + return if event.is_action_pressed("camera_zoom_in"): _apply_zoom_step(-1) elif event.is_action_pressed("camera_zoom_out"): _apply_zoom_step(1) +## Maps macOS trackpad two-finger scroll to a discrete zoom band step, or **0** when ignored. +static func zoom_step_from_pan_gesture(event: InputEvent) -> int: + if event == null or not event is InputEventPanGesture: + return 0 + var pan: InputEventPanGesture = event as InputEventPanGesture + if absf(pan.delta.y) <= absf(pan.delta.x): + return 0 + if pan.delta.y <= -PAN_ZOOM_DELTA_THRESHOLD: + return -1 + if pan.delta.y >= PAN_ZOOM_DELTA_THRESHOLD: + return 1 + return 0 + + +## Maps trackpad pinch ([InputEventMagnifyGesture]) to a discrete zoom band step, or **0**. +static func zoom_step_from_magnify_gesture(event: InputEvent) -> int: + if event == null or not event is InputEventMagnifyGesture: + return 0 + var mag: InputEventMagnifyGesture = event as InputEventMagnifyGesture + if mag.factor >= 1.0 + MAGNIFY_ZOOM_FACTOR_EPSILON: + return -1 + if mag.factor <= 1.0 - MAGNIFY_ZOOM_FACTOR_EPSILON: + return 1 + return 0 + + ## **Near** = lower band index. Zoom in → closer → decrease index (clamped). func _apply_zoom_step(delta_bands: int) -> void: if not _zoom_config_valid(): diff --git a/client/test/isometric_follow_camera_test.gd b/client/test/isometric_follow_camera_test.gd index c709394..5007ab7 100644 --- a/client/test/isometric_follow_camera_test.gd +++ b/client/test/isometric_follow_camera_test.gd @@ -138,3 +138,44 @@ func test_occluder_override_key_is_valid_false_after_free() -> void: var ok: bool = IsoScript.occluder_override_key_is_valid(n) # Assert assert_that(ok).is_false() + + +func test_zoom_step_from_pan_gesture_ignores_non_pan() -> void: + # Arrange + var key := InputEventKey.new() + # Act + var step: int = IsoScript.zoom_step_from_pan_gesture(key) + # Assert + assert_that(step).is_equal(0) + + +func test_zoom_step_from_pan_gesture_maps_vertical_scroll_to_bands() -> void: + # Arrange + var scroll_up := InputEventPanGesture.new() + scroll_up.delta = Vector2(0.0, -0.2) + var scroll_down := InputEventPanGesture.new() + scroll_down.delta = Vector2(0.0, 0.2) + var horizontal := InputEventPanGesture.new() + horizontal.delta = Vector2(0.2, 0.0) + # Act + var up_step: int = IsoScript.zoom_step_from_pan_gesture(scroll_up) + var down_step: int = IsoScript.zoom_step_from_pan_gesture(scroll_down) + var horiz_step: int = IsoScript.zoom_step_from_pan_gesture(horizontal) + # Assert + assert_that(up_step).is_equal(-1) + assert_that(down_step).is_equal(1) + assert_that(horiz_step).is_equal(0) + + +func test_zoom_step_from_magnify_gesture_maps_pinch_to_bands() -> void: + # Arrange + var pinch_in := InputEventMagnifyGesture.new() + pinch_in.factor = 0.9 + var pinch_out := InputEventMagnifyGesture.new() + pinch_out.factor = 1.1 + # Act + var in_step: int = IsoScript.zoom_step_from_magnify_gesture(pinch_in) + var out_step: int = IsoScript.zoom_step_from_magnify_gesture(pinch_out) + # Assert + assert_that(in_step).is_equal(1) + assert_that(out_step).is_equal(-1)