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 <cursoragent@cursor.com>
pull/131/head
VinPropane 2026-05-27 23:21:00 -04:00
parent 056b0e77f0
commit 90e6cfea28
3 changed files with 81 additions and 2 deletions

View File

@ -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. **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. **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.

View File

@ -14,7 +14,11 @@ extends Node3D
## [signal zoom_band_changed] when schema exists; optional occlusion/perf counters per module doc. ## [signal zoom_band_changed] when schema exists; optional occlusion/perf counters per module doc.
signal zoom_band_changed(new_index: int, distance: float) 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 ZoomBandConfigScript := preload("res://scripts/zoom_band_config.gd")
const OcclusionPolicyScript := preload("res://scripts/occlusion_policy.gd") const OcclusionPolicyScript := preload("res://scripts/occlusion_policy.gd")
@ -128,12 +132,46 @@ func _exit_tree() -> void:
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event.is_echo(): if event.is_echo():
return 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"): if event.is_action_pressed("camera_zoom_in"):
_apply_zoom_step(-1) _apply_zoom_step(-1)
elif event.is_action_pressed("camera_zoom_out"): elif event.is_action_pressed("camera_zoom_out"):
_apply_zoom_step(1) _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). ## **Near** = lower band index. Zoom in → closer → decrease index (clamped).
func _apply_zoom_step(delta_bands: int) -> void: func _apply_zoom_step(delta_bands: int) -> void:
if not _zoom_config_valid(): if not _zoom_config_valid():

View File

@ -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) var ok: bool = IsoScript.occluder_override_key_is_valid(n)
# Assert # Assert
assert_that(ok).is_false() 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)