Terrain-Pinsel: Kante-Regler (Falloff), Einebnen, Rampe, Druck-Stärke, Mal-Slot-Ersatz
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b1d8f7626d
commit
26bcbe0cdd
9 changed files with 469 additions and 61 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1673,7 +1673,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).
|
||||
|
|
@ -1924,15 +1924,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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -49,8 +53,10 @@ func build() -> void:
|
|||
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
|
||||
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 +67,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)
|
||||
|
|
@ -70,6 +82,8 @@ func build() -> void:
|
|||
mode_row.add_child(terrain_mode_lower)
|
||||
mode_row.add_child(terrain_mode_smooth)
|
||||
mode_row.add_child(terrain_mode_paint)
|
||||
mode_row.add_child(terrain_mode_flatten)
|
||||
mode_row.add_child(terrain_mode_ramp)
|
||||
|
||||
# Material-Palette (Karte t_603ec1a8): nur sichtbar im Malen-Modus.
|
||||
terrain_palette = VBoxContainer.new()
|
||||
|
|
@ -153,6 +167,20 @@ func build() -> void:
|
|||
terrain_strength_slider.value_changed.connect(_on_terrain_strength_changed)
|
||||
terrain_panel.add_child(terrain_strength_slider)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
|
@ -177,6 +205,9 @@ func build() -> void:
|
|||
terrain_weich_slider.value_changed.connect(_on_terrain_weich_changed)
|
||||
terrain_panel.add_child(terrain_weich_slider)
|
||||
|
||||
terrain_panel.add_child(_muted_label(
|
||||
"Stärke folgt dem Stift-Druck bzw. dem Controller-Trigger (Maus = voll). "
|
||||
+ "Rampe: Start klicken, ziehen, loslassen. Einebnen: Höhe am Strichstart wird zum Ziel."))
|
||||
terrain_panel.add_child(_muted_label(
|
||||
"Linksklick-Ziehen formt bzw. malt (je nach Werkzeug), Strg+Linksklick macht das Gegenteil — im Malen-Modus malt Strg+Klick Steppe (Grundboden) zurück. "
|
||||
+ "Jeder Strich ist EIN Rückgängig-Schritt (Strg+Z). Rechte Maustaste = Flug, ESC beendet den Terrain-Modus."))
|
||||
|
|
@ -257,6 +288,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
|
||||
|
||||
|
||||
|
|
@ -268,6 +303,9 @@ func _on_terrain_mode_changed(_pressed: bool) -> void:
|
|||
if terrain_strength_slider != null:
|
||||
terrain_strength_slider.visible = not paint
|
||||
terrain_strength_label.visible = not paint
|
||||
if terrain_falloff_slider != null:
|
||||
terrain_falloff_slider.visible = not paint
|
||||
terrain_falloff_label.visible = not paint
|
||||
if terrain_weich_slider != null:
|
||||
terrain_weich_slider.visible = paint
|
||||
terrain_weich_label.visible = paint
|
||||
|
|
@ -329,6 +367,12 @@ func _on_terrain_weich_changed(value: float) -> void:
|
|||
main.terrain_tool.call("set_weichheit", value / 100.0)
|
||||
|
||||
|
||||
func _on_terrain_falloff_changed(value: float) -> void:
|
||||
terrain_falloff_label.text = "Kante: %d %% weich" % int(value) if value > 0.0 else "Kante: hart"
|
||||
if main.terrain_tool != null:
|
||||
main.terrain_tool.call("set_falloff", value / 100.0)
|
||||
|
||||
|
||||
func _on_terrain_strength_changed(value: float) -> void:
|
||||
terrain_strength_label.text = "Stärke: %.1f m pro Sekunde" % value
|
||||
if main.terrain_tool != null:
|
||||
|
|
|
|||
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")
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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; 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