scopedvr-map-editor/tests/test_selection_look.gd
Hermes (scopedvr-dev) 5dca5513da U6 t_639135dd: dezente Auswahl — Kontur statt Vollorange (Textur bleibt sichtbar).
Neu shaders/selection_outline.gdshader: Inverted-Hull-Außenhülle (unshaded,
cull_front, Grow entlang der Normalen; Streifen-Uniform für den Zwilling).

Markierung jetzt über Outline-Kind am Mesh (Auswahl/Hover/Partner); Farben +
Breiten aus theme/editor_theme.tres (selection_outline*, *_width_mm) — eine
Quelle. material_override wird von der Markierung nicht mehr angefasst:
GLB-Texturen bleiben im Auswahl-Zustand voll sichtbar, der Tint bleibt beim
Markieren sichtbar (kein "Orange gewinnt" mehr).

Spawn-Zonen: konstante Disc-Deckkraft + dezenter Kontur-Ring (Torus) statt
Aufhellung/Emission; Label-Farbe aus dem Theme statt hartkodiertem Gelb.
Bau-Teile: Kontur um das Bauteil-Mesh statt halbtransparenter Vollbox.
Neu: set_hover()-API (schwächste Stufe) — Anschluss an die Mausbewegung
liegt in interaction.gd (Grenze dieser Karte) und folgt als eigene Karte.

Suite tests/test_selection_look.gd (41 checks: Theme-Quelle, Grundmaterial
unangetastet, Outline nur bei Markierung, restlos weg bei Abwahl, Zwilling
dünner+gestrichelt, Spawn-Ring, Bau-Teil-Kontur). Altsuiten an die neue
Semantik angepasst (test_glb_view: GLB-Auswahl ohne Override; test_map_tint
T3: Kontur statt Orange-gewinnt; test_object_mirror: Partner per Kontur).

Beweise: 4 Renderproben (fass, echtes GLB — vorher Vollorange / nachher
Kontur, je auf Sand und Fels) in /opt/data/scopedvr/work/u6_auswahl_beweise/.
Voll-Batterie 32/32 grün (EXIT 0) inkl. der drei angepassten Suiten.
2026-10-20 10:33:04 +00:00

287 lines
13 KiB
GDScript

extends SceneTree
## Editor-Tests (headless): U6 dezente Auswahl — Kontur statt Vollorange
## (Karte t_639135dd). Ausführen:
## godot --headless --path . -s res://tests/test_selection_look.gd
## Exit 0 = alles grün.
##
## T1 Theme ist die einzige Quelle: Kontur-Farben (Auswahl/Hover/Partner)
## + Breiten existieren, Partner/Hover sind schwächer als die Auswahl.
## T2 Grundmaterial bleibt: Auswahl setzt KEIN Vollfarben-Override mehr —
## der Mesh-Kind-Knoten trägt weiter die Editor-Grundfarbe (Platzhalter)
## bzw. null (GLB — Textur bleibt; echte GLBs deckt test_glb_view ab).
## T3 Outline-Node nur bei markierten Objekten (Auswahl/Hover/Partner),
## bei Abwahl restlos weg (kein Rest-Outline, kein Rest-Meta).
## T4 Abstufungen: Hover = schwächstes Material, Auswahl = normale Kontur,
## Spiegel-Zwilling = dünnere + gestrichelte Kontur in eigener Farbe.
## T5 Spawn-Zonen: dezente Variante — Ring statt Aufhellung, restlos weg
## bei Abwahl; Disc-Deckkraft bleibt konstant.
## T6 Bau-Teile: Kontur um das Bauteil-Mesh statt Vollbox, weg bei Abwahl.
var failures := 0
var checks := 0
func _initialize() -> void:
_run_all.call_deferred()
func _check(cond: bool, label: String) -> void:
checks += 1
print("%s: %s" % ["ok " if cond else "FAIL", label])
if not cond:
failures += 1
func _run_all() -> void:
await process_frame
_test_theme_quelle()
await _test_auswahl_platzhalter()
await _test_hover_abstufung()
await _test_zwilling()
await _test_spawn_ring()
await _test_gebaeude_kontur()
if failures == 0:
print("ALL OK: %d checks" % checks)
quit(0)
else:
print("FAILED: %d/%d checks" % [failures, checks])
quit(1)
func _obj(id: String, pos: Array, rot := 0.0) -> Dictionary:
return {"id": id, "pos": pos, "scale": 1.0, "rot_y_deg": float(rot)}
func _build(manifest: Dictionary, doc: Dictionary) -> Node3D:
var builder := Node3D.new()
builder.set_script(load("res://src/viewport/map_view_builder.gd"))
root.add_child(builder)
builder.call("rebuild", doc, manifest)
return builder
func _manifest() -> Dictionary:
return {"objects": {"crate_wood": {"collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}}
func _flat_doc() -> Dictionary:
return SvmapDocument.create_new("U6Test", 256.0, "mittag")
func _outline_von(marker: Node3D) -> MeshInstance3D:
if not marker.has_meta("outline"):
return null
var alt: Variant = marker.get_meta("outline")
if alt is MeshInstance3D and is_instance_valid(alt):
return alt
return null
func _restlos(builder: Node3D, label: String) -> void:
await process_frame
var sauber := true
for marker in builder.object_markers:
if marker == null or not is_instance_valid(marker):
continue
if marker.has_meta("outline"):
sauber = false
var mi := marker.get_child(0) as MeshInstance3D
for kind in mi.get_children():
if kind.name == "SelectionOutline" and is_instance_valid(kind) \
and not (kind as Node).is_queued_for_deletion():
sauber = false
_check(sauber, label)
# ------------------------------------------------------------------ T1 ------
func _test_theme_quelle() -> void:
print("== T1 Theme als einzige Quelle ==")
var theme_res: Variant = load("res://theme/editor_theme.tres")
_check(theme_res is Theme, "Theme lädt")
if not (theme_res is Theme):
return
var th := theme_res as Theme
var sel := th.get_color("selection_outline", "Editor")
var hover := th.get_color("selection_outline_hover", "Editor")
var partner := th.get_color("selection_outline_partner", "Editor")
_check(th.has_color("selection_outline", "Editor"), "Theme: Auswahl-Kontur-Farbe")
_check(th.has_color("selection_outline_hover", "Editor"), "Theme: Hover-Kontur-Farbe")
_check(th.has_color("selection_outline_partner", "Editor"), "Theme: Partner-Kontur-Farbe")
_check(hover.a < sel.a, "Theme: Hover schwächer als Auswahl (Deckkraft)")
_check(not partner.is_equal_approx(sel), "Theme: Partner hat EIGENE Farbe")
_check(th.get_constant("selection_outline_width_mm", "Editor") > 0,
"Theme: Auswahl-Breite > 0")
_check(th.get_constant("selection_outline_width_partner_mm", "Editor")
< th.get_constant("selection_outline_width_mm", "Editor"),
"Theme: Partner-Kontur dünner als Auswahl")
# ------------------------------------------------------------------ T2/T3 ---
func _test_auswahl_platzhalter() -> void:
print("== T2/T3 Auswahl am Platzhalter: Grundmaterial bleibt, Kontur nur bei Auswahl ==")
var doc := _flat_doc()
(doc["objects"] as Array).append(_obj("crate_wood", [4.0, 0.0, 2.0]))
(doc["objects"] as Array).append(_obj("crate_wood", [-4.0, 0.0, -2.0]))
var builder := _build(_manifest(), doc)
var m0: Node3D = builder.object_markers[0]
var m1: Node3D = builder.object_markers[1]
var mi0 := m0.get_child(0) as MeshInstance3D
var mat_sel: ShaderMaterial = builder.get("_outline_mat_sel")
_check(not bool(m0.get_meta("glb", true)), "Probenobjekt ist Platzhalter (kein GLB)")
_check(mi0.material_override == builder.get("_base_mat"),
"ungewählt: Editor-Grundfarbe am Mesh (Platzhalter)")
builder.call("set_selected_multi", [0])
await process_frame
_check(mi0.material_override == builder.get("_base_mat"),
"Auswahl: Grundmaterial UNVERÄNDERT (kein material_override mit Vollfarbe)")
var out0 := _outline_von(m0)
_check(out0 != null, "Auswahl: Outline-Node existiert")
if out0 != null:
_check(out0.mesh == mi0.mesh, "Outline trägt dasselbe Mesh (Außenhülle)")
_check(out0.get_parent() == mi0, "Outline hängt am Mesh (erbt Position/Scale)")
_check(out0.material_override == mat_sel, "Outline trägt das Auswahl-Kontur-Material")
_check(_outline_von(m1) == null, "unmarkiertes Objekt: KEIN Outline-Node")
# Abwahl: restlos weg.
builder.call("set_selected_multi", [])
await _restlos(builder, "Abwahl: kein Rest-Outline, kein Rest-Meta")
await _test_glb_regel(builder)
builder.queue_free()
## GLB-Regel auch ohne echtes Modell prüfbar machen: ein Marker, dessen Meta
## "glb" = true ist, darf NIE ein material_override bekommen. Der Platzhalter-
## Builder baut nur glb=false — die echte GLB-Strecke deckt test_glb_view ab;
## hier wird die Regel direkt am Bau-Pfad verifiziert (Quelltext-Garantie:
## material_override = null wenn echt). Wir prüfen die Meta-Abhängigkeit,
## indem wir einen Marker künstlich als GLB markieren und neu auswählen.
func _test_glb_regel(builder: Node3D) -> void:
var doc: Dictionary = builder.get("_doc_ref")
(doc["objects"] as Array).append(_obj("crate_wood", [0.0, 0.0, 8.0]))
builder.call("rebuild", doc, _manifest())
var m2: Node3D = builder.object_markers[2]
m2.set_meta("glb", true)
var mi2 := m2.get_child(0) as MeshInstance3D
mi2.material_override = null
builder.call("set_selected_multi", [2])
await process_frame
_check(mi2.material_override == null,
"GLB-Objekt ausgewählt: material_override bleibt null (Textur bleibt sichtbar)")
_check(_outline_von(m2) != null, "GLB-Objekt ausgewählt: Kontur trägt die Markierung")
builder.call("set_selected_multi", [])
# ------------------------------------------------------------------ T4 ------
func _test_hover_abstufung() -> void:
print("== T4a Hover = schwächste Markierung ==")
var doc := _flat_doc()
(doc["objects"] as Array).append(_obj("crate_wood", [4.0, 0.0, 2.0]))
var builder := _build(_manifest(), doc)
var m0: Node3D = builder.object_markers[0]
builder.call("set_hover", 0)
await process_frame
var out0 := _outline_von(m0)
_check(out0 != null, "Hover: Outline-Node existiert")
if out0 != null:
_check(out0.material_override == builder.get("_outline_mat_hover"),
"Hover: schwächstes Kontur-Material")
builder.call("set_hover", -1)
await process_frame
_check(_outline_von(m0) == null, "Hover weg: Outline restlos entfernt")
# Auswahl gewinnt gegen Hover (stärkste Stufe bleibt sichtbar).
builder.call("set_hover", 0)
builder.call("set_selected_multi", [0])
await process_frame
var out_sel := _outline_von(m0)
_check(out_sel != null and out_sel.material_override == builder.get("_outline_mat_sel"),
"Auswahl + Hover gleichzeitig: Auswahl-Kontur gewinnt")
builder.queue_free()
func _test_zwilling() -> void:
print("== T4b Spiegel-Zwilling = dünnere/gestrichelte Kontur in eigener Farbe ==")
var doc := _flat_doc()
(doc["objects"] as Array).append(_obj("crate_wood", [100.0, 0.0, 40.0], 30.0))
var res: Dictionary = ObjectOps.symmetry_place_twin(doc, 0, SymmetryCore.Mode.POINT)
_check(int(res.get("twin", -1)) == 1, "Zwilling angelegt (Index 1)")
var builder := _build(_manifest(), doc)
builder.call("set_selected_multi", [0])
await process_frame
var m0: Node3D = builder.object_markers[0]
var m1: Node3D = builder.object_markers[1]
var out0 := _outline_von(m0)
var out1 := _outline_von(m1)
_check(out0 != null and out0.material_override == builder.get("_outline_mat_sel"),
"Quelle: normale Auswahl-Kontur")
_check(out1 != null and out1.material_override == builder.get("_outline_mat_partner"),
"Zwilling: Partner-Kontur (eigene, schwächere Variante)")
var mat_sel: ShaderMaterial = builder.get("_outline_mat_sel")
var mat_partner: ShaderMaterial = builder.get("_outline_mat_partner")
var breite_sel: float = mat_sel.get_shader_parameter("breite_m")
var breite_partner: float = mat_partner.get_shader_parameter("breite_m")
var streifen_partner: float = mat_partner.get_shader_parameter("streifen_hz")
var streifen_sel: float = mat_sel.get_shader_parameter("streifen_hz")
_check(breite_partner < breite_sel, "Zwilling: dünnere Kontur (Breite aus Theme)")
_check(streifen_partner > 0.0 and streifen_sel == 0.0,
"Zwilling: gestrichelt, Auswahl durchgezogen")
_check(not (mat_partner.get_shader_parameter("linien_farbe") as Color).is_equal_approx(
mat_sel.get_shader_parameter("linien_farbe")),
"Zwilling: eigene Farbe (aus dem Theme)")
# Beide angewählt → beide volle Auswahl-Kontur.
builder.call("set_selected_multi", [0, 1])
await process_frame
_check(_outline_von(m0).material_override == builder.get("_outline_mat_sel")
and _outline_von(m1).material_override == builder.get("_outline_mat_sel"),
"beide angewählt: beide volle Auswahl-Kontur (kein Partner-Zustand)")
builder.call("set_selected_multi", [])
await _restlos(builder, "Paar abgewählt: Konturen restlos weg")
builder.queue_free()
# ------------------------------------------------------------------ T5 ------
func _test_spawn_ring() -> void:
print("== T5 Spawn-Zonen: Ring statt Aufhellung ==")
var builder := _build(_manifest(), _flat_doc())
var marker: Node3D = builder.call("_spawn_marker", Vector3.ZERO, 0, "A1", 6.0)
var disc0 := marker.get_child(0) as MeshInstance3D
var alpha0: float = (disc0.material_override as StandardMaterial3D).albedo_color.a
builder.call("style_spawn_marker", marker, 6.0, true)
await process_frame
var ring: Variant = marker.get_meta("spawn_ring") if marker.has_meta("spawn_ring") else null
_check(ring is MeshInstance3D and is_instance_valid(ring), "Auswahl: Kontur-Ring existiert")
if ring is MeshInstance3D:
_check((ring as MeshInstance3D).mesh is TorusMesh, "Ring = schmaler Torus am Zonenrand")
var rmat := (ring as MeshInstance3D).material_override as StandardMaterial3D
var theme_res: Variant = load("res://theme/editor_theme.tres")
var soll: Color = (theme_res as Theme).get_color("selection_outline", "Editor")
_check(rmat.albedo_color.is_equal_approx(soll), "Ring-Farbe kommt aus dem Theme")
var disc1 := marker.get_child(0) as MeshInstance3D
var alpha1: float = (disc1.material_override as StandardMaterial3D).albedo_color.a
_check(absf(alpha0 - alpha1) < 0.001,
"Disc-Deckkraft konstant (keine Vollflächen-Aufhellung mehr)")
builder.call("style_spawn_marker", marker, 6.0, false)
await process_frame
_check(not marker.has_meta("spawn_ring"), "Abwahl: Ring-Meta restlos weg")
builder.queue_free()
# ------------------------------------------------------------------ T6 ------
func _test_gebaeude_kontur() -> void:
print("== T6 Bau-Teile: Kontur statt Vollbox ==")
var doc := _flat_doc()
var teil: Dictionary = BuildOps.make_piece("wand", Vector3(2.0, 0.2, 4.0), 0.0)
_check(not teil.is_empty(), "Proben-Bauteil angelegt")
(doc["gebaeude"] as Array).append(teil)
var builder := _build(_manifest(), doc)
builder.call("set_gebaeude_selected", 0)
await process_frame
var sel: Variant = builder.get("_gebaeude_sel")
_check(sel is MeshInstance3D and is_instance_valid(sel), "Bauteil-Auswahl: Kontur-Mesh existiert")
if sel is MeshInstance3D:
_check((sel as MeshInstance3D).mesh != null, "Kontur trägt das Bauteil-Mesh")
_check((sel as MeshInstance3D).material_override == builder.get("_outline_mat_sel"),
"Kontur trägt das Auswahl-Material (Theme)")
builder.call("set_gebaeude_selected", -1)
await process_frame
var sel2: Variant = builder.get("_gebaeude_sel")
_check(sel2 == null, "Bauteil-Abwahl: Kontur restlos weg")
builder.queue_free()