scopedvr-map-core/ui/panels/symmetry_panel.gd
2026-11-01 19:31:33 +01:00

237 lines
8.6 KiB
GDScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)