chore: accumulate trackpad pan delta before camera zoom steps.
Mac trackpads emit many pan events per gesture; stepping one band per event jumped across all zoom levels. Accumulate delta with a threshold, cap one step per event, and add a 150 ms cooldown between trackpad steps. Co-authored-by: Cursor <cursoragent@cursor.com>pull/131/head
parent
9b8593c145
commit
9fa0fbef4b
|
|
@ -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`**. **`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.
|
||||
**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 and pinch to the same discrete zoom bands using **accumulated delta** (one band step per ~1.25 pan units, **150 ms** cooldown between trackpad steps) so small gestures do not traverse all bands at once. 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ 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)
|
||||
|
||||
## Minimum |delta.y| before a pan gesture advances one zoom band (macOS trackpad).
|
||||
const PAN_ZOOM_DELTA_THRESHOLD: float = 0.05
|
||||
## Accumulated vertical pan before one discrete zoom band step (macOS trackpad).
|
||||
const PAN_ZOOM_BAND_ACCUMULATOR_THRESHOLD: float = 1.25
|
||||
|
||||
## Magnify factor must exceed 1.0 ± this before one zoom band step (trackpad pinch).
|
||||
const MAGNIFY_ZOOM_FACTOR_EPSILON: float = 0.02
|
||||
## Accumulated log(factor) before one pinch zoom band step.
|
||||
const MAGNIFY_ZOOM_LOG_ACCUMULATOR_THRESHOLD: float = 0.15
|
||||
|
||||
## Minimum ms between trackpad-driven zoom band steps (avoids flick bursts).
|
||||
const TRACKPAD_ZOOM_STEP_COOLDOWN_MSEC: int = 150
|
||||
|
||||
const CameraStateScript := preload("res://scripts/camera_state.gd")
|
||||
const ZoomBandConfigScript := preload("res://scripts/zoom_band_config.gd")
|
||||
|
|
@ -55,6 +58,9 @@ var _state = null
|
|||
var _smoothed_eye: Vector3 = Vector3.ZERO
|
||||
var _orbit_yaw_rad: float = 0.0
|
||||
var _zoom_band_index: int = 0
|
||||
var _pan_zoom_accumulator: float = 0.0
|
||||
var _magnify_zoom_accumulator: float = 0.0
|
||||
var _last_trackpad_zoom_step_msec: int = -1_000_000
|
||||
var _warned_missing_follow_target: bool = false
|
||||
## Keys: [code]int[/code] [method Object.get_instance_id] of the occluder body. Values: Array of
|
||||
## {mesh, surface, original} restore entries.
|
||||
|
|
@ -134,13 +140,19 @@ 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)
|
||||
if event is InputEventPanGesture:
|
||||
var pan: InputEventPanGesture = event as InputEventPanGesture
|
||||
var pan_result: Dictionary = consume_pan_zoom_accumulator(_pan_zoom_accumulator, pan.delta)
|
||||
_pan_zoom_accumulator = float(pan_result.get("accumulator", _pan_zoom_accumulator))
|
||||
_try_apply_trackpad_zoom_step(int(pan_result.get("step", 0)))
|
||||
return
|
||||
var magnify_step: int = zoom_step_from_magnify_gesture(event)
|
||||
if magnify_step != 0:
|
||||
_apply_zoom_step(magnify_step)
|
||||
if event is InputEventMagnifyGesture:
|
||||
var mag: InputEventMagnifyGesture = event as InputEventMagnifyGesture
|
||||
var mag_result: Dictionary = consume_magnify_zoom_accumulator(
|
||||
_magnify_zoom_accumulator, mag.factor
|
||||
)
|
||||
_magnify_zoom_accumulator = float(mag_result.get("accumulator", _magnify_zoom_accumulator))
|
||||
_try_apply_trackpad_zoom_step(int(mag_result.get("step", 0)))
|
||||
return
|
||||
if event.is_action_pressed("camera_zoom_in"):
|
||||
_apply_zoom_step(-1)
|
||||
|
|
@ -148,30 +160,46 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
_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
|
||||
func _try_apply_trackpad_zoom_step(step: int) -> void:
|
||||
if step == 0:
|
||||
return
|
||||
var now_msec: int = Time.get_ticks_msec()
|
||||
if now_msec - _last_trackpad_zoom_step_msec < TRACKPAD_ZOOM_STEP_COOLDOWN_MSEC:
|
||||
return
|
||||
_last_trackpad_zoom_step_msec = now_msec
|
||||
_apply_zoom_step(step)
|
||||
|
||||
|
||||
## 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
|
||||
## Accumulates trackpad pan delta; emits at most **one** band step per call.
|
||||
static func consume_pan_zoom_accumulator(
|
||||
accumulator: float,
|
||||
delta: Vector2,
|
||||
threshold: float = PAN_ZOOM_BAND_ACCUMULATOR_THRESHOLD
|
||||
) -> Dictionary:
|
||||
if absf(delta.y) <= absf(delta.x):
|
||||
return {"accumulator": accumulator, "step": 0}
|
||||
var next: float = accumulator + delta.y
|
||||
if next <= -threshold:
|
||||
return {"accumulator": next + threshold, "step": -1}
|
||||
if next >= threshold:
|
||||
return {"accumulator": next - threshold, "step": 1}
|
||||
return {"accumulator": next, "step": 0}
|
||||
|
||||
|
||||
## Accumulates pinch log-scale; emits at most **one** band step per call.
|
||||
static func consume_magnify_zoom_accumulator(
|
||||
accumulator: float,
|
||||
factor: float,
|
||||
threshold: float = MAGNIFY_ZOOM_LOG_ACCUMULATOR_THRESHOLD
|
||||
) -> Dictionary:
|
||||
if factor <= 0.0:
|
||||
return {"accumulator": accumulator, "step": 0}
|
||||
var next: float = accumulator + log(factor)
|
||||
if next >= threshold:
|
||||
return {"accumulator": next - threshold, "step": -1}
|
||||
if next <= -threshold:
|
||||
return {"accumulator": next + threshold, "step": 1}
|
||||
return {"accumulator": next, "step": 0}
|
||||
|
||||
|
||||
## **Near** = lower band index. Zoom in → closer → decrease index (clamped).
|
||||
|
|
|
|||
|
|
@ -140,42 +140,43 @@ func test_occluder_override_key_is_valid_false_after_free() -> void:
|
|||
assert_that(ok).is_false()
|
||||
|
||||
|
||||
func test_zoom_step_from_pan_gesture_ignores_non_pan() -> void:
|
||||
func test_consume_pan_zoom_accumulator_ignores_horizontal_pan() -> void:
|
||||
# Arrange
|
||||
var key := InputEventKey.new()
|
||||
# Act
|
||||
var step: int = IsoScript.zoom_step_from_pan_gesture(key)
|
||||
var result: Dictionary = IsoScript.consume_pan_zoom_accumulator(0.0, Vector2(0.2, 0.0))
|
||||
# Assert
|
||||
assert_that(step).is_equal(0)
|
||||
assert_that(int(result.get("step", -9))).is_equal(0)
|
||||
assert_that(float(result.get("accumulator", -1.0))).is_equal(0.0)
|
||||
|
||||
|
||||
func test_zoom_step_from_pan_gesture_maps_vertical_scroll_to_bands() -> void:
|
||||
func test_consume_pan_zoom_accumulator_requires_threshold_before_step() -> 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)
|
||||
var partial: Dictionary = IsoScript.consume_pan_zoom_accumulator(0.0, Vector2(0.0, -0.4))
|
||||
var complete: Dictionary = IsoScript.consume_pan_zoom_accumulator(
|
||||
float(partial.get("accumulator", 0.0)), Vector2(0.0, -0.9)
|
||||
)
|
||||
# Assert
|
||||
assert_that(up_step).is_equal(-1)
|
||||
assert_that(down_step).is_equal(1)
|
||||
assert_that(horiz_step).is_equal(0)
|
||||
assert_that(int(partial.get("step", -9))).is_equal(0)
|
||||
assert_that(int(complete.get("step", -9))).is_equal(-1)
|
||||
|
||||
|
||||
func test_zoom_step_from_magnify_gesture_maps_pinch_to_bands() -> void:
|
||||
func test_consume_pan_zoom_accumulator_emits_one_step_per_call_even_for_large_delta() -> 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)
|
||||
var result: Dictionary = IsoScript.consume_pan_zoom_accumulator(0.0, Vector2(0.0, -5.0))
|
||||
# Assert
|
||||
assert_that(in_step).is_equal(1)
|
||||
assert_that(out_step).is_equal(-1)
|
||||
assert_that(int(result.get("step", -9))).is_equal(-1)
|
||||
assert_that(float(result.get("accumulator", 0.0))).is_equal(-3.75)
|
||||
|
||||
|
||||
func test_consume_magnify_zoom_accumulator_requires_threshold_before_step() -> void:
|
||||
# Arrange
|
||||
# Act
|
||||
var partial: Dictionary = IsoScript.consume_magnify_zoom_accumulator(0.0, 1.05)
|
||||
var complete: Dictionary = IsoScript.consume_magnify_zoom_accumulator(
|
||||
float(partial.get("accumulator", 0.0)), 1.12
|
||||
)
|
||||
# Assert
|
||||
assert_that(int(partial.get("step", -9))).is_equal(0)
|
||||
assert_that(int(complete.get("step", -9))).is_equal(-1)
|
||||
|
|
|
|||
Loading…
Reference in New Issue