extends "res://core/ui/panels/editor_panel.gd" ## Symmetrie-Panel (M5, Karte t_60014669 — Spec /opt/data/scopedvr/ ## KARTEN_MIRROR_vc27.md): Ein-Klick „Karte symmetrisch machen“ + Symmetrie- ## Check mit Klartext-Bericht. Erlaubnis-Regel des Operators (20.10.): ## main.gd NICHT anfassen — dieses Modul meldet sich über die Panel-Registry ## an (registry.gd, Eintrag) und greift wie alle Module über `main` auf ## Dokument/History zu. Die Spiegel-Arbeit lebt KOMPLETT in ## core/editor/symmetry_ops.gd (statisch, headless testbar). ## ## Ablauf „Karte symmetrisch machen“: ## 1. Symmetrie-Modus AUS → Hinweis, nichts passiert (Abnahme „Modus AUS ## ändert nichts“). ## 2. Analyse: nichts zu tun → Klartext „schon symmetrisch“, OHNE Undo. ## 3. Widersprüche? → Dialog nennt die ZAHL und lässt die Quell-Hälfte ## wählen (Sly entscheidet, bevor irgendwas überschrieben wird). ## 4. Anwenden = EIN Undo-Schritt (ActionHistory snapshotet das Dokument). const SymmetryOps := preload("res://core/editor/symmetry_ops.gd") var mode_label: Label var half_label: Label var half_option: OptionButton var apply_btn: Button var check_btn: Button var report_label: Label var conflict_dialog: ConfirmationDialog var _pending_high := false # Quell-Wahl des laufenden Widerspruchs-Dialogs static func spec() -> Dictionary: return {"titel": "Symmetrie", "gruppe": "map", "order": 30} func panel_title() -> String: return "Symmetrie" func build() -> void: add_child(_header_label("Symmetrie")) add_child(_muted_label("Macht die ganze Karte symmetrisch: ergänzt die andere Seite, löscht nichts.")) mode_label = _field_label(_mode_text()) add_child(mode_label) half_label = _field_label("Quell-Hälfte:") add_child(half_label) half_option = OptionButton.new() half_option.custom_minimum_size = UIScale.v2(0, 36) # U7: Klartext-Tooltip — welche Seite als Vorlage dient. half_option.tooltip_text = "Welche Hälfte die Vorlage ist (die andere wird angepasst)" half_option.item_selected.connect(_on_half_selected) add_child(half_option) _refresh_half_option() apply_btn = Button.new() apply_btn.text = "🗺️ Karte symmetrisch machen" apply_btn.custom_minimum_size = UIScale.v2(0, 48) apply_btn.tooltip_text = "Ergänzt die andere Kartenseite (EIN Strg+Z macht alles rückgängig)" apply_btn.pressed.connect(_on_apply_pressed) add_child(apply_btn) check_btn = Button.new() check_btn.text = "🔍 Symmetrie prüfen" check_btn.custom_minimum_size = UIScale.v2(0, 40) check_btn.tooltip_text = "Ehrlicher Bericht: Wo weicht die Karte von der Spiegelung ab?" check_btn.pressed.connect(_on_check_pressed) add_child(check_btn) report_label = _muted_label("") report_label.custom_minimum_size = UIScale.v2(0, 96) add_child(report_label) conflict_dialog = ConfirmationDialog.new() conflict_dialog.title = "Widersprüche — welche Seite gewinnt?" conflict_dialog.dialog_close_on_escape = true conflict_dialog.get_ok_button().text = "Anwenden" conflict_dialog.confirmed.connect(_on_conflict_confirmed) conflict_dialog.add_button("Andere Hälfte als Quelle", true, "other_half") conflict_dialog.custom_action.connect(_on_conflict_other) add_child(conflict_dialog) # ------------------------------------------------------------- Zustands-Helfer func _current_mode() -> int: ## Defensiv wie spawn_tool._sym_mode(): Mock-Mains ohne Feld → OFF. var v: Variant = main.get("sym_mode") if main != null else null return SymmetryCore.Mode.OFF if v == null else int(v) func _mode_text() -> String: var mode := _current_mode() return "Modus: %s" % SymmetryCore.mode_label(mode) func _source_high() -> bool: ## false = erste Hälfte (links/vorn), true = zweite (rechts/hinten). return half_option.selected == 1 func _refresh_half_option() -> void: var labels: Array = SymmetryOps.side_labels(_current_mode()) half_option.clear() half_option.add_item("Quelle: %s" % str(labels[0])) half_option.add_item("Quelle: %s" % str(labels[1])) half_option.selected = 0 ## Vom Einstellungen-Fenster gerufen, wenn der Modus wechselt. func refresh_mode() -> void: if mode_label == null or half_option == null: return _refresh_half_option() _refresh_mode_line() func _on_half_selected(_index: int) -> void: _refresh_mode_line() func _refresh_mode_line() -> void: mode_label.text = _mode_text() # ------------------------------------------------------------ Karte spiegeln -- func _on_apply_pressed() -> void: var mode := _current_mode() if mode == SymmetryCore.Mode.OFF: _say("Der Symmetrie-Modus ist Aus — erst in den Einstellungen (⚙) Punkt oder eine Achse wählen.") return if main.doc.is_empty(): _say("Keine Karte offen — erst eine Map anlegen oder laden.", true) return _refresh_mode_line() _refresh_half_option() var an := SymmetryOps.analyze(main.doc, mode) if int(an["total_changes"]) == 0: _say("Nichts zu tun — die Karte ist schon symmetrisch ✓") return if int(an["conflicts"]) == 0: _apply(_source_high()) return _pending_high = _source_high() conflict_dialog.dialog_text = _conflict_text(an) conflict_dialog.get_ok_button().text = "Anwenden — %s gewinnt" % _half_name(_pending_high) conflict_dialog.popup_centered(Vector2i(520, 260)) ## Klartext für den Widerspruchs-Dialog: ZAHL erst nennen, DANN anwenden ## (Spec: „Nichts wird gelöscht, ohne dass die Zahl vorher genannt wurde“). func _conflict_text(an: Dictionary) -> String: var parts: Array = [] var obj_n := int(an["objects"]["diff"]) var spawn_n := int(an["spawns"]["diff"]) var boden_n := (an["terrain_cells"] as Array).size() + (an.get("material_cells", []) as Array).size() if obj_n > 0: parts.append("%d Objekt-Paare unterscheiden sich" % obj_n) if spawn_n > 0: parts.append("%d Spawn-Punkt-Paare unterscheiden sich" % spawn_n) if boden_n > 0: parts.append("%d Bodenstellen sind unterschiedlich" % boden_n) var adds := int(an["adds"]) var text := ", ".join(PackedStringArray(parts)) + ".\n\n" text += "Beim Anwenden gewinnt die gewählte Quell-Hälfte (die andere Seite wird angepasst).\n" if adds > 0: text += "Zusätzlich werden %d fehlende Zwillinge ergänzt.\n" % adds text += "Es wird NICHTS gelöscht. Ein Strg+Z macht alles rückgängig." return text func _half_name(high: bool) -> String: var labels: Array = SymmetryOps.side_labels(_current_mode()) return str(labels[1]) if high else str(labels[0]) func _on_conflict_confirmed() -> void: # OK = die im Auswahlfeld gewählte Quelle behalten (Dialog-Text nennt sie). _apply(_pending_high) func _on_conflict_other(_action: String) -> void: conflict_dialog.hide() _pending_high = not _pending_high _apply(_pending_high) ## Snapshot über ALLE Blöcke; fehlt design_notes (alte Karte), wird es als ## null geseedet — sonst könnte das Undo den neu angelegten Block nicht ## entfernen (U3-Lektion: fehlernde Blöcke explizit als null snapshoten). func _snapshot_all() -> Dictionary: var snap: Dictionary = main.history.snapshot(main.doc.keys()) if not snap.has("design_notes") and not main.doc.has("design_notes"): snap["design_notes"] = null return snap func _apply(quelle_hoch: bool) -> void: var mode := _current_mode() var before: Dictionary = _snapshot_all() var res := SymmetryOps.apply(main.doc, mode, quelle_hoch) main.history.commit("Karte symmetrisch machen (%s → %s)" % [ _half_name(quelle_hoch), _half_name(not quelle_hoch)], before, main.history.snapshot(main.doc.keys())) main.call("_after_structure_change") var report := str(res["report"]) report_label.text = report # U7: Bericht-Farbe über Theme (Editor/colors/status_ok). report_label.add_theme_color_override("font_color", theme_color("status_ok")) main.call("_set_status", report.split(".")[0] + ".") # ---------------------------------------------------------- Symmetrie-Check -- func _on_check_pressed() -> void: var mode := _current_mode() if mode == SymmetryCore.Mode.OFF: _say("Der Symmetrie-Modus ist Aus — erst in den Einstellungen (⚙) Punkt oder eine Achse wählen, dann prüfen.") return _refresh_mode_line() var rep := SymmetryOps.check(main.doc, mode) var text := str(rep["text"]) report_label.text = text # U7: Bericht-Farben über Theme (Editor/colors/status_ok / warning). report_label.add_theme_color_override("font_color", theme_color("status_ok") if bool(rep["ok"]) else theme_color("warning")) main.call("_set_status", text) func _say(text: String, is_error := false) -> void: report_label.text = text # U7: Bericht-Farben über Theme (Editor/colors/warning / status_ok). report_label.add_theme_color_override("font_color", theme_color("warning") if is_error else theme_color("status_ok")) main.call("_set_status", text, is_error)