Compare commits

...

6 commits

32 changed files with 822 additions and 281 deletions

View file

@ -522,7 +522,7 @@ func ungroup_selection() -> void:
## Auswahl drehen (R / Mausrad / Button): bei mehreren Objekten oder einer
## Gruppe rotiert ALLES gemeinsam um den Gruppen-Mittelpunkt. Mausrad-Ticks
## derselben Auswahl laufen zu EINEM Undo-Schritt zusammen (merge_tag).
func rotate_selected(delta_deg: float) -> void:
func rotate_selected(delta_deg: float, merge_suffix: String = "") -> void:
if selected_index < 0 or selected_indices.is_empty():
return
var objects: Array = main.doc.get("objects", [])
@ -545,7 +545,7 @@ func rotate_selected(delta_deg: float) -> void:
var twins: Array = ObjectOps.symmetry_sync_pairs(
main.doc, members, _sym_mode(), selected_index)
_sync_members_to_markers(twins)
var tag := "rotmulti:%s" % str(members)
var tag := "rotmulti:%s%s" % [str(members), merge_suffix]
main.history.commit("Auswahl gedreht (%d Objekte)" % members.size(),
before, main.history.snapshot(["objects"]), tag)
_mark_dirty()
@ -560,7 +560,7 @@ func rotate_selected(delta_deg: float) -> void:
## einer Gruppe kippt die GANZE Gruppe (wie beim Drehen), Mehrfachauswahl
## kippt jedes Objekt um denselben Betrag, Symmetrie-Zwillinge laufen im
## SELBEN Commit mit. rot/rot_y_deg bleiben über SvmapIO.rot_setzen synchron.
func tilt_selected(axis_x: bool, delta_deg: float) -> void:
func tilt_selected(axis_x: bool, delta_deg: float, merge_suffix: String = "") -> void:
if selected_index < 0 or selected_indices.is_empty():
return
var objects: Array = main.doc.get("objects", [])
@ -588,8 +588,8 @@ func tilt_selected(axis_x: bool, delta_deg: float) -> void:
var twins: Array = ObjectOps.symmetry_sync_pairs(
main.doc, members, _sym_mode(), selected_index)
_sync_members_to_markers(twins)
var tag := "tilt:%d" % selected_index if members.size() == 1 \
else "tiltmulti:%s" % str(members)
var tag := ("tilt:%d" % selected_index if members.size() == 1 \
else "tiltmulti:%s" % str(members)) + merge_suffix
main.history.commit("Auswahl gekippt (%s, %d Objekte)" % [axis, members.size()],
before, main.history.snapshot(["objects"]), tag)
_mark_dirty()

View file

@ -119,8 +119,8 @@ const KAT_GROUPS := {
}
## Anzeige-Größe der Vorschaubilder im Browser (Karte t_59fc0f21): vorher 40 px,
## jetzt Kacheln mit 96 px (128-px-PNGs werden auf die Anzeige-Größe skaliert).
const THUMB_DISPLAY_PX := 96
## jetzt Kacheln mit 128 px (25.10.: größer, deutlicher) (128-px-PNGs werden auf die Anzeige-Größe skaliert).
const THUMB_DISPLAY_PX := 128
static func object_label(oid: String) -> String:

View file

@ -160,9 +160,23 @@ static func check_update(git_exe: String, project_dir: String) -> Dictionary:
"error": "Origin lieferte keinen Commit (nur Warnungen)."}
if local == remote:
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
# Befund 25.10.: „Update verfügbar“ blieb nach dem Update stehen, weil nur
# HEAD != origin/main geprüft wurde. Steht der Klon auf einem anderen
# Branch oder VOR main (z. B. Feature-Branch), ist das KEIN Update:
# nur melden, wenn der Remote-Commit NICHT schon in HEAD steckt.
if remote_in_head(git_exe, project_dir, remote):
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
return {"status": "update_available", "local": local, "remote": remote, "error": ""}
## Ist `remote` schon Vorfahre von HEAD (Klon gleich oder VORAUS)? Unbekannter
## Commit (Exit 128) = noch nicht geholt = echtes Update → false.
static func remote_in_head(git_exe: String, project_dir: String, remote: String) -> bool:
var r := git_run(git_exe, project_dir, PackedStringArray([
"merge-base", "--is-ancestor", remote, "HEAD"]))
return bool(r["ok"])
## Aus der gemischten ls-remote-Ausgabe den Commit für `branch` lesen.
## WARUM: `git_run` mischt stdout+stderr, und der git-Client schreibt bei einer
## HTTP-Umleitung zuerst "warning: Umleitung nach ..." — die alte Fassung nahm
@ -246,7 +260,8 @@ const AUTO_STASH_MESSAGE := "auto: Selbst-Update"
## fürs Statuszeile-Fenster, leer wenn nichts besonderes passiert ist).
static func pull(git_exe: String, project_dir: String) -> Dictionary:
var stash_info := _stash_local_changes(git_exe, project_dir)
var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only"]))
# Gezielt origin/main ziehen (nicht den Upstream des aktuellen Branches).
var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only", "origin", BRANCH]))
var errors: Array = []
var text := str(r["output"]).strip_edges()
var info := ""

View file

@ -9,70 +9,74 @@ extends RefCounted
static func text() -> String:
var out := ""
out += _header("Navigation (Kamera)")
out += _header("Kamera & Steuerung")
out += _table(_rows([
["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"],
["Mittlere Maustaste + Umschalt ziehen", "Pan: Karte verschieben"],
["Mausrad", "Zoom (aus, während ein Objekt gedreht wird)"],
["Rechte Maustaste GEDACHT halten", "Freelook-Flug: Umsehen + W vor / S zurück / A links / D rechts; Leertaste hoch / Shift runter (Shooter-Konvention) — funktioniert in JEDEM Werkzeug (Terrain, Bauen, Platzieren), die rechte Maustaste hat immer Vorrang"],
["Strg halten (im Flug)", "schneller fliegen (×3)"],
["Alt halten (im Flug)", "langsamer fliegen (×0,3)"],
["Mausrad (im Flug)", "Grundgeschwindigkeit dauerhaft ändern (2–300 m/s)"],
["Mittlere Maustaste + Umschalt", "Pan: Karte verschieben"],
["Mausrad", "Zoom"],
["Rechte Maustaste halten", "Freelook-Flug in jedem Werkzeug: Maus = Umsehen, W/A/S/D = Bewegen, Leertaste hoch, Umschalt runter"],
["Strg / Alt (im Flug)", "schneller (×3) / langsamer (×0,3); Mausrad ändert die Grundgeschwindigkeit"],
["Umschalt+F", "Flugmodus dauerhaft an/aus"],
["F", "Auswahl ins Bild zentrieren"],
["L", "Höhenlinien fixieren (sonst nur im Terrain-Werkzeug)"],
["H", "diese Hilfe an/aus (Esc schließt auch)"],
]))
out += _header("Auswahl")
out += _header("Objekte auswählen & platzieren (F2)")
out += _table(_rows([
["Linksklick", "Objekt auswählen (ersetzt die Auswahl)"],
["Strg+Klick (Viewport + Baum)", "Objekt zur Auswahl hinzufügen/entfernen (Mehrfachauswahl)"],
["Umschalt+Klick", "wie Strg+Klick (Mehrfachauswahl umschalten)"],
["Ziehen über leeren Boden", "Rahmen aufziehen: alle Objekte im Rahmen wählen"],
["Umschalt + Rahmen", "Rahmen-Auswahl zur bestehenden Auswahl hinzufügen"],
["Esc", "Werkzeug abbrechen → Greifen zurückrollen → Ghost abbrechen → Auswahl aufheben"],
["G", "Auswahl greifen (hängt an der Maus; Klick setzt, Esc rollt zurück)"],
]))
out += _header("Objekt-Manipulation")
out += _table(_rows([
["R", "90° drehen (Umschalt+R = −90°, ganze Gruppe dreht mit; Mausrad: 15° fein)"],
["Strg+R / Strg+Umschalt+R", "Kippen um X (+15° / −15°) — auch beim Platzieren (Karte t_e166e656)"],
["Alt+R / Alt+Umschalt+R", "Kippen um Z (+15° / −15°) — auch beim Platzieren (Karte t_e166e656)"],
["Pfeiltasten", "Auswahl im 2-m-Raster verschieben (Kamera-nächste Weltachse)"],
["Bild↑ / Bild↓", "+0,25 m / −0,25 m anheben/absenken — Absenken UNTER den Boden erlaubt (Versenken, z. B. Steine)"],
["Strg+Bild↑", "Auswahl aufs Terrain/Boden absetzen"],
["Strg halten", "temporärer Snap-Zwang (rastet auf ganze Meter)"],
["Strg+D", "Auswahl duplizieren (+2 m versetzt)"],
["Entf", "Auswahl sofort löschen (Strg+Z holt zurück)"],
["Linksklick", "Objekt wählen; Umschalt/Strg+Klick: Mehrfachauswahl"],
["Rahmen aufziehen", "alle Objekte im Rahmen wählen (Umschalt: hinzufügen)"],
["Bibliothek: Klick / Doppelklick", "Objekt auswählen / Platzieren starten; Klick in die Karte setzt, Esc bricht ab"],
["G", "greifen: Objekt hängt an der Maus (Klick setzt, Esc zurück)"],
["Pfeiltasten", "im 2-m-Raster verschieben"],
["Bild↑ / Bild↓", "0,25 m anheben / absenken; Strg+Bild↑ = auf den Boden setzen"],
["Strg halten", "Snap-Zwang auf ganze Meter"],
["Strg+D", "duplizieren"],
["Entf", "löschen"],
["Strg+G / Strg+Umschalt+G", "Gruppieren / Gruppe lösen"],
["Esc", "abbrechen, dann Auswahl aufheben"],
]))
out += _header("Terrain-Werkzeug (F1)")
out += _header("Objekte verschieben, drehen, kippen (Werkzeugleiste links oben in der 3D-Ansicht)")
out += _table(_rows([
["1 / 2 / 3 / 4", "Pinselmodus: Heben / Senken / Glätten / Malen"],
["[ / ]", "Pinselgröße: Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch"],
["Linksklick-Ziehen", "Boden formen bzw. malen"],
["Strg+Linksklick-Ziehen", "Gegenteil: senken (bei Heben) / heben (bei Senken) / Steppe zurückmalen"],
["Verschieben", "Pfeile am Objekt ziehen (X rot, Y grün, Z blau); Mausrad ändert die Schrittweite"],
["Drehen", "Ringe am Objekt ziehen: um alle drei Achsen, in 15°-Schritten; R = 90° um Y (Umschalt+R = −90°)"],
["Kippen", "Strg+R / Strg+Umschalt+R = um X ±15°; Alt+R / Alt+Umschalt+R = um Z ±15° (auch beim Platzieren)"],
["Spiegeln", "an der Stelle links↔rechts oder vor↔zurück (Panel Auswahl)"],
["Strg+Z / Strg+Y", "jeder Zug ist ein Undo-Schritt"],
]))
out += _header("Datei & Undo")
out += _header("Terrain formen (F1)")
out += _table(_rows([
["1 / 2 / 3", "Heben / Senken / Glätten"],
["5 / 6", "Einebnen (Höhe vom Strichstart) / Rampe (Start klicken, ziehen, loslassen)"],
["[ / ]", "Pinselgröße Klein / Mittel / Groß"],
["Linksklick ziehen", "Boden formen; Strg+Klick = Gegenteil (Heben ↔ Senken)"],
["Tablet/Controller", "Stärke folgt dem Druck (Maus = voll)"],
]))
out += _header("Malen (4)")
out += _table(_rows([
["Material wählen", "Tab Malen: Kacheln mit Vorschau"],
["Linksklick ziehen", "Material auf den Boden malen; Strg+Klick malt Steppe zurück"],
["Kante", "Regler: hart ↔ weich auslaufend"],
]))
out += _header("Bauen (F3)")
out += _table(_rows([
["Linksklick", "Bauteil aufs 2-m-Raster setzen, auch auf andere Teile (stapeln)"],
["R / Pfeile / Bild↑↓", "drehen / verschieben / anheben"],
["Umschalt+Klick", "Bauteil auswählen; Entf löscht"],
]))
out += _header("Spawn-Zonen (F4) und Test-Start (T)")
out += _table(_rows([
["F4: Linksklick", "Zone setzen; Ziehen verschiebt, Entf löscht; Team und Radius im Tab Prüfen"],
["T: Linksklick", "Test-Startpunkt setzen (nur für F5, kein Spawn)"],
["F5", "▶ Karte testen"],
]))
out += _header("Datei & Rückgängig")
out += _table(_rows([
["Strg+Z", "Rückgängig"],
["Strg+Y oder Strg+Umschalt+Z", "Wiederherstellen (Redo)"],
["Strg+S", "Map speichern (.svmap, bekannter Pfad)"],
["Strg+Umschalt+S", "Map speichern unter …"],
["Strg+O", "Map öffnen"],
["Strg+N", "Neue Map"],
["Datei → 🔄 Updates prüfen", "Selbst-Update: prüft auf neue Version; Banner → „Jetzt updaten“ (lädt, importiert, startet neu)"],
["Strg+Y / Strg+Umschalt+Z", "Wiederherstellen"],
["Strg+S / Strg+Umschalt+S", "Speichern / Speichern unter"],
["Strg+O / Strg+N", "Öffnen / Neue Map"],
["Datei → Updates prüfen", "prüft auf eine neue Version; Banner „Jetzt updaten“ lädt und startet neu"],
]))
out += _header("Werkzeuge & Tests")
out += _table(_rows([
["F1", "Werkzeug: Terrain formen"],
["F2", "Werkzeug: Objekte platzieren/auswählen"],
["F3", "Werkzeug: Bauen (Häuser)"],
["F4", "Werkzeug: Spawn-Zonen malen"],
["T", "Werkzeug: Test-Startpunkt setzen — Klick in die Karte = dort startet F5 (KEIN Spiel-Spawnpunkt; im Testlauf versetzt T dich sofort zum Blickpunkt)"],
["F5", "▶ Karte testen (Start: Test-Start → erster Team-Spawnpunkt → Kartenmitte)"],
["H", "Diese Kurzhilfe an/aus (Esc schließt auch)"],
["L", "Höhenlinien FIXIEREN (immer sichtbar/unsichtbar) — ohne Fix erscheinen sie nur im Terrain-Werkzeug (F1)"],
]))
out += "[color=#a8b6ad]Vor „Neue Map“ und „Öffnen“ fragt der Editor nach, wenn noch ungespeicherte Änderungen da sind. Die Statuszeile unten zeigt immer das aktive Werkzeug.[/color]\n"
return out

View file

@ -52,6 +52,7 @@ var grid_snap := false # Raster-Snap (1 m) an/aus — Objekt-Werkzeug
var history := ActionHistory.new()
var interaction # src/editor/interaction.gd (script-erweitert → untypisiert)
var viewport_toolbar # src/ui/viewport_toolbar.gd (Buttons oben links in der 3D-Ansicht)
var gizmo # src/viewport/gizmo.gd (Achsen-Pfeile, Kind von interaction)
var gizmo_step_m := 0.5 # Gizmo-Schrittweite in m (Panel-Feld; überlebt UI-Rebuilds)
var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1)
@ -79,6 +80,8 @@ var status_mod # src/ui/panels/status_bar.gd
var selection_mod # src/ui/panels/selection_panel.gd
var map_mod # src/ui/panels/map_panel.gd
var terrain_mod # src/ui/panels/terrain_panel.gd
var malen_mod # src/ui/panels/malen_panel.gd (Malen-Tab)
var bauen_mod # src/ui/panels/bauen_panel.gd (Bauen-Tab)
var check_mod # src/ui/panels/check_panel.gd
var tree_mod # src/ui/panels/scene_tree_panel.gd
var assets_mod # src/ui/panels/assets_panel.gd
@ -391,6 +394,10 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
map_mod = child
"terrain":
terrain_mod = child
"malen":
malen_mod = child
"bauen":
bauen_mod = child
"check":
check_mod = child
sel_label = selection_mod.sel_label
@ -398,9 +405,16 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
prop_name = map_mod.prop_name
spawn_status = check_mod.spawn_status
terrain_info = map_mod.terrain_info
build_edit_btn = terrain_mod.build_edit_btn
build_panel = terrain_mod.build_panel
build_kind_buttons = terrain_mod.build_kind_buttons
build_edit_btn = bauen_mod.build_edit_btn
build_panel = bauen_mod.build_panel
build_kind_buttons = bauen_mod.build_kind_buttons
# Malen-Widgets wohnen im Malen-Tab; das Terrain-Modul hält die
# alten Namen als Aliase (Tests/Werkzeug-Vertrag).
terrain_mod.terrain_palette = malen_mod.palette
terrain_mod.terrain_mat_buttons = malen_mod.mat_buttons
terrain_mod.terrain_weich_label = malen_mod.weich_label
terrain_mod.terrain_weich_slider = malen_mod.weich_slider
terrain_mod.terrain_paint_hint = malen_mod.paint_hint
# Test-/Werkzeug-Vertrag (wie vor der U1-Aufteilung): die Widgets
# wohnen in den Modulen, der Koordinator hält die alten Namen
# als Bequemlichkeits-Referenzen — nichts wird doppelt gebaut.
@ -536,7 +550,7 @@ func _build_dialogs_and_overlays() -> void:
overlay_box.add_theme_constant_override("separation", UIScale.px(6))
help_overlay.add_child(overlay_box)
help_overlay_title = Label.new()
help_overlay_title.text = "Kurzhilfe — alle Tasten auf einen Blick (H oder Esc schließt)"
help_overlay_title.text = "Hilfe — Werkzeuge, Steuerung und Tastenkürzel (H oder Esc schließt)"
help_overlay_title.add_theme_font_size_override("font_size", UIScale.heading_font_size())
help_overlay_title.add_theme_color_override("font_color", get_theme_color("accent", "Editor"))
overlay_box.add_child(help_overlay_title)
@ -616,9 +630,14 @@ func _build_center_viewport() -> SubViewportContainer:
world.add_child(test_start_tool)
test_start_tool.setup(self, camera, view_builder, interaction)
# Werkzeugleiste oben links (Verschieben / Drehen & Kippen / Aktionen)
viewport_toolbar = preload("res://src/ui/viewport_toolbar.gd").new()
container.add_child(viewport_toolbar)
viewport_toolbar.setup(self)
# Bedien-Hinweis im Viewport (Overlay)
help_label = Label.new()
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
help_label.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
help_label.offset_left = UIScale.px(12)
help_label.offset_bottom = UIScale.px(-8)
@ -1330,7 +1349,7 @@ func _update_tool_label() -> void:
"bauen": "Bauen (F3)",
"spawns": "Spawn-Zonen (F4)",
}
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
tool_label.text = "Werkzeug: %s" % str(names.get(tool_mode, tool_mode))
## Symmetrie-Schalter (Karte t_f5935023, M1): Modus aus dem Toolbar-Menü übernehmen.
@ -1756,7 +1775,9 @@ func _base_tool_status() -> void:
## Namen hier (dünne Weiterleitung) — der Werkzeug-Vertrag ist unverändert.
func _on_terrain_btn_pressed() -> void:
if terrain_mod.terrain_edit_btn.button_pressed:
if tool_mode != "terrain":
if terrain_mod.terrain_mode_paint.button_pressed:
terrain_mod.terrain_mode_raise.button_pressed = true
_set_tool_mode("terrain")
else:
_set_tool_mode("objects")
@ -1814,10 +1835,9 @@ func _enter_terrain_mode() -> void:
_sync_terrain_button()
return
tool_mode = "terrain"
terrain_mod.terrain_panel.visible = true
help_label.text = "Linksklick-Ziehen: Boden formen oder malen · Strg+Linksklick: Gegenteil / Steppe zurück ·\n1/2/3/4: Heben/Senken/Glätten/Malen · [ / ]: Pinselgröße Klein/Mittel/Groß ·\nPinselgröße & Material rechts · Rechte Maustaste: Flug · Strg+Z: letzter Strich weg"
terrain_mod.terrain_edit_btn.button_pressed = true
help_label.text = "Linksklick-Ziehen: formen bzw. malen · Strg+Klick: Gegenteil · [ ]: Pinselgröße · H: Hilfe"
terrain_tool.call("set_brush_mode", terrain_mod._selected_terrain_mode())
terrain_mod.refresh_panels()
_set_status("Terrain-Modus an: LINKSKLICK-Ziehen formt den Boden, im Malen-Modus färbt er das Gelände (Steppe/Sand/Fels …). Regler rechts stellen Pinsel und Stärke ein.")
@ -1826,6 +1846,7 @@ func _leave_terrain_mode() -> void:
terrain_tool.call("deactivate")
if terrain_mod != null:
terrain_mod.terrain_panel.visible = false
malen_mod.malen_panel.visible = false
_sync_terrain_button()
@ -1834,7 +1855,7 @@ func _enter_spawn_mode() -> void:
interaction.call("clear_selection", true)
tool_mode = "spawns"
check_mod.spawn_panel.visible = true
help_label.text = "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Entf: löschen ·\nTeam/Radius rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück"
help_label.text = "Linksklick: Zone setzen · Ziehen: verschieben · Entf: löschen · H: Hilfe"
check_mod.spawn_edit_btn.button_pressed = true
spawn_tool.call("activate")
spawn_tool.call("set_team", check_mod._selected_spawn_team())
@ -1853,7 +1874,7 @@ func _leave_spawn_mode() -> void:
func _enter_objects_mode() -> void:
tool_mode = "objects"
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
_base_tool_status()
@ -1886,8 +1907,10 @@ func _enter_build_mode() -> void:
interaction.call("clear_selection", true)
tool_mode = "bauen"
build_panel.visible = true
help_label.text = "Linksklick: Bauteil setzen (auch auf andere Teile = stapeln) · Umschalt+Klick: Bauteil auswählen ·\nR: drehen (90°) · Pfeiltasten: im 2-m-Raster verschieben · Bild↑/↓: anheben/absenken · Strg+Bild↑: auf Boden · Entf: löschen · Mittlere Maustaste: Kamera · Strg+Z: zurück"
help_label.text = "Linksklick: Bauteil setzen · R: drehen · Entf: löschen · H: Hilfe"
build_edit_btn.button_pressed = true
if ui_right_panel is TabContainer and bauen_mod != null:
(ui_right_panel as TabContainer).current_tab = bauen_mod.get_index()
build_tool.call("activate")
_set_status("Bauen-Modus an: Bauteil rechts anklicken, dann in die Karte klicken — alles rastet aufs 2-m-Raster.")
@ -1905,7 +1928,7 @@ func _sync_build_button() -> void:
func _sync_terrain_button() -> void:
terrain_mod.terrain_edit_btn.button_pressed = tool_mode == "terrain"
terrain_mod.refresh_panels()
func _terrain_brush_size_step(up: bool) -> void:
@ -1959,12 +1982,13 @@ func _recheck_terrain_mode() -> void:
terrain_tool.call("deactivate")
tool_mode = "objects"
terrain_mod.terrain_panel.visible = false
malen_mod.malen_panel.visible = false
_sync_terrain_button()
_update_tool_label()
# Karte t_89c0ec95: Dezenter Terrain-Rückschalter = ebenfalls ein
# Werkzeugwechsel — Linien wieder ausblenden (sofern nicht fixiert).
_sync_contours_to_builder()
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
_set_status("Terrain-Modus aus: Diese Map passt nicht ins Editor-MVP (Terrain-Auflösung über %d) — die Objekte bleiben normal bearbeitbar." % TerrainField.EDITOR_MAX_RESOLUTION, true)
@ -2161,9 +2185,13 @@ func _refresh_stats() -> void:
var count := SvmapDocument.object_count(doc)
var poly := SvmapDocument.poly_usage(doc, manifest)
var budget := int(SvmapIO.MAX_TOTAL_POLY)
stats_objects.text = "Objekte: %d / 5000 · Poly-Budget: %s / %s (%.0f %%) · Bauteile: %d" % [
count, _fmt_int(int(poly)), _fmt_int(budget), 100.0 * poly / budget,
BuildOps.piece_count(doc)]
var poly_pct := 100.0 * poly / budget
stats_objects.text = "%d Objekte · Poly %.0f %%" % [count, poly_pct]
stats_objects.tooltip_text = "Objekte: %d / 5000\nPoly-Budget: %s / %s (%.0f %%)\nBauteile: %d" % [
count, _fmt_int(int(poly)), _fmt_int(budget), poly_pct, BuildOps.piece_count(doc)]
# Nur bei knappem Budget auffällig färben (sonst ruhiger Nebentext).
stats_objects.add_theme_color_override("font_color", get_theme_color(
"warning" if poly_pct >= 90.0 or count >= 4500 else "field", "Editor"))
# GLB-Fehlende Objekte (Karte t_9b50b231): Platzhalter laufen, aber Sly soll
# sehen, WELCHE echten Modelle im Mesh-Lager nachgelegt werden müssen.
if view_builder != null and not (view_builder.get("fehlende_glbs") as Array).is_empty():
@ -2171,7 +2199,7 @@ func _refresh_stats() -> void:
var nennung := ", ".join(fehlend.slice(0, 3))
if fehlend.size() > 3:
nennung += " … (+%d weitere)" % (fehlend.size() - 3)
stats_objects.text += " · ⚠ GLB fehlt: %s" % nennung
stats_objects.text += " · ⚠ GLB fehlt: %s" % nennung
# Terrain-Anteil am Quest-3-Budget (res 513 = 524.288 Dreiecke, map_format.md §4)
var tris := 0
var res := 0
@ -2181,8 +2209,9 @@ func _refresh_stats() -> void:
var quest_budget := 524288
var pct := 100.0 * float(tris) / float(quest_budget)
var fps := Engine.get_frames_per_second()
stats_terrain.text = " · Terrain: %s² Grid, %s Dreiecke (%.0f %% Quest-Budget) · %d FPS" % [
str(res) if res > 0 else "—", _fmt_int(tris), pct, fps]
stats_terrain.text = "Terrain %s² · %d FPS" % [str(res) if res > 0 else "—", fps]
stats_terrain.tooltip_text = "Terrain: %s² Grid, %s Dreiecke (%.0f %% Quest-Budget)" % [
str(res) if res > 0 else "—", _fmt_int(tris), pct]
func _refresh_file_label() -> void:

View file

@ -59,7 +59,6 @@ func panel_title() -> String:
## Klapp-Gruppen-Körper („Alle") + Zuletzt-benutzt-Ansicht.
func build() -> void:
add_child(_header_label("Objekt-Bibliothek"))
add_child(_muted_label("Klick = auswählen, Doppelklick = Werkzeug starten. Ohne GLB wird der Platzhalter gesetzt und die Kachel markiert (Rechte Maustaste = Flug)."))
# Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit
# Treffern klappen auf, leere Gruppen verschwinden. Größe ist von
@ -102,7 +101,7 @@ func build() -> void:
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.custom_minimum_size = UIScale.v2(0, 280)
scroll.custom_minimum_size = UIScale.v2(0, 320)
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
library_box = VBoxContainer.new()
@ -270,7 +269,7 @@ func _on_panel_resized() -> void:
func _apply_grid_columns(width: float) -> void:
var cols := clampi(int(width / float(UIScale.px(180))), 1, 12)
var cols := clampi(int(width / float(UIScale.px(200))), 1, 12)
for row in _library_group_rows:
((row["grid"] as GridContainer)).columns = cols
if _recent_grid != null:
@ -471,7 +470,7 @@ func _build_tile(oid: String) -> Button:
btn.toggle_mode = true
btn.button_pressed = false
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
btn.custom_minimum_size = UIScale.v2(0, 126)
btn.custom_minimum_size = UIScale.v2(0, 188)
btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid]
if missing:
btn.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)"
@ -523,7 +522,6 @@ func _build_tile(oid: String) -> Button:
tile.add_child(id_lbl)
var lbl := Label.new()
lbl.text = name_txt
lbl.add_theme_font_size_override("font_size", UIScale.small_font_size())
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.clip_text = true
lbl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS

View file

@ -0,0 +1,63 @@
extends "res://src/ui/panels/editor_panel.gd"
## Bauen-Tab (25.10., UI-Aufräumen): Bauteil-Auswahl für das Bauen-Werkzeug
## (Fundamente, Wände, Dächer …). Vorher im Terrain-Tab. Die Werkzeug-Logik
## bleibt in main.gd/build_tool.gd; main reicht die Felder unter den alten
## Namen (build_edit_btn, build_panel, build_kind_buttons) weiter.
var build_edit_btn: Button
var build_panel: VBoxContainer
var build_kind_buttons: Dictionary = {} # kind_id → Button
static func spec() -> Dictionary:
return {"titel": "Bauen", "gruppe": "rechts", "order": 47}
func panel_title() -> String:
return "Bauen"
func build() -> void:
build_edit_btn = Button.new()
build_edit_btn.text = "🏠 Bauen (Häuser)"
build_edit_btn.custom_minimum_size = UIScale.v2(0, 48)
build_edit_btn.toggle_mode = true
build_edit_btn.tooltip_text = "Fundamente, Wände, Dächer, Treppen und Türen im 2-Meter-Raster bauen (F3)"
build_edit_btn.pressed.connect(_on_build_btn_pressed)
add_child(build_edit_btn)
build_panel = VBoxContainer.new()
build_panel.add_theme_constant_override("separation", UIScale.px(8))
build_panel.visible = false
add_child(build_panel)
build_panel.add_child(_field_label("Bauteil:"))
var build_grid := GridContainer.new()
build_grid.columns = 2
build_grid.add_theme_constant_override("h_separation", UIScale.px(8))
build_grid.add_theme_constant_override("v_separation", UIScale.px(8))
build_panel.add_child(build_grid)
var build_group := ButtonGroup.new()
for kind_id in BuildOps.KINDS.keys():
var kind_btn := Button.new()
kind_btn.text = str(BuildOps.KINDS[kind_id]["label"])
kind_btn.toggle_mode = true
kind_btn.button_group = build_group
kind_btn.custom_minimum_size = UIScale.v2(148, 44)
kind_btn.clip_text = true
kind_btn.tooltip_text = "%s — %s" % [str(BuildOps.KINDS[kind_id]["label"]), str(BuildOps.KINDS[kind_id].get("info", ""))]
kind_btn.pressed.connect(_on_build_kind_pressed.bind(str(kind_id)))
build_kind_buttons[str(kind_id)] = kind_btn
build_grid.add_child(kind_btn)
(build_kind_buttons["fundament"] as Button).set_pressed_no_signal(true)
func _on_build_btn_pressed() -> void:
if build_edit_btn.button_pressed:
main.call("_set_tool_mode", "bauen")
else:
main.call("_set_tool_mode", "objects")
func _on_build_kind_pressed(kind_id: String) -> void:
main.call("_on_build_kind_pressed", kind_id)

View file

@ -33,7 +33,6 @@ func build() -> void:
spawn_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
spawn_status.custom_minimum_size = UIScale.v2(0, 44)
add_child(spawn_status)
add_child(_muted_label("Der Check sagt VOR dem Export, was der Server-Loader verlangen wird (mindestens 4 Zonen pro Team, bekannte Objekt-IDs)."))
# --- Spawn-Zonen-Werkzeug: großer Button + Bedien-Panel ---
spawn_edit_btn = Button.new()
@ -82,9 +81,6 @@ func build() -> void:
spawn_delete_btn.pressed.connect(_on_spawn_delete_pressed)
spawn_panel.add_child(spawn_delete_btn)
spawn_panel.add_child(_muted_label(
"Klick setzt eine Zone, Klick+Ziehen verschiebt sie. Jede Zone ist "
+ "EIN Rückgängig-Schritt (Strg+Z). ESC beendet das Spawn-Werkzeug."))
func _spawn_team_button(text: String, group: ButtonGroup, team: int) -> Button:

View file

@ -31,6 +31,11 @@ func panel_title() -> String:
## sich vom angeschlossenen Theme-Baum (main hängt theme auf das Fenster).
func theme_color(cname: String) -> Color:
# Befund 25.10. („Texte kaum lesbar“): Panels werden gebaut, BEVOR sie im
# Baum hängen — get_theme_color liefert dort Schwarz statt der Theme-Farbe.
# Darum direkt aus dem Theme des Hauptfensters lesen.
if main != null and main.theme != null and main.theme.has_color(cname, "Editor"):
return main.theme.get_color(cname, "Editor")
return get_theme_color(cname, "Editor")

View file

@ -0,0 +1,171 @@
extends "res://src/ui/panels/editor_panel.gd"
## Malen-Tab (25.10., UI-Aufräumen): Material-Palette mit Vorschau-Kacheln,
## Pinselgröße und Kante. Malen ist technisch weiter der Pinsel-Modus PAINT des
## Terrain-Werkzeugs (terrain_tool.gd) — die Mode-Buttons und die Pinsel-Logik
## wohnen in terrain_panel.gd, dieses Panel hält nur die Mal-Widgets. main.gd
## reicht die Felder unter den alten Namen (terrain_palette …) weiter.
const ARRAYS_PATH := "res://assets/textures/terrain/terrain_arrays.res"
const MATERIAL_NAMES: Array[String] = [
"Steppe", "Sand", "Fels", "Sattgras", "Vertrocknet", "Moos", "Erde",
"Pfad", "Umgegraben", "Schotter", "Lehm", "Sand hell", "Sand rost",
"Basalt", "Geröll", "Schlamm", "Schnee", "Matschschnee", "Asche", "Stoppeln",
]
var malen_edit_btn: Button
var malen_panel: VBoxContainer
var palette: VBoxContainer
var mat_buttons: Array = []
var radius_label: Label
var radius_slider: HSlider
var weich_label: Label
var weich_slider: HSlider
var paint_hint: Label
var _syncing := false
static func spec() -> Dictionary:
return {"titel": "Malen", "gruppe": "rechts", "order": 45}
func panel_title() -> String:
return "Malen"
func build() -> void:
malen_edit_btn = Button.new()
malen_edit_btn.text = "🖌 Malen"
malen_edit_btn.custom_minimum_size = UIScale.v2(0, 48)
malen_edit_btn.toggle_mode = true
malen_edit_btn.tooltip_text = "Material auf den Boden malen (Taste 4)"
malen_edit_btn.pressed.connect(_on_malen_btn_pressed)
add_child(malen_edit_btn)
malen_panel = VBoxContainer.new()
malen_panel.add_theme_constant_override("separation", UIScale.px(8))
malen_panel.visible = false
add_child(malen_panel)
palette = VBoxContainer.new()
palette.add_theme_constant_override("separation", UIScale.px(4))
palette.visible = false
malen_panel.add_child(palette)
palette.add_child(_field_label("Material:"))
var grid := GridContainer.new()
grid.columns = 3
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
grid.add_theme_constant_override("h_separation", UIScale.px(6))
grid.add_theme_constant_override("v_separation", UIScale.px(6))
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = UIScale.v2(0, 340)
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(grid)
palette.add_child(scroll)
var previews := _material_previews()
var group := ButtonGroup.new()
for mat_id in SvmapIO.MATERIAL_IDS.size():
var btn := Button.new()
btn.text = MATERIAL_NAMES[mat_id] if mat_id < MATERIAL_NAMES.size() else str(SvmapIO.MATERIAL_IDS[mat_id])
btn.custom_minimum_size = UIScale.v2(92, 104)
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
btn.toggle_mode = true
btn.button_group = group
btn.clip_text = true
btn.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
btn.vertical_icon_alignment = VERTICAL_ALIGNMENT_TOP
btn.expand_icon = true
btn.add_theme_constant_override("icon_max_width", UIScale.px(76))
if mat_id < previews.size() and previews[mat_id] != null:
btn.icon = previews[mat_id]
btn.tooltip_text = "Malen: %s" % btn.text
if mat_id == 0:
btn.button_pressed = true
btn.toggled.connect(_on_material_changed.bind(mat_id))
mat_buttons.append(btn)
grid.add_child(btn)
radius_label = _field_label("Pinsel-Größe: 12 m")
malen_panel.add_child(radius_label)
radius_slider = HSlider.new()
radius_slider.min_value = 1.0
radius_slider.max_value = 64.0
radius_slider.step = 1.0
radius_slider.value = 12.0
radius_slider.custom_minimum_size = UIScale.v2(0, 32)
radius_slider.value_changed.connect(_on_radius_changed)
malen_panel.add_child(radius_slider)
weich_label = _field_label("Kante: 40 % weich")
malen_panel.add_child(weich_label)
weich_slider = HSlider.new()
weich_slider.min_value = 0.0
weich_slider.max_value = 100.0
weich_slider.step = 5.0
weich_slider.value = 40.0
weich_slider.custom_minimum_size = UIScale.v2(0, 32)
weich_slider.tooltip_text = "Links = harte Kante, rechts = weicher Übergang"
weich_slider.value_changed.connect(_on_weich_changed)
malen_panel.add_child(weich_slider)
paint_hint = _muted_label("")
malen_panel.add_child(paint_hint)
## Vorschaubilder der 20 Materialien aus dem Textur-Array (Albedo-Layer).
## Fehlt das Array oder ein Layer: Kachel bleibt ohne Bild (nie ein Absturz).
func _material_previews() -> Array:
var out: Array = []
if not ResourceLoader.exists(ARRAYS_PATH):
return out
var arr = load(ARRAYS_PATH)
if arr == null or not ("albedo_images" in arr):
return out
for img in arr.albedo_images:
var tex: Texture2D = null
if img is Image and not (img as Image).is_empty():
var copy: Image = (img as Image).duplicate()
if copy.is_compressed():
copy.decompress()
copy.resize(96, 96, Image.INTERPOLATE_BILINEAR)
tex = ImageTexture.create_from_image(copy)
out.append(tex)
return out
func _on_malen_btn_pressed() -> void:
var t = main.terrain_mod
if malen_edit_btn.button_pressed:
t.terrain_mode_paint.button_pressed = true
main.call("_set_tool_mode", "terrain")
t.refresh_panels()
else:
main.call("_set_tool_mode", "objects")
func _on_material_changed(pressed: bool, mat_id: int) -> void:
if pressed and main.terrain_tool != null:
main.terrain_tool.call("set_paint_material", mat_id)
func _on_radius_changed(value: float) -> void:
radius_label.text = "Pinsel-Größe: %d m" % int(value)
if _syncing:
return
var t = main.terrain_mod
if t != null and t.terrain_radius_slider != null:
t.terrain_radius_slider.value = value # → set_radius über dessen Handler
## Terrain-Tab → Malen: Radius spiegeln, ohne Rückkopplung.
func set_radius_silent(value: float) -> void:
_syncing = true
radius_slider.value = value
_syncing = false
func _on_weich_changed(value: float) -> void:
weich_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
if main.terrain_tool != null:
main.terrain_tool.call("set_weichheit", value / 100.0)

View file

@ -86,8 +86,6 @@ func build() -> void:
prop_water.tooltip_text = "Wasser: ganz links = kein Wasser, sonst Höhe der Wasseroberfläche über dem Terrain. Senken unter dieser Höhe füllen sich mit Wasser."
prop_water.set_meta("kein_wasser_value", KEIN_WASSER_WERT)
add_child(prop_water)
var wasser_hinweis := _muted_label("Senken unter der Wasserhöhe füllen sich automatisch. Wasser betreten = knietief, träge (kein Schwimmen).")
add_child(wasser_hinweis)
add_child(_separator())
add_child(_header_label("Terrain"))

View file

@ -31,4 +31,3 @@ func build() -> void:
grid_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein"
grid_check.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_snap_changed", pressed))
add_child(grid_check)
add_child(_muted_label("Raster-Snap rastet Platzieren und Ziehen auf ganze Meter."))

View file

@ -34,6 +34,11 @@ const PANELS: Array = [
"skript": "res://src/ui/panels/map_panel.gd"},
{"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40,
"skript": "res://src/ui/panels/terrain_panel.gd"},
# 25.10. UI-Aufräumen: Malen und Bauen hängen nicht mehr im Terrain-Tab.
{"id": "malen", "titel": "Malen", "gruppe": "rechts", "order": 45,
"skript": "res://src/ui/panels/malen_panel.gd"},
{"id": "bauen", "titel": "Bauen", "gruppe": "rechts", "order": 47,
"skript": "res://src/ui/panels/bauen_panel.gd"},
{"id": "check", "titel": "Prüfen", "gruppe": "rechts", "order": 50,
"skript": "res://src/ui/panels/check_panel.gd"},
{"id": "teststart", "titel": "Test-Start", "gruppe": "rechts", "order": 60,

View file

@ -58,7 +58,6 @@ func build() -> void:
_confirm_delete.cancel_button_text = "Abbrechen"
_confirm_delete.confirmed.connect(_on_delete_confirmed)
add_child(_confirm_delete)
add_child(_muted_label("Rechtsklick: Gruppieren / Auflösen / Umbenennen / Löschen. Objektzeile in eine Gruppe ziehen = aufnehmen, auf „Objekte“ zurückziehen = herauslösen."))
add_child(_separator())

View file

@ -100,7 +100,6 @@ func build() -> void:
sel_ungroup_btn.tooltip_text = "Die Gruppe der Auswahl wieder auflösen (Strg+Umschalt+G) — Objekte bleiben, nur die Bindung löst sich"
sel_ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
group_btns.add_child(sel_ungroup_btn)
add_child(_muted_label("Mehrfachauswahl: Umschalt+Klick oder Rahmen aufziehen. Eine Gruppe wandert und dreht gemeinsam — auch nach Speichern/Laden."))
# --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen-
# Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die

View file

@ -46,7 +46,6 @@ func build() -> void:
add_child(perspektive_box)
add_child(_separator())
add_child(_muted_label("Gilt im Lauf-Modus („▶ Karte testen“). Standard 1,0 — jede PC-/Maus-Kombination ist anders. Wird automatisch gespeichert."))
func _on_sens_changed(value: float) -> void:

View file

@ -26,7 +26,7 @@ func build() -> void:
tool_label = Label.new()
# U7: Werkzeug-Farbe über Theme (Editor/colors/accent).
tool_label.add_theme_color_override("font_color",
get_theme_color("accent", "Editor"))
theme_color("accent"))
# Karte t_297221b5: Statuszeilen-Labels egalisiert (min-size-Druck bei 1.5×)
tool_label.clip_text = true
tool_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
@ -37,7 +37,7 @@ func build() -> void:
sym_label = Label.new()
# U7: Symmetrie-Farbe über Theme (Editor/colors/accent).
sym_label.add_theme_color_override("font_color",
get_theme_color("accent", "Editor"))
theme_color("accent"))
sym_label.clip_text = true
sym_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
add_child(sym_label)
@ -45,7 +45,7 @@ func build() -> void:
stats_objects = Label.new()
# U7: Nebentext-Farbe über Theme (Editor/colors/field).
stats_objects.add_theme_color_override("font_color",
get_theme_color("field", "Editor"))
theme_color("field"))
stats_objects.clip_text = true
stats_objects.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
add_child(stats_objects)
@ -54,7 +54,7 @@ func build() -> void:
stats_terrain = Label.new()
# U7: Nebentext-Farbe über Theme (Editor/colors/field).
stats_terrain.add_theme_color_override("font_color",
get_theme_color("field", "Editor"))
theme_color("field"))
stats_terrain.clip_text = true
stats_terrain.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
add_child(stats_terrain)

View file

@ -38,11 +38,7 @@ func panel_title() -> String:
func build() -> void:
add_child(_header_label("Symmetrie"))
add_child(_muted_label(
"Macht die GANZE Karte mit einem Klick symmetrisch: Objekte, "
+ "Bau-Teile, Spawn-Punkte und Boden (Höhen + Material) werden "
+ "von der Quell-Hälfte auf die andere Seite ERGÄNZT. Es wird "
+ "nichts gelöscht — Widersprüche werden vorher gemeldet."))
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)
@ -75,10 +71,6 @@ func build() -> void:
report_label.custom_minimum_size = UIScale.v2(0, 96)
add_child(report_label)
add_child(_muted_label(
"Der Modus steht oben in der Leiste (Punkt/Achse X/Achse Z). "
+ "Paare, die schon passen, werden mit Spiegel-Kennungen "
+ "verkoppelt — auch bei alten Karten ohne diese Kennungen."))
conflict_dialog = ConfirmationDialog.new()
conflict_dialog.title = "Widersprüche — welche Seite gewinnt?"

View file

@ -28,11 +28,6 @@ var terrain_paint_hint: Label # Malen: Hinweis auf die Raster-Feinheit (24.10.
var terrain_weich_label: Label # Malen: Kante hart ↔ weich (25.10.)
var terrain_weich_slider: HSlider
# --- Bauen (Karte t_97ea59c5) — im selben Werkzeug-Tab (aus main.gd ausgezogen)
var build_edit_btn: Button
var build_panel: VBoxContainer
var build_kind_buttons: Dictionary = {} # kind_id → Button
static func spec() -> Dictionary:
return {"titel": "Terrain", "gruppe": "rechts", "order": 40}
@ -43,7 +38,7 @@ func build() -> void:
terrain_edit_btn.text = "⛰ Terrain formen"
terrain_edit_btn.custom_minimum_size = UIScale.v2(0, 48)
terrain_edit_btn.toggle_mode = true
terrain_edit_btn.tooltip_text = "Boden heben, senken und glätten — direkt in der Karte malen"
terrain_edit_btn.tooltip_text = "Boden formen: heben, senken, glätten, einebnen, Rampe (F1)"
terrain_edit_btn.pressed.connect(_on_terrain_btn_pressed)
add_child(terrain_edit_btn)
@ -52,7 +47,7 @@ func build() -> void:
terrain_panel.visible = false
add_child(terrain_panel)
terrain_panel.add_child(_field_label("Pinsel-Werkzeug:"))
terrain_panel.add_child(_field_label("Pinsel:"))
var mode_row := GridContainer.new()
mode_row.columns = 3
mode_row.add_theme_constant_override("h_separation", UIScale.px(8)) # U7-Raster
@ -81,52 +76,12 @@ func build() -> void:
mode_row.add_child(terrain_mode_raise)
mode_row.add_child(terrain_mode_lower)
mode_row.add_child(terrain_mode_smooth)
mode_row.add_child(terrain_mode_paint)
mode_row.add_child(terrain_mode_flatten)
mode_row.add_child(terrain_mode_ramp)
# Material-Palette (Karte t_603ec1a8): nur sichtbar im Malen-Modus.
terrain_palette = VBoxContainer.new()
terrain_palette.add_theme_constant_override("separation", UIScale.px(4))
terrain_palette.visible = false
terrain_panel.add_child(terrain_palette)
terrain_palette.add_child(_field_label("Material:"))
var mat_row := HBoxContainer.new()
mat_row.add_theme_constant_override("separation", UIScale.px(8)) # U7-Raster
terrain_palette.add_child(mat_row)
var mat_group := ButtonGroup.new()
# Karte t_ab408127: 20 Slots in scrollbarer Raster-Palette (Spec: „like
# Bibliothek") — 4 Spalten, ~2 sichtbare Reihen, scrollbar nach unten.
# Thumbnails folgen, sobald die Texturen in assets/textures/terrain/ liegen
# (gen_terrain_thumbs) — bis dahin Button-Farbfeld via icon (Default).
var mat_grid := GridContainer.new()
mat_grid.columns = 4
mat_grid.add_theme_constant_override("h_separation", UIScale.px(4))
mat_grid.add_theme_constant_override("v_separation", UIScale.px(4))
var mat_scroll := ScrollContainer.new()
mat_scroll.custom_minimum_size = UIScale.v2(0, 176) # ~3 Reihen a 48 px + Separation
mat_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
mat_scroll.add_child(mat_grid)
terrain_palette.add_child(mat_scroll)
var mat_labels := [
"🏜 Steppe", "🏖 Sand", "🪨 Fels", "🌱 Sattgras",
"🍂 Vertrocknet", "🍀 Moos", "🟫 Erde", "🛤 Pfad",
"⛏ Umgegraben", "🧱 Schotter", "🟤 Lehm", "🏜 Sand hell",
"🏜 Sand rost", "🗻 Basalt", "⛰ Geröll", "🌧 Schlamm",
"❄️ Schnee", "🥶 Matschschnee", "🌑 Asche", "🌾 Stoppeln",
]
for mat_id in SvmapIO.MATERIAL_IDS.size():
var mat_btn := Button.new()
mat_btn.text = str(mat_labels[mat_id]) if mat_id < mat_labels.size() else str(SvmapIO.MATERIAL_IDS[mat_id])
mat_btn.custom_minimum_size = UIScale.v2(0, 48)
mat_btn.toggle_mode = true
mat_btn.button_group = mat_group
mat_btn.tooltip_text = "Malen: %s (Slot %d)" % [str(SvmapIO.MATERIAL_IDS[mat_id]), mat_id]
if mat_id == 0:
mat_btn.button_pressed = true
mat_btn.toggled.connect(_on_terrain_material_changed.bind(mat_id))
terrain_mat_buttons.append(mat_btn)
mat_grid.add_child(mat_btn)
# Malen hat seit 25.10. einen eigenen Tab (malen_panel.gd); der Modus-Knopf
# bleibt unsichtbar in der ButtonGroup (Taste 4, Malen-Tab-Knopf).
terrain_mode_paint.visible = false
terrain_panel.add_child(terrain_mode_paint)
# Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider.
terrain_panel.add_child(_field_label("Pinselgröße:"))
@ -181,92 +136,14 @@ func build() -> void:
terrain_falloff_slider.value_changed.connect(_on_terrain_falloff_changed)
terrain_panel.add_child(terrain_falloff_slider)
# Malen kennt keine Stärke: das Material wird zellenweise gesetzt
# (Material-Schicht = ein Wert pro Rasterpunkt, weiche Übergänge macht
# der Splat-Renderer). Statt eines wirkungslosen Reglers steht im
# Malen-Modus die echte Feinheit da (Sly-Befund 22.10.: „Stärke ändert nichts“).
terrain_paint_hint = _muted_label("")
terrain_paint_hint.visible = false
terrain_panel.add_child(terrain_paint_hint)
# Kante beim Malen (Karte 25.10., Sly): 0 % = harter Rand, 100 % = der ganze
# Pinsel läuft weich aus (gestreut, der Shader glättet die Streuung).
terrain_weich_label = _field_label("Kante: 40 % weich")
terrain_weich_label.visible = false
terrain_panel.add_child(terrain_weich_label)
terrain_weich_slider = HSlider.new()
terrain_weich_slider.min_value = 0.0
terrain_weich_slider.max_value = 100.0
terrain_weich_slider.step = 5.0
terrain_weich_slider.value = 40.0
terrain_weich_slider.custom_minimum_size = UIScale.v2(0, 32)
terrain_weich_slider.tooltip_text = "Links = harte Kante, rechts = weicher Übergang"
terrain_weich_slider.visible = false
terrain_weich_slider.value_changed.connect(_on_terrain_weich_changed)
terrain_panel.add_child(terrain_weich_slider)
terrain_panel.add_child(_muted_label(
"Stärke folgt dem Stift-Druck bzw. dem Controller-Trigger (Maus = voll). "
+ "Rampe: Start klicken, ziehen, loslassen. Einebnen: Höhe am Strichstart wird zum Ziel."))
terrain_panel.add_child(_muted_label(
"Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Strg+Linksklick macht das Gegenteil — im Malen-Modus malt Strg+Klick Steppe (Grundboden) zurück. "
+ "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). Rechte Maustaste = Flug, ESC beendet den Terrain-Modus."))
# --- Bauen (Karte t_97ea59c5): Modular-Builder-Pieces im App-Skelett ---
# (aus main.gd ausgezogen — liegt mit im Terrain-Tab, Werkzeug-Zusammenhang)
build_edit_btn = Button.new()
build_edit_btn.text = "🏠 Bauen (Häuser)"
build_edit_btn.custom_minimum_size = UIScale.v2(0, 48)
build_edit_btn.toggle_mode = true
build_edit_btn.tooltip_text = "Fundamente, Wände, Dächer, Treppen und Türen bauen — im 2-Meter-Raster, wie Bauklötze"
build_edit_btn.pressed.connect(_on_build_btn_pressed)
add_child(build_edit_btn)
build_panel = VBoxContainer.new()
build_panel.add_theme_constant_override("separation", UIScale.px(8)) # U7-Raster
build_panel.visible = false
add_child(build_panel)
build_panel.add_child(_field_label("Bauteil:"))
var build_grid := GridContainer.new()
build_grid.columns = 2
build_grid.add_theme_constant_override("h_separation", UIScale.px(8)) # U7-Raster
build_grid.add_theme_constant_override("v_separation", UIScale.px(8)) # U7-Raster
build_panel.add_child(build_grid)
var build_group := ButtonGroup.new()
for kind_id in BuildOps.KINDS.keys():
var kind_btn := Button.new()
kind_btn.text = str(BuildOps.KINDS[kind_id]["label"])
kind_btn.toggle_mode = true
kind_btn.button_group = build_group
kind_btn.custom_minimum_size = UIScale.v2(148, 44)
kind_btn.clip_text = true
kind_btn.tooltip_text = "%s — %s" % [str(BuildOps.KINDS[kind_id]["label"]), str(BuildOps.KINDS[kind_id].get("info", ""))]
kind_btn.pressed.connect(_on_build_kind_pressed.bind(str(kind_id)))
build_kind_buttons[str(kind_id)] = kind_btn
build_grid.add_child(kind_btn)
(build_kind_buttons["fundament"] as Button).set_pressed_no_signal(true)
build_panel.add_child(_muted_label(
"Linksklick setzt das Bauteil aufs 2-m-Raster — auch AUF andere Teile "
+ "(Wände aufs Fundament stapeln, Dach auf die Wände). Jedes Bauteil ist "
+ "EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Bauen-Modus."))
func _on_build_btn_pressed() -> void:
if build_edit_btn.button_pressed:
main.call("_set_tool_mode", "bauen")
else:
main.call("_set_tool_mode", "objects")
func _on_build_kind_pressed(kind_id: String) -> void:
main.call("_on_build_kind_pressed", kind_id)
func _on_terrain_btn_pressed() -> void:
if terrain_edit_btn.button_pressed:
if terrain_mode_paint.button_pressed:
terrain_mode_raise.button_pressed = true # Malen hat seinen eigenen Tab
main.call("_set_tool_mode", "terrain")
refresh_panels()
else:
main.call("_set_tool_mode", "objects")
@ -300,22 +177,36 @@ func _on_terrain_mode_changed(_pressed: bool) -> void:
var paint := _selected_terrain_mode() == TerrainField.Mode.PAINT
if terrain_palette != null:
terrain_palette.visible = paint
if terrain_strength_slider != null:
terrain_strength_slider.visible = not paint
terrain_strength_label.visible = not paint
if terrain_falloff_slider != null:
terrain_falloff_slider.visible = not paint
terrain_falloff_label.visible = not paint
if terrain_weich_slider != null:
terrain_weich_slider.visible = paint
terrain_weich_label.visible = paint
if terrain_paint_hint != null:
terrain_paint_hint.visible = paint
terrain_paint_hint.text = paint_hint_text(main.doc.get("terrain", {}) if main.doc is Dictionary else {})
refresh_panels()
if main.terrain_tool != null:
main.terrain_tool.call("set_brush_mode", _selected_terrain_mode())
## Welche Bedienflächen sind sichtbar (Terrain-Tab bzw. Malen-Tab), welcher
## Start-Knopf steht gedrückt — Terrain und Malen teilen das Pinsel-Werkzeug.
func refresh_panels() -> void:
if terrain_panel == null:
return
var aktiv: bool = str(main.get("tool_mode")) == "terrain"
var paint := _selected_terrain_mode() == TerrainField.Mode.PAINT
terrain_panel.visible = aktiv and not paint
terrain_edit_btn.set_pressed_no_signal(aktiv and not paint)
var malen = main.get("malen_mod")
if malen != null:
malen.malen_panel.visible = aktiv and paint
malen.malen_edit_btn.set_pressed_no_signal(aktiv and paint)
if aktiv:
_show_own_tab(malen if paint else self)
func _show_own_tab(tab: Control) -> void:
var tabs = main.get("ui_right_panel")
if tabs is TabContainer and tab != null and tab.get_parent() == tabs:
(tabs as TabContainer).current_tab = tab.get_index()
## Hinweistext Malen-Modus (pure, testbar): feinste Mal-Einheit = Abstand
## zweier Rasterpunkte der Karte (size / (resolution − 1)).
static func paint_hint_text(terrain: Dictionary) -> String:
@ -329,12 +220,6 @@ static func paint_hint_text(terrain: Dictionary) -> String:
return "Malen setzt das Material zellenweise: feinste Einheit auf dieser Karte ≈ %s m. Stärke gibt es hier nicht." % m_text
## Material-Palette (Karte t_603ec1a8): gewähltes Material an den Pinsel.
func _on_terrain_material_changed(pressed: bool, mat_id: int) -> void:
if pressed and main.terrain_tool != null:
main.terrain_tool.call("set_paint_material", mat_id)
## Pinselgrößen-Presets (Karte t_603ec1a8): setzen den Radius-Slider
## (der Label-Text + Ring-Optik hängen am value_changed-Signal).
func _terrain_size_button(text: String, group: ButtonGroup, radius_m: float) -> Button:
@ -357,16 +242,13 @@ func _on_terrain_size_preset(pressed: bool, radius_m: float) -> void:
func _on_terrain_radius_changed(value: float) -> void:
terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value)
var malen = main.get("malen_mod")
if malen != null:
malen.set_radius_silent(value)
if main.terrain_tool != null:
main.terrain_tool.call("set_radius", value)
func _on_terrain_weich_changed(value: float) -> void:
terrain_weich_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
if main.terrain_tool != null:
main.terrain_tool.call("set_weichheit", value / 100.0)
func _on_terrain_falloff_changed(value: float) -> void:
terrain_falloff_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
if main.terrain_tool != null:

View file

@ -39,7 +39,7 @@ func build() -> void:
add_child(_separator())
info_hint = Label.new()
info_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info_hint.text = "In der Leiste „🎯 Test-Start“ drücken (oder Taste T), dann in die Karte klicken. Die Höhe kommt automatisch vom Terrain. Der Punkt liegt in einer Nebendatei zur .svmap — die Kartendatei selbst bleibt unverändert."
info_hint.text = "Leiste „🎯 Test-Start“ oder Taste T, dann in die Karte klicken."
add_child(info_hint)
# main holt sich das Modul hier ab (main._refresh_test_start_panel) — die

View file

@ -163,6 +163,14 @@ func build() -> void:
size_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index))
add_child(size_menu)
var help_btn := Button.new()
help_btn.text = "❓ Hilfe"
help_btn.name = "hilfe"
help_btn.custom_minimum_size = UIScale.v2(110, 40)
help_btn.tooltip_text = "Werkzeuge, Steuerung und Tastenkürzel (H)"
help_btn.pressed.connect(func() -> void: main.call("_toggle_help_overlay"))
add_child(help_btn)
title_label = Label.new()
title_label.text = " %s" % main.APP_TITLE
title_label.add_theme_font_size_override("font_size", UIScale.heading_font_size())
@ -185,7 +193,7 @@ func build() -> void:
file_label.text = ""
# U7: Farbe über Theme (Editor/colors/title) statt modulate-Wert im Code.
file_label.add_theme_color_override("font_color",
get_theme_color("title", "Editor"))
main.theme.get_color("title", "Editor"))
# Karte t_297221b5: Map-Pfad darf bei großer Stufe egalisiert werden
# (Voller Name im Tooltip, sonst drückt die Zeile die Top-Bar aus dem Fenster).
# Expand: bekommt zusammen mit dem Spacer seinen Anteil, egalisiert mit "…".

View file

@ -0,0 +1,78 @@
extends PanelContainer
## Werkzeugleiste oben links in der 3D-Ansicht (25.10.): erscheint, sobald ein
## Objekt ausgewählt ist. Umschalter Verschieben / Drehen & Kippen (steuert den
## Gizmo) plus schnelle Aktionen (Auf den Boden, Duplizieren, Löschen).
## Objekte werden bewusst NIE skaliert — es gibt kein Skalieren-Werkzeug.
var main
var move_btn: Button
var rotate_btn: Button
var ground_btn: Button
var dup_btn: Button
var delete_btn: Button
func setup(p_main) -> void:
main = p_main
set_anchors_preset(Control.PRESET_TOP_LEFT)
offset_left = UIScale.px(12)
offset_top = UIScale.px(12)
mouse_filter = Control.MOUSE_FILTER_STOP
visible = false
var box := HBoxContainer.new()
box.add_theme_constant_override("separation", UIScale.px(6))
add_child(box)
var group := ButtonGroup.new()
move_btn = _btn("↔ Verschieben", "Pfeile am Objekt ziehen: X rot, Y grün, Z blau", true, group)
move_btn.pressed.connect(func() -> void: _set_mode("move"))
box.add_child(move_btn)
rotate_btn = _btn("⟳ Drehen / Kippen", "Ringe am Objekt ziehen: um alle drei Achsen, in 15°-Schritten", true, group)
rotate_btn.pressed.connect(func() -> void: _set_mode("rotate"))
box.add_child(rotate_btn)
move_btn.set_pressed_no_signal(true)
box.add_child(VSeparator.new())
ground_btn = _btn("⤓ Auf den Boden", "Auswahl aufs Terrain absetzen (Strg+Bild↑)", false, null)
ground_btn.pressed.connect(func() -> void: main.interaction.snap_selection_to_floor())
box.add_child(ground_btn)
dup_btn = _btn("⧉ Kopie", "Duplizieren (Strg+D)", false, null)
dup_btn.pressed.connect(func() -> void: main.interaction.duplicate_selected())
box.add_child(dup_btn)
delete_btn = _btn("🗑 Löschen", "Löschen (Entf) — Strg+Z holt es zurück", false, null)
delete_btn.pressed.connect(func() -> void: main.interaction.delete_selected())
box.add_child(delete_btn)
func _btn(text: String, tip: String, toggle: bool, group: ButtonGroup) -> Button:
var b := Button.new()
b.text = text
b.tooltip_text = tip
b.toggle_mode = toggle
if group != null:
b.button_group = group
b.custom_minimum_size = UIScale.v2(0, 36)
b.focus_mode = Control.FOCUS_NONE # Leertaste/Pfeile bleiben beim Viewport
return b
func _set_mode(m: String) -> void:
if main.gizmo != null:
main.gizmo.set_mode(m)
## Sichtbarkeit pro Frame aus der Auswahl ableiten (wie der Gizmo selbst).
func _process(_delta: float) -> void:
if main == null or main.get("interaction") == null:
return
var inter = main.interaction
var mode: String = str(main.get("tool_mode"))
var want: bool = (mode == "objects" or mode == "bauen") \
and not inter.selected_indices.is_empty() \
and not bool(inter.get("placing")) \
and main.get("playtest") == null
if want != visible:
visible = want
if visible and main.gizmo != null:
var rotate_on: bool = main.gizmo.mode == "rotate"
if rotate_btn.button_pressed != rotate_on:
rotate_btn.set_pressed_no_signal(rotate_on)
move_btn.set_pressed_no_signal(not rotate_on)

View file

@ -70,12 +70,29 @@ var _drag_before: Dictionary = {} # History-Snapshot VOR dem Zug
var _arrows := {} # "x"/"y"/"z" → Node3D (Pfeil-Root)
# --- Dreh-/Kipp-Modus (25.10.): Ringe um alle drei Achsen -------------------
const RING_R := 1.5 # Ring-Radius in Metern
const RING_SEGMENTS := 48
const ROT_STEP_DEG := 15.0
var mode := "move" # "move" = Pfeile, "rotate" = Ringe
var rot_step_deg := ROT_STEP_DEG
var _rings := {} # "x"/"y"/"z" → MeshInstance3D
var _rot_dragging := false
var _rot_axis := ""
var _rot_center := Vector2.ZERO
var _rot_start_angle := 0.0
var _rot_applied := 0.0 # bisher angewendete (gerastete) Grad
var _rot_suffix := "" # merge-Kennung: EIN Undo-Schritt pro Zug
var _rot_serial := 0
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
main = p_main
camera = p_camera
view_builder = p_builder
_build_arrows()
_build_rings()
_apply_mode_visibility()
visible = false
@ -119,6 +136,53 @@ func _build_arrows() -> void:
_arrows[a] = root
## Drehringe bauen: je Achse ein dünner Torus, ungeschattet, ohne Tiefentest.
func _build_rings() -> void:
for a in AXES:
var mat := StandardMaterial3D.new()
mat.albedo_color = AXIS_COLORS[a]
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.no_depth_test = true
mat.render_priority = 20
var ring := MeshInstance3D.new()
var torus := TorusMesh.new()
torus.inner_radius = RING_R - 0.06
torus.outer_radius = RING_R + 0.06
torus.rings = 48
torus.ring_segments = 8
ring.mesh = torus
ring.material_override = mat
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
# Torus liegt lokal in der XZ-Ebene (Achse = Y).
if a == "x":
ring.rotation.z = PI / 2.0
elif a == "z":
ring.rotation.x = PI / 2.0
ring.visible = false
add_child(ring)
_rings[a] = ring
## Modus wechseln ("move" = Verschieben, "rotate" = Drehen/Kippen).
func set_mode(m: String) -> void:
if m != "move" and m != "rotate":
return
if _dragging:
_finish_drag(false)
if _rot_dragging:
_finish_rotate(false)
mode = m
_apply_mode_visibility()
func _apply_mode_visibility() -> void:
for a in _arrows:
(_arrows[a] as Node3D).visible = mode == "move"
for a in _rings:
(_rings[a] as Node3D).visible = mode == "rotate"
func _axis_vec(a: String) -> Vector3:
return AXES.get(a, Vector3.ZERO) as Vector3
@ -169,26 +233,43 @@ func _unhandled_input(event: InputEvent) -> void:
var inter = main.get("interaction")
if inter == null or bool(inter.get("placing")):
return
if mode == "rotate":
var ring_axis := pick_ring(mb.position)
if ring_axis != "":
_begin_rotate(ring_axis, mb.position)
get_viewport().set_input_as_handled()
return
var axis := pick(mb.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:
if _dragging:
if _rot_dragging:
_finish_rotate(true)
get_viewport().set_input_as_handled()
elif _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:
if _dragging:
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 _dragging:
if _rot_dragging:
_finish_rotate(false)
get_viewport().set_input_as_handled()
elif _dragging:
_finish_drag(false)
get_viewport().set_input_as_handled()
@ -215,6 +296,35 @@ func pick(screen_pos: Vector2) -> String:
return best
## Ring unter dem Cursor (Abstand zum projizierten Kreis) — "" = keiner.
func pick_ring(screen_pos: Vector2) -> String:
if not visible or mode != "rotate":
return ""
var best := ""
var best_d := HIT_PX
for a in AXES:
var pts := _ring_screen_points(a)
for i in pts.size() - 1:
if pts[i] == Vector2.INF or pts[i + 1] == Vector2.INF:
continue
var d := _dist_point_segment(screen_pos, pts[i], pts[i + 1])
if d < best_d:
best_d = d
best = a
return best
func _ring_screen_points(a: String) -> Array:
var axis := _axis_vec(a)
var u := axis.cross(Vector3.UP if absf(axis.dot(Vector3.UP)) < 0.9 else Vector3.RIGHT).normalized()
var v := axis.cross(u).normalized()
var out: Array = []
for i in RING_SEGMENTS + 1:
var t := TAU * float(i) / float(RING_SEGMENTS)
out.append(_safe_unproject(global_position + (u * cos(t) + v * sin(t)) * RING_R))
return out
func _safe_unproject(p: Vector3) -> Vector2:
if camera == null:
return Vector2.INF
@ -372,12 +482,90 @@ func _finish_drag(commit: bool) -> void:
## Für die Esc-Kette in main.gd: Zug abbrechen, falls einer läuft. true =
## es lief einer und wurde abgebrochen (idempotent, weil _dragging prüft).
func cancel_drag_if_active() -> bool:
if _rot_dragging:
_finish_rotate(false)
return true
if not _dragging:
return false
_finish_drag(false)
return true
# --------------------------------------------------- Drehen / Kippen -------
func _ring_angle(mouse: Vector2) -> float:
var d := mouse - _rot_center
return atan2(d.y, d.x)
func _begin_rotate(axis: String, mouse: Vector2) -> void:
var inter = main.get("interaction")
if inter == null or inter.selected_indices.is_empty():
return
var c := _safe_unproject(global_position)
if c == Vector2.INF:
return
_rot_axis = axis
_rot_center = c
_rot_start_angle = _ring_angle(mouse)
_rot_applied = 0.0
_rot_serial += 1
_rot_suffix = "#g%d" % _rot_serial
_rot_dragging = true
var name := "Drehen (Y)" if axis == "y" else ("Kippen um X" if axis == "x" else "Kippen um Z")
main.call("_set_status", "%s — Ring ziehen, rastet in %.0f°-Schritten; Esc bricht ab, Strg+Z nimmt den Zug zurück." % [name, rot_step_deg])
func _rotate_motion(mouse: Vector2) -> void:
# Bildschirmwinkel → Drehwinkel um die Achse (Vorzeichen je nach Blickrichtung).
var delta_screen := wrapf(_ring_angle(mouse) - _rot_start_angle, -PI, PI)
var facing := _axis_vec(_rot_axis).dot(camera.global_transform.basis.z)
var deg := -rad_to_deg(delta_screen) * (1.0 if facing >= 0.0 else -1.0)
var snapped_deg := snappedf(deg, rot_step_deg)
_rotate_to(snapped_deg)
## Auf einen Gesamtwinkel (gerastet) nachführen — in Schritten, EIN Undo-Eintrag.
func _rotate_to(total_deg: float) -> void:
var diff := total_deg - _rot_applied
if absf(diff) < 0.001:
return
var inter = main.get("interaction")
if inter == null:
return
if _rot_axis == "y":
inter.rotate_selected(diff, _rot_suffix)
else:
inter.tilt_selected(_rot_axis == "x", diff, _rot_suffix)
_rot_applied = total_deg
func _finish_rotate(commit: bool) -> void:
if not _rot_dragging:
return
_rot_dragging = false
if not commit and absf(_rot_applied) > 0.001:
main.call("_do_undo") # der ganze Zug ist EIN Eintrag → komplett zurück
main.call("_set_status", "Dreh-Zug abgebrochen — Objekt wie vorher.")
_rot_axis = ""
_rot_applied = 0.0
## Probes/Tests: derselbe Pfad wie ein echter Ring-Zug (ohne Maus-Mathe).
func demo_rotate_axis(axis: String, deg: float) -> bool:
var inter = main.get("interaction") if main != null else null
if inter == null or inter.selected_indices.is_empty() or absf(deg) < 0.001:
return false
_rot_axis = axis
_rot_serial += 1
_rot_suffix = "#g%d" % _rot_serial
_rot_applied = 0.0
_rotate_to(snappedf(deg, rot_step_deg))
_rot_axis = ""
_rot_applied = 0.0
return true
# ------------------------------------------------------ Schrittweite -------
## Mausrad WÄHREND des Ziehens: durch die Voreinstellungen blättern

View file

@ -0,0 +1 @@
uid://w5p21ia5bryo

View file

@ -0,0 +1 @@
uid://badptran2lnax

View file

@ -141,8 +141,8 @@ func _test_tiles() -> void:
_check(tr.custom_minimum_size.x >= 64, "Thumbnail-Zielgröße ≥ 64 px (ist %d)"
% int(tr.custom_minimum_size.x))
_check(tr.size.x >= 64, "Thumbnail rendert ≥ 64 px breit (ist %.0f)" % tr.size.x)
_check(tr.texture != null and tr.texture.get_width() == 96,
"Textur auf 96 px skaliert")
_check(tr.texture != null and tr.texture.get_width() == 128,
"Textur auf 128 px skaliert")
if lbl != null:
_check(lbl.text == "Wachturm", "deutscher Name unter dem Bild (%s)" % lbl.text)
_check(tile_btn.size.y >= 100, "Kachel hoch genug (%.0f px)" % tile_btn.size.y)

View file

@ -0,0 +1 @@
uid://d3lv8invgk6kv

101
tests/test_ui_aufraeumen.gd Normal file
View file

@ -0,0 +1,101 @@
extends SceneTree
## Editor-Test (headless): UI-Aufräumen 25.10. — eigene Tabs Terrain/Malen/Bauen,
## Viewport-Werkzeugleiste, Dreh-/Kipp-Gizmo (alle Achsen, Undo), Kontrast.
## Ausführen: godot --headless --path . --script res://tests/test_ui_aufraeumen.gd
var failures := 0
var checks := 0
var main: Control
func _initialize() -> void:
_run.call_deferred()
func _run() -> void:
main = (load("res://scenes/main.tscn") as PackedScene).instantiate()
root.add_child(main)
await process_frame
await process_frame
(main.doc["objects"] as Array).append({
"id": "crate_wood", "pos": [4.0, 0.0, 4.0], "scale": 1.0, "rot_y_deg": 0.0})
main.call("_after_structure_change")
main.history.clear()
await process_frame
print("== Tabs ==")
var tabs: TabContainer = main.ui_right_panel
var titel: Array = []
for i in tabs.get_tab_count():
titel.append(tabs.get_tab_title(i))
_check(titel.has("Terrain") and titel.has("Malen") and titel.has("Bauen"), "Tabs Terrain, Malen, Bauen vorhanden")
_check(main.malen_mod != null and main.bauen_mod != null, "Module malen/bauen gebaut")
_check(main.terrain_mod.terrain_mode_paint.get_parent() == main.terrain_mod.terrain_panel and not main.terrain_mod.terrain_mode_paint.visible,
"Mal-Modus nicht mehr als Knopf im Terrain-Tab")
_check(main.build_edit_btn.get_parent() == main.bauen_mod, "Bauen-Knopf wohnt im Bauen-Tab")
_check(main.malen_mod.mat_buttons.size() == SvmapIO.MATERIAL_IDS.size(), "Malen: eine Kachel je Material")
print("== Malen-Tab ==")
main.call("_set_tool_mode", "terrain")
_check(main.terrain_mod.terrain_panel.visible and not main.malen_mod.malen_panel.visible, "Terrain aktiv: nur Terrain-Panel")
main.terrain_mod.terrain_mode_paint.button_pressed = true
_check(main.malen_mod.malen_panel.visible and main.malen_mod.palette.visible and not main.terrain_mod.terrain_panel.visible,
"Malen aktiv: Malen-Panel statt Terrain-Panel")
_check(tabs.current_tab == main.malen_mod.get_index(), "Tab springt auf Malen")
main.malen_mod.radius_slider.value = 20.0
_check(int(main.terrain_mod.terrain_radius_slider.value) == 20, "Malen-Radius steuert den Pinsel")
main.call("_set_tool_mode", "objects")
print("== Lesbarkeit ==")
var muted: Color = main.malen_mod.theme_color("muted")
_check(muted.a > 0.7 and muted.r > 0.9, "muted-Farbe ist hell (nicht Schwarz)")
_check(main.status_mod.stats_objects.get_theme_color("font_color").a > 0.0, "Statuszeile lesbar")
print("== Werkzeugleiste + Dreh-Gizmo ==")
_check(not main.viewport_toolbar.visible, "ohne Auswahl keine Werkzeugleiste")
main.interaction.call("select_object", 0)
await process_frame
await process_frame
_check(main.viewport_toolbar.visible, "mit Auswahl erscheint die Werkzeugleiste")
_check(main.gizmo.mode == "move", "Standard: Verschieben")
main.viewport_toolbar.rotate_btn.pressed.emit()
_check(main.gizmo.mode == "rotate", "Knopf Drehen schaltet den Gizmo um")
var obj: Dictionary = main.doc["objects"][0]
var n0: int = main.history.entries.size()
_check(main.gizmo.demo_rotate_axis("x", 30.0), "Kippen um X")
_check(main.gizmo.demo_rotate_axis("z", -15.0), "Kippen um Z")
_check(main.gizmo.demo_rotate_axis("y", 45.0), "Drehen um Y")
var rot := SvmapIO.objekt_rotation(obj)
_check(is_equal_approx(rot.x, 30.0) and is_equal_approx(fposmod(rot.z, 360.0), 345.0) and is_equal_approx(rot.y, 45.0),
"Euler (%s) = (30, 45, 345)" % str(rot))
_check(main.history.entries.size() == n0 + 3, "jeder Zug = ein Undo-Schritt (%d neu)" % (main.history.entries.size() - n0))
main.call("_do_undo")
obj = main.doc["objects"][0]
_check(is_equal_approx(SvmapIO.objekt_rotation(obj).y, 0.0), "Undo nimmt den Y-Zug zurück")
main.call("_do_undo")
main.call("_do_undo")
obj = main.doc["objects"][0]
_check(SvmapIO.objekt_rotation(obj).is_equal_approx(Vector3.ZERO), "dreimal Undo = Ausgangslage")
main.viewport_toolbar.move_btn.pressed.emit()
_check(main.gizmo.mode == "move", "Verschieben schaltet zurück")
var scale_btns := 0
for c in main.viewport_toolbar.find_children("*", "Button", true, false):
if str((c as Button).text).to_lower().contains("skal"):
scale_btns += 1
_check(scale_btns == 0, "kein Skalieren-Werkzeug (Objekte behalten ihre Größe)")
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)
func _check(cond: bool, label: String) -> void:
checks += 1
if cond:
print(" ok: ", label)
else:
failures += 1
print(" FAIL: ", label)

View file

@ -93,7 +93,7 @@ func _test_math() -> void:
_check(v.x == 175 and v.y == 50, "Mittel: v2(140,40) → (175,50)")
UIScale.set_step_index(2)
_check(UIScale.px(40) == 60 and UIScale.font_size() == 24, "Groß: 40 px → 60 px, Schrift 24")
_check(UIScale.px(96) == 144, "Groß: Thumbnail 96 → 144")
_check(UIScale.px(96) == 144, "Groß: Thumbnail 96 → 144 (px-Rechnung)")
func _test_cfg_roundtrip() -> void:
@ -142,10 +142,10 @@ func _test_real_ui_scaling() -> void:
_check(int(main.theme.default_font_size) == 20, "Mittel: Theme-Schrift 20 (sofort wirksam)")
_check(main.undo_btn.custom_minimum_size == Vector2(175, 50), "Mittel: Undo-Knopf 175×50")
_check(main.library_search.custom_minimum_size.y == 45, "Mittel: Suchfeld 36 → 45")
var tile := _find_tile_button(158)
_check(tile != null and tile.custom_minimum_size.y == 158, "Mittel: Bibliotheks-Kachel 126 → 158")
var thumb := _find_thumb_rect(120)
_check(thumb != null and thumb.custom_minimum_size.x == 120, "Mittel: Thumbnail 96 → 120")
var tile := _find_tile_button(235)
_check(tile != null and tile.custom_minimum_size.y == 235, "Mittel: Bibliotheks-Kachel 188 → 235")
var thumb := _find_thumb_rect(160)
_check(thumb != null and thumb.custom_minimum_size.x == 160, "Mittel: Thumbnail 128 → 160")
_check(main.size_menu.get_popup().is_item_checked(1), "Menü-Häkchen wandert auf Mittel (neues Popup)")
# Groß:

View file

@ -252,6 +252,15 @@ func _run_tests() -> void:
var pulled := EditorUpdater.pull(git, work)
_check(bool(pulled["ok"]), "pull --ff-only erfolgreich (v2 wird geladen)")
_check(EditorUpdater.head_sha(git, work) == str(avail["remote"]), "nach Pull ist work auf v2")
# Befund 25.10.: Klon VOR origin/main (z. B. Feature-Branch) ist KEIN Update.
_git(work, ["config", "user.email", "t@example.invalid"])
_git(work, ["config", "user.name", "Update-Test"])
_write_file(work.path_join("lokal.txt"), "nur lokal\n")
_git(work, ["add", "lokal.txt"])
_git(work, ["commit", "-m", "lokal voraus", "lokal.txt"])
var ahead := EditorUpdater.check_update(git, work)
_check(str(ahead["status"]) == "up_to_date", "Klon VORAUS (Feature-Branch) → up_to_date, kein Dauer-Banner")
_git(work, ["reset", "--hard", "-q", str(avail["remote"])])
_git(work, ["remote", "set-url", "origin", "http://127.0.0.1:1/nicht-da.git"])
var dead_pull := EditorUpdater.pull(git, work)
_check(not bool(dead_pull["ok"]), "pull gegen toten Origin schlägt fehl")

View file

@ -222,9 +222,9 @@ default_font_size = 16
Editor/colors/accent = Color(0.55, 0.8, 1, 1)
Editor/colors/status_ok = Color(0.75, 0.95, 0.75, 1)
Editor/colors/status_error = Color(1, 0.6, 0.5, 1)
Editor/colors/muted = Color(1, 1, 1, 0.55)
Editor/colors/field = Color(1, 1, 1, 0.7)
Editor/colors/title = Color(1, 1, 1, 0.6)
Editor/colors/muted = Color(1, 1, 1, 0.74)
Editor/colors/field = Color(1, 1, 1, 0.9)
Editor/colors/title = Color(1, 1, 1, 0.82)
Editor/colors/warning = Color(1, 0.62, 0.3, 1)
Editor/colors/badge_outline = Color(0, 0, 0, 1)
Editor/colors/help_bg = Color(0.09, 0.11, 0.13, 0.97)
@ -246,7 +246,7 @@ TabContainer/styles/tab_hovered = SubResource("tab_hover")
TabContainer/styles/tab_unselected = SubResource("tab_unsel")
TabContainer/styles/tabbar_background = SubResource("tabbar")
TabContainer/colors/font_selected_color = Color(1, 1, 1, 1)
TabContainer/colors/font_unselected_color = Color(1, 1, 1, 0.7)
TabContainer/colors/font_unselected_color = Color(1, 1, 1, 0.88)
Button/styles/normal = SubResource("btn_normal")
Button/styles/hover = SubResource("btn_hover")
Button/styles/pressed = SubResource("btn_pressed")
@ -254,11 +254,11 @@ Button/styles/disabled = SubResource("btn_disabled")
Button/styles/focus = SubResource("btn_focus")
Button/colors/font_hover_color = Color(1, 1, 1, 1)
Button/colors/font_pressed_color = Color(0.8, 0.9, 1, 1)
Button/colors/font_disabled_color = Color(1, 1, 1, 0.35)
Button/colors/font_disabled_color = Color(1, 1, 1, 0.5)
LineEdit/styles/normal = SubResource("line_normal")
LineEdit/styles/focus = SubResource("line_focus")
LineEdit/colors/font_color = Color(1, 1, 1, 1)
LineEdit/colors/font_placeholder_color = Color(1, 1, 1, 0.4)
LineEdit/colors/font_placeholder_color = Color(1, 1, 1, 0.58)
HSplitContainer/styles/grabber_area = SubResource("split_grab")
HSplitContainer/styles/grabber_area_highlight = SubResource("split_grab_hover")
VSplitContainer/styles/grabber_area = SubResource("split_grab")

View file

@ -3,7 +3,7 @@
cd "$(dirname "$0")/.."
GODOT=${GODOT:-/opt/data/home/bin/godot4}
rc=0
for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector test_pinsel_werkzeuge test_terrain_mirror test_mal_fein; do
for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector test_pinsel_werkzeuge test_terrain_mirror test_mal_fein test_ui_aufraeumen test_ui_layout test_ui_scale; do
"$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1
code=$?
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)