From 902181500aee158215953bf02a74f43ee026a79c Mon Sep 17 00:00:00 2001 From: slyphax Date: Sun, 25 Oct 2026 22:50:37 +0100 Subject: [PATCH] =?UTF-8?q?Drag&Drop=20aus=20der=20Bibliothek:=20Ablege-Fl?= =?UTF-8?q?=C3=A4che=20=C3=BCber=20der=203D-Ansicht=20(echter=20Maus-Drag?= =?UTF-8?q?=20getestet),=20Drag-Vorschau,=20Doku?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- docs/bauen.md | 6 +++ src/ui/main.gd | 18 ++++++++ src/ui/panels/assets_panel.gd | 5 +-- tests/probe_drag_echt.gd | 79 +++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/probe_drag_echt.gd diff --git a/docs/bauen.md b/docs/bauen.md index 5a41668..a936afb 100644 --- a/docs/bauen.md +++ b/docs/bauen.md @@ -28,6 +28,7 @@ Ein Bauteil ist ein **ganz normales Bibliotheks-Objekt** (`bau_*`, Kategorie | Dach & Giebel (klein 2 m) | Giebelwand, Satteldach, Pyramidendach | | Dach & Giebel (mittel 4 m) | Giebelwand 4 m, Satteldach 4×4, Pyramidendach 4×4 (Spitze 2 m) | | Dach & Giebel (groß 6 m) | Giebelwand 6 m, Satteldach 6×6, Pyramidendach 6×6 (Spitze 3 m) | +| Dach aus Einzelteilen (45°) | Dachschräge 2×2 m (2 m hoch), Dachecke außen (Grat), Dachecke innen (Kehle), Dachplateau, Dachstirn — massiv, rundum geschlossen; tiefe Kante/Ecke zeigt Richtung Vorschau, R dreht | | Treppe | Treppe 4 × 2 m, 3 m hoch (15 Stufen) | Fertige Pack-Objekte (Container, Hangar-Tore, Betonsperren) stehen NICHT im Bauen-Tab, @@ -47,6 +48,11 @@ sondern wie alle Objekte in der Objekt-Bibliothek unten. Pfeiler zählen von der Unterkante einer Decke/eines Fundaments**, die Decke selbst liegt auf der Wandoberkante: jedes Geschoss liegt auf Vielfachen von 3 m, mit oder ohne Decke darunter stehen die oberen Wände gleich hoch. +* **Geschoss fest:** Bild↑/Bild↓ (oder Knöpfe im Tab) wählen ein festes Geschoss + (Basis = tiefstes Teil in der Nähe, je 3 m). Der Mausstrahl trifft dann die + Geschoss-Ebene statt Wand/Boden darunter — Decken und Böden lassen sich mitten im + großen Raum setzen. Ganz unten = Automatik. +* **Kein Z-Fighting:** Wand-Außenseite steht 1 cm vor der Plattenkante. * **Ersetzen:** gleiche Art auf demselben Platz wird ersetzt (Tür in vorhandene Wand). Dasselbe Teil nochmal wird mit Erklärung abgelehnt. diff --git a/src/ui/main.gd b/src/ui/main.gd index 34f03c8..166fb30 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -78,6 +78,7 @@ var toolbar_mod # src/ui/panels/toolbar.gd var status_mod # src/ui/panels/status_bar.gd var selection_mod # src/ui/panels/selection_panel.gd (nur Logik, kein Tab mehr) var settings_mod # src/ui/panels/settings_panel.gd (im Einstellungen-Fenster) +var drop_overlay: Control # Ablege-Fläche über der 3D-Ansicht (nur während Drag aktiv) var settings_dialog: AcceptDialog # Fenster „Einstellungen“ (Knopf in der Kopfleiste) var sym_mod # src/ui/panels/symmetry_panel.gd (im Map-Tab) var map_mod # src/ui/panels/map_panel.gd @@ -700,6 +701,18 @@ func _build_center_viewport() -> SubViewportContainer: container.add_child(viewport_toolbar) viewport_toolbar.setup(self) + # Ablege-Fläche für Bibliotheks-Kacheln: Godot ruft can_drop/drop NICHT auf einem + # SubViewportContainer auf (Test-Befund 25.10.: Ziehen startete, Loslassen setzte + # nichts — echter Maus-Drag, tests/probe_drag_echt.gd). Eine durchsichtige Fläche + # darüber nimmt den Drop an; sie ist nur WÄHREND eines Ziehens aktiv (sonst + # bleibt die Maus für Kamera/Auswahl frei). + drop_overlay = Control.new() + drop_overlay.name = "AblegeFlaeche" + drop_overlay.set_anchors_preset(Control.PRESET_FULL_RECT) + drop_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE + drop_overlay.set_drag_forwarding(Callable(), _viewport_can_drop, _viewport_drop) + container.add_child(drop_overlay) + # Bedien-Hinweis im Viewport (Overlay) help_label = Label.new() help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe" @@ -890,6 +903,11 @@ func _quit_editor() -> void: func _notification(what: int) -> void: + if drop_overlay != null and is_instance_valid(drop_overlay): + if what == NOTIFICATION_DRAG_BEGIN: + drop_overlay.mouse_filter = Control.MOUSE_FILTER_STOP + elif what == NOTIFICATION_DRAG_END: + drop_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE if what == NOTIFICATION_WM_CLOSE_REQUEST: _quit_editor() diff --git a/src/ui/panels/assets_panel.gd b/src/ui/panels/assets_panel.gd index ad9fc52..21e4de1 100644 --- a/src/ui/panels/assets_panel.gd +++ b/src/ui/panels/assets_panel.gd @@ -636,10 +636,7 @@ func _tile_drag_data(_at_position: Vector2, oid: String, btn: Button) -> Variant lbl.text = LibraryBrowser.entry_label(oid, (main.manifest.get("objects", {}) as Dictionary).get(oid, {})) box.add_child(lbl) prev.modulate.a = 0.85 - if btn.get_viewport().gui_is_dragging(): - btn.set_drag_preview(prev) - else: - prev.free() + btn.set_drag_preview(prev) # gehört in den Drag-Start: gui_is_dragging() ist hier noch false return {"svr_oid": oid} diff --git a/tests/probe_drag_echt.gd b/tests/probe_drag_echt.gd new file mode 100644 index 0000000..6f3388a --- /dev/null +++ b/tests/probe_drag_echt.gd @@ -0,0 +1,79 @@ +extends SceneTree +## Echter Maus-Drag im sichtbaren Fenster (nicht headless): Kachel drücken, über die +## Szene ziehen, loslassen → genau ein Objekt gesetzt. Ausführen: +## godot4 --rendering-driver opengl3 --path . --script res://tests/probe_drag_echt.gd + +func _mouse(pos: Vector2, pressed: bool, motion := false) -> void: + var ev: InputEvent + if motion: + var mm := InputEventMouseMotion.new() + mm.position = pos + mm.global_position = pos + mm.relative = Vector2(4, 4) + mm.button_mask = MOUSE_BUTTON_MASK_LEFT + ev = mm + else: + var mb := InputEventMouseButton.new() + mb.button_index = MOUSE_BUTTON_LEFT + mb.position = pos + mb.global_position = pos + mb.pressed = pressed + mb.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0 + ev = mb + root.push_input(ev) + + +func _init() -> void: + root.size = Vector2i(1600, 900) + var main: Control = (load("res://scenes/main.tscn") as PackedScene).instantiate() + root.add_child(main) + for i in 10: + await process_frame + main.sym_mode = SymmetryCore.Mode.OFF + var vc0: Control = main.interaction.camera.get_viewport().get_parent() + var n0: int = (main.doc["objects"] as Array).size() + var oid := "barrel_001" + main.assets_mod._on_library_expand_all() + for i in 5: + await process_frame + var btn: Button = main.library_buttons.get(oid) + print("window=", root.size, " btn=", btn) + btn.get_parent().get_parent() + # Kachel in Sicht scrollen + var p := btn.get_parent() + while p != null and not (p is ScrollContainer): + p = p.get_parent() + if p != null: + (p as ScrollContainer).ensure_control_visible(btn) + for i in 5: + await process_frame + var r := btn.get_global_rect() + print("Kachel rect=", r) + var start := r.get_center() + var vc: Control = main.interaction.camera.get_viewport().get_parent() + var vr := vc.get_global_rect() + print("Szene rect=", vr) + var ziel := vr.get_center() + _mouse(start, false, true) + await process_frame + _mouse(start, true) + await process_frame + var steps := 25 + for i in steps + 1: + var pos := start.lerp(ziel, float(i) / steps) + _mouse(pos, false, true) + await process_frame + for j in 6: + _mouse(ziel + Vector2(j * 3, j * 2), false, true) + await process_frame + var hov := root.gui_get_hovered_control() + print("Hover-Control: ", hov, " pfad=", hov.get_path() if hov != null else "-") + print("DragData=", root.gui_get_drag_data()) + print("während Drag: dragging=", root.gui_is_dragging(), " placing=", main.interaction.placing) + _mouse(ziel, false) + for i in 4: + await process_frame + var n1: int = (main.doc["objects"] as Array).size() + print("Objekte: %d → %d placing=%s" % [n0, n1, main.interaction.placing]) + print("ERGEBNIS: ", "OK" if n1 == n0 + 1 else "FEHLER") + quit(0 if n1 == n0 + 1 else 1)