From 5dca5513dad8499a07e8998b73f2768cfc033f37 Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Tue, 20 Oct 2026 10:33:04 +0000 Subject: [PATCH] =?UTF-8?q?U6=20t=5F639135dd:=20dezente=20Auswahl=20?= =?UTF-8?q?=E2=80=94=20Kontur=20statt=20Vollorange=20(Textur=20bleibt=20si?= =?UTF-8?q?chtbar).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- shaders/selection_outline.gdshader | 40 ++++ shaders/selection_outline.gdshader.uid | 1 + src/viewport/map_view_builder.gd | 281 ++++++++++++++++++------ tests/probe_u6_render.gd | 147 +++++++++++++ tests/probe_u6_render.gd.uid | 1 + tests/test_glb_view.gd | 10 +- tests/test_map_tint.gd | 16 +- tests/test_object_mirror.gd | 42 ++-- tests/test_selection_look.gd | 287 +++++++++++++++++++++++++ tests/test_selection_look.gd.uid | 1 + theme/editor_theme.tres | 6 + 11 files changed, 741 insertions(+), 91 deletions(-) create mode 100644 shaders/selection_outline.gdshader create mode 100644 shaders/selection_outline.gdshader.uid create mode 100644 tests/probe_u6_render.gd create mode 100644 tests/probe_u6_render.gd.uid create mode 100644 tests/test_selection_look.gd create mode 100644 tests/test_selection_look.gd.uid diff --git a/shaders/selection_outline.gdshader b/shaders/selection_outline.gdshader new file mode 100644 index 0000000..60d7f07 --- /dev/null +++ b/shaders/selection_outline.gdshader @@ -0,0 +1,40 @@ +shader_type spatial; +render_mode unshaded, cull_front, shadows_disabled, blend_mix; + +// Außenhüllen-Kontur (U6, Karte t_639135dd): dezente Auswahl-Markierung +// statt Vollflächen-Orange. Das Grundmaterial (inkl. Textur) bleibt komplett +// sichtbar — die Kontur lebt als eigene Hülle um das Modell: +// +// VERTEX += NORMAL * breite_m bläht das Volumen entlang der Normalen auf, +// cull_front rendert nur die RÜCKseiten der Hülle. Sichtbar bleibt darum +// genau der schmale Saum, der außerhalb der Silhouette liegt (der Rest +// wird vom Objekt selbst per Tiefentest verdeckt) — die klassische +// Inverted-Hull-Outline-Technik. +// +// Farbton, Deckkraft und Breite kommen aus dem Theme (theme/editor_theme.tres, +// Abschnitt "Editor") — der Shader selbst kennt keine festen Farben. +// +// Gestrichelte Variante (Spiegel-Zwilling): streifen_hz > 0 unterteilt die +// Kontur in Bänder entlang der Welthöhe — dünn + gestrichelt statt voll. + +uniform vec4 linien_farbe : source_color = vec4(1.0, 0.6, 0.15, 0.95); +uniform float breite_m : hint_range(0.0, 1.0) = 0.08; +uniform float streifen_hz : hint_range(0.0, 40.0) = 0.0; + +varying vec3 welt_pos; + +void vertex() { + VERTEX += NORMAL * breite_m; + welt_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; +} + +void fragment() { + if (streifen_hz > 0.5) { + // Gestrichelt (Zwilling): Bänder entlang der Welthöhe, ~55 % Strich. + if (fract(welt_pos.y * streifen_hz) > 0.55) { + discard; + } + } + ALBEDO = linien_farbe.rgb; + ALPHA = linien_farbe.a; +} diff --git a/shaders/selection_outline.gdshader.uid b/shaders/selection_outline.gdshader.uid new file mode 100644 index 0000000..ae9b0ac --- /dev/null +++ b/shaders/selection_outline.gdshader.uid @@ -0,0 +1 @@ +uid://b1wjgw8l4w77m diff --git a/src/viewport/map_view_builder.gd b/src/viewport/map_view_builder.gd index 0b2ee36..3527ecd 100644 --- a/src/viewport/map_view_builder.gd +++ b/src/viewport/map_view_builder.gd @@ -11,8 +11,15 @@ extends Node3D const GRID_COLOR := Color(0.35, 0.45, 0.55, 0.6) const AXIS_COLOR := Color(0.9, 0.7, 0.2, 0.9) const OBJECT_COLOR := Color(0.55, 0.75, 0.95) -const SELECT_COLOR := Color(1.0, 0.6, 0.15) # Auswahl klar orange const TEAM_COLORS := [Color(0.3, 0.6, 1.0), Color(1.0, 0.35, 0.3)] # Blau / Rot +# Auswahl-Markierung (U6, t_639135dd): Farbe + Breite kommen aus dem Theme +# (eine Quelle) — Auswahl-Kontur, schwächere Hover-Kontur, dünn-gestrichelte +# Partner-Kontur. Kein Vollflächen-Orange mehr: das Grundmaterial (mit Textur) +# bleibt sichtbar, die Markierung lebt als Außenhüllen-Kontur darum herum. +const THEME_PATH := "res://theme/editor_theme.tres" +const OUTLINE_SHADER := "res://shaders/selection_outline.gdshader" +# Fallback, falls das Theme (noch) nicht geladen werden kann. +const OUTLINE_FALLBACK := Color(1.0, 0.6, 0.15, 0.95) # Wetter → Licht (grobe Stimmung; Feintuning kommt mit dem Wetter-Konsum) const WEATHER_LIGHT := { @@ -22,8 +29,9 @@ const WEATHER_LIGHT := { "nacht": {"energy": 0.25, "color": Color(0.6, 0.7, 1.0), "ambient": 0.12, "sky_top": Color(0.03, 0.05, 0.12)}, } -var selected_index := -1 # aktuelles Auswahl-Objekt (orange gefärbt) +var selected_index := -1 # aktuelles Auswahl-Objekt (Kontur-Markierung) var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrfachauswahl) +var hover_index := -1 # Hover-Objekt (schwächste Kontur; U6-Abstufung) var object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc.objects) var fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis) var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius) @@ -32,13 +40,16 @@ var gebaeude_markers: Array = [] # MeshInstance3D je Bauteil (Karte t_97ea59c5) var grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014) var _gebaeude_baked: Dictionary = {} # faces/offsets für Ray-Pick (Bauen-Werkzeug) -var _gebaeude_sel: MeshInstance3D # orangener AABB-Rahmen um das gewählte Teil +var _gebaeude_sel: MeshInstance3D # Kontur-Mesh um das gewählte Bauteil var _doc_ref: Dictionary = {} # letztes rebuild-Doc (Auswahl-Markierung) var _base_mat: StandardMaterial3D -var _select_mat: StandardMaterial3D -var _partner_mat: StandardMaterial3D # schwächere Auswahl-Farbe des sym-Zwillings (M3) var _sym_partners: Array = [] # Indizes der Zwillinge der aktuellen Auswahl +# Kontur-Materialien (U6): Auswahl / Hover / Spiegel-Zwilling — Farben und +# Breiten kommen aus dem Theme, der Shader ist die Außenhüllen-Technik. +var _outline_mat_sel: ShaderMaterial +var _outline_mat_hover: ShaderMaterial +var _outline_mat_partner: ShaderMaterial func _ready() -> void: @@ -59,20 +70,10 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void: _base_mat = StandardMaterial3D.new() _base_mat.albedo_color = OBJECT_COLOR _base_mat.roughness = 0.8 - _select_mat = StandardMaterial3D.new() - _select_mat.albedo_color = SELECT_COLOR - _select_mat.emission_enabled = true - _select_mat.emission = SELECT_COLOR * 0.35 - _select_mat.roughness = 0.7 - # sym-Zwilling (M3, t_34908425): GLEICHE Auswahl-Material-Logik, nur eine - # schwächere Farbe (abgedunkeltes Orange, sehr leises Glühen) — keine zweite - # Markierungs-Technik, damit die spätere dezente Auswahl (U6) automatisch - # mitprofitiert. - _partner_mat = StandardMaterial3D.new() - _partner_mat.albedo_color = SELECT_COLOR.darkened(0.45) - _partner_mat.emission_enabled = true - _partner_mat.emission = SELECT_COLOR * 0.12 - _partner_mat.roughness = 0.7 + # U6 (t_639135dd): keine Vollfarben-Materialien mehr — die Markierung + # (Auswahl/Hover/Partner) lebt als Außenhüllen-Kontur um das Modell, + # Farben + Breiten aus dem Theme. + _markierung_theme_laden() _sym_partners = [] if data.is_empty(): @@ -252,7 +253,10 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void: mi.scale = Vector3.ONE * scale else: mi.position.y = MeshShapes.half_height(mesh) - mi.material_override = _select_mat if _is_selected(i) else (null if echt else _base_mat) + # U6 (t_639135dd): das Grundmaterial wird NIE MEHR durch die Auswahl + # ersetzt — echte GLB-Objekte tragen ihre Texturen weiter (override null), + # Platzhalter die Editor-Grundfarbe. Markiert wird per Kontur (unten). + mi.material_override = null if echt else _base_mat marker.add_child(mi) var half: Vector3 = (mesh.get_aabb().size * (scale if echt else 1.0) * 0.5) as Vector3 marker.set_meta("pick_radius", half.length() + maxf(0.6, half.y * 0.25)) @@ -263,10 +267,12 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void: add_child(marker) # Farbe je Objekt (Karte t_b0edb807): optionales Feld "tint" — nur # Objekte MIT Feld bekommen die Überschreibung (Rückwärtskompatibel). - # Ausgewählte Objekte bleiben orange (Auswahl gewinnt weiter, Tint - # kommt beim Abwählen über set_selected_multi). - if SvmapTint.hat(obj) and not _is_selected(i): + # Die Farbe bleibt jetzt auch bei Markierung sichtbar (U6: Auswahl = Kontur, + # Grundmaterial bleibt) — kein „Orange gewinnt“ mehr. + if SvmapTint.hat(obj): SvmapTint.anwenden(marker, SvmapTint.von(obj)) + # Markierung (Auswahl/Hover/Partner) als Kontur-Hülle anlegen. + _outline_anwenden(marker, i) ## Auswahl nachträglich umfärben (ohne ganzen Rebuild) — z. B. nach Drag. @@ -276,34 +282,29 @@ func set_selected(idx: int) -> void: set_selected_multi([idx] if idx >= 0 else []) -## Mehrfachauswahl (Karte t_f6f29d86): ALLE markierten Objekte färben sich -## orange — Gruppen/Mehrfachauswahl sind auf einen Blick erkennbar. -## Echte GLB-Objekte (Meta „glb") bekommen im ungewählten Zustand KEINE -## Grundfarbe drübergelegt — material_override würde die Texturen verstecken -## (Karte t_9b50b231); orange bleibt auch dort das Auswahl-Signal. +## Mehrfachauswahl (Karte t_f6f29d86): ALLE markierten Objekte bekommen die +## Auswahl-Kontur (U6, t_639135dd) — Gruppen/Mehrfachauswahl sind auf einen +## Blick erkennbar, OHNE die Textur zu übermalen. Das Grundmaterial bleibt +## überall sichtbar: echte GLB-Objekte tragen ihre Texturen weiter (Karte +## t_9b50b231), Platzhalter die Editor-Grundfarbe — material_override wird +## von der Markierung nicht mehr angefasst. func set_selected_multi(indices: Array) -> void: selected_indices = indices.duplicate() selected_index = indices.back() if not indices.is_empty() else -1 # sym-Zwillinge (M3, t_34908425): aus dem doc ableiten — Partner der - # Auswahl, die nicht selbst ausgewählt sind, werden schwächer markiert. + # Auswahl, die nicht selbst ausgewählt sind, bekommen die dünnere, + # gestrichelte Partner-Kontur (U6-Abstufung statt dunkler Vollfarbe). _sym_partners = _sym_partners_of(indices) var objects: Array = _doc_ref.get("objects", []) for marker in object_markers: if marker == null or not is_instance_valid(marker): continue var idx := int(marker.get_meta("obj_index", -1)) - var mi: MeshInstance3D = marker.get_child(0) - var echt := bool(marker.get_meta("glb", false)) - var ist_sel := _is_selected(idx) - var ist_partner := not ist_sel and _is_partner(idx) - mi.material_override = _select_mat if ist_sel \ - else (_partner_mat if ist_partner else (null if echt else _base_mat)) - # Tint-Abgleich (Karte t_b0edb807): unmarkierte Objekte zeigen ihre - # doc-Farbe (oder — nach „Zurücksetzen“ — wieder die Pack-Farbe). - # Markierte (Auswahl ODER Zwilling) bleiben orange; ihr Tint liegt als - # doc-Feld bereit und erscheint beim Abwählen. - if not ist_sel and not ist_partner and idx >= 0 and idx < objects.size() \ - and objects[idx] is Dictionary: + _outline_anwenden(marker, idx) + # Tint-Abgleich (Karte t_b0edb807): ALLE Objekte zeigen ihre doc-Farbe + # (oder — nach „Zurücksetzen“ — wieder die Pack-Farbe). Der Tint bleibt + # jetzt auch bei Markierung sichtbar (Auswahl = Kontur um das Modell). + if idx >= 0 and idx < objects.size() and objects[idx] is Dictionary: var obj: Dictionary = objects[idx] if SvmapTint.hat(obj): SvmapTint.anwenden(marker, SvmapTint.von(obj)) @@ -334,8 +335,8 @@ func _sym_partners_of(indices: Array) -> Array: ## Tint frisch aus dem doc auf die MARKIERTEN Objekte anwenden (Inspector- -## Änderung ohne Rebuild). Markierte Objekte zeigen weiter Auswahl-Orange — -## die Farbe liegt im doc und erscheint beim Abwählen (set_selected_multi). +## Änderung ohne Rebuild). U6: die Farbe bleibt auch bei Markierung sichtbar +## (Auswahl = Kontur um das Modell) — kein „Orange gewinnt“-Skip mehr. func tint_refresh(indices: Array) -> void: var objects: Array = _doc_ref.get("objects", []) for i in indices: @@ -344,8 +345,6 @@ func tint_refresh(indices: Array) -> void: var marker = object_markers[i] if marker == null or not is_instance_valid(marker): continue - if _is_selected(i): - continue if i >= objects.size() or not (objects[i] is Dictionary): continue var obj: Dictionary = objects[i] @@ -414,8 +413,10 @@ func _spawn_marker(pos: Vector3, team: int, pid: String, radius: float) -> Node3 ## Kreisfläche EINES Markers (neu) bauen — Werkzeuge rufen nach Radius-Änderung -## style_spawn_marker auf. -func _spawn_disc(radius: float, team: int, selected := false) -> MeshInstance3D: +## style_spawn_marker auf. U6 (t_639135dd): die Fläche bleibt IMMER gleich +## dezent (Teamfarbe, halbtransparent) — die Auswahl zeigt sich als schmaler +## Kontur-Ring am Zonenrand (Theme-Farbe), nicht als aufgehellte Vollfläche. +func _spawn_disc(radius: float, team: int, _selected := false) -> MeshInstance3D: var mi := MeshInstance3D.new() var cyl := CylinderMesh.new() cyl.top_radius = radius @@ -425,12 +426,28 @@ func _spawn_disc(radius: float, team: int, selected := false) -> MeshInstance3D: mi.position.y = 0.12 var col: Color = TEAM_COLORS[team] if team >= 0 and team < TEAM_COLORS.size() else Color.WHITE var mat := StandardMaterial3D.new() - mat.albedo_color = Color(col.r, col.g, col.b, 0.55 if not selected else 0.8) + mat.albedo_color = Color(col.r, col.g, col.b, 0.55) + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mi.material_override = mat + return mi + + +## Dezenter Auswahl-Ring EINES Spawn-Markers (U6): dünner Torus am Zonenrand, +## Farbe aus dem Theme. Muss NUR bei Auswahl existieren (restlos weg bei +## Abwahl) — style_spawn_marker legt ihn an/entfernt ihn. +func _spawn_ring(radius: float) -> MeshInstance3D: + var mi := MeshInstance3D.new() + var torus := TorusMesh.new() + var t := maxf(0.15, radius * 0.06) + torus.inner_radius = maxf(0.01, radius - t) + torus.outer_radius = radius + t + mi.mesh = torus + mi.position.y = 0.26 + var mat := StandardMaterial3D.new() + mat.albedo_color = _markierung_farbe("selection_outline") mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - if selected: - mat.emission_enabled = true - mat.emission = col * 0.6 mi.material_override = mat return mi @@ -450,6 +467,17 @@ func style_spawn_marker(marker: Node3D, radius: float, selected: bool) -> void: var disc := _spawn_disc(radius, team, selected) marker.add_child(disc) marker.move_child(disc, 0) + # Auswahl-Kontur: alter Ring weg, bei Auswahl neuer (restloser Wechsel). + var alter_ring: Node = null + if marker.has_meta("spawn_ring"): + alter_ring = marker.get_meta("spawn_ring") + if alter_ring != null and is_instance_valid(alter_ring): + alter_ring.queue_free() + marker.remove_meta("spawn_ring") + if selected: + var ring := _spawn_ring(radius) + marker.add_child(ring) + marker.set_meta("spawn_ring", ring) ## Auswahl-Highlight über ALLE Spawn-Marker (Werkzeug ruft bei Änderung). @@ -462,11 +490,12 @@ func set_spawn_selected(team: int, pid: String) -> void: if bool(marker.get_meta("spawn_selected", false)) == is_sel: continue style_spawn_marker(marker, float(marker.get_meta("spawn_radius", 6.0)), is_sel) - # Label sofort umfärben (auch ohne Disc-Neubau sichtbar). + # Label sofort umfärben (auch ohne Disc-Neubau sichtbar) — Farbe aus + # dem Theme statt hartkodiertem Gelb (U6). var col: Color = TEAM_COLORS[clampi(team, 0, 1)] if team >= 0 else Color.WHITE if marker.get_child_count() > 1 and marker.get_child(1) is Label3D: var label := marker.get_child(1) as Label3D - label.modulate = Color(1.0, 0.85, 0.2) if is_sel else col + label.modulate = _markierung_farbe("selection_outline") if is_sel else col # ------------------------------------------------------------ Bauteile ------ @@ -503,7 +532,10 @@ func gebaeude_baked() -> Dictionary: return _gebaeude_baked -## Auswahl-Markierung: orangener, halbtransparenter Rahmen um das Bauteil. +## Auswahl-Markierung (U6, t_639135dd): dezente Außenhüllen-Kontur um das +## Bauteil-Mesh — gleiche Technik wie bei den Objekten (selection_outline- +## Shader, Theme-Farbe). Keine halbtransparente Vollbox mehr, die das Teil +## verdeckt. func set_gebaeude_selected(idx: int) -> void: if _gebaeude_sel != null and is_instance_valid(_gebaeude_sel): _gebaeude_sel.queue_free() @@ -514,17 +546,130 @@ func set_gebaeude_selected(idx: int) -> void: var aabb := BuildOps.piece_aabb(list[idx]) if aabb.size.length() < 0.001: return - var box := MeshInstance3D.new() - var bm := BoxMesh.new() - bm.size = aabb.size + Vector3(0.06, 0.06, 0.06) - box.mesh = bm - box.position = aabb.get_center() - var mat := StandardMaterial3D.new() - mat.albedo_color = Color(SELECT_COLOR.r, SELECT_COLOR.g, SELECT_COLOR.b, 0.35) - mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - mat.emission_enabled = true - mat.emission = SELECT_COLOR * 0.3 - box.material_override = mat - add_child(box) - _gebaeude_sel = box \ No newline at end of file + var mesh := BuildOps.piece_mesh(list[idx]) + if mesh == null: + return + # Kontur in GLEICHER Transform wie das echte Bauteil-Mesh (aabb ist + # welt-transformiert — nur als Größen-Guard; das Mesh selbst sitzt am + # Teil-Anker mit rot). + var kontur := MeshInstance3D.new() + kontur.mesh = mesh + var pos: Array = list[idx].get("pos", [0.0, 0.0, 0.0]) + kontur.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2])) + kontur.rotation.y = deg_to_rad(float(list[idx].get("rot", 0.0))) + kontur.material_override = _outline_mat_sel + add_child(kontur) + _gebaeude_sel = kontur + + +# ------------------------------------------------------ U6-Markierung ------- +# (Karte t_639135dd — dezente Auswahl: Kontur statt Vollorange. Eine Quelle +# für Farben/Breiten ist das Theme; der Shader ist die Außenhüllen-Technik.) + +## Theme-Farben + -Breiten in die drei Kontur-Materialien überführen +## (Auswahl / Hover / Partner). Fehlende Einträge → Fallback-Farbe. +func _markierung_theme_laden() -> void: + _outline_mat_sel = _outline_material( + _markierung_farbe("selection_outline"), + _markierung_breite("selection_outline_width_mm"), 0.0) + _outline_mat_hover = _outline_material( + _markierung_farbe("selection_outline_hover"), + _markierung_breite("selection_outline_width_hover_mm"), 0.0) + _outline_mat_partner = _outline_material( + _markierung_farbe("selection_outline_partner"), + _markierung_breite("selection_outline_width_partner_mm"), 3.5) + + +## Eine Markierungs-Farbe aus dem Theme lesen (fehlt sie → Fallback). +func _markierung_farbe(name: String) -> Color: + var theme_res: Variant = load(THEME_PATH) + if theme_res is Theme: + var th := theme_res as Theme + if th.has_color(name, "Editor"): + return th.get_color(name, "Editor") + return OUTLINE_FALLBACK + + +## Eine Markierungs-Breite aus dem Theme lesen (Theme speichert Millimeter +## als Ganzzahl — Theme kennt keine Gleitkommakonstanten). +func _markierung_breite(name: String) -> float: + var theme_res: Variant = load(THEME_PATH) + if theme_res is Theme: + var th := theme_res as Theme + if th.has_constant(name, "Editor"): + return float(th.get_constant(name, "Editor")) / 1000.0 + return 0.06 + + +## Kontur-Material für einen Zustand bauen (streifen > 0 = gestrichelt). +func _outline_material(farbe: Color, breite: float, streifen: float) -> ShaderMaterial: + var mat := ShaderMaterial.new() + var shader: Variant = load(OUTLINE_SHADER) + if shader is Shader: + mat.shader = shader + mat.set_shader_parameter("linien_farbe", farbe) + mat.set_shader_parameter("breite_m", breite) + mat.set_shader_parameter("streifen_hz", streifen) + return mat + + +## Zustand eines Objekts (U6-Abstufung): Auswahl > Partner > Hover > nichts. +func _markierung_zustand(idx: int) -> String: + if _is_selected(idx): + return "sel" + if _is_partner(idx): + return "partner" + if idx >= 0 and idx == hover_index: + return "hover" + return "" + + +## Kontur-Hülle eines Objekt-Markers anlegen/umschalten/restlos entfernen. +## Die Hülle hängt als Kind UNTER dem Mesh (erbt Position + Scale — GLB- +## Skalierung bleibt korrekt) und trägt das Mesh ein zweites Mal mit dem +## Außenhüllen-Material (visuell = zweite Render-Passage um das Modell, +## das Grundmaterial mit seiner Textur bleibt unangetastet). +func _outline_anwenden(marker: Node3D, idx: int) -> void: + if marker == null or not is_instance_valid(marker): + return + var zustand := _markierung_zustand(idx) + var alt: Node = null + if marker.has_meta("outline"): + alt = marker.get_meta("outline") + if zustand.is_empty(): + if alt != null and is_instance_valid(alt): + alt.queue_free() + if marker.has_meta("outline"): + marker.remove_meta("outline") + return + var mat: ShaderMaterial = _outline_mat_sel + if zustand == "hover": + mat = _outline_mat_hover + elif zustand == "partner": + mat = _outline_mat_partner + if alt != null and is_instance_valid(alt): + (alt as MeshInstance3D).material_override = mat + return + var mi := marker.get_child(0) as MeshInstance3D + if mi == null or mi.mesh == null: + return + var huelle := MeshInstance3D.new() + huelle.name = "SelectionOutline" + huelle.mesh = mi.mesh + huelle.material_override = mat + mi.add_child(huelle) + marker.set_meta("outline", huelle) + + +## Hover-Markierung (U6-Abstufung, schwächste Kontur). idx = obj_index oder +## -1 für „kein Hover“. Der Aufrufer ist das Objekt-Werkzeug — der Anschluss +## an die Mausbewegung liegt in interaction.gd (Grenze dieser Karte) und +## kommt als Folge-Karte; die Abstufung selbst ist hier voll wirksam. +func set_hover(idx: int) -> void: + if hover_index == idx: + return + hover_index = idx + for marker in object_markers: + if marker == null or not is_instance_valid(marker): + continue + _outline_anwenden(marker, int(marker.get_meta("obj_index", -1))) \ No newline at end of file diff --git a/tests/probe_u6_render.gd b/tests/probe_u6_render.gd new file mode 100644 index 0000000..471ad34 --- /dev/null +++ b/tests/probe_u6_render.gd @@ -0,0 +1,147 @@ +extends SceneTree +## Beweis-Probe (Karte t_639135dd, WEGWERF — nicht Teil der Suite): +## U6 dezente Auswahl — dasselbe Objekt (fass, echtes GLB mit Textur) auf +## Sand und auf Fels, Auswahl-Markierung im Bild. Zwei Modi über Env: +## U6_MODUS=vorher → alter Baumzustand (Vollorange) — Beweis „vorher“ +## U6_MODUS=nachher → neuer Baumzustand (Kontur, Textur sichtbar) +## Das Terrain ist eine echte Material-Schicht: linke Hälfte Sand (1), +## rechte Hälfte Fels (2) — das Objekt wird je Bild auf die eine oder +## andere Hälfte gesetzt. +## Aufruf: xvfb-run -a godot4 --path . --rendering-driver opengl3 \ +## -s res://tests/probe_u6_render.gd (mit ../scopedvr-client) + +const SAND := 1 +const FELS := 2 + + +func _initialize() -> void: + _run.call_deferred() + + +func _mats_halbe(res: int) -> PackedByteArray: + # Linke Kartenhälfte (x < Mitte) Sand, rechte Fels — Zeile für Zeile. + var mats := PackedByteArray() + mats.resize(res * res) + var i := 0 + for z in res: + for x in res: + mats[i] = SAND if x < res / 2 else FELS + i += 1 + return mats + + +func _x_von_material(material: int, size: float) -> float: + # Objekt-Anker je Hälfte: Sand links (−35 % Größe), Fels rechts (+35 %). + return -0.35 * size if material == SAND else 0.35 * size + + +func _shot(pfad: String) -> void: + await process_frame + await RenderingServer.frame_post_draw + var img := root.get_viewport().get_texture().get_image() + img.save_png(pfad) + print("Bild: %s (%dx%d)" % [pfad, img.get_width(), img.get_height()]) + + +## „Modelle fehlen“-AcceptDialog schließen (kommt erst nach 2 Frames von +## utils/modelle_status.gd — mehrfach probieren, mit Wartezeit dazwischen). +func _dismiss_dialogs() -> void: + for i in 6: + await process_frame + var stack: Array = [root] + while not stack.is_empty(): + var n: Node = stack.pop_back() + if n is AcceptDialog: + (n as AcceptDialog).hide() + n.queue_free() + stack.append_array(n.get_children()) + + +## UI-Panels ausblenden: die 3D-Ansicht bekommt fast das ganze Fenster — +## das Beweisbild zeigt die Kontur groß statt im schmalen Streifen. Vom +## SubViewport-Container aufwärts werden alle GESCHWISTER versteckt. +func _maximiere_3d(main: Node) -> void: + var vp_container: Node = null + var stack: Array = [main] + while not stack.is_empty(): + var n: Node = stack.pop_back() + if n is SubViewportContainer: + vp_container = n + break + stack.append_array(n.get_children()) + if vp_container == null: + return + var laeufer: Node = vp_container + while laeufer != null and laeufer != main: + var elter: Node = laeufer.get_parent() + if elter == null: + break + for kind in elter.get_children(): + if kind != laeufer and kind is Control: + (kind as Control).visible = false + laeufer = elter + + +func _run() -> void: + var modus := OS.get_environment("U6_MODUS") + if modus.is_empty(): + modus = "nachher" + var material := SAND if OS.get_environment("U6_BODEN") != "fels" else FELS + var scene: PackedScene = load("res://scenes/main.tscn") + var main = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + await _dismiss_dialogs() + _maximiere_3d(main) + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_set_size(Vector2i(2328, 1310)) + # Echte Karte: flaches Terrain, Material-Schicht Sand/Fels. + var size := 1024.0 + var res := 129 + var doc: Dictionary = SvmapDocument.create_new("U6Probe", size, "mittag") + var terrain: Dictionary = doc.get("terrain", {}) + terrain["materials_r8_b64"] = SvmapIO.encode_materials(_mats_halbe(res)) + var x := _x_von_material(material, size) + (doc["objects"] as Array).append({ + "id": "fass", "pos": [x, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 20.0, + }) + main.doc = doc + main.call("_after_structure_change") + await process_frame + await process_frame + var cam = main.get("camera") + if cam != null: + # Nah dran: Distanz 5 m, Pivot 1,2 m westlich vom Fass — das Fass + # füllt sichtbar Fläche und sitzt nicht hinter dem Pivot-Gizmo. + cam.set("distance", 5.0) + cam.position = Vector3(x + 2.5, 1.8, 4.0) + cam.call("focus_on", Vector3(x - 1.2, 0.6, 0.0)) + await process_frame + await process_frame + # Objekt über die ECHTE Auswahl anwählen (wie ein Klick ins Editor-UI). + main.interaction.call("select_object", 0) + await process_frame + await process_frame + # Zustands-Beleg in der Konsole (je Modus die passende Beobachtung). + var builder = main.get("view_builder") + var marker: Node3D = builder.object_markers[0] + var mi := marker.get_child(0) as MeshInstance3D + if cam != null: + print("PRUEF cam: pos=%s target=%s fly=%s dist=%s" % [ + str(cam.position), str(cam.get("target")), + str(cam.call("fly_active")), str(cam.get("distance"))]) + var hat_outline: bool = marker.has_meta("outline") \ + and is_instance_valid(marker.get_meta("outline")) + print("PRUEF modus=%s boden=%s glb=%s material_override=%s outline=%s" % [ + modus, "sand" if material == SAND else "fels", + str(bool(marker.get_meta("glb", false))), + str(mi.material_override != null), str(hat_outline)]) + var dir := OS.get_environment("U6_DIR") + if dir.is_empty(): + dir = "/tmp/u6_beweise" + DirAccess.make_dir_recursive_absolute(dir) + var name := "u6_%s_%s.png" % [modus, "sand" if material == SAND else "fels"] + await _shot(dir.path_join(name)) + print("PROBE_U6_%s_%s" % [modus.to_upper(), "OK"]) + quit(0) diff --git a/tests/probe_u6_render.gd.uid b/tests/probe_u6_render.gd.uid new file mode 100644 index 0000000..6b0711a --- /dev/null +++ b/tests/probe_u6_render.gd.uid @@ -0,0 +1 @@ +uid://cu3rrquankmwk diff --git a/tests/test_glb_view.gd b/tests/test_glb_view.gd index d5721ee..2f0ebee 100644 --- a/tests/test_glb_view.gd +++ b/tests/test_glb_view.gd @@ -334,11 +334,17 @@ func _test_viewport_rebuild_echt() -> void: "Mesh-Unterkante == Terrainhöhe (%.3f m, ist %.3f m — b20dff2-Konvention)" % [boden, unten]) _check(mi.material_override == null, "keine Grundfarbe drüber — Texturen des echten Modells bleiben sichtbar") - # Auswahl färbt trotzdem orange (Material-Override NUR im Auswahl-Zustand). + # U6 (t_639135dd): Auswahl = Außenhüllen-Kontur um das Modell — das + # Grundmaterial (mit Textur) bleibt im Auswahl-Zustand KOMPLETT sichtbar, + # es gibt kein material_override mehr für die Markierung. builder.call("set_selected_multi", [0]) - _check(mi.material_override != null, "Auswahl: orange Override an") + _check(mi.material_override == null, + "Auswahl: Textur bleibt sichtbar (kein material_override mehr, U6)") + _check(marker.get_meta("outline", null) is MeshInstance3D, + "Auswahl: Kontur-Outline am echten GLB-Marker (U6)") builder.call("set_selected_multi", []) _check(mi.material_override == null, "Auswahl weg: Texturen wieder frei") + _check(not marker.has_meta("outline"), "Auswahl weg: kein Rest-Outline (U6)") # Skalierung: scale 2.0 → AABB-Doppelte Höhe im Marker-Meta. builder.call("rebuild", _proben_doc(PROBE_OID, 2.0), _load_manifest()) var m2: Node3D = (builder.get("object_markers") as Array)[0] diff --git a/tests/test_map_tint.gd b/tests/test_map_tint.gd index b566ea1..f9e663a 100644 --- a/tests/test_map_tint.gd +++ b/tests/test_map_tint.gd @@ -8,8 +8,8 @@ extends SceneTree ## T2 Farbfeld-Klick → doc-Feld "tint" ([r,g,b], 0..1) + Status „Farbe: Oliv“; ## gleiche Farbe erneut = KEIN Leer-Undo-Schritt. ## T3 Editor-Ansicht: unmarkiertes Objekt zeigt tint (Surface-Override- -## Material albedo = doc-Wert); Auswahl-Orange gewinnt weiter -## (material_override), nach dem Abwählen ist die Farbe wieder sichtbar. +## Material albedo = doc-Wert); Markierung = Auswahl-Kontur (U6), die +## Farbe bleibt sichtbar und liegt trotzdem im doc. ## T4 Mehrfachauswahl: eine Farbe auf beide Objekte = GENAU EIN Undo-Schritt. ## T5 Zurücksetzen (Pack-Farbe): Feld entfernt, Override weg, EIN Undo-Schritt. ## T6 save→load-Roundtrip: tint überlebt; alte Karte ohne Feld lädt @@ -141,7 +141,10 @@ func _t3_editor_ansicht() -> void: insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[3][1], "Stahlgrau") var c: Color = SvmapTint.von(_sel_obj(0)) var mi := _marker_mi(0) - _check(mi.material_override != null, "T3 markiertes Objekt bleibt orange (material_override an)") + # U6 (t_639135dd): Markierung = Kontur am Marker — die doc-Farbe bleibt + # sichtbar (kein „Orange gewinnt“ mehr), das Feld liegt trotzdem im doc. + var out0: Variant = main.view_builder.object_markers[0].get_meta("outline", null) + _check(out0 is MeshInstance3D, "T3 markiertes Objekt zeigt Auswahl-Kontur (U6)") # Abwählen → Farbe sichtbar: Surface-Override mit albedo = doc-Wert. main.interaction.call("clear_selection", true) await process_frame @@ -160,11 +163,12 @@ func _t3_editor_ansicht() -> void: var ov1 := mi1.get_surface_override_material(0) _check(ov1 is BaseMaterial3D and SvmapTint.gleich((ov1 as BaseMaterial3D).albedo_color, SvmapTint.von(_sel_obj(1))), "T3 Platzhalter zeigt doc-Farbe (Holzbraun)") - # Orange gewinnt weiter, FARBE KOMMT ZURÜCK beim Abwählen (Auswahl des - # Objekts 0 lässt 1 unberührt). + # Auswahl-Kontur (U6) statt Orange; FARBE KOMMT ZURÜCK beim Abwählen + # (Auswahl des Objekts 0 lässt 1 unberührt). main.interaction.call("select_object", 1) await process_frame - _check(_marker_mi(1).material_override != null, "T3 Auswahl-Orange gewinnt weiter") + var out1: Variant = main.view_builder.object_markers[1].get_meta("outline", null) + _check(out1 is MeshInstance3D, "T3 Auswahl-Kontur am markierten Objekt (U6)") func _t4_mehrfach() -> void: diff --git a/tests/test_object_mirror.gd b/tests/test_object_mirror.gd index df097e1..8561a18 100644 --- a/tests/test_object_mirror.gd +++ b/tests/test_object_mirror.gd @@ -518,25 +518,37 @@ func _test_partner_marking() -> void: root.add_child(builder) var manifest := {"objects": {"crate_wood": {"collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}} builder.call("rebuild", doc, manifest) - # Quelle (0) anwählen → Zwilling (1) schwächer markiert. + # Quelle (0) anwählen → Zwilling (1) schwächer markiert. U6 (t_639135dd): + # Markierung = Außenhüllen-Kontur statt Vollfarbe — Quelle trägt die + # normale Auswahl-Kontur, der Zwilling die dünnere/gestrichelte Partner- + # Kontur in eigener Theme-Farbe. builder.call("set_selected_multi", [0]) - var select_mat: StandardMaterial3D = builder.get("_select_mat") - var partner_mat: StandardMaterial3D = builder.get("_partner_mat") - var m0: MeshInstance3D = (builder.get("object_markers") as Array)[0].get_child(0) - var m1: MeshInstance3D = (builder.get("object_markers") as Array)[1].get_child(0) - _check(m0.material_override == select_mat, "Auswahl: Quelle volle Orange-Markierung") - _check(m1.material_override == partner_mat, "Zwilling: schwächere Partner-Markierung") - _check(partner_mat != select_mat and partner_mat != builder.get("_base_mat"), + var sel_mat: ShaderMaterial = builder.get("_outline_mat_sel") + var partner_mat: ShaderMaterial = builder.get("_outline_mat_partner") + var markers: Array = builder.get("object_markers") + var k0: Variant = (markers[0] as Node3D).get_meta("outline") \ + if (markers[0] as Node3D).has_meta("outline") else null + var k1: Variant = (markers[1] as Node3D).get_meta("outline") \ + if (markers[1] as Node3D).has_meta("outline") else null + _check(k0 is MeshInstance3D and (k0 as MeshInstance3D).material_override == sel_mat, + "Auswahl: Quelle trägt die Auswahl-Kontur") + _check(k1 is MeshInstance3D and (k1 as MeshInstance3D).material_override == partner_mat, + "Zwilling: schwächere Partner-Kontur") + _check(partner_mat != sel_mat, "Partner-Material ist eigenes, abgeschwächtes Material (gleiche Technik)") - # Beide angewählt → beide volle Markierung, kein Partner-Zustand. + # Beide angewählt → beide volle Kontur, kein Partner-Zustand. builder.call("set_selected_multi", [0, 1]) - _check(m0.material_override == select_mat and m1.material_override == select_mat, - "beide angewählt: beide volle Auswahl-Markierung") - # Abwählen: Basiszustand zurück (GLB fehlt → Platzhalter mit Basis-Farbe). + k0 = (markers[0] as Node3D).get_meta("outline", null) + k1 = (markers[1] as Node3D).get_meta("outline", null) + _check(k0 is MeshInstance3D and (k0 as MeshInstance3D).material_override == sel_mat + and k1 is MeshInstance3D and (k1 as MeshInstance3D).material_override == sel_mat, + "beide angewählt: beide volle Auswahl-Kontur") + # Abwählen: Basiszustand zurück — Konturen restlos weg (Meta entfernt, + # Grundmaterial blieb die ganze Zeit unangetastet). builder.call("set_selected_multi", []) - _check(m0.material_override == builder.get("_base_mat") - and m1.material_override == builder.get("_base_mat"), - "ohne Auswahl: Basis-Farben (wie bisher)") + _check(not (markers[0] as Node3D).has_meta("outline") + and not (markers[1] as Node3D).has_meta("outline"), + "ohne Auswahl: keine Rest-Markierung (U6)") builder.queue_free() diff --git a/tests/test_selection_look.gd b/tests/test_selection_look.gd new file mode 100644 index 0000000..dcaecd7 --- /dev/null +++ b/tests/test_selection_look.gd @@ -0,0 +1,287 @@ +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() diff --git a/tests/test_selection_look.gd.uid b/tests/test_selection_look.gd.uid new file mode 100644 index 0000000..f195af4 --- /dev/null +++ b/tests/test_selection_look.gd.uid @@ -0,0 +1 @@ +uid://eoayxyagpeig diff --git a/theme/editor_theme.tres b/theme/editor_theme.tres index f8f1559..8295b38 100644 --- a/theme/editor_theme.tres +++ b/theme/editor_theme.tres @@ -109,6 +109,12 @@ Editor/colors/update_bg = Color(0.36, 0.25, 0.06, 0.97) Editor/colors/update_border = Color(0.95, 0.72, 0.15, 1) Editor/colors/rubber_fill = Color(0.45, 0.8, 1, 0.16) Editor/colors/rubber_line = Color(0.45, 0.8, 1, 0.9) +Editor/colors/selection_outline = Color(1, 0.6, 0.15, 0.95) +Editor/colors/selection_outline_hover = Color(1, 0.6, 0.15, 0.45) +Editor/colors/selection_outline_partner = Color(1, 0.78, 0.4, 0.85) +Editor/constants/selection_outline_width_mm = 80 +Editor/constants/selection_outline_width_hover_mm = 45 +Editor/constants/selection_outline_width_partner_mm = 45 PanelContainer/styles/panel = SubResource("panel") TabContainer/styles/panel = SubResource("panel") TabContainer/styles/tab_selected = SubResource("tab_sel")