Compare commits

..

7 commits

38 changed files with 1280 additions and 331 deletions

View file

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

View file

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

View file

@ -60,6 +60,8 @@ const ACTIONS := {
"editor_brush_lower": [KEY_2], "editor_brush_lower": [KEY_2],
"editor_brush_smooth": [KEY_3], "editor_brush_smooth": [KEY_3],
"editor_brush_paint": [KEY_4], "editor_brush_paint": [KEY_4],
"editor_brush_flatten": [KEY_5],
"editor_brush_ramp": [KEY_6],
# --- Datei & Undo (Karte t_11b5c25b, Karte 3/3) -------------------------- # --- Datei & Undo (Karte t_11b5c25b, Karte 3/3) --------------------------
# Strg+Z/Strg+Y laufen seit dem M2-UX-Fundament; hier NACHGEBUGGEN als # Strg+Z/Strg+Y laufen seit dem M2-UX-Fundament; hier NACHGEBUGGEN als
# dokumentierte Actions (alle 3 Shortcut-Karten: ACTIONS-Dict = Source of # dokumentierte Actions (alle 3 Shortcut-Karten: ACTIONS-Dict = Source of

View file

@ -20,7 +20,8 @@ extends Node3D
## liest daraus. Undo/Redo läuft über die gemeinsame ActionHistory (terrain-Block). ## liest daraus. Undo/Redo läuft über die gemeinsame ActionHistory (terrain-Block).
const SMOOTH_RATE_DIVISOR := 20.0 # Stärke-Slider (m/s) → Glätt-Tempo (Anteil/s) const SMOOTH_RATE_DIVISOR := 20.0 # Stärke-Slider (m/s) → Glätt-Tempo (Anteil/s)
const MODE_LABELS := ["Heben", "Senken", "Glätten", "Malen"] const MODE_LABELS := ["Heben", "Senken", "Glätten", "Malen", "Einebnen", "Rampe"]
const FLATTEN_RATE_DIVISOR := 8.0 # Stärke-Slider → Einebnen-Tempo (Anteil/s)
var main # Hauptfenster (script-erweitert, Variant) var main # Hauptfenster (script-erweitert, Variant)
var camera: Camera3D var camera: Camera3D
@ -32,6 +33,10 @@ var brush_mode: int = TerrainField.Mode.RAISE
var brush_radius := 12.0 # Meter var brush_radius := 12.0 # Meter
## Kante beim Malen: 0 = hart, 1 = ganz weich (Karte 25.10.). ## Kante beim Malen: 0 = hart, 1 = ganz weich (Karte 25.10.).
var brush_weichheit := 0.4 var brush_weichheit := 0.4
## Pinsel-Falloff (Karte 25.10.): 0 = hart (linear), 1 = weich (quadratisch).
var brush_falloff := 0.5
## Druck 0.05–1 (Pen-Druck / Controller-Trigger / Maus = 1.0), skaliert die Stärke.
var pressure := 1.0
var brush_strength := 8.0 # Meter pro Sekunde (Heben/Senken) var brush_strength := 8.0 # Meter pro Sekunde (Heben/Senken)
var paint_material: int = 0 # Material-ID im Malen-Modus (0=Gras, 1=Sand, 2=Fels) var paint_material: int = 0 # Material-ID im Malen-Modus (0=Gras, 1=Sand, 2=Fels)
@ -39,6 +44,10 @@ var paint_material: int = 0 # Material-ID im Malen-Modus (0=Gras, 1=Sand, 2=Fels
var _stroking := false var _stroking := false
var _stroke_mode: int = TerrainField.Mode.RAISE var _stroke_mode: int = TerrainField.Mode.RAISE
var _stroke_material: int = 0 var _stroke_material: int = 0
var _flatten_target := 0.0 # Einebnen: Höhe unter dem Cursor beim Strichstart
var _ramp_a := Vector2.ZERO # Rampe: Startpunkt (Welt-X/Z) + Höhe
var _ramp_ha := 0.0
var _ramp_b := Vector2.ZERO # Rampe: aktueller Endpunkt
var _heights := PackedFloat32Array() var _heights := PackedFloat32Array()
var _mats := PackedByteArray() # Material-Schicht (res²) — seit _refresh_ var _mats := PackedByteArray() # Material-Schicht (res²) — seit _refresh_
# _from_doc immer dekodiert (t_55ed041a), # _from_doc immer dekodiert (t_55ed041a),
@ -56,6 +65,7 @@ var _cursor := Vector3.ZERO
# Pinsel-Ring # Pinsel-Ring
var _ring: MeshInstance3D var _ring: MeshInstance3D
var _ramp_line: MeshInstance3D # Rampen-Vorschau: Linie Start → Cursor
var _ring_radius := -1.0 var _ring_radius := -1.0
var _ring_mode := -1 var _ring_mode := -1
@ -74,6 +84,15 @@ func _ensure_ring() -> void:
_ring = MeshInstance3D.new() _ring = MeshInstance3D.new()
_ring.visible = false _ring.visible = false
add_child(_ring) add_child(_ring)
_ramp_line = MeshInstance3D.new()
_ramp_line.visible = false
_ramp_line.mesh = ImmediateMesh.new()
var lm := StandardMaterial3D.new()
lm.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
lm.albedo_color = Color(1.0, 0.85, 0.3)
lm.no_depth_test = true
_ramp_line.material_override = lm
add_child(_ramp_line)
# ------------------------------------------------------------ Aktivierung --- # ------------------------------------------------------------ Aktivierung ---
@ -102,6 +121,8 @@ func deactivate() -> void:
_end_stroke() _end_stroke()
if _ring != null and is_instance_valid(_ring): if _ring != null and is_instance_valid(_ring):
_ring.visible = false _ring.visible = false
if _ramp_line != null and is_instance_valid(_ramp_line):
_ramp_line.visible = false
## Nach Undo/Redo/Laden/Neue-Map: Feld neu dekodieren (Cache ist sonst alt). ## Nach Undo/Redo/Laden/Neue-Map: Feld neu dekodieren (Cache ist sonst alt).
@ -177,6 +198,16 @@ func set_weichheit(w: float) -> void:
brush_weichheit = clampf(w, 0.0, 1.0) brush_weichheit = clampf(w, 0.0, 1.0)
func set_falloff(f: float) -> void:
brush_falloff = clampf(f, 0.0, 1.0)
## Druck setzen (XR-Controller-Trigger 0–1 ruft das direkt; Pen/Maus laufen
## über _unhandled_input). Wert wird über TerrainField.pressure_of begrenzt.
func set_pressure(p: float) -> void:
pressure = TerrainField.pressure_of(p, 0.0)
func set_strength(mps: float) -> void: func set_strength(mps: float) -> void:
brush_strength = clampf(mps, 1.0, 20.0) brush_strength = clampf(mps, 1.0, 20.0)
@ -192,7 +223,10 @@ func _unhandled_input(event: InputEvent) -> void:
if not active: if not active:
return return
if event is InputEventMouseMotion: if event is InputEventMouseMotion:
_update_cursor((event as InputEventMouseMotion).position) var mm := event as InputEventMouseMotion
_update_cursor(mm.position)
# Stift: pressure > 0; Maus meldet 0 (oder 1) → bleibt voll.
pressure = TerrainField.pressure_of(mm.pressure, _trigger_axis())
return return
if event is InputEventMouseButton: if event is InputEventMouseButton:
var mb := event as InputEventMouseButton var mb := event as InputEventMouseButton
@ -220,6 +254,15 @@ func _unhandled_input(event: InputEvent) -> void:
# t_2470de89), in jedem Werkzeugzustand. # t_2470de89), in jedem Werkzeugzustand.
## Gamepad-Trigger (rechts) des ersten Pads; 0 ohne Pad. Nur Eingabe-Lesen,
## keine OS-Annahme.
func _trigger_axis() -> float:
var pads := Input.get_connected_joypads()
if pads.is_empty():
return 0.0
return Input.get_joy_axis(pads[0], JOY_AXIS_TRIGGER_RIGHT)
func _update_cursor(viewport_pos: Vector2) -> void: func _update_cursor(viewport_pos: Vector2) -> void:
var hit: Variant = TerrainField.raycast(_heights, _res, _sx, _sz, var hit: Variant = TerrainField.raycast(_heights, _res, _sx, _sz,
camera.project_ray_origin(viewport_pos), camera.project_ray_normal(viewport_pos)) camera.project_ray_origin(viewport_pos), camera.project_ray_normal(viewport_pos))
@ -256,6 +299,12 @@ func _begin_stroke(mode: int, material_id: int = -1) -> void:
_stroke_mode = mode _stroke_mode = mode
_stroke_material = paint_material if material_id < 0 else material_id _stroke_material = paint_material if material_id < 0 else material_id
_stroking = true _stroking = true
if mode == TerrainField.Mode.FLATTEN:
_flatten_target = _height_at_stroke(_cursor.x, _cursor.z)
elif mode == TerrainField.Mode.RAMP:
_ramp_a = Vector2(_cursor.x, _cursor.z)
_ramp_ha = _height_at_stroke(_cursor.x, _cursor.z)
_ramp_b = _ramp_a
# Im Malen-Modus lebt die Material-Schicht während des Strichs # Im Malen-Modus lebt die Material-Schicht während des Strichs
# (dekodiert — Legacy-Maps ohne Schicht starten mit alles-Gras). # (dekodiert — Legacy-Maps ohne Schicht starten mit alles-Gras).
if mode == TerrainField.Mode.PAINT: if mode == TerrainField.Mode.PAINT:
@ -288,11 +337,12 @@ func _process(delta: float) -> void:
view_builder.call("update_terrain", _heights, _mats, _splat_rect) view_builder.call("update_terrain", _heights, _mats, _splat_rect)
_ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3 _ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3
return return
var amount: float if _stroke_mode == TerrainField.Mode.RAMP:
if _stroke_mode == TerrainField.Mode.SMOOTH: # Rampe wirkt erst beim Loslassen — hier nur der Endpunkt + Ring-Hinweis.
amount = clampf(brush_strength / SMOOTH_RATE_DIVISOR * delta, 0.0, 1.0) _ramp_b = Vector2(_cursor.x, _cursor.z)
else: _update_ramp_line()
amount = brush_strength * delta return
var amount := _step_amount(_stroke_mode, delta)
# Während des Strichs NICHT auf die Format-Range clampen — die zieht der # Während des Strichs NICHT auf die Format-Range clampen — die zieht der
# Range-Expander danach nach (sonst friert Heben/Senken an der Kante ein). # Range-Expander danach nach (sonst friert Heben/Senken an der Kante ein).
var changed := _height_step(_cursor.x, _cursor.z, amount) var changed := _height_step(_cursor.x, _cursor.z, amount)
@ -306,6 +356,17 @@ func _process(delta: float) -> void:
_ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3 _ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3
## Stärke dieses Frames: Meter (Heben/Senken) bzw. Mischfaktor 0–1 (Glätten/
## Einebnen) — skaliert mit dem Druck.
func _step_amount(mode: int, dt: float) -> float:
match mode:
TerrainField.Mode.SMOOTH:
return clampf(brush_strength / SMOOTH_RATE_DIVISOR * dt * pressure, 0.0, 1.0)
TerrainField.Mode.FLATTEN:
return clampf(brush_strength / FLATTEN_RATE_DIVISOR * dt * pressure, 0.0, 1.0)
return brush_strength * dt * pressure
## Höhe spannt das Höhenfeld automatisch auf, wenn der Pinsel über die ## Höhe spannt das Höhenfeld automatisch auf, wenn der Pinsel über die
## Format-Grenzen (min/max_height) hinaus will — sonst wäre Senken auf einer ## Format-Grenzen (min/max_height) hinaus will — sonst wäre Senken auf einer
## frisch angelegten (flachen) Map unmöglich und langes Heben würde clampen. ## frisch angelegten (flachen) Map unmöglich und langes Heben würde clampen.
@ -329,6 +390,11 @@ func _end_stroke() -> void:
if not _stroking: if not _stroking:
return return
_stroking = false _stroking = false
if _stroke_mode == TerrainField.Mode.RAMP:
_ramp_line.visible = false
if not _ramp_apply():
main.call("_set_status", "Rampe: Start und Ende liegen zu nah beieinander — Klick, ziehen, loslassen.")
return # nichts geändert → kein leerer Undo-Eintrag
# Doc erst jetzt ändern: der GANZE Strich ist ein History-Eintrag. # Doc erst jetzt ändern: der GANZE Strich ist ein History-Eintrag.
var before: Dictionary = main.history.snapshot(["terrain"]) var before: Dictionary = main.history.snapshot(["terrain"])
var terrain: Dictionary = main.doc.get("terrain", {}) var terrain: Dictionary = main.doc.get("terrain", {})
@ -346,6 +412,40 @@ func _end_stroke() -> void:
main.call("_on_terrain_stroke_committed") main.call("_on_terrain_stroke_committed")
## Rampe anwenden (Strichende): Start-Höhe = Höhe beim Klick, End-Höhe = Höhe
## unter dem Endpunkt. Breite = 2 × Radius. Symmetrie wie bei den Pinseln.
func _ramp_apply() -> bool:
if _ramp_a.distance_to(_ramp_b) < 1.0:
return false
var hb := _height_at_stroke(_ramp_b.x, _ramp_b.y)
var width := brush_radius * 2.0
var snap := {}
var g := _world_to_grid(_ramp_a.x, _ramp_a.y)
if _sym_active():
var g2 := _world_to_grid(_ramp_b.x, _ramp_b.y)
var r := _brush_region(g).merge(_brush_region(g2))
snap = _snap_heights(r)
var changed := TerrainField.apply_ramp(_heights, _res, _sx, _sz,
_ramp_a, _ramp_b, _ramp_ha, hb, width, brush_falloff)
if changed and not snap.is_empty():
_mirror_heights(snap, g)
if changed:
_expand_range_if_needed()
view_builder.call("update_terrain", _heights, _mats)
return changed
## Rampen-Vorschau: gelbe Linie vom Startpunkt zum Cursor (auf Geländehöhe).
func _update_ramp_line() -> void:
var im := _ramp_line.mesh as ImmediateMesh
im.clear_surfaces()
im.surface_begin(Mesh.PRIMITIVE_LINES)
im.surface_add_vertex(Vector3(_ramp_a.x, _ramp_ha + 0.5, _ramp_a.y))
im.surface_add_vertex(Vector3(_ramp_b.x, _height_at_stroke(_ramp_b.x, _ramp_b.y) + 0.5, _ramp_b.y))
im.surface_end()
_ramp_line.visible = true
## Programmgesteuerter Stroke (Demo/Screenshot-Hook): macht exakt, was eine ## Programmgesteuerter Stroke (Demo/Screenshot-Hook): macht exakt, was eine
## Maus auch tun würde — inkl. History-Eintrag. ## Maus auch tun würde — inkl. History-Eintrag.
func demo_stroke(x: float, z: float, mode: int, seconds: float) -> void: func demo_stroke(x: float, z: float, mode: int, seconds: float) -> void:
@ -358,11 +458,7 @@ func demo_stroke(x: float, z: float, mode: int, seconds: float) -> void:
var save_x := _cursor.x var save_x := _cursor.x
var save_z := _cursor.z var save_z := _cursor.z
for i in steps: for i in steps:
var amount: float var amount := _step_amount(mode, dt)
if mode == TerrainField.Mode.SMOOTH:
amount = clampf(brush_strength / SMOOTH_RATE_DIVISOR * dt, 0.0, 1.0)
else:
amount = brush_strength * dt
if mode == TerrainField.Mode.PAINT: if mode == TerrainField.Mode.PAINT:
_paint_step(save_x, save_z) _paint_step(save_x, save_z)
continue continue
@ -472,7 +568,8 @@ func _height_step(x: float, z: float, amount: float) -> bool:
if _sym_active(): if _sym_active():
snap = _snap_heights(_brush_region(g)) snap = _snap_heights(_brush_region(g))
var changed := TerrainField.apply_brush(_heights, _res, _sx, _sz, var changed := TerrainField.apply_brush(_heights, _res, _sx, _sz,
-100000.0, 100000.0, x, z, brush_radius, _stroke_mode, amount) -100000.0, 100000.0, x, z, brush_radius, _stroke_mode, amount,
brush_falloff, _flatten_target)
if changed and not snap.is_empty(): if changed and not snap.is_empty():
_mirror_heights(snap, g) _mirror_heights(snap, g)
return changed return changed
@ -489,7 +586,7 @@ func _paint_step(x: float, z: float) -> bool:
if _sym_active() and _mats.size() >= _mres * _mres: if _sym_active() and _mats.size() >= _mres * _mres:
snap = _snap_mats(_brush_region_r(g, _mres)) snap = _snap_mats(_brush_region_r(g, _mres))
var changed := TerrainField.apply_material_brush_mix(_mats, _mix_b, _mix_w, _mres, var changed := TerrainField.apply_material_brush_mix(_mats, _mix_b, _mix_w, _mres,
_sx, _sz, x, z, brush_radius, _stroke_material, brush_weichheit) _sx, _sz, x, z, brush_radius, _stroke_material, brush_weichheit, pressure)
if changed and not snap.is_empty(): if changed and not snap.is_empty():
_mirror_mats(snap, g) _mirror_mats(snap, g)
return changed return changed
@ -614,6 +711,10 @@ func _refresh_ring_mesh() -> void:
var mc: int = clampi(paint_material, 0, SvmapIO.MATERIAL_IDS.size() - 1) var mc: int = clampi(paint_material, 0, SvmapIO.MATERIAL_IDS.size() - 1)
mat.albedo_color = (terrain_mesh_color(mc) as Color) mat.albedo_color = (terrain_mesh_color(mc) as Color)
mat.albedo_color.a = 0.9 mat.albedo_color.a = 0.9
TerrainField.Mode.FLATTEN:
mat.albedo_color = Color(1.0, 0.85, 0.3, 0.9)
TerrainField.Mode.RAMP:
mat.albedo_color = Color(0.85, 0.5, 1.0, 0.9)
_: _:
mat.albedo_color = Color(0.4, 0.7, 1.0, 0.9) mat.albedo_color = Color(0.4, 0.7, 1.0, 0.9)
mat.emission_enabled = true mat.emission_enabled = true

View file

@ -160,9 +160,23 @@ static func check_update(git_exe: String, project_dir: String) -> Dictionary:
"error": "Origin lieferte keinen Commit (nur Warnungen)."} "error": "Origin lieferte keinen Commit (nur Warnungen)."}
if local == remote: if local == remote:
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""} 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": ""} 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. ## 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 ## WARUM: `git_run` mischt stdout+stderr, und der git-Client schreibt bei einer
## HTTP-Umleitung zuerst "warning: Umleitung nach ..." — die alte Fassung nahm ## 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). ## fürs Statuszeile-Fenster, leer wenn nichts besonderes passiert ist).
static func pull(git_exe: String, project_dir: String) -> Dictionary: static func pull(git_exe: String, project_dir: String) -> Dictionary:
var stash_info := _stash_local_changes(git_exe, project_dir) 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 errors: Array = []
var text := str(r["output"]).strip_edges() var text := str(r["output"]).strip_edges()
var info := "" var info := ""

View file

@ -10,7 +10,7 @@ class_name TerrainField
## ##
## Der eigentliche Doc-Mutator bleibt main.gd/terrain_tool.gd — hier nur Mathematik. ## Der eigentliche Doc-Mutator bleibt main.gd/terrain_tool.gd — hier nur Mathematik.
enum Mode { RAISE, LOWER, SMOOTH, PAINT } enum Mode { RAISE, LOWER, SMOOTH, PAINT, FLATTEN, RAMP }
## Editor-MVP-Perf-Limit (Karte t_9aba04f1): das Format erlaubt 17–513, aber ## Editor-MVP-Perf-Limit (Karte t_9aba04f1): das Format erlaubt 17–513, aber
## der Editor formt im MVP nur Grids bis 129 (≈ 32k Dreiecke — flüssig beim ## der Editor formt im MVP nur Grids bis 129 (≈ 32k Dreiecke — flüssig beim
@ -39,6 +39,99 @@ static func height_at(heights: PackedFloat32Array, res: int, size_x: float, size
return lerpf(lerpf(h00, h10, tx), lerpf(h01, h11, tx), tz) return lerpf(lerpf(h00, h10, tx), lerpf(h01, h11, tx), tz)
## Pinsel-Gewicht über einen einzigen Regler (Karte 25.10.): t = 1 − d/radius,
## w = lerp(t, t·t, falloff). falloff 0 = linear (hart, breite Mitte),
## 1 = quadratisch (weich, spitze Mitte). Ausserhalb des Radius 0.
static func falloff_weight(d: float, radius: float, falloff: float) -> float:
if radius <= 0.0 or d >= radius:
return 0.0
var t := clampf(1.0 - d / radius, 0.0, 1.0)
return lerpf(t, t * t, clampf(falloff, 0.0, 1.0))
## Pinsel-Stärke aus Eingabe: Pen-Druck (>0) gewinnt, sonst Controller-Trigger
## (> 0.02), sonst Maus = 1.0. Immer 0.05…1.
static func pressure_of(pen: float, trigger: float) -> float:
if pen > 0.0:
return clampf(pen, 0.05, 1.0)
if trigger > 0.02:
return clampf(trigger, 0.05, 1.0)
return 1.0
## Flatten: zieht Höhen im Kreis gewichtet zur Zielhöhe. amount = Mischfaktor 0–1.
## (Eigene Funktion, damit apply_brush-Signatur stabil bleibt.)
static func apply_flatten(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
cx: float, cz: float, radius: float, target: float, amount: float,
falloff: float) -> bool:
if res < 2 or radius <= 0.0 or amount <= 0.0 or heights.size() < res * res:
return false
var cell_x := size_x / float(res - 1)
var cell_z := size_z / float(res - 1)
var gx := (cx / size_x + 0.5) * float(res - 1)
var gz := (cz / size_z + 0.5) * float(res - 1)
var x0 := maxi(int(floor(gx - radius / cell_x)) - 1, 0)
var x1 := mini(int(ceil(gx + radius / cell_x)) + 1, res - 1)
var z0 := maxi(int(floor(gz - radius / cell_z)) - 1, 0)
var z1 := mini(int(ceil(gz + radius / cell_z)) + 1, res - 1)
var changed := false
for z in range(z0, z1 + 1):
for x in range(x0, x1 + 1):
var dx := (float(x) - gx) * cell_x
var dz := (float(z) - gz) * cell_z
var w := falloff_weight(sqrt(dx * dx + dz * dz), radius, falloff)
if w <= 0.0:
continue
var idx := z * res + x
var h := heights[idx]
var nh := lerpf(h, target, clampf(amount * w, 0.0, 1.0))
if nh != h:
heights[idx] = nh
changed = true
return changed
## Rampe: Höhe linear von ha (Punkt a) nach hb (Punkt b), Breite `width` m
## quer zur Achse; Kern (halbe Breite) voll, Falloff am Rand. a/b = Welt-X/Z (Vector2). Rückgabe: geändert.
static func apply_ramp(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
a: Vector2, b: Vector2, ha: float, hb: float, width: float, falloff: float) -> bool:
if res < 2 or width <= 0.0 or heights.size() < res * res:
return false
var cell_x := size_x / float(res - 1)
var cell_z := size_z / float(res - 1)
var half := width * 0.5
var ab := b - a
var len2 := ab.length_squared()
var lo := Vector2(minf(a.x, b.x) - half, minf(a.y, b.y) - half)
var hi := Vector2(maxf(a.x, b.x) + half, maxf(a.y, b.y) + half)
var x0 := maxi(int(floor((lo.x / size_x + 0.5) * float(res - 1))) - 1, 0)
var x1 := mini(int(ceil((hi.x / size_x + 0.5) * float(res - 1))) + 1, res - 1)
var z0 := maxi(int(floor((lo.y / size_z + 0.5) * float(res - 1))) - 1, 0)
var z1 := mini(int(ceil((hi.y / size_z + 0.5) * float(res - 1))) + 1, res - 1)
var changed := false
for z in range(z0, z1 + 1):
for x in range(x0, x1 + 1):
var p := Vector2((float(x) / float(res - 1) - 0.5) * size_x,
(float(z) / float(res - 1) - 0.5) * size_z)
var s := 0.0 if len2 < 0.000001 else clampf((p - a).dot(ab) / len2, 0.0, 1.0)
var d := p.distance_to(a + ab * s)
# Innere Hälfte der Breite voll (ebene Fahrfläche), äußere Hälfte
# läuft mit dem Falloff-Regler ins Gelände aus.
var core := half * 0.5
var w := 1.0 if d <= core else falloff_weight(d - core, half - core, falloff)
if d >= half:
w = 0.0
if w <= 0.0:
continue
var idx := z * res + x
var h := heights[idx]
var nh := lerpf(h, lerpf(ha, hb, s), w)
if nh != h:
heights[idx] = nh
changed = true
return changed
## Pinsel anwenden. amount: ## Pinsel anwenden. amount:
## RAISE/LOWER → Meter (bereits strength × dt vom Aufrufer), ## RAISE/LOWER → Meter (bereits strength × dt vom Aufrufer),
## SMOOTH → Mischfaktor 0–1 (Anteil Richtung 3×3-Umgebungsmittel). ## SMOOTH → Mischfaktor 0–1 (Anteil Richtung 3×3-Umgebungsmittel).
@ -46,7 +139,14 @@ static func height_at(heights: PackedFloat32Array, res: int, size_x: float, size
## Kegel. Rückgabe true, wenn mindestens ein Punkt verändert wurde. ## Kegel. Rückgabe true, wenn mindestens ein Punkt verändert wurde.
static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, size_z: float, static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, size_z: float,
hmin: float, hmax: float, cx: float, cz: float, radius: float, hmin: float, hmax: float, cx: float, cz: float, radius: float,
mode: int, amount: float) -> bool: mode: int, amount: float, falloff: float = -1.0, target: float = 0.0) -> bool:
## falloff < 0 = alter Kosinus-Falloff (Kompatibilität), sonst Regler-Falloff.
## target nur für FLATTEN (Zielhöhe).
if mode == Mode.RAMP:
return false # Rampe hat ein eigenes Zwei-Punkt-API (apply_ramp)
if mode == Mode.FLATTEN:
return apply_flatten(heights, res, size_x, size_z, cx, cz, radius, target,
clampf(amount, 0.0, 1.0), maxf(falloff, 0.0))
if mode == Mode.PAINT: if mode == Mode.PAINT:
# PAINT läuft über apply_material_brush (Material-Schicht, Karte # PAINT läuft über apply_material_brush (Material-Schicht, Karte
# t_603ec1a8) — Höhe bleibt unangetastet. Hier ohne Material-Argument # t_603ec1a8) — Höhe bleibt unangetastet. Hier ohne Material-Argument
@ -66,7 +166,7 @@ static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, si
var z1 := mini(int(ceil(gz + r_cells_z)) + 1, res - 1) var z1 := mini(int(ceil(gz + r_cells_z)) + 1, res - 1)
if mode == Mode.SMOOTH: if mode == Mode.SMOOTH:
return _apply_smooth(heights, res, hmin, hmax, gx, gz, r_cells_x, r_cells_z, return _apply_smooth(heights, res, hmin, hmax, gx, gz, r_cells_x, r_cells_z,
x0, x1, z0, z1, clampf(amount, 0.0, 1.0)) x0, x1, z0, z1, clampf(amount, 0.0, 1.0), falloff)
var changed := false var changed := false
var z := z0 var z := z0
while z <= z1: while z <= z1:
@ -78,7 +178,8 @@ static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, si
if d >= radius: if d >= radius:
x += 1 x += 1
continue continue
var w := 0.5 * (1.0 + cos(PI * d / radius)) var w := 0.5 * (1.0 + cos(PI * d / radius)) if falloff < 0.0 \
else falloff_weight(d, radius, falloff)
var idx := z * res + x var idx := z * res + x
var h := heights[idx] var h := heights[idx]
var nh := h + amount * w if mode == Mode.RAISE else h - amount * w var nh := h + amount * w if mode == Mode.RAISE else h - amount * w
@ -131,10 +232,13 @@ static func apply_material_brush(mats: PackedByteArray, res: int, size_x: float,
## radius·(1−weichheit) wird voll gemalt, außen fällt der Anteil des neuen ## radius·(1−weichheit) wird voll gemalt, außen fällt der Anteil des neuen
## Materials weich auf 0 (Smoothstep). Ein Punkt wird nur verstärkt, nie ## Materials weich auf 0 (Smoothstep). Ein Punkt wird nur verstärkt, nie
## abgeschwächt (Nachmalen am Rand macht ihn kräftiger, nicht schwächer). ## abgeschwächt (Nachmalen am Rand macht ihn kräftiger, nicht schwächer).
## Mischung bleibt zweiteilig: das neue Material + das bisher stärkste andere. ## Mischung bleibt zweiteilig: das neue Material + das bisher stärkste andere
## (kommt ein drittes Material dazu, wird das schwächere der beiden ersetzt).
## staerke (0–1, Controller-Trigger/Pen-Druck) deckelt den Zielanteil.
static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArray, static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArray,
mix_w: PackedByteArray, res: int, size_x: float, size_z: float, mix_w: PackedByteArray, res: int, size_x: float, size_z: float,
cx: float, cz: float, radius: float, material_id: int, weichheit: float) -> bool: cx: float, cz: float, radius: float, material_id: int, weichheit: float,
staerke: float = 1.0) -> bool:
if res < 2 or radius <= 0.0 or mats.size() < res * res \ if res < 2 or radius <= 0.0 or mats.size() < res * res \
or mix_b.size() < res * res or mix_w.size() < res * res: or mix_b.size() < res * res or mix_w.size() < res * res:
return false return false
@ -160,7 +264,7 @@ static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArr
if d > inner: if d > inner:
var t := clampf((d - inner) / maxf(radius - inner, 0.0001), 0.0, 1.0) var t := clampf((d - inner) / maxf(radius - inner, 0.0001), 0.0, 1.0)
f = 1.0 - t * t * (3.0 - 2.0 * t) f = 1.0 - t * t * (3.0 - 2.0 * t)
var fb := int(roundf(f * 255.0)) var fb := int(roundf(f * clampf(staerke, 0.0, 1.0) * 255.0))
if fb <= 0: if fb <= 0:
continue continue
var idx := z * res + x var idx := z * res + x
@ -175,7 +279,15 @@ static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArr
mix_b[idx] = material_id mix_b[idx] = material_id
mix_w[idx] = 0 mix_w[idx] = 0
else: else:
var andere := a if a != material_id else b # Ziel in keinem Slot → das SCHWÄCHERE Slot-Material wird ersetzt
# (Anteil von b = w/255, a hat 255−w): w ≥ 128 → b bleibt, sonst a.
var andere: int
if a == material_id:
andere = b
elif b == material_id:
andere = a
else:
andere = b if w >= 128 else a
mats[idx] = andere mats[idx] = andere
mix_b[idx] = material_id mix_b[idx] = material_id
mix_w[idx] = fb mix_w[idx] = fb
@ -187,52 +299,35 @@ static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArr
## aus dem STAND der Feldes berechnet, sonst wandert die Welle in eine Richtung). ## aus dem STAND der Feldes berechnet, sonst wandert die Welle in eine Richtung).
static func _apply_smooth(heights: PackedFloat32Array, res: int, hmin: float, hmax: float, static func _apply_smooth(heights: PackedFloat32Array, res: int, hmin: float, hmax: float,
gx: float, gz: float, r_cells_x: float, r_cells_z: float, gx: float, gz: float, r_cells_x: float, r_cells_z: float,
x0: int, x1: int, z0: int, z1: int, amount: float) -> bool: x0: int, x1: int, z0: int, z1: int, amount: float, falloff: float = -1.0) -> bool:
# Mittelwerte nur für das Pinsel-Rechteck (statt Puffer in Feldgröße pro Schritt).
var bw := x1 - x0 + 1
var avg := PackedFloat32Array() var avg := PackedFloat32Array()
avg.resize(heights.size()) avg.resize(bw * (z1 - z0 + 1))
var z := z0 for z in range(z0, z1 + 1):
while z <= z1: for x in range(x0, x1 + 1):
var x := x0
while x <= x1:
var dx := (float(x) - gx) / maxf(r_cells_x, 0.0001)
var dz := (float(z) - gz) / maxf(r_cells_z, 0.0001)
var d := sqrt(dx * dx + dz * dz)
if d >= 1.0:
x += 1
continue
var sum := 0.0 var sum := 0.0
var count := 0 var count := 0
var zi := maxi(z - 1, 0) for zi in range(maxi(z - 1, 0), mini(z + 1, res - 1) + 1):
while zi <= mini(z + 1, res - 1): for xi in range(maxi(x - 1, 0), mini(x + 1, res - 1) + 1):
var xi := maxi(x - 1, 0)
while xi <= mini(x + 1, res - 1):
sum += heights[zi * res + xi] sum += heights[zi * res + xi]
count += 1 count += 1
xi += 1 avg[(z - z0) * bw + (x - x0)] = sum / float(maxi(count, 1))
zi += 1
avg[z * res + x] = sum / float(maxi(count, 1))
x += 1
z += 1
var changed := false var changed := false
z = z0 for z in range(z0, z1 + 1):
while z <= z1: for x in range(x0, x1 + 1):
var x := x0
while x <= x1:
var dx := (float(x) - gx) / maxf(r_cells_x, 0.0001) var dx := (float(x) - gx) / maxf(r_cells_x, 0.0001)
var dz := (float(z) - gz) / maxf(r_cells_z, 0.0001) var dz := (float(z) - gz) / maxf(r_cells_z, 0.0001)
var d := sqrt(dx * dx + dz * dz) var d := sqrt(dx * dx + dz * dz)
if d >= 1.0: if d >= 1.0:
x += 1
continue continue
var w := 0.5 * (1.0 + cos(PI * d)) var w := falloff_weight(d, 1.0, falloff)
var idx := z * res + x var idx := z * res + x
var nh := lerpf(heights[idx], avg[idx], clampf(amount * w, 0.0, 1.0)) var nh := lerpf(heights[idx], avg[(z - z0) * bw + (x - x0)], clampf(amount * w, 0.0, 1.0))
nh = clampf(nh, hmin, hmax) nh = clampf(nh, hmin, hmax)
if nh != heights[idx]: if nh != heights[idx]:
heights[idx] = nh heights[idx] = nh
changed = true changed = true
x += 1
z += 1
return changed return changed

View file

@ -9,70 +9,74 @@ extends RefCounted
static func text() -> String: static func text() -> String:
var out := "" var out := ""
out += _header("Navigation (Kamera)") out += _header("Kamera & Steuerung")
out += _table(_rows([ out += _table(_rows([
["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"], ["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"],
["Mittlere Maustaste + Umschalt ziehen", "Pan: Karte verschieben"], ["Mittlere Maustaste + Umschalt", "Pan: Karte verschieben"],
["Mausrad", "Zoom (aus, während ein Objekt gedreht wird)"], ["Mausrad", "Zoom"],
["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"], ["Rechte Maustaste halten", "Freelook-Flug in jedem Werkzeug: Maus = Umsehen, W/A/S/D = Bewegen, Leertaste hoch, Umschalt runter"],
["Strg halten (im Flug)", "schneller fliegen (×3)"], ["Strg / Alt (im Flug)", "schneller (×3) / langsamer (×0,3); Mausrad ändert die Grundgeschwindigkeit"],
["Alt halten (im Flug)", "langsamer fliegen (×0,3)"],
["Mausrad (im Flug)", "Grundgeschwindigkeit dauerhaft ändern (2–300 m/s)"],
["Umschalt+F", "Flugmodus dauerhaft an/aus"], ["Umschalt+F", "Flugmodus dauerhaft an/aus"],
["F", "Auswahl ins Bild zentrieren"], ["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([ out += _table(_rows([
["Linksklick", "Objekt auswählen (ersetzt die Auswahl)"], ["Linksklick", "Objekt wählen; Umschalt/Strg+Klick: Mehrfachauswahl"],
["Strg+Klick (Viewport + Baum)", "Objekt zur Auswahl hinzufügen/entfernen (Mehrfachauswahl)"], ["Rahmen aufziehen", "alle Objekte im Rahmen wählen (Umschalt: hinzufügen)"],
["Umschalt+Klick", "wie Strg+Klick (Mehrfachauswahl umschalten)"], ["Bibliothek: Klick / Doppelklick", "Objekt auswählen / Platzieren starten; Klick in die Karte setzt, Esc bricht ab"],
["Ziehen über leeren Boden", "Rahmen aufziehen: alle Objekte im Rahmen wählen"], ["G", "greifen: Objekt hängt an der Maus (Klick setzt, Esc zurück)"],
["Umschalt + Rahmen", "Rahmen-Auswahl zur bestehenden Auswahl hinzufügen"], ["Pfeiltasten", "im 2-m-Raster verschieben"],
["Esc", "Werkzeug abbrechen → Greifen zurückrollen → Ghost abbrechen → Auswahl aufheben"], ["Bild↑ / Bild↓", "0,25 m anheben / absenken; Strg+Bild↑ = auf den Boden setzen"],
["G", "Auswahl greifen (hängt an der Maus; Klick setzt, Esc rollt zurück)"], ["Strg halten", "Snap-Zwang auf ganze Meter"],
])) ["Strg+D", "duplizieren"],
out += _header("Objekt-Manipulation") ["Entf", "löschen"],
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)"],
["Strg+G / Strg+Umschalt+G", "Gruppieren / Gruppe lösen"], ["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([ out += _table(_rows([
["1 / 2 / 3 / 4", "Pinselmodus: Heben / Senken / Glätten / Malen"], ["Verschieben", "Pfeile am Objekt ziehen (X rot, Y grün, Z blau); Mausrad ändert die Schrittweite"],
["[ / ]", "Pinselgröße: Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch"], ["Drehen", "Ringe am Objekt ziehen: um alle drei Achsen, in 15°-Schritten; R = 90° um Y (Umschalt+R = −90°)"],
["Linksklick-Ziehen", "Boden formen bzw. malen"], ["Kippen", "Strg+R / Strg+Umschalt+R = um X ±15°; Alt+R / Alt+Umschalt+R = um Z ±15° (auch beim Platzieren)"],
["Strg+Linksklick-Ziehen", "Gegenteil: senken (bei Heben) / heben (bei Senken) / Steppe zurückmalen"], ["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([ out += _table(_rows([
["Strg+Z", "Rückgängig"], ["Strg+Z", "Rückgängig"],
["Strg+Y oder Strg+Umschalt+Z", "Wiederherstellen (Redo)"], ["Strg+Y / Strg+Umschalt+Z", "Wiederherstellen"],
["Strg+S", "Map speichern (.svmap, bekannter Pfad)"], ["Strg+S / Strg+Umschalt+S", "Speichern / Speichern unter"],
["Strg+Umschalt+S", "Map speichern unter …"], ["Strg+O / Strg+N", "Öffnen / Neue Map"],
["Strg+O", "Map öffnen"], ["Datei → Updates prüfen", "prüft auf eine neue Version; Banner „Jetzt updaten“ lädt und startet neu"],
["Strg+N", "Neue Map"],
["Datei → 🔄 Updates prüfen", "Selbst-Update: prüft auf neue Version; Banner → „Jetzt updaten“ (lädt, importiert, 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 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 history := ActionHistory.new()
var interaction # src/editor/interaction.gd (script-erweitert → untypisiert) 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 # 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 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) 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 selection_mod # src/ui/panels/selection_panel.gd
var map_mod # src/ui/panels/map_panel.gd var map_mod # src/ui/panels/map_panel.gd
var terrain_mod # src/ui/panels/terrain_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 check_mod # src/ui/panels/check_panel.gd
var tree_mod # src/ui/panels/scene_tree_panel.gd var tree_mod # src/ui/panels/scene_tree_panel.gd
var assets_mod # src/ui/panels/assets_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 map_mod = child
"terrain": "terrain":
terrain_mod = child terrain_mod = child
"malen":
malen_mod = child
"bauen":
bauen_mod = child
"check": "check":
check_mod = child check_mod = child
sel_label = selection_mod.sel_label 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 prop_name = map_mod.prop_name
spawn_status = check_mod.spawn_status spawn_status = check_mod.spawn_status
terrain_info = map_mod.terrain_info terrain_info = map_mod.terrain_info
build_edit_btn = terrain_mod.build_edit_btn build_edit_btn = bauen_mod.build_edit_btn
build_panel = terrain_mod.build_panel build_panel = bauen_mod.build_panel
build_kind_buttons = terrain_mod.build_kind_buttons 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 # Test-/Werkzeug-Vertrag (wie vor der U1-Aufteilung): die Widgets
# wohnen in den Modulen, der Koordinator hält die alten Namen # wohnen in den Modulen, der Koordinator hält die alten Namen
# als Bequemlichkeits-Referenzen — nichts wird doppelt gebaut. # 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)) overlay_box.add_theme_constant_override("separation", UIScale.px(6))
help_overlay.add_child(overlay_box) help_overlay.add_child(overlay_box)
help_overlay_title = Label.new() 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_font_size_override("font_size", UIScale.heading_font_size())
help_overlay_title.add_theme_color_override("font_color", get_theme_color("accent", "Editor")) help_overlay_title.add_theme_color_override("font_color", get_theme_color("accent", "Editor"))
overlay_box.add_child(help_overlay_title) overlay_box.add_child(help_overlay_title)
@ -616,9 +630,14 @@ func _build_center_viewport() -> SubViewportContainer:
world.add_child(test_start_tool) world.add_child(test_start_tool)
test_start_tool.setup(self, camera, view_builder, interaction) 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) # Bedien-Hinweis im Viewport (Overlay)
help_label = Label.new() 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.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
help_label.offset_left = UIScale.px(12) help_label.offset_left = UIScale.px(12)
help_label.offset_bottom = UIScale.px(-8) help_label.offset_bottom = UIScale.px(-8)
@ -1330,7 +1349,7 @@ func _update_tool_label() -> void:
"bauen": "Bauen (F3)", "bauen": "Bauen (F3)",
"spawns": "Spawn-Zonen (F4)", "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. ## Symmetrie-Schalter (Karte t_f5935023, M1): Modus aus dem Toolbar-Menü übernehmen.
@ -1673,7 +1692,7 @@ func _unhandled_key_input(event: InputEvent) -> void:
if tool_mode == "terrain": if tool_mode == "terrain":
_terrain_brush_size_step(key == KEY_BRACKETRIGHT) _terrain_brush_size_step(key == KEY_BRACKETRIGHT)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
KEY_1, KEY_2, KEY_3, KEY_4: KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6:
# Terrain-Pinsel-Modi 1–4 (Karte t_55257792): Heben/Senken/Glätten/ # Terrain-Pinsel-Modi 1–4 (Karte t_55257792): Heben/Senken/Glätten/
# Malen — NUR im Terrain-Werkzeug (Konfliktfreiheit, Punkt 6; im # Malen — NUR im Terrain-Werkzeug (Konfliktfreiheit, Punkt 6; im
# Objekt-/Bauen-Modus bleiben die Ziffern unbenutzt). # Objekt-/Bauen-Modus bleiben die Ziffern unbenutzt).
@ -1756,7 +1775,9 @@ func _base_tool_status() -> void:
## Namen hier (dünne Weiterleitung) — der Werkzeug-Vertrag ist unverändert. ## Namen hier (dünne Weiterleitung) — der Werkzeug-Vertrag ist unverändert.
func _on_terrain_btn_pressed() -> void: 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") _set_tool_mode("terrain")
else: else:
_set_tool_mode("objects") _set_tool_mode("objects")
@ -1814,10 +1835,9 @@ func _enter_terrain_mode() -> void:
_sync_terrain_button() _sync_terrain_button()
return return
tool_mode = "terrain" tool_mode = "terrain"
terrain_mod.terrain_panel.visible = true help_label.text = "Linksklick-Ziehen: formen bzw. malen · Strg+Klick: Gegenteil · [ ]: Pinselgröße · H: Hilfe"
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
terrain_tool.call("set_brush_mode", terrain_mod._selected_terrain_mode()) 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.") _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") terrain_tool.call("deactivate")
if terrain_mod != null: if terrain_mod != null:
terrain_mod.terrain_panel.visible = false terrain_mod.terrain_panel.visible = false
malen_mod.malen_panel.visible = false
_sync_terrain_button() _sync_terrain_button()
@ -1834,7 +1855,7 @@ func _enter_spawn_mode() -> void:
interaction.call("clear_selection", true) interaction.call("clear_selection", true)
tool_mode = "spawns" tool_mode = "spawns"
check_mod.spawn_panel.visible = true 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 check_mod.spawn_edit_btn.button_pressed = true
spawn_tool.call("activate") spawn_tool.call("activate")
spawn_tool.call("set_team", check_mod._selected_spawn_team()) spawn_tool.call("set_team", check_mod._selected_spawn_team())
@ -1853,7 +1874,7 @@ func _leave_spawn_mode() -> void:
func _enter_objects_mode() -> void: func _enter_objects_mode() -> void:
tool_mode = "objects" 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() _base_tool_status()
@ -1886,8 +1907,10 @@ func _enter_build_mode() -> void:
interaction.call("clear_selection", true) interaction.call("clear_selection", true)
tool_mode = "bauen" tool_mode = "bauen"
build_panel.visible = true 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 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") build_tool.call("activate")
_set_status("Bauen-Modus an: Bauteil rechts anklicken, dann in die Karte klicken — alles rastet aufs 2-m-Raster.") _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: 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: func _terrain_brush_size_step(up: bool) -> void:
@ -1924,15 +1947,19 @@ func _terrain_brush_mode_key(mode_idx: int) -> void:
btn = t.terrain_mode_smooth btn = t.terrain_mode_smooth
3: 3:
btn = t.terrain_mode_paint btn = t.terrain_mode_paint
4:
btn = t.terrain_mode_flatten
5:
btn = t.terrain_mode_ramp
if btn == null or terrain_tool == null: if btn == null or terrain_tool == null:
return return
btn.set_pressed(true) btn.set_pressed(true)
var labels := ["Heben", "Senken", "Glätten", "Malen"] var labels := ["Heben", "Senken", "Glätten", "Malen", "Einebnen", "Rampe"]
var extra := "" var extra := ""
if mode_idx == 3: if mode_idx == 3:
var mats: Array = SvmapIO.MATERIAL_IDS var mats: Array = SvmapIO.MATERIAL_IDS
extra = " (Material: %s — mit der Palette rechts wechseln)" % str(mats[clampi(int(terrain_tool.get("paint_material")), 0, mats.size() - 1)]) extra = " (Material: %s — mit der Palette rechts wechseln)" % str(mats[clampi(int(terrain_tool.get("paint_material")), 0, mats.size() - 1)])
_set_status("Pinsel-Modus: %s%s — Linksklick-Ziehen formt/malt jetzt so (Ziffern 1–4 wechseln)." % [str(labels[mode_idx]), extra]) _set_status("Pinsel-Modus: %s%s — Linksklick-Ziehen formt/malt jetzt so (Ziffern 1–6 wechseln)." % [str(labels[mode_idx]), extra])
## Ein Pinsel-Strich ist fertig (Doc + History schon aktualisiert vom Tool). ## Ein Pinsel-Strich ist fertig (Doc + History schon aktualisiert vom Tool).
@ -1955,12 +1982,13 @@ func _recheck_terrain_mode() -> void:
terrain_tool.call("deactivate") terrain_tool.call("deactivate")
tool_mode = "objects" tool_mode = "objects"
terrain_mod.terrain_panel.visible = false terrain_mod.terrain_panel.visible = false
malen_mod.malen_panel.visible = false
_sync_terrain_button() _sync_terrain_button()
_update_tool_label() _update_tool_label()
# Karte t_89c0ec95: Dezenter Terrain-Rückschalter = ebenfalls ein # Karte t_89c0ec95: Dezenter Terrain-Rückschalter = ebenfalls ein
# Werkzeugwechsel — Linien wieder ausblenden (sofern nicht fixiert). # Werkzeugwechsel — Linien wieder ausblenden (sofern nicht fixiert).
_sync_contours_to_builder() _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) _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)
@ -2157,9 +2185,13 @@ func _refresh_stats() -> void:
var count := SvmapDocument.object_count(doc) var count := SvmapDocument.object_count(doc)
var poly := SvmapDocument.poly_usage(doc, manifest) var poly := SvmapDocument.poly_usage(doc, manifest)
var budget := int(SvmapIO.MAX_TOTAL_POLY) var budget := int(SvmapIO.MAX_TOTAL_POLY)
stats_objects.text = "Objekte: %d / 5000 · Poly-Budget: %s / %s (%.0f %%) · Bauteile: %d" % [ var poly_pct := 100.0 * poly / budget
count, _fmt_int(int(poly)), _fmt_int(budget), 100.0 * poly / budget, stats_objects.text = "%d Objekte · Poly %.0f %%" % [count, poly_pct]
BuildOps.piece_count(doc)] 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 # GLB-Fehlende Objekte (Karte t_9b50b231): Platzhalter laufen, aber Sly soll
# sehen, WELCHE echten Modelle im Mesh-Lager nachgelegt werden müssen. # 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(): if view_builder != null and not (view_builder.get("fehlende_glbs") as Array).is_empty():
@ -2167,7 +2199,7 @@ func _refresh_stats() -> void:
var nennung := ", ".join(fehlend.slice(0, 3)) var nennung := ", ".join(fehlend.slice(0, 3))
if fehlend.size() > 3: if fehlend.size() > 3:
nennung += " … (+%d weitere)" % (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) # Terrain-Anteil am Quest-3-Budget (res 513 = 524.288 Dreiecke, map_format.md §4)
var tris := 0 var tris := 0
var res := 0 var res := 0
@ -2177,8 +2209,9 @@ func _refresh_stats() -> void:
var quest_budget := 524288 var quest_budget := 524288
var pct := 100.0 * float(tris) / float(quest_budget) var pct := 100.0 * float(tris) / float(quest_budget)
var fps := Engine.get_frames_per_second() var fps := Engine.get_frames_per_second()
stats_terrain.text = " · Terrain: %s² Grid, %s Dreiecke (%.0f %% Quest-Budget) · %d FPS" % [ stats_terrain.text = "Terrain %s² · %d FPS" % [str(res) if res > 0 else "—", fps]
str(res) if res > 0 else "—", _fmt_int(tris), pct, 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: func _refresh_file_label() -> void:

View file

@ -59,7 +59,6 @@ func panel_title() -> String:
## Klapp-Gruppen-Körper („Alle") + Zuletzt-benutzt-Ansicht. ## Klapp-Gruppen-Körper („Alle") + Zuletzt-benutzt-Ansicht.
func build() -> void: func build() -> void:
add_child(_header_label("Objekt-Bibliothek")) 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 # Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit
# Treffern klappen auf, leere Gruppen verschwinden. Größe ist von # Treffern klappen auf, leere Gruppen verschwinden. Größe ist von
@ -102,7 +101,7 @@ func build() -> void:
var scroll := ScrollContainer.new() var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL 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 scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll) add_child(scroll)
library_box = VBoxContainer.new() library_box = VBoxContainer.new()
@ -270,7 +269,7 @@ func _on_panel_resized() -> void:
func _apply_grid_columns(width: float) -> 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: for row in _library_group_rows:
((row["grid"] as GridContainer)).columns = cols ((row["grid"] as GridContainer)).columns = cols
if _recent_grid != null: if _recent_grid != null:
@ -471,7 +470,7 @@ func _build_tile(oid: String) -> Button:
btn.toggle_mode = true btn.toggle_mode = true
btn.button_pressed = false btn.button_pressed = false
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL 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] btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid]
if missing: if missing:
btn.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)" btn.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)"
@ -523,7 +522,6 @@ func _build_tile(oid: String) -> Button:
tile.add_child(id_lbl) tile.add_child(id_lbl)
var lbl := Label.new() var lbl := Label.new()
lbl.text = name_txt lbl.text = name_txt
lbl.add_theme_font_size_override("font_size", UIScale.small_font_size())
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
lbl.clip_text = true lbl.clip_text = true
lbl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS 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.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
spawn_status.custom_minimum_size = UIScale.v2(0, 44) spawn_status.custom_minimum_size = UIScale.v2(0, 44)
add_child(spawn_status) 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-Zonen-Werkzeug: großer Button + Bedien-Panel ---
spawn_edit_btn = Button.new() spawn_edit_btn = Button.new()
@ -82,9 +81,6 @@ func build() -> void:
spawn_delete_btn.pressed.connect(_on_spawn_delete_pressed) spawn_delete_btn.pressed.connect(_on_spawn_delete_pressed)
spawn_panel.add_child(spawn_delete_btn) 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: 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). ## sich vom angeschlossenen Theme-Baum (main hängt theme auf das Fenster).
func theme_color(cname: String) -> Color: 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") 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.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) prop_water.set_meta("kein_wasser_value", KEIN_WASSER_WERT)
add_child(prop_water) 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(_separator())
add_child(_header_label("Terrain")) 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.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)) grid_check.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_snap_changed", pressed))
add_child(grid_check) 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"}, "skript": "res://src/ui/panels/map_panel.gd"},
{"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40, {"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40,
"skript": "res://src/ui/panels/terrain_panel.gd"}, "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, {"id": "check", "titel": "Prüfen", "gruppe": "rechts", "order": 50,
"skript": "res://src/ui/panels/check_panel.gd"}, "skript": "res://src/ui/panels/check_panel.gd"},
{"id": "teststart", "titel": "Test-Start", "gruppe": "rechts", "order": 60, {"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.cancel_button_text = "Abbrechen"
_confirm_delete.confirmed.connect(_on_delete_confirmed) _confirm_delete.confirmed.connect(_on_delete_confirmed)
add_child(_confirm_delete) 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()) 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.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()) sel_ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
group_btns.add_child(sel_ungroup_btn) 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- # --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen-
# Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die # 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(perspektive_box)
add_child(_separator()) 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: func _on_sens_changed(value: float) -> void:

View file

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

View file

@ -38,11 +38,7 @@ func panel_title() -> String:
func build() -> void: func build() -> void:
add_child(_header_label("Symmetrie")) add_child(_header_label("Symmetrie"))
add_child(_muted_label( add_child(_muted_label("Macht die ganze Karte symmetrisch: ergänzt die andere Seite, löscht nichts."))
"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."))
mode_label = _field_label(_mode_text()) mode_label = _field_label(_mode_text())
add_child(mode_label) add_child(mode_label)
@ -75,10 +71,6 @@ func build() -> void:
report_label.custom_minimum_size = UIScale.v2(0, 96) report_label.custom_minimum_size = UIScale.v2(0, 96)
add_child(report_label) 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 = ConfirmationDialog.new()
conflict_dialog.title = "Widersprüche — welche Seite gewinnt?" conflict_dialog.title = "Widersprüche — welche Seite gewinnt?"

View file

@ -11,6 +11,10 @@ var terrain_mode_raise: Button
var terrain_mode_lower: Button var terrain_mode_lower: Button
var terrain_mode_smooth: Button var terrain_mode_smooth: Button
var terrain_mode_paint: Button var terrain_mode_paint: Button
var terrain_mode_flatten: Button
var terrain_mode_ramp: Button
var terrain_falloff_label: Label
var terrain_falloff_slider: HSlider
var terrain_palette: VBoxContainer var terrain_palette: VBoxContainer
var terrain_mat_buttons: Array = [] # Gras/Sand/Fels (ButtonGroup) var terrain_mat_buttons: Array = [] # Gras/Sand/Fels (ButtonGroup)
var terrain_size_btn_small: Button var terrain_size_btn_small: Button
@ -24,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_label: Label # Malen: Kante hart ↔ weich (25.10.)
var terrain_weich_slider: HSlider 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: static func spec() -> Dictionary:
return {"titel": "Terrain", "gruppe": "rechts", "order": 40} return {"titel": "Terrain", "gruppe": "rechts", "order": 40}
@ -39,7 +38,7 @@ func build() -> void:
terrain_edit_btn.text = "⛰ Terrain formen" terrain_edit_btn.text = "⛰ Terrain formen"
terrain_edit_btn.custom_minimum_size = UIScale.v2(0, 48) terrain_edit_btn.custom_minimum_size = UIScale.v2(0, 48)
terrain_edit_btn.toggle_mode = true 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) terrain_edit_btn.pressed.connect(_on_terrain_btn_pressed)
add_child(terrain_edit_btn) add_child(terrain_edit_btn)
@ -48,9 +47,11 @@ func build() -> void:
terrain_panel.visible = false terrain_panel.visible = false
add_child(terrain_panel) add_child(terrain_panel)
terrain_panel.add_child(_field_label("Pinsel-Werkzeug:")) terrain_panel.add_child(_field_label("Pinsel:"))
var mode_row := HBoxContainer.new() var mode_row := GridContainer.new()
mode_row.add_theme_constant_override("separation", UIScale.px(8)) # U7-Raster mode_row.columns = 3
mode_row.add_theme_constant_override("h_separation", UIScale.px(8)) # U7-Raster
mode_row.add_theme_constant_override("v_separation", UIScale.px(8))
terrain_panel.add_child(mode_row) terrain_panel.add_child(mode_row)
var mode_group := ButtonGroup.new() var mode_group := ButtonGroup.new()
terrain_mode_raise = _terrain_mode_button("Heben ⬆", mode_group, terrain_mode_raise = _terrain_mode_button("Heben ⬆", mode_group,
@ -61,7 +62,13 @@ func build() -> void:
"Pinsel-Modus Glätten: Höhenunterschiede verrutschen (Taste 3)") "Pinsel-Modus Glätten: Höhenunterschiede verrutschen (Taste 3)")
terrain_mode_paint = _terrain_mode_button("Malen 🖌", mode_group, terrain_mode_paint = _terrain_mode_button("Malen 🖌", mode_group,
"Pinsel-Modus Malen: Material auf den Boden malen (Taste 4)") "Pinsel-Modus Malen: Material auf den Boden malen (Taste 4)")
terrain_mode_flatten = _terrain_mode_button("Einebnen ▬", mode_group,
"Pinsel-Modus Einebnen: zieht den Boden auf die Höhe, die beim Strichstart unter dem Cursor war")
terrain_mode_ramp = _terrain_mode_button("Rampe ◢", mode_group,
"Pinsel-Modus Rampe: Klick = Start, ziehen und loslassen = Ende — Höhe läuft gleichmäßig dazwischen (Breite = Pinselgröße)")
terrain_mode_raise.button_pressed = true terrain_mode_raise.button_pressed = true
terrain_mode_flatten.toggled.connect(_on_terrain_mode_changed)
terrain_mode_ramp.toggled.connect(_on_terrain_mode_changed)
terrain_mode_raise.toggled.connect(_on_terrain_mode_changed) terrain_mode_raise.toggled.connect(_on_terrain_mode_changed)
terrain_mode_lower.toggled.connect(_on_terrain_mode_changed) terrain_mode_lower.toggled.connect(_on_terrain_mode_changed)
terrain_mode_smooth.toggled.connect(_on_terrain_mode_changed) terrain_mode_smooth.toggled.connect(_on_terrain_mode_changed)
@ -69,50 +76,12 @@ func build() -> void:
mode_row.add_child(terrain_mode_raise) mode_row.add_child(terrain_mode_raise)
mode_row.add_child(terrain_mode_lower) mode_row.add_child(terrain_mode_lower)
mode_row.add_child(terrain_mode_smooth) 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. # Malen hat seit 25.10. einen eigenen Tab (malen_panel.gd); der Modus-Knopf
terrain_palette = VBoxContainer.new() # bleibt unsichtbar in der ButtonGroup (Taste 4, Malen-Tab-Knopf).
terrain_palette.add_theme_constant_override("separation", UIScale.px(4)) terrain_mode_paint.visible = false
terrain_palette.visible = false terrain_panel.add_child(terrain_mode_paint)
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)
# Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider. # Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider.
terrain_panel.add_child(_field_label("Pinselgröße:")) terrain_panel.add_child(_field_label("Pinselgröße:"))
@ -153,89 +122,28 @@ func build() -> void:
terrain_strength_slider.value_changed.connect(_on_terrain_strength_changed) terrain_strength_slider.value_changed.connect(_on_terrain_strength_changed)
terrain_panel.add_child(terrain_strength_slider) terrain_panel.add_child(terrain_strength_slider)
# Malen kennt keine Stärke: das Material wird zellenweise gesetzt # Pinsel-Kante (Falloff, Karte 25.10.): ein Regler hart ↔ weich für alle
# (Material-Schicht = ein Wert pro Rasterpunkt, weiche Übergänge macht # Höhen-Pinsel (Malen hat seinen eigenen Kanten-Regler weiter unten).
# der Splat-Renderer). Statt eines wirkungslosen Reglers steht im terrain_falloff_label = _field_label("Kante: 50 % weich")
# Malen-Modus die echte Feinheit da (Sly-Befund 22.10.: „Stärke ändert nichts“). terrain_panel.add_child(terrain_falloff_label)
terrain_paint_hint = _muted_label("") terrain_falloff_slider = HSlider.new()
terrain_paint_hint.visible = false terrain_falloff_slider.min_value = 0.0
terrain_panel.add_child(terrain_paint_hint) terrain_falloff_slider.max_value = 100.0
terrain_falloff_slider.step = 5.0
terrain_falloff_slider.value = 50.0
terrain_falloff_slider.custom_minimum_size = UIScale.v2(0, 32)
terrain_falloff_slider.tooltip_text = "Links = harte Kante (breiter Kern), rechts = weich auslaufend"
terrain_falloff_slider.value_changed.connect(_on_terrain_falloff_changed)
terrain_panel.add_child(terrain_falloff_slider)
# 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(
"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: func _on_terrain_btn_pressed() -> void:
if terrain_edit_btn.button_pressed: 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") main.call("_set_tool_mode", "terrain")
refresh_panels()
else: else:
main.call("_set_tool_mode", "objects") main.call("_set_tool_mode", "objects")
@ -257,6 +165,10 @@ func _selected_terrain_mode() -> int:
return TerrainField.Mode.SMOOTH return TerrainField.Mode.SMOOTH
if terrain_mode_paint.button_pressed: if terrain_mode_paint.button_pressed:
return TerrainField.Mode.PAINT return TerrainField.Mode.PAINT
if terrain_mode_flatten != null and terrain_mode_flatten.button_pressed:
return TerrainField.Mode.FLATTEN
if terrain_mode_ramp != null and terrain_mode_ramp.button_pressed:
return TerrainField.Mode.RAMP
return TerrainField.Mode.RAISE return TerrainField.Mode.RAISE
@ -265,19 +177,36 @@ func _on_terrain_mode_changed(_pressed: bool) -> void:
var paint := _selected_terrain_mode() == TerrainField.Mode.PAINT var paint := _selected_terrain_mode() == TerrainField.Mode.PAINT
if terrain_palette != null: if terrain_palette != null:
terrain_palette.visible = paint terrain_palette.visible = paint
if terrain_strength_slider != null:
terrain_strength_slider.visible = not paint
terrain_strength_label.visible = not paint
if terrain_weich_slider != null:
terrain_weich_slider.visible = paint
terrain_weich_label.visible = paint
if terrain_paint_hint != null: 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 {}) terrain_paint_hint.text = paint_hint_text(main.doc.get("terrain", {}) if main.doc is Dictionary else {})
refresh_panels()
if main.terrain_tool != null: if main.terrain_tool != null:
main.terrain_tool.call("set_brush_mode", _selected_terrain_mode()) 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 ## Hinweistext Malen-Modus (pure, testbar): feinste Mal-Einheit = Abstand
## zweier Rasterpunkte der Karte (size / (resolution − 1)). ## zweier Rasterpunkte der Karte (size / (resolution − 1)).
static func paint_hint_text(terrain: Dictionary) -> String: static func paint_hint_text(terrain: Dictionary) -> String:
@ -291,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 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 ## Pinselgrößen-Presets (Karte t_603ec1a8): setzen den Radius-Slider
## (der Label-Text + Ring-Optik hängen am value_changed-Signal). ## (der Label-Text + Ring-Optik hängen am value_changed-Signal).
func _terrain_size_button(text: String, group: ButtonGroup, radius_m: float) -> Button: func _terrain_size_button(text: String, group: ButtonGroup, radius_m: float) -> Button:
@ -319,14 +242,17 @@ func _on_terrain_size_preset(pressed: bool, radius_m: float) -> void:
func _on_terrain_radius_changed(value: float) -> void: func _on_terrain_radius_changed(value: float) -> void:
terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value) 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: if main.terrain_tool != null:
main.terrain_tool.call("set_radius", value) main.terrain_tool.call("set_radius", value)
func _on_terrain_weich_changed(value: float) -> void: func _on_terrain_falloff_changed(value: float) -> void:
terrain_weich_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart" terrain_falloff_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
if main.terrain_tool != null: if main.terrain_tool != null:
main.terrain_tool.call("set_weichheit", value / 100.0) main.terrain_tool.call("set_falloff", value / 100.0)
func _on_terrain_strength_changed(value: float) -> void: func _on_terrain_strength_changed(value: float) -> void:

View file

@ -39,7 +39,7 @@ func build() -> void:
add_child(_separator()) add_child(_separator())
info_hint = Label.new() info_hint = Label.new()
info_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART 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) add_child(info_hint)
# main holt sich das Modul hier ab (main._refresh_test_start_panel) — die # 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)) size_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index))
add_child(size_menu) 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 = Label.new()
title_label.text = " %s" % main.APP_TITLE title_label.text = " %s" % main.APP_TITLE
title_label.add_theme_font_size_override("font_size", UIScale.heading_font_size()) title_label.add_theme_font_size_override("font_size", UIScale.heading_font_size())
@ -185,7 +193,7 @@ func build() -> void:
file_label.text = "" file_label.text = ""
# U7: Farbe über Theme (Editor/colors/title) statt modulate-Wert im Code. # U7: Farbe über Theme (Editor/colors/title) statt modulate-Wert im Code.
file_label.add_theme_color_override("font_color", 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 # 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). # (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 "…". # 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) 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: func setup(p_main, p_camera: Camera3D, p_builder) -> void:
main = p_main main = p_main
camera = p_camera camera = p_camera
view_builder = p_builder view_builder = p_builder
_build_arrows() _build_arrows()
_build_rings()
_apply_mode_visibility()
visible = false visible = false
@ -119,6 +136,53 @@ func _build_arrows() -> void:
_arrows[a] = root _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: func _axis_vec(a: String) -> Vector3:
return AXES.get(a, Vector3.ZERO) as Vector3 return AXES.get(a, Vector3.ZERO) as Vector3
@ -169,26 +233,43 @@ func _unhandled_input(event: InputEvent) -> void:
var inter = main.get("interaction") var inter = main.get("interaction")
if inter == null or bool(inter.get("placing")): if inter == null or bool(inter.get("placing")):
return 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) var axis := pick(mb.position)
if axis != "": if axis != "":
_begin_drag(axis, mb.position) _begin_drag(axis, mb.position)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed: 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) _finish_drag(true)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif (mb.button_index == MOUSE_BUTTON_WHEEL_UP \ elif (mb.button_index == MOUSE_BUTTON_WHEEL_UP \
or mb.button_index == MOUSE_BUTTON_WHEEL_DOWN) and mb.pressed: 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 var dir := 1 if mb.button_index == MOUSE_BUTTON_WHEEL_UP else -1
cycle_step(dir) cycle_step(dir)
get_viewport().set_input_as_handled() 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: elif event is InputEventMouseMotion and _dragging:
_drag_motion((event as InputEventMouseMotion).position) _drag_motion((event as InputEventMouseMotion).position)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif event is InputEventKey and (event as InputEventKey).pressed \ elif event is InputEventKey and (event as InputEventKey).pressed \
and (event as InputEventKey).keycode == KEY_ESCAPE: 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) _finish_drag(false)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
@ -215,6 +296,35 @@ func pick(screen_pos: Vector2) -> String:
return best 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: func _safe_unproject(p: Vector3) -> Vector2:
if camera == null: if camera == null:
return Vector2.INF 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 = ## 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). ## es lief einer und wurde abgebrochen (idempotent, weil _dragging prüft).
func cancel_drag_if_active() -> bool: func cancel_drag_if_active() -> bool:
if _rot_dragging:
_finish_rotate(false)
return true
if not _dragging: if not _dragging:
return false return false
_finish_drag(false) _finish_drag(false)
return true 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 ------- # ------------------------------------------------------ Schrittweite -------
## Mausrad WÄHREND des Ziehens: durch die Voreinstellungen blättern ## 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)" _check(tr.custom_minimum_size.x >= 64, "Thumbnail-Zielgröße ≥ 64 px (ist %d)"
% int(tr.custom_minimum_size.x)) % int(tr.custom_minimum_size.x))
_check(tr.size.x >= 64, "Thumbnail rendert ≥ 64 px breit (ist %.0f)" % tr.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, _check(tr.texture != null and tr.texture.get_width() == 128,
"Textur auf 96 px skaliert") "Textur auf 128 px skaliert")
if lbl != null: if lbl != null:
_check(lbl.text == "Wachturm", "deutscher Name unter dem Bild (%s)" % lbl.text) _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) _check(tile_btn.size.y >= 100, "Kachel hoch genug (%.0f px)" % tile_btn.size.y)

View file

@ -0,0 +1,160 @@
extends SceneTree
## Pinsel-Falloff, Mal-Slot-Logik, Einebnen, Rampe, Druck (Karte 25.10.).
## Ausführen: godot --headless --path . --script res://tests/test_pinsel_werkzeuge.gd
var failures := 0
var checks := 0
const R := 129
const S := 1024.0
func _init() -> void:
_falloff()
_druck()
_flatten()
_ramp()
_glaetten()
_mal_slots()
_undo()
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: %s" % label)
else:
failures += 1
print(" FAIL: %s" % label)
func _flat(h := 0.0) -> PackedFloat32Array:
var a := PackedFloat32Array()
a.resize(R * R)
a.fill(h)
return a
func _falloff() -> void:
print("Testgruppe: Falloff lerp(t, t², f)")
_check(is_equal_approx(TerrainField.falloff_weight(0.0, 10.0, 0.0), 1.0), "Mitte = 1 (hart)")
_check(is_equal_approx(TerrainField.falloff_weight(0.0, 10.0, 1.0), 1.0), "Mitte = 1 (weich)")
_check(is_equal_approx(TerrainField.falloff_weight(5.0, 10.0, 0.0), 0.5), "hart: halber Radius = 0.5")
_check(is_equal_approx(TerrainField.falloff_weight(5.0, 10.0, 1.0), 0.25), "weich: halber Radius = 0.25")
_check(is_equal_approx(TerrainField.falloff_weight(5.0, 10.0, 0.5), 0.375), "mittel: lerp = 0.375")
_check(TerrainField.falloff_weight(10.0, 10.0, 0.5) == 0.0, "Rand = 0")
var h := _flat()
TerrainField.apply_brush(h, R, S, S, -1e5, 1e5, 0.0, 0.0, 40.0, TerrainField.Mode.RAISE, 1.0, 0.0)
var h2 := _flat()
TerrainField.apply_brush(h2, R, S, S, -1e5, 1e5, 0.0, 0.0, 40.0, TerrainField.Mode.RAISE, 1.0, 1.0)
var i := 64 * R + 64 + 3 # ~24 m neben der Mitte
_check(h[i] > h2[i] and h2[i] > 0.0, "Raise: hart hebt am Rand mehr als weich")
func _druck() -> void:
print("Testgruppe: Druck")
_check(TerrainField.pressure_of(0.0, 0.0) == 1.0, "Maus = 1.0")
_check(is_equal_approx(TerrainField.pressure_of(0.4, 0.9), 0.4), "Pen vor Trigger")
_check(is_equal_approx(TerrainField.pressure_of(0.0, 0.6), 0.6), "Trigger")
_check(TerrainField.pressure_of(0.001, 0.0) == 0.05 or TerrainField.pressure_of(0.001, 0.0) == 1.0, "Minimum begrenzt")
var t := Node3D.new()
t.set_script(load("res://src/editor/terrain_tool.gd"))
t.set_pressure(0.5)
var a: float = t.call("_step_amount", TerrainField.Mode.RAISE, 1.0)
t.set_pressure(1.0)
var b: float = t.call("_step_amount", TerrainField.Mode.RAISE, 1.0)
_check(is_equal_approx(a * 2.0, b), "Stärke skaliert mit Druck")
t.free()
func _flatten() -> void:
print("Testgruppe: Einebnen")
var h := _flat(0.0)
for i in h.size():
h[i] = float((i % R)) * 0.1 # Rampe in x
var target := 6.4
for k in 40:
TerrainField.apply_brush(h, R, S, S, -1e5, 1e5, 0.0, 0.0, 60.0, TerrainField.Mode.FLATTEN, 0.3, 0.3, target)
_check(absf(h[64 * R + 64] - target) < 0.05, "Mitte erreicht Zielhöhe")
_check(absf(h[64 * R + 4] - 0.4) < 0.0001, "weit außerhalb unverändert")
_check(absf(h[64 * R + 60] - target) < absf(0.1 * 60 - target) , "Rand näher am Ziel als vorher")
func _ramp() -> void:
print("Testgruppe: Rampe")
var h := _flat(0.0)
var a := Vector2(-80.0, 0.0)
var b := Vector2(80.0, 0.0)
TerrainField.apply_ramp(h, R, S, S, a, b, 2.0, 12.0, 30.0, 0.5)
var cx := func(wx: float) -> int: return int(round((wx / S + 0.5) * float(R - 1)))
var iz := 64
var mid: float = h[iz * R + cx.call(0.0)]
_check(absf(mid - 7.0) < 0.3, "Mitte ≈ lineare Höhe 7 (%.2f)" % mid)
var q: float = h[iz * R + cx.call(40.0)]
_check(absf(q - 9.5) < 0.4, "Viertelpunkt ≈ 9.5 (%.2f)" % q)
_check(h[iz * R + cx.call(-200.0)] == 0.0 and h[iz * R + cx.call(200.0)] == 0.0, "außerhalb der Enden unverändert")
var seitlich: float = h[(iz + 4) * R + cx.call(0.0)] # ≈ 32 m quer: außerhalb der halben Breite 15
_check(seitlich == 0.0, "außerhalb der Breite unverändert")
var rand: float = h[(iz + 1) * R + cx.call(0.0)] # 8 m quer: Falloff
_check(rand > 0.0 and rand < mid, "Rand mit Falloff schwächer als Achse")
func _glaetten() -> void:
print("Testgruppe: Glätten mit Falloff")
var h := _flat(0.0)
h[64 * R + 64] = 10.0
var changed := TerrainField.apply_brush(h, R, S, S, -1e5, 1e5, 0.0, 0.0, 30.0, TerrainField.Mode.SMOOTH, 1.0, 0.5)
_check(changed and h[64 * R + 64] < 10.0 and h[64 * R + 65] > 0.0, "Spitze verteilt sich")
var h2 := _flat(0.0)
h2[64 * R + 64] = 10.0
TerrainField.apply_brush(h2, R, S, S, -1e5, 1e5, 0.0, 0.0, 30.0, TerrainField.Mode.SMOOTH, 1.0)
_check(h2[64 * R + 64] < 10.0, "ohne Falloff (alt: Kosinus) funktioniert weiter")
func _mal_slots() -> void:
print("Testgruppe: Mal-Slots")
var n := 33
var a := PackedByteArray(); a.resize(n * n); a.fill(0)
var b := PackedByteArray(); b.resize(n * n); b.fill(0)
var w := PackedByteArray(); w.resize(n * n); w.fill(0)
var mid := 16 * n + 16
# Mitte: Slot a=1 (Anteil 255−200=55), Slot b=2 (Anteil 200 = stärker).
a[mid] = 1; b[mid] = 2; w[mid] = 200
# Ziel 3 mit voller Stärke: ersetzt das SCHWÄCHERE (1) → übrig: 2 + 3.
TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 3, 0.0, 0.5)
var slots := [int(a[mid]), int(b[mid])]
_check(slots.has(2) and slots.has(3) and not slots.has(1), "schwächeres Slot-Material ersetzt (%s)" % str(slots))
_check(int(w[mid]) > 100 and int(w[mid]) < 140 and int(b[mid]) == 3, "Anteil schiebt weich Richtung Ziel (w=%d)" % int(w[mid]))
TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 3, 0.0, 1.0)
_check(int(a[mid]) == 3 and int(w[mid]) == 0, "voll gemalt → reines Zielmaterial")
# Ziel ist schon in Slot b: Anteil wächst, kein Wechsel.
a[mid] = 1; b[mid] = 2; w[mid] = 100
TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 2, 0.0, 0.8)
_check(int(a[mid]) == 1 and int(b[mid]) == 2 and int(w[mid]) > 100, "Ziel in Slot b: Anteil wächst")
# Schwacher Strich schwächt nie ab.
var before := int(w[mid])
TerrainField.apply_material_brush_mix(a, b, w, n, 32.0, 32.0, 0.0, 0.0, 6.0, 2, 0.0, 0.1)
_check(int(w[mid]) == before, "schwächerer Strich ändert nichts")
func _undo() -> void:
print("Testgruppe: Einebnen/Rampe + Undo")
var doc := SvmapDocument.create_new("Pinsel", 1024.0, "mittag")
var hist := ActionHistory.new()
hist.begin(doc)
var terrain: Dictionary = doc["terrain"]
var h := SvmapIO.decode_heights(terrain)
var vor := str(terrain["heightmap_r16_b64"])
var before: Dictionary = hist.snapshot(["terrain"])
TerrainField.apply_ramp(h, 129, 1024.0, 1024.0, Vector2(-50, 0), Vector2(50, 0), 0.0, 20.0, 20.0, 0.3)
terrain["max_height_m"] = 30.0
terrain["heightmap_r16_b64"] = TerrainField.encode(h, float(terrain["min_height_m"]), 30.0)
hist.commit("Boden geformt (Rampe)", before, hist.snapshot(["terrain"]))
_check(str(terrain["heightmap_r16_b64"]) != vor, "Rampe verändert Doc")
hist.undo()
_check(str(terrain["heightmap_r16_b64"]) == vor, "Undo stellt Höhenfeld wieder her")

View file

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

View file

@ -180,6 +180,8 @@ func _test_inputmap_actions() -> void:
"editor_brush_lower": KEY_2, "editor_brush_lower": KEY_2,
"editor_brush_smooth": KEY_3, "editor_brush_smooth": KEY_3,
"editor_brush_paint": KEY_4, "editor_brush_paint": KEY_4,
"editor_brush_flatten": KEY_5,
"editor_brush_ramp": KEY_6,
} }
for action in expected_keys_2: for action in expected_keys_2:
if not InputMap.has_action(action): if not InputMap.has_action(action):
@ -918,4 +920,4 @@ func _test_brush_paint_mode() -> void:
tool.call("set_brush_mode", TerrainField.Mode.RAISE) tool.call("set_brush_mode", TerrainField.Mode.RAISE)
_check(int(tool.get("brush_mode")) == TerrainField.Mode.RAISE, "set_brush_mode(Heben) bleibt Heben") _check(int(tool.get("brush_mode")) == TerrainField.Mode.RAISE, "set_brush_mode(Heben) bleibt Heben")
tool.call("set_brush_mode", 99) tool.call("set_brush_mode", 99)
_check(int(tool.get("brush_mode")) == TerrainField.Mode.PAINT, "Out-of-Range clampet auf den höchsten Modus") _check(int(tool.get("brush_mode")) == TerrainField.Mode.RAMP, "Out-of-Range clampet auf den höchsten Modus")

View file

@ -177,7 +177,7 @@ func _reference_heights(steps: int, x: float, z: float, mode: int) -> PackedFloa
var amount := STRENGTH * dt var amount := STRENGTH * dt
for i in steps: for i in steps:
TerrainField.apply_brush(ref, RES, SIZE, SIZE, -100000.0, 100000.0, TerrainField.apply_brush(ref, RES, SIZE, SIZE, -100000.0, 100000.0,
x, z, RADIUS, mode, amount) x, z, RADIUS, mode, amount, 0.5) # Falloff = Tool-Standard
return ref return ref

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)") _check(v.x == 175 and v.y == 50, "Mittel: v2(140,40) → (175,50)")
UIScale.set_step_index(2) 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(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: 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(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.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") _check(main.library_search.custom_minimum_size.y == 45, "Mittel: Suchfeld 36 → 45")
var tile := _find_tile_button(158) var tile := _find_tile_button(235)
_check(tile != null and tile.custom_minimum_size.y == 158, "Mittel: Bibliotheks-Kachel 126 → 158") _check(tile != null and tile.custom_minimum_size.y == 235, "Mittel: Bibliotheks-Kachel 188 → 235")
var thumb := _find_thumb_rect(120) var thumb := _find_thumb_rect(160)
_check(thumb != null and thumb.custom_minimum_size.x == 120, "Mittel: Thumbnail 96 → 120") _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)") _check(main.size_menu.get_popup().is_item_checked(1), "Menü-Häkchen wandert auf Mittel (neues Popup)")
# Groß: # Groß:

View file

@ -252,6 +252,15 @@ func _run_tests() -> void:
var pulled := EditorUpdater.pull(git, work) var pulled := EditorUpdater.pull(git, work)
_check(bool(pulled["ok"]), "pull --ff-only erfolgreich (v2 wird geladen)") _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") _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"]) _git(work, ["remote", "set-url", "origin", "http://127.0.0.1:1/nicht-da.git"])
var dead_pull := EditorUpdater.pull(git, work) var dead_pull := EditorUpdater.pull(git, work)
_check(not bool(dead_pull["ok"]), "pull gegen toten Origin schlägt fehl") _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/accent = Color(0.55, 0.8, 1, 1)
Editor/colors/status_ok = Color(0.75, 0.95, 0.75, 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/status_error = Color(1, 0.6, 0.5, 1)
Editor/colors/muted = Color(1, 1, 1, 0.55) Editor/colors/muted = Color(1, 1, 1, 0.74)
Editor/colors/field = Color(1, 1, 1, 0.7) Editor/colors/field = Color(1, 1, 1, 0.9)
Editor/colors/title = Color(1, 1, 1, 0.6) Editor/colors/title = Color(1, 1, 1, 0.82)
Editor/colors/warning = Color(1, 0.62, 0.3, 1) Editor/colors/warning = Color(1, 0.62, 0.3, 1)
Editor/colors/badge_outline = Color(0, 0, 0, 1) Editor/colors/badge_outline = Color(0, 0, 0, 1)
Editor/colors/help_bg = Color(0.09, 0.11, 0.13, 0.97) 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/tab_unselected = SubResource("tab_unsel")
TabContainer/styles/tabbar_background = SubResource("tabbar") TabContainer/styles/tabbar_background = SubResource("tabbar")
TabContainer/colors/font_selected_color = Color(1, 1, 1, 1) 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/normal = SubResource("btn_normal")
Button/styles/hover = SubResource("btn_hover") Button/styles/hover = SubResource("btn_hover")
Button/styles/pressed = SubResource("btn_pressed") Button/styles/pressed = SubResource("btn_pressed")
@ -254,11 +254,11 @@ Button/styles/disabled = SubResource("btn_disabled")
Button/styles/focus = SubResource("btn_focus") Button/styles/focus = SubResource("btn_focus")
Button/colors/font_hover_color = Color(1, 1, 1, 1) 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_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/normal = SubResource("line_normal")
LineEdit/styles/focus = SubResource("line_focus") LineEdit/styles/focus = SubResource("line_focus")
LineEdit/colors/font_color = Color(1, 1, 1, 1) 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 = SubResource("split_grab")
HSplitContainer/styles/grabber_area_highlight = SubResource("split_grab_hover") HSplitContainer/styles/grabber_area_highlight = SubResource("split_grab_hover")
VSplitContainer/styles/grabber_area = SubResource("split_grab") VSplitContainer/styles/grabber_area = SubResource("split_grab")

View file

@ -1,9 +1,9 @@
#!/bin/bash #!/bin/bash
# Regression: alle Editor-Testsuiten (Karte t_3a998ae4) # Regression: alle Editor-Testsuiten (Karte t_3a998ae4)
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
GODOT=/opt/data/home/bin/godot4 GODOT=${GODOT:-/opt/data/home/bin/godot4}
rc=0 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; 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 "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1
code=$? code=$?
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1) line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)