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.
This commit is contained in:
Hermes (scopedvr-dev) 2026-10-20 10:33:04 +00:00
parent f16b9c8fb6
commit 5dca5513da
11 changed files with 741 additions and 91 deletions

View file

@ -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;
}

View file

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

View file

@ -11,8 +11,15 @@ extends Node3D
const GRID_COLOR := Color(0.35, 0.45, 0.55, 0.6) 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 AXIS_COLOR := Color(0.9, 0.7, 0.2, 0.9)
const OBJECT_COLOR := Color(0.55, 0.75, 0.95) 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 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) # Wetter → Licht (grobe Stimmung; Feintuning kommt mit dem Wetter-Konsum)
const WEATHER_LIGHT := { 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)}, "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 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 object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc.objects)
var fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis) 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) 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 grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014)
var _gebaeude_baked: Dictionary = {} # faces/offsets für Ray-Pick (Bauen-Werkzeug) 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 _doc_ref: Dictionary = {} # letztes rebuild-Doc (Auswahl-Markierung)
var _base_mat: StandardMaterial3D 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 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: func _ready() -> void:
@ -59,20 +70,10 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void:
_base_mat = StandardMaterial3D.new() _base_mat = StandardMaterial3D.new()
_base_mat.albedo_color = OBJECT_COLOR _base_mat.albedo_color = OBJECT_COLOR
_base_mat.roughness = 0.8 _base_mat.roughness = 0.8
_select_mat = StandardMaterial3D.new() # U6 (t_639135dd): keine Vollfarben-Materialien mehr — die Markierung
_select_mat.albedo_color = SELECT_COLOR # (Auswahl/Hover/Partner) lebt als Außenhüllen-Kontur um das Modell,
_select_mat.emission_enabled = true # Farben + Breiten aus dem Theme.
_select_mat.emission = SELECT_COLOR * 0.35 _markierung_theme_laden()
_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
_sym_partners = [] _sym_partners = []
if data.is_empty(): if data.is_empty():
@ -252,7 +253,10 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void:
mi.scale = Vector3.ONE * scale mi.scale = Vector3.ONE * scale
else: else:
mi.position.y = MeshShapes.half_height(mesh) 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) marker.add_child(mi)
var half: Vector3 = (mesh.get_aabb().size * (scale if echt else 1.0) * 0.5) as Vector3 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)) 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) add_child(marker)
# Farbe je Objekt (Karte t_b0edb807): optionales Feld "tint" — nur # Farbe je Objekt (Karte t_b0edb807): optionales Feld "tint" — nur
# Objekte MIT Feld bekommen die Überschreibung (Rückwärtskompatibel). # Objekte MIT Feld bekommen die Überschreibung (Rückwärtskompatibel).
# Ausgewählte Objekte bleiben orange (Auswahl gewinnt weiter, Tint # Die Farbe bleibt jetzt auch bei Markierung sichtbar (U6: Auswahl = Kontur,
# kommt beim Abwählen über set_selected_multi). # Grundmaterial bleibt) — kein „Orange gewinnt“ mehr.
if SvmapTint.hat(obj) and not _is_selected(i): if SvmapTint.hat(obj):
SvmapTint.anwenden(marker, SvmapTint.von(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. ## 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 []) set_selected_multi([idx] if idx >= 0 else [])
## Mehrfachauswahl (Karte t_f6f29d86): ALLE markierten Objekte färben sich ## Mehrfachauswahl (Karte t_f6f29d86): ALLE markierten Objekte bekommen die
## orange — Gruppen/Mehrfachauswahl sind auf einen Blick erkennbar. ## Auswahl-Kontur (U6, t_639135dd) — Gruppen/Mehrfachauswahl sind auf einen
## Echte GLB-Objekte (Meta „glb") bekommen im ungewählten Zustand KEINE ## Blick erkennbar, OHNE die Textur zu übermalen. Das Grundmaterial bleibt
## Grundfarbe drübergelegt — material_override würde die Texturen verstecken ## überall sichtbar: echte GLB-Objekte tragen ihre Texturen weiter (Karte
## (Karte t_9b50b231); orange bleibt auch dort das Auswahl-Signal. ## t_9b50b231), Platzhalter die Editor-Grundfarbe — material_override wird
## von der Markierung nicht mehr angefasst.
func set_selected_multi(indices: Array) -> void: func set_selected_multi(indices: Array) -> void:
selected_indices = indices.duplicate() selected_indices = indices.duplicate()
selected_index = indices.back() if not indices.is_empty() else -1 selected_index = indices.back() if not indices.is_empty() else -1
# sym-Zwillinge (M3, t_34908425): aus dem doc ableiten — Partner der # 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) _sym_partners = _sym_partners_of(indices)
var objects: Array = _doc_ref.get("objects", []) var objects: Array = _doc_ref.get("objects", [])
for marker in object_markers: for marker in object_markers:
if marker == null or not is_instance_valid(marker): if marker == null or not is_instance_valid(marker):
continue continue
var idx := int(marker.get_meta("obj_index", -1)) var idx := int(marker.get_meta("obj_index", -1))
var mi: MeshInstance3D = marker.get_child(0) _outline_anwenden(marker, idx)
var echt := bool(marker.get_meta("glb", false)) # Tint-Abgleich (Karte t_b0edb807): ALLE Objekte zeigen ihre doc-Farbe
var ist_sel := _is_selected(idx) # (oder — nach „Zurücksetzen“ — wieder die Pack-Farbe). Der Tint bleibt
var ist_partner := not ist_sel and _is_partner(idx) # jetzt auch bei Markierung sichtbar (Auswahl = Kontur um das Modell).
mi.material_override = _select_mat if ist_sel \ if idx >= 0 and idx < objects.size() and objects[idx] is Dictionary:
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:
var obj: Dictionary = objects[idx] var obj: Dictionary = objects[idx]
if SvmapTint.hat(obj): if SvmapTint.hat(obj):
SvmapTint.anwenden(marker, SvmapTint.von(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- ## Tint frisch aus dem doc auf die MARKIERTEN Objekte anwenden (Inspector-
## Änderung ohne Rebuild). Markierte Objekte zeigen weiter Auswahl-Orange — ## Änderung ohne Rebuild). U6: die Farbe bleibt auch bei Markierung sichtbar
## die Farbe liegt im doc und erscheint beim Abwählen (set_selected_multi). ## (Auswahl = Kontur um das Modell) — kein „Orange gewinnt“-Skip mehr.
func tint_refresh(indices: Array) -> void: func tint_refresh(indices: Array) -> void:
var objects: Array = _doc_ref.get("objects", []) var objects: Array = _doc_ref.get("objects", [])
for i in indices: for i in indices:
@ -344,8 +345,6 @@ func tint_refresh(indices: Array) -> void:
var marker = object_markers[i] var marker = object_markers[i]
if marker == null or not is_instance_valid(marker): if marker == null or not is_instance_valid(marker):
continue continue
if _is_selected(i):
continue
if i >= objects.size() or not (objects[i] is Dictionary): if i >= objects.size() or not (objects[i] is Dictionary):
continue continue
var obj: Dictionary = objects[i] 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 ## Kreisfläche EINES Markers (neu) bauen — Werkzeuge rufen nach Radius-Änderung
## style_spawn_marker auf. ## style_spawn_marker auf. U6 (t_639135dd): die Fläche bleibt IMMER gleich
func _spawn_disc(radius: float, team: int, selected := false) -> MeshInstance3D: ## 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 mi := MeshInstance3D.new()
var cyl := CylinderMesh.new() var cyl := CylinderMesh.new()
cyl.top_radius = radius cyl.top_radius = radius
@ -425,12 +426,28 @@ func _spawn_disc(radius: float, team: int, selected := false) -> MeshInstance3D:
mi.position.y = 0.12 mi.position.y = 0.12
var col: Color = TEAM_COLORS[team] if team >= 0 and team < TEAM_COLORS.size() else Color.WHITE var col: Color = TEAM_COLORS[team] if team >= 0 and team < TEAM_COLORS.size() else Color.WHITE
var mat := StandardMaterial3D.new() 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.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
if selected:
mat.emission_enabled = true
mat.emission = col * 0.6
mi.material_override = mat mi.material_override = mat
return mi return mi
@ -450,6 +467,17 @@ func style_spawn_marker(marker: Node3D, radius: float, selected: bool) -> void:
var disc := _spawn_disc(radius, team, selected) var disc := _spawn_disc(radius, team, selected)
marker.add_child(disc) marker.add_child(disc)
marker.move_child(disc, 0) 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). ## 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: if bool(marker.get_meta("spawn_selected", false)) == is_sel:
continue continue
style_spawn_marker(marker, float(marker.get_meta("spawn_radius", 6.0)), is_sel) 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 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: if marker.get_child_count() > 1 and marker.get_child(1) is Label3D:
var label := marker.get_child(1) as 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 ------ # ------------------------------------------------------------ Bauteile ------
@ -503,7 +532,10 @@ func gebaeude_baked() -> Dictionary:
return _gebaeude_baked 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: func set_gebaeude_selected(idx: int) -> void:
if _gebaeude_sel != null and is_instance_valid(_gebaeude_sel): if _gebaeude_sel != null and is_instance_valid(_gebaeude_sel):
_gebaeude_sel.queue_free() _gebaeude_sel.queue_free()
@ -514,17 +546,130 @@ func set_gebaeude_selected(idx: int) -> void:
var aabb := BuildOps.piece_aabb(list[idx]) var aabb := BuildOps.piece_aabb(list[idx])
if aabb.size.length() < 0.001: if aabb.size.length() < 0.001:
return return
var box := MeshInstance3D.new() var mesh := BuildOps.piece_mesh(list[idx])
var bm := BoxMesh.new() if mesh == null:
bm.size = aabb.size + Vector3(0.06, 0.06, 0.06) return
box.mesh = bm # Kontur in GLEICHER Transform wie das echte Bauteil-Mesh (aabb ist
box.position = aabb.get_center() # welt-transformiert — nur als Größen-Guard; das Mesh selbst sitzt am
var mat := StandardMaterial3D.new() # Teil-Anker mit rot).
mat.albedo_color = Color(SELECT_COLOR.r, SELECT_COLOR.g, SELECT_COLOR.b, 0.35) var kontur := MeshInstance3D.new()
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA kontur.mesh = mesh
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED var pos: Array = list[idx].get("pos", [0.0, 0.0, 0.0])
mat.emission_enabled = true kontur.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
mat.emission = SELECT_COLOR * 0.3 kontur.rotation.y = deg_to_rad(float(list[idx].get("rot", 0.0)))
box.material_override = mat kontur.material_override = _outline_mat_sel
add_child(box) add_child(kontur)
_gebaeude_sel = box _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)))

147
tests/probe_u6_render.gd Normal file
View file

@ -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)

View file

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

View file

@ -334,11 +334,17 @@ func _test_viewport_rebuild_echt() -> void:
"Mesh-Unterkante == Terrainhöhe (%.3f m, ist %.3f m — b20dff2-Konvention)" % [boden, unten]) "Mesh-Unterkante == Terrainhöhe (%.3f m, ist %.3f m — b20dff2-Konvention)" % [boden, unten])
_check(mi.material_override == null, _check(mi.material_override == null,
"keine Grundfarbe drüber — Texturen des echten Modells bleiben sichtbar") "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]) 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", []) builder.call("set_selected_multi", [])
_check(mi.material_override == null, "Auswahl weg: Texturen wieder frei") _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. # Skalierung: scale 2.0 → AABB-Doppelte Höhe im Marker-Meta.
builder.call("rebuild", _proben_doc(PROBE_OID, 2.0), _load_manifest()) builder.call("rebuild", _proben_doc(PROBE_OID, 2.0), _load_manifest())
var m2: Node3D = (builder.get("object_markers") as Array)[0] var m2: Node3D = (builder.get("object_markers") as Array)[0]

View file

@ -8,8 +8,8 @@ extends SceneTree
## T2 Farbfeld-Klick → doc-Feld "tint" ([r,g,b], 0..1) + Status „Farbe: Oliv“; ## T2 Farbfeld-Klick → doc-Feld "tint" ([r,g,b], 0..1) + Status „Farbe: Oliv“;
## gleiche Farbe erneut = KEIN Leer-Undo-Schritt. ## gleiche Farbe erneut = KEIN Leer-Undo-Schritt.
## T3 Editor-Ansicht: unmarkiertes Objekt zeigt tint (Surface-Override- ## T3 Editor-Ansicht: unmarkiertes Objekt zeigt tint (Surface-Override-
## Material albedo = doc-Wert); Auswahl-Orange gewinnt weiter ## Material albedo = doc-Wert); Markierung = Auswahl-Kontur (U6), die
## (material_override), nach dem Abwählen ist die Farbe wieder sichtbar. ## Farbe bleibt sichtbar und liegt trotzdem im doc.
## T4 Mehrfachauswahl: eine Farbe auf beide Objekte = GENAU EIN Undo-Schritt. ## T4 Mehrfachauswahl: eine Farbe auf beide Objekte = GENAU EIN Undo-Schritt.
## T5 Zurücksetzen (Pack-Farbe): Feld entfernt, Override weg, 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 ## 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") insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[3][1], "Stahlgrau")
var c: Color = SvmapTint.von(_sel_obj(0)) var c: Color = SvmapTint.von(_sel_obj(0))
var mi := _marker_mi(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. # Abwählen → Farbe sichtbar: Surface-Override mit albedo = doc-Wert.
main.interaction.call("clear_selection", true) main.interaction.call("clear_selection", true)
await process_frame await process_frame
@ -160,11 +163,12 @@ func _t3_editor_ansicht() -> void:
var ov1 := mi1.get_surface_override_material(0) var ov1 := mi1.get_surface_override_material(0)
_check(ov1 is BaseMaterial3D and SvmapTint.gleich((ov1 as BaseMaterial3D).albedo_color, _check(ov1 is BaseMaterial3D and SvmapTint.gleich((ov1 as BaseMaterial3D).albedo_color,
SvmapTint.von(_sel_obj(1))), "T3 Platzhalter zeigt doc-Farbe (Holzbraun)") SvmapTint.von(_sel_obj(1))), "T3 Platzhalter zeigt doc-Farbe (Holzbraun)")
# Orange gewinnt weiter, FARBE KOMMT ZURÜCK beim Abwählen (Auswahl des # Auswahl-Kontur (U6) statt Orange; FARBE KOMMT ZURÜCK beim Abwählen
# Objekts 0 lässt 1 unberührt). # (Auswahl des Objekts 0 lässt 1 unberührt).
main.interaction.call("select_object", 1) main.interaction.call("select_object", 1)
await process_frame 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: func _t4_mehrfach() -> void:

View file

@ -518,25 +518,37 @@ func _test_partner_marking() -> void:
root.add_child(builder) root.add_child(builder)
var manifest := {"objects": {"crate_wood": {"collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}} var manifest := {"objects": {"crate_wood": {"collision": {"shape": "box", "size": [1.2, 1.2, 1.2]}}}}
builder.call("rebuild", doc, manifest) 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]) builder.call("set_selected_multi", [0])
var select_mat: StandardMaterial3D = builder.get("_select_mat") var sel_mat: ShaderMaterial = builder.get("_outline_mat_sel")
var partner_mat: StandardMaterial3D = builder.get("_partner_mat") var partner_mat: ShaderMaterial = builder.get("_outline_mat_partner")
var m0: MeshInstance3D = (builder.get("object_markers") as Array)[0].get_child(0) var markers: Array = builder.get("object_markers")
var m1: MeshInstance3D = (builder.get("object_markers") as Array)[1].get_child(0) var k0: Variant = (markers[0] as Node3D).get_meta("outline") \
_check(m0.material_override == select_mat, "Auswahl: Quelle volle Orange-Markierung") if (markers[0] as Node3D).has_meta("outline") else null
_check(m1.material_override == partner_mat, "Zwilling: schwächere Partner-Markierung") var k1: Variant = (markers[1] as Node3D).get_meta("outline") \
_check(partner_mat != select_mat and partner_mat != builder.get("_base_mat"), 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)") "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]) builder.call("set_selected_multi", [0, 1])
_check(m0.material_override == select_mat and m1.material_override == select_mat, k0 = (markers[0] as Node3D).get_meta("outline", null)
"beide angewählt: beide volle Auswahl-Markierung") k1 = (markers[1] as Node3D).get_meta("outline", null)
# Abwählen: Basiszustand zurück (GLB fehlt → Platzhalter mit Basis-Farbe). _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", []) builder.call("set_selected_multi", [])
_check(m0.material_override == builder.get("_base_mat") _check(not (markers[0] as Node3D).has_meta("outline")
and m1.material_override == builder.get("_base_mat"), and not (markers[1] as Node3D).has_meta("outline"),
"ohne Auswahl: Basis-Farben (wie bisher)") "ohne Auswahl: keine Rest-Markierung (U6)")
builder.queue_free() builder.queue_free()

View file

@ -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()

View file

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

View file

@ -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/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_fill = Color(0.45, 0.8, 1, 0.16)
Editor/colors/rubber_line = Color(0.45, 0.8, 1, 0.9) 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") PanelContainer/styles/panel = SubResource("panel")
TabContainer/styles/panel = SubResource("panel") TabContainer/styles/panel = SubResource("panel")
TabContainer/styles/tab_selected = SubResource("tab_sel") TabContainer/styles/tab_selected = SubResource("tab_sel")