Compare commits
7 commits
b1d8f7626d
...
35a047cc2a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35a047cc2a | ||
|
|
93cdcb0cd4 | ||
|
|
59c932dc35 | ||
|
|
3094395444 | ||
|
|
3d083648d8 | ||
|
|
8cde37b85c | ||
|
|
26bcbe0cdd |
38 changed files with 1280 additions and 331 deletions
|
|
@ -522,7 +522,7 @@ func ungroup_selection() -> void:
|
|||
## Auswahl drehen (R / Mausrad / Button): bei mehreren Objekten oder einer
|
||||
## Gruppe rotiert ALLES gemeinsam um den Gruppen-Mittelpunkt. Mausrad-Ticks
|
||||
## derselben Auswahl laufen zu EINEM Undo-Schritt zusammen (merge_tag).
|
||||
func rotate_selected(delta_deg: float) -> void:
|
||||
func rotate_selected(delta_deg: float, merge_suffix: String = "") -> void:
|
||||
if selected_index < 0 or selected_indices.is_empty():
|
||||
return
|
||||
var objects: Array = main.doc.get("objects", [])
|
||||
|
|
@ -545,7 +545,7 @@ func rotate_selected(delta_deg: float) -> void:
|
|||
var twins: Array = ObjectOps.symmetry_sync_pairs(
|
||||
main.doc, members, _sym_mode(), selected_index)
|
||||
_sync_members_to_markers(twins)
|
||||
var tag := "rotmulti:%s" % str(members)
|
||||
var tag := "rotmulti:%s%s" % [str(members), merge_suffix]
|
||||
main.history.commit("Auswahl gedreht (%d Objekte)" % members.size(),
|
||||
before, main.history.snapshot(["objects"]), tag)
|
||||
_mark_dirty()
|
||||
|
|
@ -560,7 +560,7 @@ func rotate_selected(delta_deg: float) -> void:
|
|||
## einer Gruppe kippt die GANZE Gruppe (wie beim Drehen), Mehrfachauswahl
|
||||
## kippt jedes Objekt um denselben Betrag, Symmetrie-Zwillinge laufen im
|
||||
## SELBEN Commit mit. rot/rot_y_deg bleiben über SvmapIO.rot_setzen synchron.
|
||||
func tilt_selected(axis_x: bool, delta_deg: float) -> void:
|
||||
func tilt_selected(axis_x: bool, delta_deg: float, merge_suffix: String = "") -> void:
|
||||
if selected_index < 0 or selected_indices.is_empty():
|
||||
return
|
||||
var objects: Array = main.doc.get("objects", [])
|
||||
|
|
@ -588,8 +588,8 @@ func tilt_selected(axis_x: bool, delta_deg: float) -> void:
|
|||
var twins: Array = ObjectOps.symmetry_sync_pairs(
|
||||
main.doc, members, _sym_mode(), selected_index)
|
||||
_sync_members_to_markers(twins)
|
||||
var tag := "tilt:%d" % selected_index if members.size() == 1 \
|
||||
else "tiltmulti:%s" % str(members)
|
||||
var tag := ("tilt:%d" % selected_index if members.size() == 1 \
|
||||
else "tiltmulti:%s" % str(members)) + merge_suffix
|
||||
main.history.commit("Auswahl gekippt (%s, %d Objekte)" % [axis, members.size()],
|
||||
before, main.history.snapshot(["objects"]), tag)
|
||||
_mark_dirty()
|
||||
|
|
|
|||
|
|
@ -119,8 +119,8 @@ const KAT_GROUPS := {
|
|||
}
|
||||
|
||||
## Anzeige-Größe der Vorschaubilder im Browser (Karte t_59fc0f21): vorher 40 px,
|
||||
## jetzt Kacheln mit 96 px (128-px-PNGs werden auf die Anzeige-Größe skaliert).
|
||||
const THUMB_DISPLAY_PX := 96
|
||||
## jetzt Kacheln mit 128 px (25.10.: größer, deutlicher) (128-px-PNGs werden auf die Anzeige-Größe skaliert).
|
||||
const THUMB_DISPLAY_PX := 128
|
||||
|
||||
|
||||
static func object_label(oid: String) -> String:
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ const ACTIONS := {
|
|||
"editor_brush_lower": [KEY_2],
|
||||
"editor_brush_smooth": [KEY_3],
|
||||
"editor_brush_paint": [KEY_4],
|
||||
"editor_brush_flatten": [KEY_5],
|
||||
"editor_brush_ramp": [KEY_6],
|
||||
# --- Datei & Undo (Karte t_11b5c25b, Karte 3/3) --------------------------
|
||||
# Strg+Z/Strg+Y laufen seit dem M2-UX-Fundament; hier NACHGEBUGGEN als
|
||||
# dokumentierte Actions (alle 3 Shortcut-Karten: ACTIONS-Dict = Source of
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ extends Node3D
|
|||
## 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 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 camera: Camera3D
|
||||
|
|
@ -32,6 +33,10 @@ var brush_mode: int = TerrainField.Mode.RAISE
|
|||
var brush_radius := 12.0 # Meter
|
||||
## Kante beim Malen: 0 = hart, 1 = ganz weich (Karte 25.10.).
|
||||
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 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 _stroke_mode: int = TerrainField.Mode.RAISE
|
||||
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 _mats := PackedByteArray() # Material-Schicht (res²) — seit _refresh_
|
||||
# _from_doc immer dekodiert (t_55ed041a),
|
||||
|
|
@ -56,6 +65,7 @@ var _cursor := Vector3.ZERO
|
|||
|
||||
# Pinsel-Ring
|
||||
var _ring: MeshInstance3D
|
||||
var _ramp_line: MeshInstance3D # Rampen-Vorschau: Linie Start → Cursor
|
||||
var _ring_radius := -1.0
|
||||
var _ring_mode := -1
|
||||
|
||||
|
|
@ -74,6 +84,15 @@ func _ensure_ring() -> void:
|
|||
_ring = MeshInstance3D.new()
|
||||
_ring.visible = false
|
||||
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 ---
|
||||
|
|
@ -102,6 +121,8 @@ func deactivate() -> void:
|
|||
_end_stroke()
|
||||
if _ring != null and is_instance_valid(_ring):
|
||||
_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).
|
||||
|
|
@ -177,6 +198,16 @@ func set_weichheit(w: float) -> void:
|
|||
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:
|
||||
brush_strength = clampf(mps, 1.0, 20.0)
|
||||
|
||||
|
|
@ -192,7 +223,10 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
if not active:
|
||||
return
|
||||
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
|
||||
if event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
|
|
@ -220,6 +254,15 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
# 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:
|
||||
var hit: Variant = TerrainField.raycast(_heights, _res, _sx, _sz,
|
||||
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_material = paint_material if material_id < 0 else material_id
|
||||
_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
|
||||
# (dekodiert — Legacy-Maps ohne Schicht starten mit alles-Gras).
|
||||
if mode == TerrainField.Mode.PAINT:
|
||||
|
|
@ -288,11 +337,12 @@ func _process(delta: float) -> void:
|
|||
view_builder.call("update_terrain", _heights, _mats, _splat_rect)
|
||||
_ring.position.y = _height_at_stroke(_cursor.x, _cursor.z) + 0.3
|
||||
return
|
||||
var amount: float
|
||||
if _stroke_mode == TerrainField.Mode.SMOOTH:
|
||||
amount = clampf(brush_strength / SMOOTH_RATE_DIVISOR * delta, 0.0, 1.0)
|
||||
else:
|
||||
amount = brush_strength * delta
|
||||
if _stroke_mode == TerrainField.Mode.RAMP:
|
||||
# Rampe wirkt erst beim Loslassen — hier nur der Endpunkt + Ring-Hinweis.
|
||||
_ramp_b = Vector2(_cursor.x, _cursor.z)
|
||||
_update_ramp_line()
|
||||
return
|
||||
var amount := _step_amount(_stroke_mode, delta)
|
||||
# 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).
|
||||
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
|
||||
|
||||
|
||||
## 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
|
||||
## 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.
|
||||
|
|
@ -329,6 +390,11 @@ func _end_stroke() -> void:
|
|||
if not _stroking:
|
||||
return
|
||||
_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.
|
||||
var before: Dictionary = main.history.snapshot(["terrain"])
|
||||
var terrain: Dictionary = main.doc.get("terrain", {})
|
||||
|
|
@ -346,6 +412,40 @@ func _end_stroke() -> void:
|
|||
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
|
||||
## Maus auch tun würde — inkl. History-Eintrag.
|
||||
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_z := _cursor.z
|
||||
for i in steps:
|
||||
var amount: float
|
||||
if mode == TerrainField.Mode.SMOOTH:
|
||||
amount = clampf(brush_strength / SMOOTH_RATE_DIVISOR * dt, 0.0, 1.0)
|
||||
else:
|
||||
amount = brush_strength * dt
|
||||
var amount := _step_amount(mode, dt)
|
||||
if mode == TerrainField.Mode.PAINT:
|
||||
_paint_step(save_x, save_z)
|
||||
continue
|
||||
|
|
@ -472,7 +568,8 @@ func _height_step(x: float, z: float, amount: float) -> bool:
|
|||
if _sym_active():
|
||||
snap = _snap_heights(_brush_region(g))
|
||||
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():
|
||||
_mirror_heights(snap, g)
|
||||
return changed
|
||||
|
|
@ -489,7 +586,7 @@ func _paint_step(x: float, z: float) -> bool:
|
|||
if _sym_active() and _mats.size() >= _mres * _mres:
|
||||
snap = _snap_mats(_brush_region_r(g, _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():
|
||||
_mirror_mats(snap, g)
|
||||
return changed
|
||||
|
|
@ -614,6 +711,10 @@ func _refresh_ring_mesh() -> void:
|
|||
var mc: int = clampi(paint_material, 0, SvmapIO.MATERIAL_IDS.size() - 1)
|
||||
mat.albedo_color = (terrain_mesh_color(mc) as Color)
|
||||
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.emission_enabled = true
|
||||
|
|
|
|||
|
|
@ -160,9 +160,23 @@ static func check_update(git_exe: String, project_dir: String) -> Dictionary:
|
|||
"error": "Origin lieferte keinen Commit (nur Warnungen)."}
|
||||
if local == remote:
|
||||
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
|
||||
# Befund 25.10.: „Update verfügbar“ blieb nach dem Update stehen, weil nur
|
||||
# HEAD != origin/main geprüft wurde. Steht der Klon auf einem anderen
|
||||
# Branch oder VOR main (z. B. Feature-Branch), ist das KEIN Update:
|
||||
# nur melden, wenn der Remote-Commit NICHT schon in HEAD steckt.
|
||||
if remote_in_head(git_exe, project_dir, remote):
|
||||
return {"status": "up_to_date", "local": local, "remote": remote, "error": ""}
|
||||
return {"status": "update_available", "local": local, "remote": remote, "error": ""}
|
||||
|
||||
|
||||
## Ist `remote` schon Vorfahre von HEAD (Klon gleich oder VORAUS)? Unbekannter
|
||||
## Commit (Exit 128) = noch nicht geholt = echtes Update → false.
|
||||
static func remote_in_head(git_exe: String, project_dir: String, remote: String) -> bool:
|
||||
var r := git_run(git_exe, project_dir, PackedStringArray([
|
||||
"merge-base", "--is-ancestor", remote, "HEAD"]))
|
||||
return bool(r["ok"])
|
||||
|
||||
|
||||
## Aus der gemischten ls-remote-Ausgabe den Commit für `branch` lesen.
|
||||
## WARUM: `git_run` mischt stdout+stderr, und der git-Client schreibt bei einer
|
||||
## HTTP-Umleitung zuerst "warning: Umleitung nach ..." — die alte Fassung nahm
|
||||
|
|
@ -246,7 +260,8 @@ const AUTO_STASH_MESSAGE := "auto: Selbst-Update"
|
|||
## fürs Statuszeile-Fenster, leer wenn nichts besonderes passiert ist).
|
||||
static func pull(git_exe: String, project_dir: String) -> Dictionary:
|
||||
var stash_info := _stash_local_changes(git_exe, project_dir)
|
||||
var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only"]))
|
||||
# Gezielt origin/main ziehen (nicht den Upstream des aktuellen Branches).
|
||||
var r := git_run(git_exe, project_dir, PackedStringArray(["pull", "--ff-only", "origin", BRANCH]))
|
||||
var errors: Array = []
|
||||
var text := str(r["output"]).strip_edges()
|
||||
var info := ""
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class_name TerrainField
|
|||
##
|
||||
## 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
|
||||
## 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)
|
||||
|
||||
|
||||
## 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:
|
||||
## RAISE/LOWER → Meter (bereits strength × dt vom Aufrufer),
|
||||
## 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.
|
||||
static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, size_z: 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:
|
||||
# PAINT läuft über apply_material_brush (Material-Schicht, Karte
|
||||
# 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)
|
||||
if mode == Mode.SMOOTH:
|
||||
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 z := z0
|
||||
while z <= z1:
|
||||
|
|
@ -78,7 +178,8 @@ static func apply_brush(heights: PackedFloat32Array, res: int, size_x: float, si
|
|||
if d >= radius:
|
||||
x += 1
|
||||
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 h := heights[idx]
|
||||
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
|
||||
## Materials weich auf 0 (Smoothstep). Ein Punkt wird nur verstärkt, nie
|
||||
## 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,
|
||||
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 \
|
||||
or mix_b.size() < res * res or mix_w.size() < res * res:
|
||||
return false
|
||||
|
|
@ -160,7 +264,7 @@ static func apply_material_brush_mix(mats: PackedByteArray, mix_b: PackedByteArr
|
|||
if d > inner:
|
||||
var t := clampf((d - inner) / maxf(radius - inner, 0.0001), 0.0, 1.0)
|
||||
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:
|
||||
continue
|
||||
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_w[idx] = 0
|
||||
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
|
||||
mix_b[idx] = material_id
|
||||
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).
|
||||
static func _apply_smooth(heights: PackedFloat32Array, res: int, hmin: float, hmax: 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()
|
||||
avg.resize(heights.size())
|
||||
var z := z0
|
||||
while z <= z1:
|
||||
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
|
||||
avg.resize(bw * (z1 - z0 + 1))
|
||||
for z in range(z0, z1 + 1):
|
||||
for x in range(x0, x1 + 1):
|
||||
var sum := 0.0
|
||||
var count := 0
|
||||
var zi := maxi(z - 1, 0)
|
||||
while zi <= mini(z + 1, res - 1):
|
||||
var xi := maxi(x - 1, 0)
|
||||
while xi <= mini(x + 1, res - 1):
|
||||
for zi in range(maxi(z - 1, 0), mini(z + 1, res - 1) + 1):
|
||||
for xi in range(maxi(x - 1, 0), mini(x + 1, res - 1) + 1):
|
||||
sum += heights[zi * res + xi]
|
||||
count += 1
|
||||
xi += 1
|
||||
zi += 1
|
||||
avg[z * res + x] = sum / float(maxi(count, 1))
|
||||
x += 1
|
||||
z += 1
|
||||
avg[(z - z0) * bw + (x - x0)] = sum / float(maxi(count, 1))
|
||||
var changed := false
|
||||
z = z0
|
||||
while z <= z1:
|
||||
var x := x0
|
||||
while x <= x1:
|
||||
for z in range(z0, z1 + 1):
|
||||
for x in range(x0, x1 + 1):
|
||||
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 w := 0.5 * (1.0 + cos(PI * d))
|
||||
var w := falloff_weight(d, 1.0, falloff)
|
||||
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)
|
||||
if nh != heights[idx]:
|
||||
heights[idx] = nh
|
||||
changed = true
|
||||
x += 1
|
||||
z += 1
|
||||
return changed
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,70 +9,74 @@ extends RefCounted
|
|||
|
||||
static func text() -> String:
|
||||
var out := ""
|
||||
out += _header("Navigation (Kamera)")
|
||||
out += _header("Kamera & Steuerung")
|
||||
out += _table(_rows([
|
||||
["Mittlere Maustaste ziehen", "Orbit: um die Karte drehen"],
|
||||
["Mittlere Maustaste + Umschalt ziehen", "Pan: Karte verschieben"],
|
||||
["Mausrad", "Zoom (aus, während ein Objekt gedreht wird)"],
|
||||
["Rechte Maustaste GEDACHT halten", "Freelook-Flug: Umsehen + W vor / S zurück / A links / D rechts; Leertaste hoch / Shift runter (Shooter-Konvention) — funktioniert in JEDEM Werkzeug (Terrain, Bauen, Platzieren), die rechte Maustaste hat immer Vorrang"],
|
||||
["Strg halten (im Flug)", "schneller fliegen (×3)"],
|
||||
["Alt halten (im Flug)", "langsamer fliegen (×0,3)"],
|
||||
["Mausrad (im Flug)", "Grundgeschwindigkeit dauerhaft ändern (2–300 m/s)"],
|
||||
["Mittlere Maustaste + Umschalt", "Pan: Karte verschieben"],
|
||||
["Mausrad", "Zoom"],
|
||||
["Rechte Maustaste halten", "Freelook-Flug in jedem Werkzeug: Maus = Umsehen, W/A/S/D = Bewegen, Leertaste hoch, Umschalt runter"],
|
||||
["Strg / Alt (im Flug)", "schneller (×3) / langsamer (×0,3); Mausrad ändert die Grundgeschwindigkeit"],
|
||||
["Umschalt+F", "Flugmodus dauerhaft an/aus"],
|
||||
["F", "Auswahl ins Bild zentrieren"],
|
||||
["L", "Höhenlinien fixieren (sonst nur im Terrain-Werkzeug)"],
|
||||
["H", "diese Hilfe an/aus (Esc schließt auch)"],
|
||||
]))
|
||||
out += _header("Auswahl")
|
||||
out += _header("Objekte auswählen & platzieren (F2)")
|
||||
out += _table(_rows([
|
||||
["Linksklick", "Objekt auswählen (ersetzt die Auswahl)"],
|
||||
["Strg+Klick (Viewport + Baum)", "Objekt zur Auswahl hinzufügen/entfernen (Mehrfachauswahl)"],
|
||||
["Umschalt+Klick", "wie Strg+Klick (Mehrfachauswahl umschalten)"],
|
||||
["Ziehen über leeren Boden", "Rahmen aufziehen: alle Objekte im Rahmen wählen"],
|
||||
["Umschalt + Rahmen", "Rahmen-Auswahl zur bestehenden Auswahl hinzufügen"],
|
||||
["Esc", "Werkzeug abbrechen → Greifen zurückrollen → Ghost abbrechen → Auswahl aufheben"],
|
||||
["G", "Auswahl greifen (hängt an der Maus; Klick setzt, Esc rollt zurück)"],
|
||||
]))
|
||||
out += _header("Objekt-Manipulation")
|
||||
out += _table(_rows([
|
||||
["R", "90° drehen (Umschalt+R = −90°, ganze Gruppe dreht mit; Mausrad: 15° fein)"],
|
||||
["Strg+R / Strg+Umschalt+R", "Kippen um X (+15° / −15°) — auch beim Platzieren (Karte t_e166e656)"],
|
||||
["Alt+R / Alt+Umschalt+R", "Kippen um Z (+15° / −15°) — auch beim Platzieren (Karte t_e166e656)"],
|
||||
["Pfeiltasten", "Auswahl im 2-m-Raster verschieben (Kamera-nächste Weltachse)"],
|
||||
["Bild↑ / Bild↓", "+0,25 m / −0,25 m anheben/absenken — Absenken UNTER den Boden erlaubt (Versenken, z. B. Steine)"],
|
||||
["Strg+Bild↑", "Auswahl aufs Terrain/Boden absetzen"],
|
||||
["Strg halten", "temporärer Snap-Zwang (rastet auf ganze Meter)"],
|
||||
["Strg+D", "Auswahl duplizieren (+2 m versetzt)"],
|
||||
["Entf", "Auswahl sofort löschen (Strg+Z holt zurück)"],
|
||||
["Linksklick", "Objekt wählen; Umschalt/Strg+Klick: Mehrfachauswahl"],
|
||||
["Rahmen aufziehen", "alle Objekte im Rahmen wählen (Umschalt: hinzufügen)"],
|
||||
["Bibliothek: Klick / Doppelklick", "Objekt auswählen / Platzieren starten; Klick in die Karte setzt, Esc bricht ab"],
|
||||
["G", "greifen: Objekt hängt an der Maus (Klick setzt, Esc zurück)"],
|
||||
["Pfeiltasten", "im 2-m-Raster verschieben"],
|
||||
["Bild↑ / Bild↓", "0,25 m anheben / absenken; Strg+Bild↑ = auf den Boden setzen"],
|
||||
["Strg halten", "Snap-Zwang auf ganze Meter"],
|
||||
["Strg+D", "duplizieren"],
|
||||
["Entf", "löschen"],
|
||||
["Strg+G / Strg+Umschalt+G", "Gruppieren / Gruppe lösen"],
|
||||
["Esc", "abbrechen, dann Auswahl aufheben"],
|
||||
]))
|
||||
out += _header("Terrain-Werkzeug (F1)")
|
||||
out += _header("Objekte verschieben, drehen, kippen (Werkzeugleiste links oben in der 3D-Ansicht)")
|
||||
out += _table(_rows([
|
||||
["1 / 2 / 3 / 4", "Pinselmodus: Heben / Senken / Glätten / Malen"],
|
||||
["[ / ]", "Pinselgröße: Klein (4 m) / Mittel (12 m) / Groß (32 m) zyklisch"],
|
||||
["Linksklick-Ziehen", "Boden formen bzw. malen"],
|
||||
["Strg+Linksklick-Ziehen", "Gegenteil: senken (bei Heben) / heben (bei Senken) / Steppe zurückmalen"],
|
||||
["Verschieben", "Pfeile am Objekt ziehen (X rot, Y grün, Z blau); Mausrad ändert die Schrittweite"],
|
||||
["Drehen", "Ringe am Objekt ziehen: um alle drei Achsen, in 15°-Schritten; R = 90° um Y (Umschalt+R = −90°)"],
|
||||
["Kippen", "Strg+R / Strg+Umschalt+R = um X ±15°; Alt+R / Alt+Umschalt+R = um Z ±15° (auch beim Platzieren)"],
|
||||
["Spiegeln", "an der Stelle links↔rechts oder vor↔zurück (Panel Auswahl)"],
|
||||
["Strg+Z / Strg+Y", "jeder Zug ist ein Undo-Schritt"],
|
||||
]))
|
||||
out += _header("Datei & Undo")
|
||||
out += _header("Terrain formen (F1)")
|
||||
out += _table(_rows([
|
||||
["1 / 2 / 3", "Heben / Senken / Glätten"],
|
||||
["5 / 6", "Einebnen (Höhe vom Strichstart) / Rampe (Start klicken, ziehen, loslassen)"],
|
||||
["[ / ]", "Pinselgröße Klein / Mittel / Groß"],
|
||||
["Linksklick ziehen", "Boden formen; Strg+Klick = Gegenteil (Heben ↔ Senken)"],
|
||||
["Tablet/Controller", "Stärke folgt dem Druck (Maus = voll)"],
|
||||
]))
|
||||
out += _header("Malen (4)")
|
||||
out += _table(_rows([
|
||||
["Material wählen", "Tab Malen: Kacheln mit Vorschau"],
|
||||
["Linksklick ziehen", "Material auf den Boden malen; Strg+Klick malt Steppe zurück"],
|
||||
["Kante", "Regler: hart ↔ weich auslaufend"],
|
||||
]))
|
||||
out += _header("Bauen (F3)")
|
||||
out += _table(_rows([
|
||||
["Linksklick", "Bauteil aufs 2-m-Raster setzen, auch auf andere Teile (stapeln)"],
|
||||
["R / Pfeile / Bild↑↓", "drehen / verschieben / anheben"],
|
||||
["Umschalt+Klick", "Bauteil auswählen; Entf löscht"],
|
||||
]))
|
||||
out += _header("Spawn-Zonen (F4) und Test-Start (T)")
|
||||
out += _table(_rows([
|
||||
["F4: Linksklick", "Zone setzen; Ziehen verschiebt, Entf löscht; Team und Radius im Tab Prüfen"],
|
||||
["T: Linksklick", "Test-Startpunkt setzen (nur für F5, kein Spawn)"],
|
||||
["F5", "▶ Karte testen"],
|
||||
]))
|
||||
out += _header("Datei & Rückgängig")
|
||||
out += _table(_rows([
|
||||
["Strg+Z", "Rückgängig"],
|
||||
["Strg+Y oder Strg+Umschalt+Z", "Wiederherstellen (Redo)"],
|
||||
["Strg+S", "Map speichern (.svmap, bekannter Pfad)"],
|
||||
["Strg+Umschalt+S", "Map speichern unter …"],
|
||||
["Strg+O", "Map öffnen"],
|
||||
["Strg+N", "Neue Map"],
|
||||
["Datei → 🔄 Updates prüfen", "Selbst-Update: prüft auf neue Version; Banner → „Jetzt updaten“ (lädt, importiert, startet neu)"],
|
||||
["Strg+Y / Strg+Umschalt+Z", "Wiederherstellen"],
|
||||
["Strg+S / Strg+Umschalt+S", "Speichern / Speichern unter"],
|
||||
["Strg+O / Strg+N", "Öffnen / Neue Map"],
|
||||
["Datei → Updates prüfen", "prüft auf eine neue Version; Banner „Jetzt updaten“ lädt und startet neu"],
|
||||
]))
|
||||
out += _header("Werkzeuge & Tests")
|
||||
out += _table(_rows([
|
||||
["F1", "Werkzeug: Terrain formen"],
|
||||
["F2", "Werkzeug: Objekte platzieren/auswählen"],
|
||||
["F3", "Werkzeug: Bauen (Häuser)"],
|
||||
["F4", "Werkzeug: Spawn-Zonen malen"],
|
||||
["T", "Werkzeug: Test-Startpunkt setzen — Klick in die Karte = dort startet F5 (KEIN Spiel-Spawnpunkt; im Testlauf versetzt T dich sofort zum Blickpunkt)"],
|
||||
["F5", "▶ Karte testen (Start: Test-Start → erster Team-Spawnpunkt → Kartenmitte)"],
|
||||
["H", "Diese Kurzhilfe an/aus (Esc schließt auch)"],
|
||||
["L", "Höhenlinien FIXIEREN (immer sichtbar/unsichtbar) — ohne Fix erscheinen sie nur im Terrain-Werkzeug (F1)"],
|
||||
]))
|
||||
out += "[color=#a8b6ad]Vor „Neue Map“ und „Öffnen“ fragt der Editor nach, wenn noch ungespeicherte Änderungen da sind. Die Statuszeile unten zeigt immer das aktive Werkzeug.[/color]\n"
|
||||
return out
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ var grid_snap := false # Raster-Snap (1 m) an/aus — Objekt-Werkzeug
|
|||
|
||||
var history := ActionHistory.new()
|
||||
var interaction # src/editor/interaction.gd (script-erweitert → untypisiert)
|
||||
var viewport_toolbar # src/ui/viewport_toolbar.gd (Buttons oben links in der 3D-Ansicht)
|
||||
var gizmo # src/viewport/gizmo.gd (Achsen-Pfeile, Kind von interaction)
|
||||
var gizmo_step_m := 0.5 # Gizmo-Schrittweite in m (Panel-Feld; überlebt UI-Rebuilds)
|
||||
var terrain_tool # src/editor/terrain_tool.gd (Pinsel, Karte t_9aba04f1)
|
||||
|
|
@ -79,6 +80,8 @@ var status_mod # src/ui/panels/status_bar.gd
|
|||
var selection_mod # src/ui/panels/selection_panel.gd
|
||||
var map_mod # src/ui/panels/map_panel.gd
|
||||
var terrain_mod # src/ui/panels/terrain_panel.gd
|
||||
var malen_mod # src/ui/panels/malen_panel.gd (Malen-Tab)
|
||||
var bauen_mod # src/ui/panels/bauen_panel.gd (Bauen-Tab)
|
||||
var check_mod # src/ui/panels/check_panel.gd
|
||||
var tree_mod # src/ui/panels/scene_tree_panel.gd
|
||||
var assets_mod # src/ui/panels/assets_panel.gd
|
||||
|
|
@ -391,6 +394,10 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
|
|||
map_mod = child
|
||||
"terrain":
|
||||
terrain_mod = child
|
||||
"malen":
|
||||
malen_mod = child
|
||||
"bauen":
|
||||
bauen_mod = child
|
||||
"check":
|
||||
check_mod = child
|
||||
sel_label = selection_mod.sel_label
|
||||
|
|
@ -398,9 +405,16 @@ func _panel_group_built(gruppe: String, tabs: TabContainer) -> void:
|
|||
prop_name = map_mod.prop_name
|
||||
spawn_status = check_mod.spawn_status
|
||||
terrain_info = map_mod.terrain_info
|
||||
build_edit_btn = terrain_mod.build_edit_btn
|
||||
build_panel = terrain_mod.build_panel
|
||||
build_kind_buttons = terrain_mod.build_kind_buttons
|
||||
build_edit_btn = bauen_mod.build_edit_btn
|
||||
build_panel = bauen_mod.build_panel
|
||||
build_kind_buttons = bauen_mod.build_kind_buttons
|
||||
# Malen-Widgets wohnen im Malen-Tab; das Terrain-Modul hält die
|
||||
# alten Namen als Aliase (Tests/Werkzeug-Vertrag).
|
||||
terrain_mod.terrain_palette = malen_mod.palette
|
||||
terrain_mod.terrain_mat_buttons = malen_mod.mat_buttons
|
||||
terrain_mod.terrain_weich_label = malen_mod.weich_label
|
||||
terrain_mod.terrain_weich_slider = malen_mod.weich_slider
|
||||
terrain_mod.terrain_paint_hint = malen_mod.paint_hint
|
||||
# Test-/Werkzeug-Vertrag (wie vor der U1-Aufteilung): die Widgets
|
||||
# wohnen in den Modulen, der Koordinator hält die alten Namen
|
||||
# als Bequemlichkeits-Referenzen — nichts wird doppelt gebaut.
|
||||
|
|
@ -536,7 +550,7 @@ func _build_dialogs_and_overlays() -> void:
|
|||
overlay_box.add_theme_constant_override("separation", UIScale.px(6))
|
||||
help_overlay.add_child(overlay_box)
|
||||
help_overlay_title = Label.new()
|
||||
help_overlay_title.text = "Kurzhilfe — alle Tasten auf einen Blick (H oder Esc schließt)"
|
||||
help_overlay_title.text = "Hilfe — Werkzeuge, Steuerung und Tastenkürzel (H oder Esc schließt)"
|
||||
help_overlay_title.add_theme_font_size_override("font_size", UIScale.heading_font_size())
|
||||
help_overlay_title.add_theme_color_override("font_color", get_theme_color("accent", "Editor"))
|
||||
overlay_box.add_child(help_overlay_title)
|
||||
|
|
@ -616,9 +630,14 @@ func _build_center_viewport() -> SubViewportContainer:
|
|||
world.add_child(test_start_tool)
|
||||
test_start_tool.setup(self, camera, view_builder, interaction)
|
||||
|
||||
# Werkzeugleiste oben links (Verschieben / Drehen & Kippen / Aktionen)
|
||||
viewport_toolbar = preload("res://src/ui/viewport_toolbar.gd").new()
|
||||
container.add_child(viewport_toolbar)
|
||||
viewport_toolbar.setup(self)
|
||||
|
||||
# Bedien-Hinweis im Viewport (Overlay)
|
||||
help_label = Label.new()
|
||||
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
||||
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
|
||||
help_label.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
|
||||
help_label.offset_left = UIScale.px(12)
|
||||
help_label.offset_bottom = UIScale.px(-8)
|
||||
|
|
@ -1330,7 +1349,7 @@ func _update_tool_label() -> void:
|
|||
"bauen": "Bauen (F3)",
|
||||
"spawns": "Spawn-Zonen (F4)",
|
||||
}
|
||||
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
|
||||
tool_label.text = "Werkzeug: %s" % str(names.get(tool_mode, tool_mode))
|
||||
|
||||
|
||||
## Symmetrie-Schalter (Karte t_f5935023, M1): Modus aus dem Toolbar-Menü übernehmen.
|
||||
|
|
@ -1673,7 +1692,7 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||
if tool_mode == "terrain":
|
||||
_terrain_brush_size_step(key == KEY_BRACKETRIGHT)
|
||||
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/
|
||||
# Malen — NUR im Terrain-Werkzeug (Konfliktfreiheit, Punkt 6; im
|
||||
# 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.
|
||||
|
||||
func _on_terrain_btn_pressed() -> void:
|
||||
if terrain_mod.terrain_edit_btn.button_pressed:
|
||||
if tool_mode != "terrain":
|
||||
if terrain_mod.terrain_mode_paint.button_pressed:
|
||||
terrain_mod.terrain_mode_raise.button_pressed = true
|
||||
_set_tool_mode("terrain")
|
||||
else:
|
||||
_set_tool_mode("objects")
|
||||
|
|
@ -1814,10 +1835,9 @@ func _enter_terrain_mode() -> void:
|
|||
_sync_terrain_button()
|
||||
return
|
||||
tool_mode = "terrain"
|
||||
terrain_mod.terrain_panel.visible = true
|
||||
help_label.text = "Linksklick-Ziehen: Boden formen oder malen · Strg+Linksklick: Gegenteil / Steppe zurück ·\n1/2/3/4: Heben/Senken/Glätten/Malen · [ / ]: Pinselgröße Klein/Mittel/Groß ·\nPinselgröße & Material rechts · Rechte Maustaste: Flug · Strg+Z: letzter Strich weg"
|
||||
terrain_mod.terrain_edit_btn.button_pressed = true
|
||||
help_label.text = "Linksklick-Ziehen: formen bzw. malen · Strg+Klick: Gegenteil · [ ]: Pinselgröße · H: Hilfe"
|
||||
terrain_tool.call("set_brush_mode", terrain_mod._selected_terrain_mode())
|
||||
terrain_mod.refresh_panels()
|
||||
_set_status("Terrain-Modus an: LINKSKLICK-Ziehen formt den Boden, im Malen-Modus färbt er das Gelände (Steppe/Sand/Fels …). Regler rechts stellen Pinsel und Stärke ein.")
|
||||
|
||||
|
||||
|
|
@ -1826,6 +1846,7 @@ func _leave_terrain_mode() -> void:
|
|||
terrain_tool.call("deactivate")
|
||||
if terrain_mod != null:
|
||||
terrain_mod.terrain_panel.visible = false
|
||||
malen_mod.malen_panel.visible = false
|
||||
_sync_terrain_button()
|
||||
|
||||
|
||||
|
|
@ -1834,7 +1855,7 @@ func _enter_spawn_mode() -> void:
|
|||
interaction.call("clear_selection", true)
|
||||
tool_mode = "spawns"
|
||||
check_mod.spawn_panel.visible = true
|
||||
help_label.text = "Linksklick: Zone setzen · Klick+Ziehen: Zone verschieben · Entf: löschen ·\nTeam/Radius rechts · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
||||
help_label.text = "Linksklick: Zone setzen · Ziehen: verschieben · Entf: löschen · H: Hilfe"
|
||||
check_mod.spawn_edit_btn.button_pressed = true
|
||||
spawn_tool.call("activate")
|
||||
spawn_tool.call("set_team", check_mod._selected_spawn_team())
|
||||
|
|
@ -1853,7 +1874,7 @@ func _leave_spawn_mode() -> void:
|
|||
|
||||
func _enter_objects_mode() -> void:
|
||||
tool_mode = "objects"
|
||||
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
||||
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
|
||||
_base_tool_status()
|
||||
|
||||
|
||||
|
|
@ -1886,8 +1907,10 @@ func _enter_build_mode() -> void:
|
|||
interaction.call("clear_selection", true)
|
||||
tool_mode = "bauen"
|
||||
build_panel.visible = true
|
||||
help_label.text = "Linksklick: Bauteil setzen (auch auf andere Teile = stapeln) · Umschalt+Klick: Bauteil auswählen ·\nR: drehen (90°) · Pfeiltasten: im 2-m-Raster verschieben · Bild↑/↓: anheben/absenken · Strg+Bild↑: auf Boden · Entf: löschen · Mittlere Maustaste: Kamera · Strg+Z: zurück"
|
||||
help_label.text = "Linksklick: Bauteil setzen · R: drehen · Entf: löschen · H: Hilfe"
|
||||
build_edit_btn.button_pressed = true
|
||||
if ui_right_panel is TabContainer and bauen_mod != null:
|
||||
(ui_right_panel as TabContainer).current_tab = bauen_mod.get_index()
|
||||
build_tool.call("activate")
|
||||
_set_status("Bauen-Modus an: Bauteil rechts anklicken, dann in die Karte klicken — alles rastet aufs 2-m-Raster.")
|
||||
|
||||
|
|
@ -1905,7 +1928,7 @@ func _sync_build_button() -> void:
|
|||
|
||||
|
||||
func _sync_terrain_button() -> void:
|
||||
terrain_mod.terrain_edit_btn.button_pressed = tool_mode == "terrain"
|
||||
terrain_mod.refresh_panels()
|
||||
|
||||
|
||||
func _terrain_brush_size_step(up: bool) -> void:
|
||||
|
|
@ -1924,15 +1947,19 @@ func _terrain_brush_mode_key(mode_idx: int) -> void:
|
|||
btn = t.terrain_mode_smooth
|
||||
3:
|
||||
btn = t.terrain_mode_paint
|
||||
4:
|
||||
btn = t.terrain_mode_flatten
|
||||
5:
|
||||
btn = t.terrain_mode_ramp
|
||||
if btn == null or terrain_tool == null:
|
||||
return
|
||||
btn.set_pressed(true)
|
||||
var labels := ["Heben", "Senken", "Glätten", "Malen"]
|
||||
var labels := ["Heben", "Senken", "Glätten", "Malen", "Einebnen", "Rampe"]
|
||||
var extra := ""
|
||||
if mode_idx == 3:
|
||||
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)])
|
||||
_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).
|
||||
|
|
@ -1955,12 +1982,13 @@ func _recheck_terrain_mode() -> void:
|
|||
terrain_tool.call("deactivate")
|
||||
tool_mode = "objects"
|
||||
terrain_mod.terrain_panel.visible = false
|
||||
malen_mod.malen_panel.visible = false
|
||||
_sync_terrain_button()
|
||||
_update_tool_label()
|
||||
# Karte t_89c0ec95: Dezenter Terrain-Rückschalter = ebenfalls ein
|
||||
# Werkzeugwechsel — Linien wieder ausblenden (sofern nicht fixiert).
|
||||
_sync_contours_to_builder()
|
||||
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR: 90° drehen (Mausrad: 15° fein) · Pfeiltasten: 2-m-Raster · G: greifen (Esc: zurück) · Strg+D: duplizieren · Bild↑/↓: anheben/absenken\nStrg+Bild↑: auf Boden setzen · Strg halten: Snap-Zwang · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
||||
help_label.text = "Linksklick: auswählen · Rahmen: mehrere · R: drehen · Entf: löschen · H: Hilfe"
|
||||
_set_status("Terrain-Modus aus: Diese Map passt nicht ins Editor-MVP (Terrain-Auflösung über %d) — die Objekte bleiben normal bearbeitbar." % TerrainField.EDITOR_MAX_RESOLUTION, true)
|
||||
|
||||
|
||||
|
|
@ -2157,9 +2185,13 @@ func _refresh_stats() -> void:
|
|||
var count := SvmapDocument.object_count(doc)
|
||||
var poly := SvmapDocument.poly_usage(doc, manifest)
|
||||
var budget := int(SvmapIO.MAX_TOTAL_POLY)
|
||||
stats_objects.text = "Objekte: %d / 5000 · Poly-Budget: %s / %s (%.0f %%) · Bauteile: %d" % [
|
||||
count, _fmt_int(int(poly)), _fmt_int(budget), 100.0 * poly / budget,
|
||||
BuildOps.piece_count(doc)]
|
||||
var poly_pct := 100.0 * poly / budget
|
||||
stats_objects.text = "%d Objekte · Poly %.0f %%" % [count, poly_pct]
|
||||
stats_objects.tooltip_text = "Objekte: %d / 5000\nPoly-Budget: %s / %s (%.0f %%)\nBauteile: %d" % [
|
||||
count, _fmt_int(int(poly)), _fmt_int(budget), poly_pct, BuildOps.piece_count(doc)]
|
||||
# Nur bei knappem Budget auffällig färben (sonst ruhiger Nebentext).
|
||||
stats_objects.add_theme_color_override("font_color", get_theme_color(
|
||||
"warning" if poly_pct >= 90.0 or count >= 4500 else "field", "Editor"))
|
||||
# GLB-Fehlende Objekte (Karte t_9b50b231): Platzhalter laufen, aber Sly soll
|
||||
# sehen, WELCHE echten Modelle im Mesh-Lager nachgelegt werden müssen.
|
||||
if view_builder != null and not (view_builder.get("fehlende_glbs") as Array).is_empty():
|
||||
|
|
@ -2177,8 +2209,9 @@ func _refresh_stats() -> void:
|
|||
var quest_budget := 524288
|
||||
var pct := 100.0 * float(tris) / float(quest_budget)
|
||||
var fps := Engine.get_frames_per_second()
|
||||
stats_terrain.text = " · Terrain: %s² Grid, %s Dreiecke (%.0f %% Quest-Budget) · %d FPS" % [
|
||||
str(res) if res > 0 else "—", _fmt_int(tris), pct, fps]
|
||||
stats_terrain.text = "Terrain %s² · %d FPS" % [str(res) if res > 0 else "—", fps]
|
||||
stats_terrain.tooltip_text = "Terrain: %s² Grid, %s Dreiecke (%.0f %% Quest-Budget)" % [
|
||||
str(res) if res > 0 else "—", _fmt_int(tris), pct]
|
||||
|
||||
|
||||
func _refresh_file_label() -> void:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ func panel_title() -> String:
|
|||
## Klapp-Gruppen-Körper („Alle") + Zuletzt-benutzt-Ansicht.
|
||||
func build() -> void:
|
||||
add_child(_header_label("Objekt-Bibliothek"))
|
||||
add_child(_muted_label("Klick = auswählen, Doppelklick = Werkzeug starten. Ohne GLB wird der Platzhalter gesetzt und die Kachel markiert (Rechte Maustaste = Flug)."))
|
||||
|
||||
# Suche (t_59fc0f21): tippen filtert live nach Name/ID — Gruppen mit
|
||||
# Treffern klappen auf, leere Gruppen verschwinden. Größe ist von
|
||||
|
|
@ -102,7 +101,7 @@ func build() -> void:
|
|||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.custom_minimum_size = UIScale.v2(0, 280)
|
||||
scroll.custom_minimum_size = UIScale.v2(0, 320)
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
add_child(scroll)
|
||||
library_box = VBoxContainer.new()
|
||||
|
|
@ -270,7 +269,7 @@ func _on_panel_resized() -> void:
|
|||
|
||||
|
||||
func _apply_grid_columns(width: float) -> void:
|
||||
var cols := clampi(int(width / float(UIScale.px(180))), 1, 12)
|
||||
var cols := clampi(int(width / float(UIScale.px(200))), 1, 12)
|
||||
for row in _library_group_rows:
|
||||
((row["grid"] as GridContainer)).columns = cols
|
||||
if _recent_grid != null:
|
||||
|
|
@ -471,7 +470,7 @@ func _build_tile(oid: String) -> Button:
|
|||
btn.toggle_mode = true
|
||||
btn.button_pressed = false
|
||||
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
btn.custom_minimum_size = UIScale.v2(0, 126)
|
||||
btn.custom_minimum_size = UIScale.v2(0, 188)
|
||||
btn.tooltip_text = "%s · %d Polys · %s" % [name_txt, poly, oid]
|
||||
if missing:
|
||||
btn.tooltip_text += " · MODELL FEHLT (Platzhalter-Kiste)"
|
||||
|
|
@ -523,7 +522,6 @@ func _build_tile(oid: String) -> Button:
|
|||
tile.add_child(id_lbl)
|
||||
var lbl := Label.new()
|
||||
lbl.text = name_txt
|
||||
lbl.add_theme_font_size_override("font_size", UIScale.small_font_size())
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
lbl.clip_text = true
|
||||
lbl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
|
|
|
|||
63
src/ui/panels/bauen_panel.gd
Normal file
63
src/ui/panels/bauen_panel.gd
Normal 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)
|
||||
|
|
@ -33,7 +33,6 @@ func build() -> void:
|
|||
spawn_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
spawn_status.custom_minimum_size = UIScale.v2(0, 44)
|
||||
add_child(spawn_status)
|
||||
add_child(_muted_label("Der Check sagt VOR dem Export, was der Server-Loader verlangen wird (mindestens 4 Zonen pro Team, bekannte Objekt-IDs)."))
|
||||
|
||||
# --- Spawn-Zonen-Werkzeug: großer Button + Bedien-Panel ---
|
||||
spawn_edit_btn = Button.new()
|
||||
|
|
@ -82,9 +81,6 @@ func build() -> void:
|
|||
spawn_delete_btn.pressed.connect(_on_spawn_delete_pressed)
|
||||
spawn_panel.add_child(spawn_delete_btn)
|
||||
|
||||
spawn_panel.add_child(_muted_label(
|
||||
"Klick setzt eine Zone, Klick+Ziehen verschiebt sie. Jede Zone ist "
|
||||
+ "EIN Rückgängig-Schritt (Strg+Z). ESC beendet das Spawn-Werkzeug."))
|
||||
|
||||
|
||||
func _spawn_team_button(text: String, group: ButtonGroup, team: int) -> Button:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ func panel_title() -> String:
|
|||
## sich vom angeschlossenen Theme-Baum (main hängt theme auf das Fenster).
|
||||
|
||||
func theme_color(cname: String) -> Color:
|
||||
# Befund 25.10. („Texte kaum lesbar“): Panels werden gebaut, BEVOR sie im
|
||||
# Baum hängen — get_theme_color liefert dort Schwarz statt der Theme-Farbe.
|
||||
# Darum direkt aus dem Theme des Hauptfensters lesen.
|
||||
if main != null and main.theme != null and main.theme.has_color(cname, "Editor"):
|
||||
return main.theme.get_color(cname, "Editor")
|
||||
return get_theme_color(cname, "Editor")
|
||||
|
||||
|
||||
|
|
|
|||
171
src/ui/panels/malen_panel.gd
Normal file
171
src/ui/panels/malen_panel.gd
Normal 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)
|
||||
|
|
@ -86,8 +86,6 @@ func build() -> void:
|
|||
prop_water.tooltip_text = "Wasser: ganz links = kein Wasser, sonst Höhe der Wasseroberfläche über dem Terrain. Senken unter dieser Höhe füllen sich mit Wasser."
|
||||
prop_water.set_meta("kein_wasser_value", KEIN_WASSER_WERT)
|
||||
add_child(prop_water)
|
||||
var wasser_hinweis := _muted_label("Senken unter der Wasserhöhe füllen sich automatisch. Wasser betreten = knietief, träge (kein Schwimmen).")
|
||||
add_child(wasser_hinweis)
|
||||
|
||||
add_child(_separator())
|
||||
add_child(_header_label("Terrain"))
|
||||
|
|
|
|||
|
|
@ -31,4 +31,3 @@ func build() -> void:
|
|||
grid_check.tooltip_text = "Neue und verschobene Objekte rasten auf ein 1-Meter-Raster ein"
|
||||
grid_check.toggled.connect(func(pressed: bool) -> void: main.call("_on_grid_snap_changed", pressed))
|
||||
add_child(grid_check)
|
||||
add_child(_muted_label("Raster-Snap rastet Platzieren und Ziehen auf ganze Meter."))
|
||||
|
|
@ -34,6 +34,11 @@ const PANELS: Array = [
|
|||
"skript": "res://src/ui/panels/map_panel.gd"},
|
||||
{"id": "terrain", "titel": "Terrain", "gruppe": "rechts", "order": 40,
|
||||
"skript": "res://src/ui/panels/terrain_panel.gd"},
|
||||
# 25.10. UI-Aufräumen: Malen und Bauen hängen nicht mehr im Terrain-Tab.
|
||||
{"id": "malen", "titel": "Malen", "gruppe": "rechts", "order": 45,
|
||||
"skript": "res://src/ui/panels/malen_panel.gd"},
|
||||
{"id": "bauen", "titel": "Bauen", "gruppe": "rechts", "order": 47,
|
||||
"skript": "res://src/ui/panels/bauen_panel.gd"},
|
||||
{"id": "check", "titel": "Prüfen", "gruppe": "rechts", "order": 50,
|
||||
"skript": "res://src/ui/panels/check_panel.gd"},
|
||||
{"id": "teststart", "titel": "Test-Start", "gruppe": "rechts", "order": 60,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ func build() -> void:
|
|||
_confirm_delete.cancel_button_text = "Abbrechen"
|
||||
_confirm_delete.confirmed.connect(_on_delete_confirmed)
|
||||
add_child(_confirm_delete)
|
||||
add_child(_muted_label("Rechtsklick: Gruppieren / Auflösen / Umbenennen / Löschen. Objektzeile in eine Gruppe ziehen = aufnehmen, auf „Objekte“ zurückziehen = herauslösen."))
|
||||
add_child(_separator())
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ func build() -> void:
|
|||
sel_ungroup_btn.tooltip_text = "Die Gruppe der Auswahl wieder auflösen (Strg+Umschalt+G) — Objekte bleiben, nur die Bindung löst sich"
|
||||
sel_ungroup_btn.pressed.connect(func() -> void: main.interaction.ungroup_selection())
|
||||
group_btns.add_child(sel_ungroup_btn)
|
||||
add_child(_muted_label("Mehrfachauswahl: Umschalt+Klick oder Rahmen aufziehen. Eine Gruppe wandert und dreht gemeinsam — auch nach Speichern/Laden."))
|
||||
|
||||
# --- Gizmo-Einstellungen (Karte t_e0120dc2): Schrittweite der Achsen-
|
||||
# Züge. Das Mausrad WÄHREND eines Gizmo-Zugs blättert durch die
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ func build() -> void:
|
|||
add_child(perspektive_box)
|
||||
|
||||
add_child(_separator())
|
||||
add_child(_muted_label("Gilt im Lauf-Modus („▶ Karte testen“). Standard 1,0 — jede PC-/Maus-Kombination ist anders. Wird automatisch gespeichert."))
|
||||
|
||||
|
||||
func _on_sens_changed(value: float) -> void:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ func build() -> void:
|
|||
tool_label = Label.new()
|
||||
# U7: Werkzeug-Farbe über Theme (Editor/colors/accent).
|
||||
tool_label.add_theme_color_override("font_color",
|
||||
get_theme_color("accent", "Editor"))
|
||||
theme_color("accent"))
|
||||
# Karte t_297221b5: Statuszeilen-Labels egalisiert (min-size-Druck bei 1.5×)
|
||||
tool_label.clip_text = true
|
||||
tool_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
|
|
@ -37,7 +37,7 @@ func build() -> void:
|
|||
sym_label = Label.new()
|
||||
# U7: Symmetrie-Farbe über Theme (Editor/colors/accent).
|
||||
sym_label.add_theme_color_override("font_color",
|
||||
get_theme_color("accent", "Editor"))
|
||||
theme_color("accent"))
|
||||
sym_label.clip_text = true
|
||||
sym_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
add_child(sym_label)
|
||||
|
|
@ -45,7 +45,7 @@ func build() -> void:
|
|||
stats_objects = Label.new()
|
||||
# U7: Nebentext-Farbe über Theme (Editor/colors/field).
|
||||
stats_objects.add_theme_color_override("font_color",
|
||||
get_theme_color("field", "Editor"))
|
||||
theme_color("field"))
|
||||
stats_objects.clip_text = true
|
||||
stats_objects.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
add_child(stats_objects)
|
||||
|
|
@ -54,7 +54,7 @@ func build() -> void:
|
|||
stats_terrain = Label.new()
|
||||
# U7: Nebentext-Farbe über Theme (Editor/colors/field).
|
||||
stats_terrain.add_theme_color_override("font_color",
|
||||
get_theme_color("field", "Editor"))
|
||||
theme_color("field"))
|
||||
stats_terrain.clip_text = true
|
||||
stats_terrain.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
add_child(stats_terrain)
|
||||
|
|
|
|||
|
|
@ -38,11 +38,7 @@ func panel_title() -> String:
|
|||
|
||||
func build() -> void:
|
||||
add_child(_header_label("Symmetrie"))
|
||||
add_child(_muted_label(
|
||||
"Macht die GANZE Karte mit einem Klick symmetrisch: Objekte, "
|
||||
+ "Bau-Teile, Spawn-Punkte und Boden (Höhen + Material) werden "
|
||||
+ "von der Quell-Hälfte auf die andere Seite ERGÄNZT. Es wird "
|
||||
+ "nichts gelöscht — Widersprüche werden vorher gemeldet."))
|
||||
add_child(_muted_label("Macht die ganze Karte symmetrisch: ergänzt die andere Seite, löscht nichts."))
|
||||
|
||||
mode_label = _field_label(_mode_text())
|
||||
add_child(mode_label)
|
||||
|
|
@ -75,10 +71,6 @@ func build() -> void:
|
|||
report_label.custom_minimum_size = UIScale.v2(0, 96)
|
||||
add_child(report_label)
|
||||
|
||||
add_child(_muted_label(
|
||||
"Der Modus steht oben in der Leiste (Punkt/Achse X/Achse Z). "
|
||||
+ "Paare, die schon passen, werden mit Spiegel-Kennungen "
|
||||
+ "verkoppelt — auch bei alten Karten ohne diese Kennungen."))
|
||||
|
||||
conflict_dialog = ConfirmationDialog.new()
|
||||
conflict_dialog.title = "Widersprüche — welche Seite gewinnt?"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ var terrain_mode_raise: Button
|
|||
var terrain_mode_lower: Button
|
||||
var terrain_mode_smooth: 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_mat_buttons: Array = [] # Gras/Sand/Fels (ButtonGroup)
|
||||
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_slider: HSlider
|
||||
|
||||
# --- Bauen (Karte t_97ea59c5) — im selben Werkzeug-Tab (aus main.gd ausgezogen)
|
||||
var build_edit_btn: Button
|
||||
var build_panel: VBoxContainer
|
||||
var build_kind_buttons: Dictionary = {} # kind_id → Button
|
||||
|
||||
|
||||
static func spec() -> Dictionary:
|
||||
return {"titel": "Terrain", "gruppe": "rechts", "order": 40}
|
||||
|
|
@ -39,7 +38,7 @@ func build() -> void:
|
|||
terrain_edit_btn.text = "⛰ Terrain formen"
|
||||
terrain_edit_btn.custom_minimum_size = UIScale.v2(0, 48)
|
||||
terrain_edit_btn.toggle_mode = true
|
||||
terrain_edit_btn.tooltip_text = "Boden heben, senken und glätten — direkt in der Karte malen"
|
||||
terrain_edit_btn.tooltip_text = "Boden formen: heben, senken, glätten, einebnen, Rampe (F1)"
|
||||
terrain_edit_btn.pressed.connect(_on_terrain_btn_pressed)
|
||||
add_child(terrain_edit_btn)
|
||||
|
||||
|
|
@ -48,9 +47,11 @@ func build() -> void:
|
|||
terrain_panel.visible = false
|
||||
add_child(terrain_panel)
|
||||
|
||||
terrain_panel.add_child(_field_label("Pinsel-Werkzeug:"))
|
||||
var mode_row := HBoxContainer.new()
|
||||
mode_row.add_theme_constant_override("separation", UIScale.px(8)) # U7-Raster
|
||||
terrain_panel.add_child(_field_label("Pinsel:"))
|
||||
var mode_row := GridContainer.new()
|
||||
mode_row.columns = 3
|
||||
mode_row.add_theme_constant_override("h_separation", UIScale.px(8)) # U7-Raster
|
||||
mode_row.add_theme_constant_override("v_separation", UIScale.px(8))
|
||||
terrain_panel.add_child(mode_row)
|
||||
var mode_group := ButtonGroup.new()
|
||||
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)")
|
||||
terrain_mode_paint = _terrain_mode_button("Malen 🖌", mode_group,
|
||||
"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_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_lower.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_lower)
|
||||
mode_row.add_child(terrain_mode_smooth)
|
||||
mode_row.add_child(terrain_mode_paint)
|
||||
|
||||
# Material-Palette (Karte t_603ec1a8): nur sichtbar im Malen-Modus.
|
||||
terrain_palette = VBoxContainer.new()
|
||||
terrain_palette.add_theme_constant_override("separation", UIScale.px(4))
|
||||
terrain_palette.visible = false
|
||||
terrain_panel.add_child(terrain_palette)
|
||||
terrain_palette.add_child(_field_label("Material:"))
|
||||
var mat_row := HBoxContainer.new()
|
||||
mat_row.add_theme_constant_override("separation", UIScale.px(8)) # U7-Raster
|
||||
terrain_palette.add_child(mat_row)
|
||||
var mat_group := ButtonGroup.new()
|
||||
# Karte t_ab408127: 20 Slots in scrollbarer Raster-Palette (Spec: „like
|
||||
# Bibliothek") — 4 Spalten, ~2 sichtbare Reihen, scrollbar nach unten.
|
||||
# Thumbnails folgen, sobald die Texturen in assets/textures/terrain/ liegen
|
||||
# (gen_terrain_thumbs) — bis dahin Button-Farbfeld via icon (Default).
|
||||
var mat_grid := GridContainer.new()
|
||||
mat_grid.columns = 4
|
||||
mat_grid.add_theme_constant_override("h_separation", UIScale.px(4))
|
||||
mat_grid.add_theme_constant_override("v_separation", UIScale.px(4))
|
||||
var mat_scroll := ScrollContainer.new()
|
||||
mat_scroll.custom_minimum_size = UIScale.v2(0, 176) # ~3 Reihen a 48 px + Separation
|
||||
mat_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
mat_scroll.add_child(mat_grid)
|
||||
terrain_palette.add_child(mat_scroll)
|
||||
var mat_labels := [
|
||||
"🏜 Steppe", "🏖 Sand", "🪨 Fels", "🌱 Sattgras",
|
||||
"🍂 Vertrocknet", "🍀 Moos", "🟫 Erde", "🛤 Pfad",
|
||||
"⛏ Umgegraben", "🧱 Schotter", "🟤 Lehm", "🏜 Sand hell",
|
||||
"🏜 Sand rost", "🗻 Basalt", "⛰ Geröll", "🌧 Schlamm",
|
||||
"❄️ Schnee", "🥶 Matschschnee", "🌑 Asche", "🌾 Stoppeln",
|
||||
]
|
||||
for mat_id in SvmapIO.MATERIAL_IDS.size():
|
||||
var mat_btn := Button.new()
|
||||
mat_btn.text = str(mat_labels[mat_id]) if mat_id < mat_labels.size() else str(SvmapIO.MATERIAL_IDS[mat_id])
|
||||
mat_btn.custom_minimum_size = UIScale.v2(0, 48)
|
||||
mat_btn.toggle_mode = true
|
||||
mat_btn.button_group = mat_group
|
||||
mat_btn.tooltip_text = "Malen: %s (Slot %d)" % [str(SvmapIO.MATERIAL_IDS[mat_id]), mat_id]
|
||||
if mat_id == 0:
|
||||
mat_btn.button_pressed = true
|
||||
mat_btn.toggled.connect(_on_terrain_material_changed.bind(mat_id))
|
||||
terrain_mat_buttons.append(mat_btn)
|
||||
mat_grid.add_child(mat_btn)
|
||||
mode_row.add_child(terrain_mode_flatten)
|
||||
mode_row.add_child(terrain_mode_ramp)
|
||||
# Malen hat seit 25.10. einen eigenen Tab (malen_panel.gd); der Modus-Knopf
|
||||
# bleibt unsichtbar in der ButtonGroup (Taste 4, Malen-Tab-Knopf).
|
||||
terrain_mode_paint.visible = false
|
||||
terrain_panel.add_child(terrain_mode_paint)
|
||||
|
||||
# Pinselgrößen-Presets (Karte t_603ec1a8): drei große Buttons + freier Slider.
|
||||
terrain_panel.add_child(_field_label("Pinselgröße:"))
|
||||
|
|
@ -153,89 +122,28 @@ func build() -> void:
|
|||
terrain_strength_slider.value_changed.connect(_on_terrain_strength_changed)
|
||||
terrain_panel.add_child(terrain_strength_slider)
|
||||
|
||||
# Malen kennt keine Stärke: das Material wird zellenweise gesetzt
|
||||
# (Material-Schicht = ein Wert pro Rasterpunkt, weiche Übergänge macht
|
||||
# der Splat-Renderer). Statt eines wirkungslosen Reglers steht im
|
||||
# Malen-Modus die echte Feinheit da (Sly-Befund 22.10.: „Stärke ändert nichts“).
|
||||
terrain_paint_hint = _muted_label("")
|
||||
terrain_paint_hint.visible = false
|
||||
terrain_panel.add_child(terrain_paint_hint)
|
||||
# Pinsel-Kante (Falloff, Karte 25.10.): ein Regler hart ↔ weich für alle
|
||||
# Höhen-Pinsel (Malen hat seinen eigenen Kanten-Regler weiter unten).
|
||||
terrain_falloff_label = _field_label("Kante: 50 % weich")
|
||||
terrain_panel.add_child(terrain_falloff_label)
|
||||
terrain_falloff_slider = HSlider.new()
|
||||
terrain_falloff_slider.min_value = 0.0
|
||||
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:
|
||||
if terrain_edit_btn.button_pressed:
|
||||
if terrain_mode_paint.button_pressed:
|
||||
terrain_mode_raise.button_pressed = true # Malen hat seinen eigenen Tab
|
||||
main.call("_set_tool_mode", "terrain")
|
||||
refresh_panels()
|
||||
else:
|
||||
main.call("_set_tool_mode", "objects")
|
||||
|
||||
|
|
@ -257,6 +165,10 @@ func _selected_terrain_mode() -> int:
|
|||
return TerrainField.Mode.SMOOTH
|
||||
if terrain_mode_paint.button_pressed:
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -265,19 +177,36 @@ func _on_terrain_mode_changed(_pressed: bool) -> void:
|
|||
var paint := _selected_terrain_mode() == TerrainField.Mode.PAINT
|
||||
if terrain_palette != null:
|
||||
terrain_palette.visible = paint
|
||||
if terrain_strength_slider != null:
|
||||
terrain_strength_slider.visible = not paint
|
||||
terrain_strength_label.visible = not paint
|
||||
if terrain_weich_slider != null:
|
||||
terrain_weich_slider.visible = paint
|
||||
terrain_weich_label.visible = paint
|
||||
if terrain_paint_hint != null:
|
||||
terrain_paint_hint.visible = paint
|
||||
terrain_paint_hint.text = paint_hint_text(main.doc.get("terrain", {}) if main.doc is Dictionary else {})
|
||||
refresh_panels()
|
||||
if main.terrain_tool != null:
|
||||
main.terrain_tool.call("set_brush_mode", _selected_terrain_mode())
|
||||
|
||||
|
||||
## Welche Bedienflächen sind sichtbar (Terrain-Tab bzw. Malen-Tab), welcher
|
||||
## Start-Knopf steht gedrückt — Terrain und Malen teilen das Pinsel-Werkzeug.
|
||||
func refresh_panels() -> void:
|
||||
if terrain_panel == null:
|
||||
return
|
||||
var aktiv: bool = str(main.get("tool_mode")) == "terrain"
|
||||
var paint := _selected_terrain_mode() == TerrainField.Mode.PAINT
|
||||
terrain_panel.visible = aktiv and not paint
|
||||
terrain_edit_btn.set_pressed_no_signal(aktiv and not paint)
|
||||
var malen = main.get("malen_mod")
|
||||
if malen != null:
|
||||
malen.malen_panel.visible = aktiv and paint
|
||||
malen.malen_edit_btn.set_pressed_no_signal(aktiv and paint)
|
||||
if aktiv:
|
||||
_show_own_tab(malen if paint else self)
|
||||
|
||||
|
||||
func _show_own_tab(tab: Control) -> void:
|
||||
var tabs = main.get("ui_right_panel")
|
||||
if tabs is TabContainer and tab != null and tab.get_parent() == tabs:
|
||||
(tabs as TabContainer).current_tab = tab.get_index()
|
||||
|
||||
|
||||
## Hinweistext Malen-Modus (pure, testbar): feinste Mal-Einheit = Abstand
|
||||
## zweier Rasterpunkte der Karte (size / (resolution − 1)).
|
||||
static func paint_hint_text(terrain: Dictionary) -> String:
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
## Material-Palette (Karte t_603ec1a8): gewähltes Material an den Pinsel.
|
||||
func _on_terrain_material_changed(pressed: bool, mat_id: int) -> void:
|
||||
if pressed and main.terrain_tool != null:
|
||||
main.terrain_tool.call("set_paint_material", mat_id)
|
||||
|
||||
|
||||
## Pinselgrößen-Presets (Karte t_603ec1a8): setzen den Radius-Slider
|
||||
## (der Label-Text + Ring-Optik hängen am value_changed-Signal).
|
||||
func _terrain_size_button(text: String, group: ButtonGroup, radius_m: float) -> Button:
|
||||
|
|
@ -319,14 +242,17 @@ func _on_terrain_size_preset(pressed: bool, radius_m: float) -> void:
|
|||
|
||||
func _on_terrain_radius_changed(value: float) -> void:
|
||||
terrain_radius_label.text = "Pinsel-Größe: %d m" % int(value)
|
||||
var malen = main.get("malen_mod")
|
||||
if malen != null:
|
||||
malen.set_radius_silent(value)
|
||||
if main.terrain_tool != null:
|
||||
main.terrain_tool.call("set_radius", value)
|
||||
|
||||
|
||||
func _on_terrain_weich_changed(value: float) -> void:
|
||||
terrain_weich_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
|
||||
func _on_terrain_falloff_changed(value: float) -> void:
|
||||
terrain_falloff_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
|
||||
if main.terrain_tool != null:
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func build() -> void:
|
|||
add_child(_separator())
|
||||
info_hint = Label.new()
|
||||
info_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
info_hint.text = "In der Leiste „🎯 Test-Start“ drücken (oder Taste T), dann in die Karte klicken. Die Höhe kommt automatisch vom Terrain. Der Punkt liegt in einer Nebendatei zur .svmap — die Kartendatei selbst bleibt unverändert."
|
||||
info_hint.text = "Leiste „🎯 Test-Start“ oder Taste T, dann in die Karte klicken."
|
||||
add_child(info_hint)
|
||||
|
||||
# main holt sich das Modul hier ab (main._refresh_test_start_panel) — die
|
||||
|
|
|
|||
|
|
@ -163,6 +163,14 @@ func build() -> void:
|
|||
size_popup.index_pressed.connect(func(index: int) -> void: main.call("_on_ui_size_selected", index))
|
||||
add_child(size_menu)
|
||||
|
||||
var help_btn := Button.new()
|
||||
help_btn.text = "❓ Hilfe"
|
||||
help_btn.name = "hilfe"
|
||||
help_btn.custom_minimum_size = UIScale.v2(110, 40)
|
||||
help_btn.tooltip_text = "Werkzeuge, Steuerung und Tastenkürzel (H)"
|
||||
help_btn.pressed.connect(func() -> void: main.call("_toggle_help_overlay"))
|
||||
add_child(help_btn)
|
||||
|
||||
title_label = Label.new()
|
||||
title_label.text = " %s" % main.APP_TITLE
|
||||
title_label.add_theme_font_size_override("font_size", UIScale.heading_font_size())
|
||||
|
|
@ -185,7 +193,7 @@ func build() -> void:
|
|||
file_label.text = ""
|
||||
# U7: Farbe über Theme (Editor/colors/title) statt modulate-Wert im Code.
|
||||
file_label.add_theme_color_override("font_color",
|
||||
get_theme_color("title", "Editor"))
|
||||
main.theme.get_color("title", "Editor"))
|
||||
# Karte t_297221b5: Map-Pfad darf bei großer Stufe egalisiert werden
|
||||
# (Voller Name im Tooltip, sonst drückt die Zeile die Top-Bar aus dem Fenster).
|
||||
# Expand: bekommt zusammen mit dem Spacer seinen Anteil, egalisiert mit "…".
|
||||
|
|
|
|||
78
src/ui/viewport_toolbar.gd
Normal file
78
src/ui/viewport_toolbar.gd
Normal 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)
|
||||
|
|
@ -70,12 +70,29 @@ var _drag_before: Dictionary = {} # History-Snapshot VOR dem Zug
|
|||
|
||||
var _arrows := {} # "x"/"y"/"z" → Node3D (Pfeil-Root)
|
||||
|
||||
# --- Dreh-/Kipp-Modus (25.10.): Ringe um alle drei Achsen -------------------
|
||||
const RING_R := 1.5 # Ring-Radius in Metern
|
||||
const RING_SEGMENTS := 48
|
||||
const ROT_STEP_DEG := 15.0
|
||||
var mode := "move" # "move" = Pfeile, "rotate" = Ringe
|
||||
var rot_step_deg := ROT_STEP_DEG
|
||||
var _rings := {} # "x"/"y"/"z" → MeshInstance3D
|
||||
var _rot_dragging := false
|
||||
var _rot_axis := ""
|
||||
var _rot_center := Vector2.ZERO
|
||||
var _rot_start_angle := 0.0
|
||||
var _rot_applied := 0.0 # bisher angewendete (gerastete) Grad
|
||||
var _rot_suffix := "" # merge-Kennung: EIN Undo-Schritt pro Zug
|
||||
var _rot_serial := 0
|
||||
|
||||
|
||||
func setup(p_main, p_camera: Camera3D, p_builder) -> void:
|
||||
main = p_main
|
||||
camera = p_camera
|
||||
view_builder = p_builder
|
||||
_build_arrows()
|
||||
_build_rings()
|
||||
_apply_mode_visibility()
|
||||
visible = false
|
||||
|
||||
|
||||
|
|
@ -119,6 +136,53 @@ func _build_arrows() -> void:
|
|||
_arrows[a] = root
|
||||
|
||||
|
||||
## Drehringe bauen: je Achse ein dünner Torus, ungeschattet, ohne Tiefentest.
|
||||
func _build_rings() -> void:
|
||||
for a in AXES:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = AXIS_COLORS[a]
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.no_depth_test = true
|
||||
mat.render_priority = 20
|
||||
var ring := MeshInstance3D.new()
|
||||
var torus := TorusMesh.new()
|
||||
torus.inner_radius = RING_R - 0.06
|
||||
torus.outer_radius = RING_R + 0.06
|
||||
torus.rings = 48
|
||||
torus.ring_segments = 8
|
||||
ring.mesh = torus
|
||||
ring.material_override = mat
|
||||
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
# Torus liegt lokal in der XZ-Ebene (Achse = Y).
|
||||
if a == "x":
|
||||
ring.rotation.z = PI / 2.0
|
||||
elif a == "z":
|
||||
ring.rotation.x = PI / 2.0
|
||||
ring.visible = false
|
||||
add_child(ring)
|
||||
_rings[a] = ring
|
||||
|
||||
|
||||
## Modus wechseln ("move" = Verschieben, "rotate" = Drehen/Kippen).
|
||||
func set_mode(m: String) -> void:
|
||||
if m != "move" and m != "rotate":
|
||||
return
|
||||
if _dragging:
|
||||
_finish_drag(false)
|
||||
if _rot_dragging:
|
||||
_finish_rotate(false)
|
||||
mode = m
|
||||
_apply_mode_visibility()
|
||||
|
||||
|
||||
func _apply_mode_visibility() -> void:
|
||||
for a in _arrows:
|
||||
(_arrows[a] as Node3D).visible = mode == "move"
|
||||
for a in _rings:
|
||||
(_rings[a] as Node3D).visible = mode == "rotate"
|
||||
|
||||
|
||||
func _axis_vec(a: String) -> Vector3:
|
||||
return AXES.get(a, Vector3.ZERO) as Vector3
|
||||
|
||||
|
|
@ -169,26 +233,43 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
var inter = main.get("interaction")
|
||||
if inter == null or bool(inter.get("placing")):
|
||||
return
|
||||
if mode == "rotate":
|
||||
var ring_axis := pick_ring(mb.position)
|
||||
if ring_axis != "":
|
||||
_begin_rotate(ring_axis, mb.position)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var axis := pick(mb.position)
|
||||
if axis != "":
|
||||
_begin_drag(axis, mb.position)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif mb.button_index == MOUSE_BUTTON_LEFT and not mb.pressed:
|
||||
if _dragging:
|
||||
if _rot_dragging:
|
||||
_finish_rotate(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _dragging:
|
||||
_finish_drag(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif (mb.button_index == MOUSE_BUTTON_WHEEL_UP \
|
||||
or mb.button_index == MOUSE_BUTTON_WHEEL_DOWN) and mb.pressed:
|
||||
if _dragging:
|
||||
if _rot_dragging:
|
||||
get_viewport().set_input_as_handled() # Mausrad zoomt nicht mitten im Dreh-Zug
|
||||
elif _dragging:
|
||||
var dir := 1 if mb.button_index == MOUSE_BUTTON_WHEEL_UP else -1
|
||||
cycle_step(dir)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseMotion and _rot_dragging:
|
||||
_rotate_motion((event as InputEventMouseMotion).position)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventMouseMotion and _dragging:
|
||||
_drag_motion((event as InputEventMouseMotion).position)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event is InputEventKey and (event as InputEventKey).pressed \
|
||||
and (event as InputEventKey).keycode == KEY_ESCAPE:
|
||||
if _dragging:
|
||||
if _rot_dragging:
|
||||
_finish_rotate(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif _dragging:
|
||||
_finish_drag(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
|
@ -215,6 +296,35 @@ func pick(screen_pos: Vector2) -> String:
|
|||
return best
|
||||
|
||||
|
||||
## Ring unter dem Cursor (Abstand zum projizierten Kreis) — "" = keiner.
|
||||
func pick_ring(screen_pos: Vector2) -> String:
|
||||
if not visible or mode != "rotate":
|
||||
return ""
|
||||
var best := ""
|
||||
var best_d := HIT_PX
|
||||
for a in AXES:
|
||||
var pts := _ring_screen_points(a)
|
||||
for i in pts.size() - 1:
|
||||
if pts[i] == Vector2.INF or pts[i + 1] == Vector2.INF:
|
||||
continue
|
||||
var d := _dist_point_segment(screen_pos, pts[i], pts[i + 1])
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = a
|
||||
return best
|
||||
|
||||
|
||||
func _ring_screen_points(a: String) -> Array:
|
||||
var axis := _axis_vec(a)
|
||||
var u := axis.cross(Vector3.UP if absf(axis.dot(Vector3.UP)) < 0.9 else Vector3.RIGHT).normalized()
|
||||
var v := axis.cross(u).normalized()
|
||||
var out: Array = []
|
||||
for i in RING_SEGMENTS + 1:
|
||||
var t := TAU * float(i) / float(RING_SEGMENTS)
|
||||
out.append(_safe_unproject(global_position + (u * cos(t) + v * sin(t)) * RING_R))
|
||||
return out
|
||||
|
||||
|
||||
func _safe_unproject(p: Vector3) -> Vector2:
|
||||
if camera == null:
|
||||
return Vector2.INF
|
||||
|
|
@ -372,12 +482,90 @@ func _finish_drag(commit: bool) -> void:
|
|||
## Für die Esc-Kette in main.gd: Zug abbrechen, falls einer läuft. true =
|
||||
## es lief einer und wurde abgebrochen (idempotent, weil _dragging prüft).
|
||||
func cancel_drag_if_active() -> bool:
|
||||
if _rot_dragging:
|
||||
_finish_rotate(false)
|
||||
return true
|
||||
if not _dragging:
|
||||
return false
|
||||
_finish_drag(false)
|
||||
return true
|
||||
|
||||
|
||||
# --------------------------------------------------- Drehen / Kippen -------
|
||||
|
||||
func _ring_angle(mouse: Vector2) -> float:
|
||||
var d := mouse - _rot_center
|
||||
return atan2(d.y, d.x)
|
||||
|
||||
|
||||
func _begin_rotate(axis: String, mouse: Vector2) -> void:
|
||||
var inter = main.get("interaction")
|
||||
if inter == null or inter.selected_indices.is_empty():
|
||||
return
|
||||
var c := _safe_unproject(global_position)
|
||||
if c == Vector2.INF:
|
||||
return
|
||||
_rot_axis = axis
|
||||
_rot_center = c
|
||||
_rot_start_angle = _ring_angle(mouse)
|
||||
_rot_applied = 0.0
|
||||
_rot_serial += 1
|
||||
_rot_suffix = "#g%d" % _rot_serial
|
||||
_rot_dragging = true
|
||||
var name := "Drehen (Y)" if axis == "y" else ("Kippen um X" if axis == "x" else "Kippen um Z")
|
||||
main.call("_set_status", "%s — Ring ziehen, rastet in %.0f°-Schritten; Esc bricht ab, Strg+Z nimmt den Zug zurück." % [name, rot_step_deg])
|
||||
|
||||
|
||||
func _rotate_motion(mouse: Vector2) -> void:
|
||||
# Bildschirmwinkel → Drehwinkel um die Achse (Vorzeichen je nach Blickrichtung).
|
||||
var delta_screen := wrapf(_ring_angle(mouse) - _rot_start_angle, -PI, PI)
|
||||
var facing := _axis_vec(_rot_axis).dot(camera.global_transform.basis.z)
|
||||
var deg := -rad_to_deg(delta_screen) * (1.0 if facing >= 0.0 else -1.0)
|
||||
var snapped_deg := snappedf(deg, rot_step_deg)
|
||||
_rotate_to(snapped_deg)
|
||||
|
||||
|
||||
## Auf einen Gesamtwinkel (gerastet) nachführen — in Schritten, EIN Undo-Eintrag.
|
||||
func _rotate_to(total_deg: float) -> void:
|
||||
var diff := total_deg - _rot_applied
|
||||
if absf(diff) < 0.001:
|
||||
return
|
||||
var inter = main.get("interaction")
|
||||
if inter == null:
|
||||
return
|
||||
if _rot_axis == "y":
|
||||
inter.rotate_selected(diff, _rot_suffix)
|
||||
else:
|
||||
inter.tilt_selected(_rot_axis == "x", diff, _rot_suffix)
|
||||
_rot_applied = total_deg
|
||||
|
||||
|
||||
func _finish_rotate(commit: bool) -> void:
|
||||
if not _rot_dragging:
|
||||
return
|
||||
_rot_dragging = false
|
||||
if not commit and absf(_rot_applied) > 0.001:
|
||||
main.call("_do_undo") # der ganze Zug ist EIN Eintrag → komplett zurück
|
||||
main.call("_set_status", "Dreh-Zug abgebrochen — Objekt wie vorher.")
|
||||
_rot_axis = ""
|
||||
_rot_applied = 0.0
|
||||
|
||||
|
||||
## Probes/Tests: derselbe Pfad wie ein echter Ring-Zug (ohne Maus-Mathe).
|
||||
func demo_rotate_axis(axis: String, deg: float) -> bool:
|
||||
var inter = main.get("interaction") if main != null else null
|
||||
if inter == null or inter.selected_indices.is_empty() or absf(deg) < 0.001:
|
||||
return false
|
||||
_rot_axis = axis
|
||||
_rot_serial += 1
|
||||
_rot_suffix = "#g%d" % _rot_serial
|
||||
_rot_applied = 0.0
|
||||
_rotate_to(snappedf(deg, rot_step_deg))
|
||||
_rot_axis = ""
|
||||
_rot_applied = 0.0
|
||||
return true
|
||||
|
||||
|
||||
# ------------------------------------------------------ Schrittweite -------
|
||||
|
||||
## Mausrad WÄHREND des Ziehens: durch die Voreinstellungen blättern
|
||||
|
|
|
|||
1
tests/probe_mal_weich_render.gd.uid
Normal file
1
tests/probe_mal_weich_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://w5p21ia5bryo
|
||||
1
tests/probe_steppe_tiling.gd.uid
Normal file
1
tests/probe_steppe_tiling.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://badptran2lnax
|
||||
|
|
@ -141,8 +141,8 @@ func _test_tiles() -> void:
|
|||
_check(tr.custom_minimum_size.x >= 64, "Thumbnail-Zielgröße ≥ 64 px (ist %d)"
|
||||
% int(tr.custom_minimum_size.x))
|
||||
_check(tr.size.x >= 64, "Thumbnail rendert ≥ 64 px breit (ist %.0f)" % tr.size.x)
|
||||
_check(tr.texture != null and tr.texture.get_width() == 96,
|
||||
"Textur auf 96 px skaliert")
|
||||
_check(tr.texture != null and tr.texture.get_width() == 128,
|
||||
"Textur auf 128 px skaliert")
|
||||
if lbl != null:
|
||||
_check(lbl.text == "Wachturm", "deutscher Name unter dem Bild (%s)" % lbl.text)
|
||||
_check(tile_btn.size.y >= 100, "Kachel hoch genug (%.0f px)" % tile_btn.size.y)
|
||||
|
|
|
|||
160
tests/test_pinsel_werkzeuge.gd
Normal file
160
tests/test_pinsel_werkzeuge.gd
Normal 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")
|
||||
1
tests/test_pinsel_werkzeuge.gd.uid
Normal file
1
tests/test_pinsel_werkzeuge.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d3lv8invgk6kv
|
||||
|
|
@ -180,6 +180,8 @@ func _test_inputmap_actions() -> void:
|
|||
"editor_brush_lower": KEY_2,
|
||||
"editor_brush_smooth": KEY_3,
|
||||
"editor_brush_paint": KEY_4,
|
||||
"editor_brush_flatten": KEY_5,
|
||||
"editor_brush_ramp": KEY_6,
|
||||
}
|
||||
for action in expected_keys_2:
|
||||
if not InputMap.has_action(action):
|
||||
|
|
@ -918,4 +920,4 @@ func _test_brush_paint_mode() -> void:
|
|||
tool.call("set_brush_mode", TerrainField.Mode.RAISE)
|
||||
_check(int(tool.get("brush_mode")) == TerrainField.Mode.RAISE, "set_brush_mode(Heben) bleibt Heben")
|
||||
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")
|
||||
|
|
@ -177,7 +177,7 @@ func _reference_heights(steps: int, x: float, z: float, mode: int) -> PackedFloa
|
|||
var amount := STRENGTH * dt
|
||||
for i in steps:
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
101
tests/test_ui_aufraeumen.gd
Normal file
101
tests/test_ui_aufraeumen.gd
Normal 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)
|
||||
|
|
@ -93,7 +93,7 @@ func _test_math() -> void:
|
|||
_check(v.x == 175 and v.y == 50, "Mittel: v2(140,40) → (175,50)")
|
||||
UIScale.set_step_index(2)
|
||||
_check(UIScale.px(40) == 60 and UIScale.font_size() == 24, "Groß: 40 px → 60 px, Schrift 24")
|
||||
_check(UIScale.px(96) == 144, "Groß: Thumbnail 96 → 144")
|
||||
_check(UIScale.px(96) == 144, "Groß: Thumbnail 96 → 144 (px-Rechnung)")
|
||||
|
||||
|
||||
func _test_cfg_roundtrip() -> void:
|
||||
|
|
@ -142,10 +142,10 @@ func _test_real_ui_scaling() -> void:
|
|||
_check(int(main.theme.default_font_size) == 20, "Mittel: Theme-Schrift 20 (sofort wirksam)")
|
||||
_check(main.undo_btn.custom_minimum_size == Vector2(175, 50), "Mittel: Undo-Knopf 175×50")
|
||||
_check(main.library_search.custom_minimum_size.y == 45, "Mittel: Suchfeld 36 → 45")
|
||||
var tile := _find_tile_button(158)
|
||||
_check(tile != null and tile.custom_minimum_size.y == 158, "Mittel: Bibliotheks-Kachel 126 → 158")
|
||||
var thumb := _find_thumb_rect(120)
|
||||
_check(thumb != null and thumb.custom_minimum_size.x == 120, "Mittel: Thumbnail 96 → 120")
|
||||
var tile := _find_tile_button(235)
|
||||
_check(tile != null and tile.custom_minimum_size.y == 235, "Mittel: Bibliotheks-Kachel 188 → 235")
|
||||
var thumb := _find_thumb_rect(160)
|
||||
_check(thumb != null and thumb.custom_minimum_size.x == 160, "Mittel: Thumbnail 128 → 160")
|
||||
_check(main.size_menu.get_popup().is_item_checked(1), "Menü-Häkchen wandert auf Mittel (neues Popup)")
|
||||
|
||||
# Groß:
|
||||
|
|
|
|||
|
|
@ -252,6 +252,15 @@ func _run_tests() -> void:
|
|||
var pulled := EditorUpdater.pull(git, work)
|
||||
_check(bool(pulled["ok"]), "pull --ff-only erfolgreich (v2 wird geladen)")
|
||||
_check(EditorUpdater.head_sha(git, work) == str(avail["remote"]), "nach Pull ist work auf v2")
|
||||
# Befund 25.10.: Klon VOR origin/main (z. B. Feature-Branch) ist KEIN Update.
|
||||
_git(work, ["config", "user.email", "t@example.invalid"])
|
||||
_git(work, ["config", "user.name", "Update-Test"])
|
||||
_write_file(work.path_join("lokal.txt"), "nur lokal\n")
|
||||
_git(work, ["add", "lokal.txt"])
|
||||
_git(work, ["commit", "-m", "lokal voraus", "lokal.txt"])
|
||||
var ahead := EditorUpdater.check_update(git, work)
|
||||
_check(str(ahead["status"]) == "up_to_date", "Klon VORAUS (Feature-Branch) → up_to_date, kein Dauer-Banner")
|
||||
_git(work, ["reset", "--hard", "-q", str(avail["remote"])])
|
||||
_git(work, ["remote", "set-url", "origin", "http://127.0.0.1:1/nicht-da.git"])
|
||||
var dead_pull := EditorUpdater.pull(git, work)
|
||||
_check(not bool(dead_pull["ok"]), "pull gegen toten Origin schlägt fehl")
|
||||
|
|
|
|||
|
|
@ -222,9 +222,9 @@ default_font_size = 16
|
|||
Editor/colors/accent = Color(0.55, 0.8, 1, 1)
|
||||
Editor/colors/status_ok = Color(0.75, 0.95, 0.75, 1)
|
||||
Editor/colors/status_error = Color(1, 0.6, 0.5, 1)
|
||||
Editor/colors/muted = Color(1, 1, 1, 0.55)
|
||||
Editor/colors/field = Color(1, 1, 1, 0.7)
|
||||
Editor/colors/title = Color(1, 1, 1, 0.6)
|
||||
Editor/colors/muted = Color(1, 1, 1, 0.74)
|
||||
Editor/colors/field = Color(1, 1, 1, 0.9)
|
||||
Editor/colors/title = Color(1, 1, 1, 0.82)
|
||||
Editor/colors/warning = Color(1, 0.62, 0.3, 1)
|
||||
Editor/colors/badge_outline = Color(0, 0, 0, 1)
|
||||
Editor/colors/help_bg = Color(0.09, 0.11, 0.13, 0.97)
|
||||
|
|
@ -246,7 +246,7 @@ TabContainer/styles/tab_hovered = SubResource("tab_hover")
|
|||
TabContainer/styles/tab_unselected = SubResource("tab_unsel")
|
||||
TabContainer/styles/tabbar_background = SubResource("tabbar")
|
||||
TabContainer/colors/font_selected_color = Color(1, 1, 1, 1)
|
||||
TabContainer/colors/font_unselected_color = Color(1, 1, 1, 0.7)
|
||||
TabContainer/colors/font_unselected_color = Color(1, 1, 1, 0.88)
|
||||
Button/styles/normal = SubResource("btn_normal")
|
||||
Button/styles/hover = SubResource("btn_hover")
|
||||
Button/styles/pressed = SubResource("btn_pressed")
|
||||
|
|
@ -254,11 +254,11 @@ Button/styles/disabled = SubResource("btn_disabled")
|
|||
Button/styles/focus = SubResource("btn_focus")
|
||||
Button/colors/font_hover_color = Color(1, 1, 1, 1)
|
||||
Button/colors/font_pressed_color = Color(0.8, 0.9, 1, 1)
|
||||
Button/colors/font_disabled_color = Color(1, 1, 1, 0.35)
|
||||
Button/colors/font_disabled_color = Color(1, 1, 1, 0.5)
|
||||
LineEdit/styles/normal = SubResource("line_normal")
|
||||
LineEdit/styles/focus = SubResource("line_focus")
|
||||
LineEdit/colors/font_color = Color(1, 1, 1, 1)
|
||||
LineEdit/colors/font_placeholder_color = Color(1, 1, 1, 0.4)
|
||||
LineEdit/colors/font_placeholder_color = Color(1, 1, 1, 0.58)
|
||||
HSplitContainer/styles/grabber_area = SubResource("split_grab")
|
||||
HSplitContainer/styles/grabber_area_highlight = SubResource("split_grab_hover")
|
||||
VSplitContainer/styles/grabber_area = SubResource("split_grab")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#!/bin/bash
|
||||
# Regression: alle Editor-Testsuiten (Karte t_3a998ae4)
|
||||
cd "$(dirname "$0")/.."
|
||||
GODOT=/opt/data/home/bin/godot4
|
||||
GODOT=${GODOT:-/opt/data/home/bin/godot4}
|
||||
rc=0
|
||||
for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector; do
|
||||
for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector test_pinsel_werkzeuge test_terrain_mirror test_mal_fein test_ui_aufraeumen test_ui_layout test_ui_scale; do
|
||||
"$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1
|
||||
code=$?
|
||||
line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)
|
||||
|
|
|
|||
Loading…
Reference in a new issue