From 295f79011ffb1870e84a35c8fa58fab0cdc80c24 Mon Sep 17 00:00:00 2001 From: slyphax Date: Sun, 1 Nov 2026 19:26:17 +0100 Subject: [PATCH] =?UTF-8?q?Werkzeuge=20h=C3=A4ngen=20nicht=20mehr=20an=20d?= =?UTF-8?q?er=20Maus:=20PointerEvent,=20MousePointer,=20PointerRouter;=20T?= =?UTF-8?q?est=20Maus=20gegen=20Zeiger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- core | 2 +- src/editor/interaction.gd | 163 ++++++++++++------------- src/editor/mouse_pointer.gd | 39 ++++++ src/editor/mouse_pointer.gd.uid | 1 + src/editor/playtest/test_start_tool.gd | 22 ++-- src/editor/pointer_router.gd | 13 ++ src/editor/pointer_router.gd.uid | 1 + src/editor/spawn_tool.gd | 64 +++++----- src/editor/terrain_tool.gd | 69 ++++++----- src/viewport/gizmo.gd | 84 +++++++------ tests/test_pointer_router.gd | 156 +++++++++++++++++++++++ tests/test_pointer_router.gd.uid | 1 + 12 files changed, 420 insertions(+), 195 deletions(-) create mode 100644 src/editor/mouse_pointer.gd create mode 100644 src/editor/mouse_pointer.gd.uid create mode 100644 src/editor/pointer_router.gd create mode 100644 src/editor/pointer_router.gd.uid create mode 100644 tests/test_pointer_router.gd create mode 100644 tests/test_pointer_router.gd.uid diff --git a/core b/core index 92ee3fb..3ee60fb 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 92ee3fbdd1102d24bd34ad9302a494973a525fc3 +Subproject commit 3ee60fb9297a3a850f0a504a7032f44dc8ad2dea diff --git a/src/editor/interaction.gd b/src/editor/interaction.gd index 773f27e..266078f 100644 --- a/src/editor/interaction.gd +++ b/src/editor/interaction.gd @@ -1401,111 +1401,106 @@ func mirror_selected(axis: String) -> void: func _unhandled_input(event: InputEvent) -> void: # Strg-Zwang (Karte t_55257792) aus JEDEM durchgelassenen Event verfolgen — # Maus-Events tragen den Modifier, Tasten-Events ebenso (nicht konsumieren!). - if event is InputEventMouseMotion: - _ctrl_held = (event as InputEventMouseMotion).ctrl_pressed - elif event is InputEventMouseButton: - _ctrl_held = (event as InputEventMouseButton).ctrl_pressed - elif event is InputEventKey: + if event is InputEventKey: _ctrl_held = (event as InputEventKey).ctrl_pressed + var p := MousePointer.from_input(event) + if p != null and handle_pointer(p): + get_viewport().set_input_as_handled() + + +## Rechte Maustaste kommt hier nie an (Karte t_2470de89): sie gehört mit +## absolutem Vorrang dem Freelook-Flug (orbit_camera.gd). Beim Platzieren +## bleibt das Objekt IN der Hand, Abwurf nur über Esc. +func handle_pointer(p: PointerEvent) -> bool: + if p.kind != PointerEvent.Kind.CANCEL: + _ctrl_held = p.ctrl var mode: String = main.get("tool_mode") if mode == "terrain" or mode == "spawns": - return # Terrain-Pinsel / Spawn-Zonen haben das Zepter (eigene Werkzeuge) - if event is InputEventMouseMotion: - var mm := event as InputEventMouseMotion - if placing: - _ghost_update(mm.position) - if _grabbing: - var gh: Variant = _ground_point(mm.position) - if gh != null: - grab_move(gh as Vector3) - return - if _rubber_pending or _rubber_active: - var dist := Vector2(mm.position.x - _press_pos.x, mm.position.y - _press_pos.y).length() - if dist > DRAG_THRESHOLD_PX: - _rubber_active = true - _rubber_pending = false - if _rubber_active: - main.call("_rubberband_update", _rubber_start, mm.position) - return - if _dragging and not placing and not _drag_members.is_empty(): - _drag_members_move(mm.position) - elif _dragging and not placing and selected_index >= 0: - # Einzel-Greifen (alter Pfad, ohne Gruppe/Mehrfachauswahl) - var p: Variant = _ground_point(mm.position) - if p == null: - return - var new_pos: Vector3 = _apply_grid((p as Vector3) + _grab_offset) - var half := _terrain_half() - new_pos.x = clampf(new_pos.x, -half.x, half.x) - new_pos.z = clampf(new_pos.z, -half.y, half.y) - # Höhe: der Terrain-Abstand bleibt beim Ziehen erhalten (angehoben - # schwebt weiter oben) — kein harter Terrain-Clamp mehr, t_7567a06f. - new_pos.y = SvmapDocument.terrain_height_at(main.doc, new_pos.x, new_pos.z) + _drag_lift - var dist := Vector2(mm.position.x - _press_pos.x, mm.position.y - _press_pos.y).length() - if dist > DRAG_THRESHOLD_PX: - _drag_moved = true - if _drag_moved: - var marker: Node3D = view_builder.object_markers[selected_index] - if marker != null and is_instance_valid(marker): - marker.position = new_pos - return - if event is InputEventMouseButton: - var mb := event as InputEventMouseButton - if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: + return false # Terrain-Pinsel / Spawn-Zonen haben das Zepter (eigene Werkzeuge) + match p.kind: + PointerEvent.Kind.MOVE: + _on_pointer_move(p.position) + PointerEvent.Kind.PRESS: if _grabbing: grab_confirm() - get_viewport().set_input_as_handled() - return - _on_left_press(mb) - get_viewport().set_input_as_handled() - elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: - _on_left_release(mb) - # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) — - # sie gehört mit absolutem Vorrang dem Freelook-Flug (orbit_camera.gd). - # Beim Platzieren bleibt das Objekt IN der Hand, Abwurf nur über Esc. - elif mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed: + return true + _on_left_press(p) + return true + PointerEvent.Kind.RELEASE: + _on_left_release(p) + PointerEvent.Kind.SCROLL: if _camera_flying(): - return # Mausrad = Flug-Tempo (orbit_camera), kein Objekt-Drehen + return false # Mausrad = Flug-Tempo (orbit_camera), kein Objekt-Drehen if placing: - place_rot_deg = fmod(place_rot_deg + (90.0 if BauKatalog.ist_bau(place_oid) else ROT_STEP_DEG) + 3600.0, 360.0) + place_rot_deg = fmod(place_rot_deg + p.scroll * (90.0 if BauKatalog.ist_bau(place_oid) else ROT_STEP_DEG) + 3600.0, 360.0) _ghost_rotation_anwenden() - get_viewport().set_input_as_handled() - elif selected_index >= 0 and not _grabbing: - rotate_selected(ROT_STEP_DEG) - get_viewport().set_input_as_handled() - elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed: - if _camera_flying(): - return # Mausrad = Flug-Tempo (orbit_camera), kein Objekt-Drehen - if placing: - place_rot_deg = fmod(place_rot_deg - (90.0 if BauKatalog.ist_bau(place_oid) else ROT_STEP_DEG) + 3600.0, 360.0) - _ghost_rotation_anwenden() - get_viewport().set_input_as_handled() - elif selected_index >= 0 and not _grabbing: - rotate_selected(-ROT_STEP_DEG) - get_viewport().set_input_as_handled() + return true + if selected_index >= 0 and not _grabbing: + rotate_selected(p.scroll * ROT_STEP_DEG) + return true + return false -func _on_left_press(mb: InputEventMouseButton) -> void: +func _on_pointer_move(pos: Vector2) -> void: if placing: - _place_object(mb.position, mb.shift_pressed) # U5: Shift = Serienmodus + _ghost_update(pos) + if _grabbing: + var gh: Variant = _ground_point(pos) + if gh != null: + grab_move(gh as Vector3) return - _press_pos = mb.position - var hit := _pick_object(mb.position) + if _rubber_pending or _rubber_active: + var dist := Vector2(pos.x - _press_pos.x, pos.y - _press_pos.y).length() + if dist > DRAG_THRESHOLD_PX: + _rubber_active = true + _rubber_pending = false + if _rubber_active: + main.call("_rubberband_update", _rubber_start, pos) + return + if _dragging and not placing and not _drag_members.is_empty(): + _drag_members_move(pos) + elif _dragging and not placing and selected_index >= 0: + # Einzel-Greifen (alter Pfad, ohne Gruppe/Mehrfachauswahl) + var p: Variant = _ground_point(pos) + if p == null: + return + var new_pos: Vector3 = _apply_grid((p as Vector3) + _grab_offset) + var half := _terrain_half() + new_pos.x = clampf(new_pos.x, -half.x, half.x) + new_pos.z = clampf(new_pos.z, -half.y, half.y) + # Höhe: der Terrain-Abstand bleibt beim Ziehen erhalten (angehoben + # schwebt weiter oben) — kein harter Terrain-Clamp mehr, t_7567a06f. + new_pos.y = SvmapDocument.terrain_height_at(main.doc, new_pos.x, new_pos.z) + _drag_lift + var dist := Vector2(pos.x - _press_pos.x, pos.y - _press_pos.y).length() + if dist > DRAG_THRESHOLD_PX: + _drag_moved = true + if _drag_moved: + var marker: Node3D = view_builder.object_markers[selected_index] + if marker != null and is_instance_valid(marker): + marker.position = new_pos + + +func _on_left_press(p: PointerEvent) -> void: + if placing: + _place_object(p.position, p.shift) # U5: Shift = Serienmodus + return + _press_pos = p.position + var hit := _pick_object(p.position) if hit >= 0: - if mb.shift_pressed or mb.is_command_or_control_pressed(): + if p.shift or p.command: # Umschalt+Klick / Strg+Klick (Karte t_c46d31c2): zur Auswahl # hinzufügen/entfernen — kein Ziehen. select_toggle(hit) return if not selected_indices.has(hit): select_object(hit) - _begin_drag(hit, mb.position) + _begin_drag(hit, p.position) else: # Leere Stelle: Rahmen-Ausziehen vorbereiten; loser Klick = Auswahl weg. _rubber_pending = true _rubber_active = false - _rubber_start = mb.position - _rubber_shift = mb.shift_pressed + _rubber_start = p.position + _rubber_shift = p.shift ## Zieh-Menge festlegen: Mehrfachauswahl oder komplette Gruppe ziehen GEMEINSAM. @@ -1606,14 +1601,14 @@ func _drag_members_move(viewport_pos: Vector2) -> void: _sym_drag_sync_live() -func _on_left_release(mb: InputEventMouseButton) -> void: +func _on_left_release(p: PointerEvent) -> void: if _rubber_pending or _rubber_active: var was_active := _rubber_active _rubber_pending = false _rubber_active = false main.call("_rubberband_clear") if was_active: - _finish_rubberband(mb.position) + _finish_rubberband(p.position) elif not _rubber_shift: # Loser Klick auf leere Fläche: Auswahl aufheben (Klick statt Auswendiglernen). if not selected_indices.is_empty(): diff --git a/src/editor/mouse_pointer.gd b/src/editor/mouse_pointer.gd new file mode 100644 index 0000000..cff83d9 --- /dev/null +++ b/src/editor/mouse_pointer.gd @@ -0,0 +1,39 @@ +class_name MousePointer +extends RefCounted +## Übersetzt Maus und Esc-Taste in PointerEvent. Die rechte Maustaste fehlt +## absichtlich: sie gehört dem Freelook-Flug (orbit_camera.gd). + + +static func from_input(event: InputEvent) -> PointerEvent: + if event is InputEventMouseMotion: + var mm := event as InputEventMouseMotion + var p := PointerEvent.make(PointerEvent.Kind.MOVE, mm.position) + p.shift = mm.shift_pressed + p.ctrl = mm.ctrl_pressed + p.command = mm.is_command_or_control_pressed() + p.pressure = mm.pressure + return p + if event is InputEventMouseButton: + var mb := event as InputEventMouseButton + var kind := PointerEvent.Kind.MOVE + var scroll := 0 + match mb.button_index: + MOUSE_BUTTON_LEFT: + kind = PointerEvent.Kind.PRESS if mb.pressed else PointerEvent.Kind.RELEASE + MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN: + if not mb.pressed: + return null + kind = PointerEvent.Kind.SCROLL + scroll = 1 if mb.button_index == MOUSE_BUTTON_WHEEL_UP else -1 + _: + return null + var p := PointerEvent.make(kind, mb.position) + p.shift = mb.shift_pressed + p.ctrl = mb.ctrl_pressed + p.command = mb.is_command_or_control_pressed() + p.scroll = scroll + return p + if event is InputEventKey and (event as InputEventKey).pressed \ + and (event as InputEventKey).keycode == KEY_ESCAPE: + return PointerEvent.make(PointerEvent.Kind.CANCEL) + return null diff --git a/src/editor/mouse_pointer.gd.uid b/src/editor/mouse_pointer.gd.uid new file mode 100644 index 0000000..3b06f99 --- /dev/null +++ b/src/editor/mouse_pointer.gd.uid @@ -0,0 +1 @@ +uid://1nyr0qwwdvnv diff --git a/src/editor/playtest/test_start_tool.gd b/src/editor/playtest/test_start_tool.gd index 88ea183..702c1ff 100644 --- a/src/editor/playtest/test_start_tool.gd +++ b/src/editor/playtest/test_start_tool.gd @@ -83,16 +83,20 @@ func refresh_from_doc() -> void: # --------------------------------------------------------------- Eingaben --- func _unhandled_input(event: InputEvent) -> void: + var p := MousePointer.from_input(event) + if p != null and handle_pointer(p): + get_viewport().set_input_as_handled() + + +func handle_pointer(p: PointerEvent) -> bool: if not active: - return - if event is InputEventMouseMotion: - _update_cursor((event as InputEventMouseMotion).position) - return - if event is InputEventMouseButton: - var mb := event as InputEventMouseButton - if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: - get_viewport().set_input_as_handled() - _click(mb.position) + return false + if p.kind == PointerEvent.Kind.MOVE: + _update_cursor(p.position) + elif p.kind == PointerEvent.Kind.PRESS: + _click(p.position) + return true + return false func _click(viewport_pos: Vector2) -> void: diff --git a/src/editor/pointer_router.gd b/src/editor/pointer_router.gd new file mode 100644 index 0000000..782764b --- /dev/null +++ b/src/editor/pointer_router.gd @@ -0,0 +1,13 @@ +class_name PointerRouter +extends RefCounted +## Gibt eine Zeige-Eingabe in derselben Reihenfolge an die Werkzeuge wie Godot +## die Maus: tiefer und später im Baum zuerst, das erste Werkzeug nimmt sie. +## Für Eingaben, die nicht über den Viewport kommen, also den VR-Controller. + + +static func dispatch(main: Node, p: PointerEvent) -> bool: + for key in ["test_start_tool", "spawn_tool", "terrain_tool", "gizmo", "interaction"]: + var werkzeug: Variant = main.get(key) + if werkzeug != null and bool(werkzeug.call("handle_pointer", p)): + return true + return false diff --git a/src/editor/pointer_router.gd.uid b/src/editor/pointer_router.gd.uid new file mode 100644 index 0000000..445bba9 --- /dev/null +++ b/src/editor/pointer_router.gd.uid @@ -0,0 +1 @@ +uid://bem4edb2s282 diff --git a/src/editor/spawn_tool.gd b/src/editor/spawn_tool.gd index 23df0fe..e30090c 100644 --- a/src/editor/spawn_tool.gd +++ b/src/editor/spawn_tool.gd @@ -158,38 +158,42 @@ func set_radius(m: float) -> void: # --------------------------------------------------------------- Eingaben --- func _unhandled_input(event: InputEvent) -> void: + var p := MousePointer.from_input(event) + if p != null and handle_pointer(p): + get_viewport().set_input_as_handled() + + +## Rechte Maustaste kommt hier nie an (Karte t_2470de89): sie gehört mit +## absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug beenden läuft über +## ESC (main.gd), die Auswahl bleibt im Flug erhalten. +func handle_pointer(p: PointerEvent) -> bool: if not active: - return - if event is InputEventMouseMotion: - var mm := event as InputEventMouseMotion - _update_cursor(mm.position) - if not _arrow_grab.is_empty(): - _drag_arrow(mm.position) - return - if _dragging and selected_team >= 0: - var hit: Variant = _ray_hit(mm.position) - if hit == null: - return - var pos: Vector3 = _clamped(hit as Vector3) + _grab_offset - var half := _terrain_half() - pos.x = clampf(pos.x, -half.x + zone_radius, half.x - zone_radius) - pos.z = clampf(pos.z, -half.y + zone_radius, half.y - zone_radius) - pos.y = SvmapDocument.terrain_height_at(main.doc, pos.x, pos.z) - _drag_moved = true - var marker: Node3D = _marker_of(selected_team, selected_id) - if marker != null and is_instance_valid(marker): - marker.position = pos - return - if event is InputEventMouseButton: - var mb := event as InputEventMouseButton - if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: - _on_left_press(mb.position) - get_viewport().set_input_as_handled() - elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: + return false + match p.kind: + PointerEvent.Kind.MOVE: + _update_cursor(p.position) + if not _arrow_grab.is_empty(): + _drag_arrow(p.position) + return false + if _dragging and selected_team >= 0: + var hit: Variant = _ray_hit(p.position) + if hit == null: + return false + var pos: Vector3 = _clamped(hit as Vector3) + _grab_offset + var half := _terrain_half() + pos.x = clampf(pos.x, -half.x + zone_radius, half.x - zone_radius) + pos.z = clampf(pos.z, -half.y + zone_radius, half.y - zone_radius) + pos.y = SvmapDocument.terrain_height_at(main.doc, pos.x, pos.z) + _drag_moved = true + var marker: Node3D = _marker_of(selected_team, selected_id) + if marker != null and is_instance_valid(marker): + marker.position = pos + PointerEvent.Kind.PRESS: + _on_left_press(p.position) + return true + PointerEvent.Kind.RELEASE: _on_left_release() - # Rechte Maustaste: absichtlich NICHT behandelt (Karte t_2470de89) — - # sie gehört mit absolutem Vorrang dem Freelook-Flug. Deselect/Werkzeug - # beenden läuft über ESC (main.gd), die Auswahl bleibt im Flug erhalten. + return false func _update_cursor(viewport_pos: Vector2) -> void: diff --git a/src/editor/terrain_tool.gd b/src/editor/terrain_tool.gd index 9468dd8..0404d18 100644 --- a/src/editor/terrain_tool.gd +++ b/src/editor/terrain_tool.gd @@ -220,38 +220,45 @@ func set_paint_material(id: int) -> void: # --------------------------------------------------------------- Eingaben --- func _unhandled_input(event: InputEvent) -> void: + var p := MousePointer.from_input(event) + if p == null: + return + # Stift: pressure > 0; Maus meldet 0 (oder 1) → bleibt voll. + if p.kind == PointerEvent.Kind.MOVE: + p.pressure = TerrainField.pressure_of(p.pressure, _trigger_axis()) + if handle_pointer(p): + get_viewport().set_input_as_handled() + + +## Rechte Maustaste kommt hier nie an: sie gehört mit absolutem Vorrang dem +## Freelook-Flug (orbit_camera.gd, Karte t_2470de89), in jedem Werkzeugzustand. +func handle_pointer(p: PointerEvent) -> bool: if not active: - return - if event is InputEventMouseMotion: - var mm := event as InputEventMouseMotion - _update_cursor(mm.position) - # Stift: pressure > 0; Maus meldet 0 (oder 1) → bleibt voll. - pressure = TerrainField.pressure_of(mm.pressure, _trigger_axis()) - return - if event is InputEventMouseButton: - var mb := event as InputEventMouseButton - if mb.button_index == MOUSE_BUTTON_LEFT: - if mb.pressed: - # Karte t_2470de89: Strg = Invert-Modifier. Heben↔Senken - # tauschen (Blender/Photoshop-Konvention), im Malen-Modus - # malt Strg+Klick Gras zurück (alter Radierer-Weg). - var mode := brush_mode - var mat := -1 - if mb.ctrl_pressed: - match brush_mode: - TerrainField.Mode.RAISE: - mode = TerrainField.Mode.LOWER - TerrainField.Mode.LOWER: - mode = TerrainField.Mode.RAISE - TerrainField.Mode.PAINT: - mat = 0 # Gras zurückmalen - _begin_stroke(mode, mat) - else: - _end_stroke() - get_viewport().set_input_as_handled() - # Rechte Maustaste: absichtlich NICHT behandelt — sie gehört mit - # absolutem Vorrang dem Freelook-Flug (orbit_camera.gd, Karte - # t_2470de89), in jedem Werkzeugzustand. + return false + match p.kind: + PointerEvent.Kind.MOVE: + _update_cursor(p.position) + pressure = p.pressure + PointerEvent.Kind.PRESS: + # Karte t_2470de89: Strg = Invert-Modifier. Heben↔Senken + # tauschen (Blender/Photoshop-Konvention), im Malen-Modus + # malt Strg+Klick Gras zurück (alter Radierer-Weg). + var mode := brush_mode + var mat := -1 + if p.ctrl: + match brush_mode: + TerrainField.Mode.RAISE: + mode = TerrainField.Mode.LOWER + TerrainField.Mode.LOWER: + mode = TerrainField.Mode.RAISE + TerrainField.Mode.PAINT: + mat = 0 # Gras zurückmalen + _begin_stroke(mode, mat) + return true + PointerEvent.Kind.RELEASE: + _end_stroke() + return true + return false ## Gamepad-Trigger (rechts) des ersten Pads; 0 ohne Pad. Nur Eingabe-Lesen, diff --git a/src/viewport/gizmo.gd b/src/viewport/gizmo.gd index aff4d5d..e57dc4d 100644 --- a/src/viewport/gizmo.gd +++ b/src/viewport/gizmo.gd @@ -220,61 +220,65 @@ func _process(_delta: float) -> void: func _unhandled_input(event: InputEvent) -> void: + var p := MousePointer.from_input(event) + if p != null and handle_pointer(p): + get_viewport().set_input_as_handled() + + +func handle_pointer(p: PointerEvent) -> bool: if main == null or camera == null: - return + return false var doc: Dictionary = main.get("doc") if doc == null or doc.is_empty(): - return + return false var tool_mode: String = str(main.get("tool_mode")) if tool_mode == "terrain" or tool_mode == "spawns": - return # Terrain-Pinsel / Spawn-Zonen haben das Zepter - if event is InputEventMouseButton: - var mb := event as InputEventMouseButton - if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed: + return false # Terrain-Pinsel / Spawn-Zonen haben das Zepter + match p.kind: + PointerEvent.Kind.PRESS: if _dragging: - return + return false var inter = main.get("interaction") if inter == null or bool(inter.get("placing")): - return + return false if mode == "rotate": - var ring_axis := pick_ring(mb.position) + var ring_axis := pick_ring(p.position) if ring_axis != "": - _begin_rotate(ring_axis, mb.position) - get_viewport().set_input_as_handled() - return - var axis := pick(mb.position) + _begin_rotate(ring_axis, p.position) + return true + return false + var axis := pick(p.position) if axis != "": - _begin_drag(axis, mb.position) - get_viewport().set_input_as_handled() - elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: + _begin_drag(axis, p.position) + return true + PointerEvent.Kind.RELEASE: if _rot_dragging: _finish_rotate(true) - get_viewport().set_input_as_handled() - elif _dragging: + return true + if _dragging: _finish_drag(true) - get_viewport().set_input_as_handled() - elif (mb.button_index == MOUSE_BUTTON_WHEEL_UP \ - or mb.button_index == MOUSE_BUTTON_WHEEL_DOWN) and mb.pressed: + return true + PointerEvent.Kind.SCROLL: if _rot_dragging: - get_viewport().set_input_as_handled() # Mausrad zoomt nicht mitten im Dreh-Zug - elif _dragging: - var dir := 1 if mb.button_index == MOUSE_BUTTON_WHEEL_UP else -1 - cycle_step(dir) - get_viewport().set_input_as_handled() - elif event is InputEventMouseMotion and _rot_dragging: - _rotate_motion((event as InputEventMouseMotion).position) - get_viewport().set_input_as_handled() - elif event is InputEventMouseMotion and _dragging: - _drag_motion((event as InputEventMouseMotion).position) - get_viewport().set_input_as_handled() - elif event is InputEventKey and (event as InputEventKey).pressed \ - and (event as InputEventKey).keycode == KEY_ESCAPE: - if _rot_dragging: - _finish_rotate(false) - get_viewport().set_input_as_handled() - elif _dragging: - _finish_drag(false) - get_viewport().set_input_as_handled() + return true # Mausrad zoomt nicht mitten im Dreh-Zug + if _dragging: + cycle_step(p.scroll) + return true + PointerEvent.Kind.MOVE: + if _rot_dragging: + _rotate_motion(p.position) + return true + if _dragging: + _drag_motion(p.position) + return true + PointerEvent.Kind.CANCEL: + if _rot_dragging: + _finish_rotate(false) + return true + if _dragging: + _finish_drag(false) + return true + return false # ------------------------------------------------------------- Treffer ----- diff --git a/tests/test_pointer_router.gd b/tests/test_pointer_router.gd new file mode 100644 index 0000000..9bd315d --- /dev/null +++ b/tests/test_pointer_router.gd @@ -0,0 +1,156 @@ +extends SceneTree +## Editor-Test (headless): Werkzeuge laufen ohne Maus. Dieselben Züge einmal als +## echte Maus-Ereignisse und einmal als PointerEvent müssen dieselbe Karte ergeben. +## Ausführen: godot --headless --path . --script res://tests/test_pointer_router.gd + +const OID := "box_001" + +var failures := 0 +var checks := 0 + + +func _initialize() -> void: + _run.call_deferred() + + +func _check(cond: bool, label: String) -> void: + checks += 1 + if cond: + print(" ok: ", label) + else: + failures += 1 + print(" FAIL: ", label) + + +func _run() -> void: + print("== Objekte: Maus gegen PointerEvent ==") + var maus := await _objekt_zuege(true) + var zeiger := await _objekt_zuege(false) + # Neue Karten spiegeln: jedes gesetzte Objekt bekommt einen Zwilling. + _check(int(zeiger["anzahl"]) == 4, "Zeiger: zwei Objekte mit Zwillingen gesetzt (%d)" % int(zeiger["anzahl"])) + _check(int(zeiger["verschoben"]) == 1, "Zeiger: Ziehen hat ein Objekt bewegt") + _check(int(zeiger["undo"]) == 3, "Zeiger: drei Undo-Schritte") + _check(str(maus["objekte"]) == str(zeiger["objekte"]), "Maus und Zeiger ergeben dieselben Objekte") + _check(int(maus["undo"]) == int(zeiger["undo"]), "Maus und Zeiger ergeben gleich viele Undo-Schritte") + + print("== Terrain und Abbruch ==") + await _terrain_und_abbruch() + + if failures == 0: + print("ALL OK: %d checks" % checks) + quit(0) + else: + print("FAILED: %d/%d checks" % [failures, checks]) + quit(1) + + +func _neu() -> Control: + var main: Control = (load("res://scenes/main.tscn") as PackedScene).instantiate() + root.add_child(main) + await process_frame + await process_frame + main.camera.global_position = Vector3(0.0, 200.0, 0.0) + main.camera.look_at(Vector3.ZERO, Vector3.FORWARD) + main.call("_set_tool_mode", "objects") + await process_frame + return main + + +func _bild(main: Control, welt: Vector3) -> Vector2: + return main.camera.unproject_position(welt) + + +## Ein Zug: als Mausereignis durch den echten Viewport oder direkt über den Router. +func _zug(main: Control, ueber_maus: bool, kind: PointerEvent.Kind, pos: Vector2, shift := false) -> void: + if ueber_maus: + var ev: InputEvent + if kind == PointerEvent.Kind.MOVE: + var mm := InputEventMouseMotion.new() + mm.position = pos + ev = mm + else: + var mb := InputEventMouseButton.new() + mb.position = pos + mb.button_index = MOUSE_BUTTON_LEFT + mb.pressed = kind == PointerEvent.Kind.PRESS + mb.shift_pressed = shift + ev = mb + main.camera.get_viewport().push_input(ev) + else: + var p := PointerEvent.make(kind, pos) + p.shift = shift + PointerRouter.dispatch(main, p) + await process_frame + + +func _objekt_zuege(ueber_maus: bool) -> Dictionary: + var main := await _neu() + var davor: int = (main.doc.get("objects", []) as Array).size() + var a := Vector3(10.0, 0.0, 10.0) + var b := Vector3(-20.0, 0.0, 5.0) + var ziel := Vector3(30.0, 0.0, -15.0) + # Serienmodus: Umschalt setzt und behält das Objekt in der Hand. + main.interaction.start_placement(OID) + await _zug(main, ueber_maus, PointerEvent.Kind.MOVE, _bild(main, a)) + await _zug(main, ueber_maus, PointerEvent.Kind.PRESS, _bild(main, a), true) + await _zug(main, ueber_maus, PointerEvent.Kind.RELEASE, _bild(main, a), true) + await _zug(main, ueber_maus, PointerEvent.Kind.MOVE, _bild(main, b)) + await _zug(main, ueber_maus, PointerEvent.Kind.PRESS, _bild(main, b)) + await _zug(main, ueber_maus, PointerEvent.Kind.RELEASE, _bild(main, b)) + main.interaction.cancel_placement() + main.interaction.clear_selection() + await process_frame + var objekte: Array = main.doc.get("objects", []) + var vorher: Array = (objekte[0]["pos"] as Array).duplicate() + # Greifen, ziehen, loslassen. Die Mitte des Objekts statt des Gizmos treffen: + # ohne Auswahl ist das Gizmo unsichtbar. + var start := Vector3(float(vorher[0]), float(vorher[1]), float(vorher[2])) + await _zug(main, ueber_maus, PointerEvent.Kind.PRESS, _bild(main, start)) + main.gizmo.visible = false + await _zug(main, ueber_maus, PointerEvent.Kind.MOVE, _bild(main, ziel)) + await _zug(main, ueber_maus, PointerEvent.Kind.RELEASE, _bild(main, ziel)) + var ergebnis := { + "anzahl": objekte.size() - davor, + "objekte": JSON.stringify(objekte), + "verschoben": 1 if str(objekte[0]["pos"]) != str(vorher) else 0, + "undo": (main.history.index + 1), + } + main.queue_free() + await process_frame + return ergebnis + + +func _terrain_und_abbruch() -> void: + var main := await _neu() + main.call("_set_tool_mode", "terrain") + await process_frame + var hoehen_vorher := str(main.doc["terrain"].get("heightmap_r16_b64", "")) + var undo_vorher: int = main.history.index + 1 + var mitte := _bild(main, Vector3.ZERO) + _check(not PointerRouter.dispatch(main, PointerEvent.make(PointerEvent.Kind.MOVE, mitte)), + "Terrain: Bewegen wird nicht verschluckt") + _check(PointerRouter.dispatch(main, PointerEvent.make(PointerEvent.Kind.PRESS, mitte)), + "Terrain: Drücken nimmt der Pinsel") + for i in 6: + await process_frame + _check(PointerRouter.dispatch(main, PointerEvent.make(PointerEvent.Kind.RELEASE, mitte)), + "Terrain: Loslassen nimmt der Pinsel") + await process_frame + _check(str(main.doc["terrain"].get("heightmap_r16_b64", "")) != hoehen_vorher, "Terrain: Strich ändert die Höhen") + _check((main.history.index + 1) == undo_vorher + 1, "Terrain: ein Strich, ein Undo-Schritt") + + main.call("_set_tool_mode", "objects") + await process_frame + _check(not PointerRouter.dispatch(main, PointerEvent.make(PointerEvent.Kind.CANCEL)), + "Esc ohne laufenden Zug bleibt frei fürs Hauptfenster") + var ohne := MousePointer.from_input(_rechts()) + _check(ohne == null, "Rechte Maustaste bleibt beim Flug") + main.queue_free() + await process_frame + + +func _rechts() -> InputEvent: + var mb := InputEventMouseButton.new() + mb.button_index = MOUSE_BUTTON_RIGHT + mb.pressed = true + return mb diff --git a/tests/test_pointer_router.gd.uid b/tests/test_pointer_router.gd.uid new file mode 100644 index 0000000..63a5107 --- /dev/null +++ b/tests/test_pointer_router.gd.uid @@ -0,0 +1 @@ +uid://dej077j3x2b1u