237 lines
8.6 KiB
GDScript
237 lines
8.6 KiB
GDScript
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)
|