Farbe je Objekt (t_b0edb807): Inspektor-Abschnitt 'Farbe / Material' mit 7er-Palette (Wüstensand/Oliv/Tarn-Grün/Stahlgrau/Holzbraun/Dunkelbraun/Weiß) + 'Zurücksetzen (Pack-Farbe)'; Mehrfachauswahl = EIN Undo-Schritt; Speichern als OPTIONALES Objekt-Feld tint:[r,g,b] (Anzeige-sRGB, 4 Stellen — Rückwärtskompatibel, Karten ohne Feld unverändert). Neuer Helfer src/svmap/svmap_tint.gd (1:1-Spiegel im Client, gemeinsame Semantik Editor+Spiel): albedo_color-ERSATZ via Surface-Override-Kopien — Original-Materialien bleiben unangetastet, Auswahl-Orange (material_override) gewinnt weiter; set_selected_multi re-synced Tint beim Abwählen, tint_refresh für Inspector-Änderungen ohne Rebuild. Map-View-Bauer wendet tint beim Aufbau an (echte GLBs UND Platzhalter). tests/test_map_tint.gd neu (31 Checks grün), Editor-Gesamtsuite 18/18 (gui_smoke-F5-Artefakt per Gegenprobe entkräftet, updater falsch-rot im Sammel-Filter). Rauheit/Metallisch bewusst weggelassen (Begründung im Handoff/BEWEIS.md).

This commit is contained in:
Hermes (scopedvr-dev) 2026-10-19 20:54:34 +00:00
parent c8b9c59fda
commit 9fa8029910
8 changed files with 661 additions and 1 deletions

149
src/svmap/svmap_tint.gd Normal file
View file

@ -0,0 +1,149 @@
extends RefCounted
class_name SvmapTint
## Per-Objekt-Farbe (Tint) für den Map Builder — Karte t_b0edb807.
##
## Format (svmap, OPTIONALES Feld je Objekt):
## "tint": [r, g, b] — Display-Farben (sRGB, 0..1, auf 4 Stellen gerundet)
##
## FARBRAUM-REGEL (Lektion D9, 19.10.): gespeichert wird der ANZEIGE-Wert
## (sRGB, wie der Godot-Farbpicker ihn zeigt). Beim Anwenden wird der Wert
## DIREKT in `albedo_color` geschrieben — Godot interpretiert albedo_color
## als Anzeige-Farbe (flach beleuchtet zeigt der Pixel exakt diesen Wert).
## Das ist bewusst NICHT der glTF-Weg (dort wäre baseColorFactor linear):
## solange Editor und Client denselben Wert denselben Weg schreiben,
## rendern sie identisch — und genau das garantiert dieser Helfer.
##
## Semantik des Anwendens: `albedo_color` wird ERSETZT (nicht multipliziert).
## - Ungetexturierte Materialien (Military-Desert-Pack, bewiesenermaßen ohne
## Texturen) nehmen die gewählte Farbe 1:1 an — das ist die intuitive
## Erwartung an „Farbe wählen“.
## - Texturierte Materialien (Alt-Bestand/Waffen) bekommen die Textur ×
## Farbe (Godot multipliziert albedo_texture × albedo_color) — Weiß der
## Palette lässt sie unverändert, dunkle Palette tönt sie ab.
## „Zurücksetzen (Pack-Farbe)“ entfernt das Feld; ohne Feld läuft ALLES wie
## vorher (Rückwärtskompatibilität: alte Maps ohne tint laden unverändert).
##
## Anwendung als SURFACE-OVERRIDE-Materialien (Kopien): die Original-
## Materialien am Mesh bleiben unangetastet, das Feld ist damit reversibel,
## und das Auswahl-Orange (material_override) gewinnt weiterhin — Godot
## gibt material_override Vorrang vor surface_override_materials.
##
## DIESE DATEI ist bewusst 1:1 im Client-Repo vorhanden
## (scopedvr-client scripts/world/svmap_tint.gd) — Editor und Client müssen
## denselben Code nutzen, damit gespeicherte Farbe = gespielte Farbe.
## Bei Änderungen: BEIDE Kopien identisch anpassen.
const FIELD := "tint"
## Feste Palette (Namen wie auf der Karte vereinbart). Reihenfolge = UI-Reihenfolge.
const PALETTE := [
["Wüstensand", Color(0.76, 0.70, 0.50)],
["Oliv", Color(0.50, 0.52, 0.30)],
["Tarn-Grün", Color(0.30, 0.40, 0.24)],
["Stahlgrau", Color(0.45, 0.47, 0.50)],
["Holzbraun", Color(0.55, 0.40, 0.25)],
["Dunkelbraun", Color(0.32, 0.23, 0.14)],
["Weiß", Color(0.92, 0.92, 0.92)],
]
const EPS := 0.003 # Farb-Gleichheit (4 Nachkommastellen gerundet)
## Objekt hat ein gültiges tint-Feld?
static func hat(obj: Dictionary) -> bool:
return gueltig(obj.get(FIELD, null))
## Feld typo-/bereichsgeprüft: Array mit 3 Zahlen 0..1.
static func gueltig(v: Variant) -> bool:
if not (v is Array) or (v as Array).size() != 3:
return false
for c in v:
if not (c is float or c is int):
return false
if float(c) < 0.0 or float(c) > 1.0:
return false
return true
## Farbe eines Objekts lesen (ohne Feld → Weiß = neutral, Anzeigewert).
static func von(obj: Dictionary) -> Color:
var v: Variant = obj.get(FIELD, null)
if gueltig(v):
var a: Array = v
return Color(float(a[0]), float(a[1]), float(a[2]))
return Color.WHITE
## Farbe speichern (Anzeigewert, 4 Stellen — reicht für Visuelles, klein im JSON).
static func setze(obj: Dictionary, c: Color) -> void:
obj[FIELD] = [
roundf(c.r * 10000.0) / 10000.0,
roundf(c.g * 10000.0) / 10000.0,
roundf(c.b * 10000.0) / 10000.0,
]
## Feld entfernen („Zurücksetzen (Pack-Farbe)“).
static func entferne(obj: Dictionary) -> void:
obj.erase(FIELD)
## Zwei tint-Werte „gleich“ (Anzeige-Toleranz)?
static func gleich(a: Color, b: Color) -> bool:
return absf(a.r - b.r) <= EPS and absf(a.g - b.g) <= EPS and absf(a.b - b.b) <= EPS
## Paletten-Namen zu einer Farbe finden („“ = keine Palette-Farbe / gemischt).
static func palette_name(c: Color) -> String:
for eintrag in PALETTE:
if gleich(c, eintrag[1]):
return str(eintrag[0])
return ""
## Tint auf eine Visual-Struktur anwenden: ALLE MeshInstance3D darunter
## bekommen pro Fläche eine KOPIE ihres aktiven Materials mit der neuen
## albedo_color. Vorhandene Surface-Overrides werden ersetzt (idempotent).
## material_override (Auswahl-Orange) bleibt unangetastet und gewinnt.
static func anwenden(visual: Node, c: Color) -> void:
if visual == null or not is_instance_valid(visual):
return
_walk_anwenden(visual, c)
static func _walk_anwenden(n: Node, c: Color) -> void:
if n is MeshInstance3D:
var mi := n as MeshInstance3D
if mi.mesh != null:
for s in mi.mesh.get_surface_count():
var mat := mi.get_active_material(s)
# Ausgewählte Objekte (material_override = Auswahl-Material)
# überspringt der AUFRUFER, indem er tint erst nach dem
# Abwählen anwendet — hier nichts Sonderbehandeln.
if mat is BaseMaterial3D:
var dup: BaseMaterial3D = (mat as BaseMaterial3D).duplicate()
dup.albedo_color = Color(c.r, c.g, c.b,
(mat as BaseMaterial3D).albedo_color.a)
mi.set_surface_override_material(s, dup)
for kind in n.get_children():
_walk_anwenden(kind, c)
## Tint visuell zurücknehmen: alle gesetzten Surface-Override-Materialien
## entfernen → die Original-Materialien am Mesh zeigen wieder die Pack-Farbe
## (Platzhalter fallen auf ihr material_override/_base zurück).
static func zuruecknehmen(visual: Node) -> void:
if visual == null or not is_instance_valid(visual):
return
_walk_zurueck(visual)
static func _walk_zurueck(n: Node) -> void:
if n is MeshInstance3D:
var mi := n as MeshInstance3D
if mi.mesh != null:
for s in mi.mesh.get_surface_count():
mi.set_surface_override_material(s, null)
for kind in n.get_children():
_walk_zurueck(kind)

View file

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

View file

@ -154,6 +154,9 @@ var _info: Label
var _hint: Label var _hint: Label
var _reset_btn: Button var _reset_btn: Button
var _ground_btn: Button var _ground_btn: Button
var _farben_grid: GridContainer # Farbfeld-Raster (Karte t_b0edb807)
var _farbe_reset_btn: Button
var _farbe_status: Label
var _rows := {} # field -> DragValue var _rows := {} # field -> DragValue
var _built := false var _built := false
@ -227,6 +230,31 @@ func setup(p_main) -> void:
btn_row.add_child(_ground_btn) btn_row.add_child(_ground_btn)
_grid.add_child(btn_row) _grid.add_child(btn_row)
# --- Farbe / Material (Karte t_b0edb807) ---
# Feste Palette als Farbfeld-Raster; Klick = Farbe auf ALLE markierten
# Objekte (EIN Undo-Schritt). „Zurücksetzen“ entfernt das tint-Feld →
# wieder die Pack-Farbe (Rückwärtskompatibel, wie vor der Karte).
_grid.add_child(_field("Farbe / Material"))
_farben_grid = GridContainer.new()
_farben_grid.columns = 4
_farben_grid.add_theme_constant_override("h_separation", 6)
_farben_grid.add_theme_constant_override("v_separation", 6)
_grid.add_child(_farben_grid)
for eintrag in SvmapTint.PALETTE:
_farben_grid.add_child(_swatch(eintrag[1], str(eintrag[0])))
var farbe_row := HBoxContainer.new()
farbe_row.add_theme_constant_override("separation", 6)
_farbe_reset_btn = Button.new()
_farbe_reset_btn.text = "Zurücksetzen (Pack-Farbe)"
_farbe_reset_btn.custom_minimum_size = UIScale.v2(0, 40)
_farbe_reset_btn.tooltip_text = "Farbe der markierten Objekte entfernen — sie zeigen wieder die Pack-Farbe — EIN Rückgängig-Schritt"
_farbe_reset_btn.pressed.connect(_on_farbe_reset)
farbe_row.add_child(_farbe_reset_btn)
_grid.add_child(farbe_row)
_farbe_status = Label.new()
_farbe_status.text = ""
_grid.add_child(_farbe_status)
# --- Info (nur lesen) --- # --- Info (nur lesen) ---
_info = Label.new() _info = Label.new()
_info.text = "" _info.text = ""
@ -248,6 +276,92 @@ func _field(text: String) -> Label:
return l return l
## Ein Farbfeld-Knopf (Karte t_b0edb807): flacher Farb-Button mit Tooltip.
func _swatch(c: Color, name: String) -> Button:
var btn := Button.new()
btn.custom_minimum_size = UIScale.v2(34, 34)
btn.tooltip_text = "%s — Farbe auf alle markierten Objekte anwenden (EIN Rückgängig-Schritt)" % name
btn.focus_mode = Control.FOCUS_NONE
var sb := StyleBoxFlat.new()
sb.bg_color = c
sb.set_corner_radius_all(6)
btn.add_theme_stylebox_override("normal", sb)
var sb_hover: StyleBoxFlat = sb.duplicate()
sb_hover.bg_color = Color(minf(c.r + 0.08, 1.0), minf(c.g + 0.08, 1.0), minf(c.b + 0.08, 1.0))
btn.add_theme_stylebox_override("hover", sb_hover)
btn.add_theme_stylebox_override("pressed", sb_hover)
btn.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
btn.pressed.connect(_on_farbe_gewaehlt.bind(c, name))
return btn
# ------------------------------------------------------------- Farbe/Material ---
## Palette-Farbe auf ALLE markierten Objekte — EIN Undo-Schritt.
func _on_farbe_gewaehlt(c: Color, name: String) -> void:
var indices := _selected()
if indices.is_empty():
return
var before: Dictionary = _main.history.snapshot(["objects"])
var objects: Array = _main.doc.get("objects", [])
var changed := false
for i in indices:
var obj: Dictionary = objects[i]
var alt := SvmapTint.von(obj)
SvmapTint.setze(obj, c)
if not SvmapTint.gleich(alt, c):
changed = true
if not changed:
return # gleiche Farbe erneut → kein Leer-Schritt
_commit_common("Inspektor: Farbe %s" % name, before)
_main.view_builder.call("tint_refresh", indices)
## tint-Feld entfernen → Pack-Farbe — EIN Undo-Schritt.
func _on_farbe_reset() -> void:
var indices := _selected()
if indices.is_empty():
return
var before: Dictionary = _main.history.snapshot(["objects"])
var objects: Array = _main.doc.get("objects", [])
var changed := false
for i in indices:
var obj: Dictionary = objects[i]
if SvmapTint.hat(obj):
SvmapTint.entferne(obj)
changed = true
if not changed:
return # nichts hatte eine Farbe → kein Leer-Schritt
_commit_common("Inspektor: Farbe zurückgesetzt (Pack-Farbe)", before)
_main.view_builder.call("tint_refresh", indices)
## Status-Zeile der Farbsektion: Pack-Farbe / Palettenname / eigene Farbe /
## gemischt (Mehrfachauswahl mit verschiedenen Farben).
func _farbe_text(objs: Array) -> String:
if objs.is_empty():
return ""
if objs.size() == 1:
var obj: Dictionary = objs[0]
if not SvmapTint.hat(obj):
return "Farbe: Pack-Farbe"
var c := SvmapTint.von(obj)
var pn := SvmapTint.palette_name(c)
return "Farbe: %s" % (pn if pn != "" else "eigene Farbe")
# Mehrfachauswahl: alle ohne Feld = Pack-Farbe, alle gleich = Name, sonst gemischt.
var first_hat := SvmapTint.hat(objs[0])
var first_c := SvmapTint.von(objs[0])
for obj in objs:
if SvmapTint.hat(obj) != first_hat:
return "Farbe: gemischt"
if first_hat and not SvmapTint.gleich(SvmapTint.von(obj), first_c):
return "Farbe: gemischt"
if not first_hat:
return "Farbe: Pack-Farbe"
var pn2 := SvmapTint.palette_name(first_c)
return "Farbe: %s" % (pn2 if pn2 != "" else "eigene Farbe")
func _make_row(field: String, lbl: String, tip: String, step: float, fmt: String, suffix: String, func _make_row(field: String, lbl: String, tip: String, step: float, fmt: String, suffix: String,
min_v := -1000000.0, max_v := 1000000.0) -> DragValue: min_v := -1000000.0, max_v := 1000000.0) -> DragValue:
var w := DragValue.new() var w := DragValue.new()
@ -383,6 +497,9 @@ func refresh() -> void:
else: else:
w.set_shown(_get_value(field)) w.set_shown(_get_value(field))
# Farbsektion (Karte t_b0edb807): aktueller Farbstand der Auswahl.
_farbe_status.text = _farbe_text(objs)
# Info-Block (nur lesen). # Info-Block (nur lesen).
var lift := ObjectOps.lift_offset(_main.doc, indices[0]) var lift := ObjectOps.lift_offset(_main.doc, indices[0])
var lift_txt := "am Boden" if absf(lift) < ObjectOps.FLOAT_EPSILON else ("%+.2f m über dem Boden" % lift) var lift_txt := "am Boden" if absf(lift) < ObjectOps.FLOAT_EPSILON else ("%+.2f m über dem Boden" % lift)

View file

@ -249,6 +249,12 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void:
marker.set_meta("glb", echt) marker.set_meta("glb", echt)
object_markers.append(marker) object_markers.append(marker)
add_child(marker) 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):
SvmapTint.anwenden(marker, SvmapTint.von(obj))
## Auswahl nachträglich umfärben (ohne ganzen Rebuild) — z. B. nach Drag. ## Auswahl nachträglich umfärben (ohne ganzen Rebuild) — z. B. nach Drag.
@ -266,12 +272,48 @@ func set_selected(idx: int) -> void:
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
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 mi: MeshInstance3D = marker.get_child(0) var mi: MeshInstance3D = marker.get_child(0)
var echt := bool(marker.get_meta("glb", false)) var echt := bool(marker.get_meta("glb", false))
mi.material_override = _select_mat if _is_selected(int(marker.get_meta("obj_index", -1))) else (null if echt else _base_mat) var ist_sel := _is_selected(idx)
mi.material_override = _select_mat if ist_sel 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 bleiben orange; ihr Tint liegt als doc-Feld bereit und
# erscheint beim Abwählen.
if not ist_sel and 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))
else:
SvmapTint.zuruecknehmen(marker)
## 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).
func tint_refresh(indices: Array) -> void:
var objects: Array = _doc_ref.get("objects", [])
for i in indices:
if i < 0 or i >= object_markers.size():
continue
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]
if SvmapTint.hat(obj):
SvmapTint.anwenden(marker, SvmapTint.von(obj))
else:
SvmapTint.zuruecknehmen(marker)
func _is_selected(idx: int) -> bool: func _is_selected(idx: int) -> bool:

107
tests/probe_tint_render.gd Normal file
View file

@ -0,0 +1,107 @@
extends SceneTree
## Beweis-Probe (Karte t_b0edb807, WEGWERF — nicht Teil der Suite):
## Editor-Renderprobe für „Farbe je Objekt“ — 3 verschieden gefärbte Objekte
## (tint-Feld) im echten Map Builder, ungefärbtes Objekt daneben als Referenz
## (Pack-Farbe). Speichert einen Screenshot (PROBE_PNG-Umgebungsvariable).
## Aufruf: xvfb-run -a godot4 --path . --rendering-driver opengl3 \
## -s res://tests/probe_tint_render.gd
const PLATZ := [
{"id": "barrel_001", "pos": [8.0, 0.0, 8.0], "scale": 1.5, "rot_y_deg": 30.0,
"tint": [0.5, 0.52, 0.3]}, # Oliv (Palette)
{"id": "crate_wood", "pos": [14.0, 0.0, 6.0], "scale": 1.0, "rot_y_deg": 0.0,
"tint": [0.92, 0.92, 0.92]}, # Weiß (Palette)
{"id": "tank_001", "pos": [2.0, 0.0, 16.0], "scale": 1.0, "rot_y_deg": 200.0,
"tint": [0.32, 0.23, 0.14]}, # Dunkelbraun (Palette)
{"id": "apc_001", "pos": [-8.0, 0.0, 14.0], "scale": 1.0, "rot_y_deg": 0.0},
]
func _initialize() -> void:
_run.call_deferred()
func _run() -> void:
# GLB-Quelle = Client-Repo (dort liegen NACH D11-Camo die aktuellen GLBs
# mit Camo-Textur). Damit zeigen Editor-Probe und Client-Probe DENSELBEN
# Asset-Stand — genau das ist der Beweis (Editor-Ansicht == Spiel-Ansicht).
# Ohne diese Zeile würde der Editor die Alt-GLBs aus dem Match-Server-
# Lager laden (Quelle 1 hat Vorrang) und der Farb-Beweis vergliche
# unterschiedliche Asset-Stände.
ProjectSettings.set_setting("scopedvr/editor/glb_quellen",
["../scopedvr-client/assets/objekte"])
load("res://src/viewport/glb_mesh_store.gd").call("cache_leeren")
var scene: PackedScene = load("res://scenes/main.tscn")
var main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
# Erst NACH _ready setzen — die Main-UI setzt ihren Fensterzustand selbst
# (maximiert/Lage-Restore) und überschreibt frühere Größen (xvfb ohne WM).
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_size(Vector2i(2328, 1310))
for p in PLATZ:
(main.doc["objects"] as Array).append(p.duplicate(true))
main.call("_after_structure_change")
await process_frame
await process_frame
var cam = main.get("camera")
if cam != null:
cam.set("distance", 28.0) # nah ran — Objekte sollen das Bild füllen
cam.call("focus_on", Vector3(4.0, 0.0, 10.0))
await process_frame
await process_frame
# Beweis-Zeilen: die 3 tint-Objekte tragen das Surface-Override mit der
# doc-Farbe (bei echten GLBs UND bei Platzhaltern); das ungefärbte echte
# GLB hat KEINEN Surface-Override (Pack-Farbe bleibt sichtbar).
# (VOR der Auswahl geprüft — die Auswahl legt ihr eigenes
# material_override auf das markierte Objekt.)
var builder = main.get("view_builder")
var pruef_ok := true
for i in PLATZ.size():
var marker = builder.object_markers[i]
var mi := marker.get_child(0) as MeshInstance3D
var ov := mi.get_surface_override_material(0)
var obj: Dictionary = PLATZ[i]
if obj.has("tint"):
var soll_c := Color(obj["tint"][0], obj["tint"][1], obj["tint"][2])
var ok := ov is BaseMaterial3D \
and (ov as BaseMaterial3D).albedo_color.is_equal_approx(soll_c)
if not ok:
pruef_ok = false
print("PRUEF FAIL: obj %d (%s) albedo=%s (soll %s)" % [i, obj["id"],
str((ov as BaseMaterial3D).albedo_color if ov is BaseMaterial3D else "?"), str(soll_c)])
else:
print("PRUEF ok: obj %d (%s) albedo=%s" % [i, obj["id"],
str((ov as BaseMaterial3D).albedo_color)])
else:
var ok2 := ov == null and mi.material_override == null
if not ok2:
pruef_ok = false
print("PRUEF FAIL: obj %d (%s) hat Override ohne tint-Feld" % [i, obj["id"]])
else:
print("PRUEF ok: obj %d (%s) ohne Override (Pack-Farbe)" % [i, obj["id"]])
await process_frame
await process_frame
await process_frame
await process_frame
# APC (ungefärbt, Objekt 3) auswählen — NUR für den Screenshot: der
# Inspektor zeigt dann das Farbfeld-Raster mit „Farbe: Pack-Farbe“,
# während die 3 gefärbten Objekte ihre tint-Farben unverändert zeigen.
# PROBE_NO_SELECT=1 → kein Auswahl (reiner Farb-Vergleich zum Client).
if OS.get_environment("PROBE_NO_SELECT") == "":
main.interaction.call("select_object", 3)
await process_frame
await process_frame
var pfad := OS.get_environment("PROBE_PNG")
if pfad.is_empty():
pfad = "user://probe_tint_editor.png"
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()])
print("PROBE_TINT_EDITOR_%s" % ("OK" if pruef_ok else "FAIL"))
quit(0 if pruef_ok else 1)

View file

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

242
tests/test_map_tint.gd Normal file
View file

@ -0,0 +1,242 @@
extends SceneTree
## Editor-Test (headless): Farbe je Objekt (Tint) im Inspektor + doc + View —
## Karte t_b0edb807.
## Ausführen: godot --headless --path . --script res://tests/test_map_tint.gd
## Exit 0 = alles grün.
##
## T1 Palette: 7 feste Farben mit den vereinbarten Namen.
## 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.
## 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
## unverändert (Validierung ohne Fehler, kein Feld erfunden).
var failures := 0
var checks := 0
var main
var insp
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 _undo_steps_to_clear() -> int:
var n := 0
while main.history.can_undo() and n < 50:
main.call("_do_undo")
n += 1
return n
func _sel_obj(idx: int) -> Dictionary:
return (main.doc["objects"] as Array)[idx]
func _swatch(name: String) -> Button:
for kind in insp.get("_farben_grid").get_children():
if kind is Button and str(kind.tooltip_text).begins_with(name):
return kind
return null
func _marker_mi(idx: int) -> MeshInstance3D:
var marker = main.view_builder.object_markers[idx]
return marker.get_child(0) as MeshInstance3D
func _run_all() -> void:
var scene: PackedScene = load("res://scenes/main.tscn")
_check(scene != null, "scenes/main.tscn laedt")
if scene == null:
_finish()
return
main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
insp = main.get("inspector")
_check(insp != null, "Inspektor ist in main eingebaut")
if insp == null:
_finish()
return
# Zwei Objekte ins Doc: barrel_001 (echtes GLB) + crate_wood (Platzhalter).
(main.doc["objects"] as Array).append({
"id": "barrel_001", "pos": [4.0, 0.0, 4.0], "scale": 1.0, "rot_y_deg": 0.0})
(main.doc["objects"] as Array).append({
"id": "crate_wood", "pos": [-6.0, 0.0, 2.0], "scale": 2.0, "rot_y_deg": 10.0})
main.call("_after_structure_change")
main.history.clear()
await process_frame
await _t1_palette()
await _t2_farbe_doc()
await _t3_editor_ansicht()
await _t4_mehrfach()
await _t5_reset()
await _t6_roundtrip()
_finish()
func _t1_palette() -> void:
var palette: Array = SvmapTint.PALETTE
_check(palette.size() == 7, "T1 Palette hat 7 Farben (ist %d)" % palette.size())
var namen := PackedStringArray()
for e in palette:
namen.append(str(e[0]))
var soll := ["Wüstensand", "Oliv", "Tarn-Grün", "Stahlgrau", "Holzbraun", "Dunkelbraun", "Weiß"]
var alle := true
for n in soll:
if not namen.has(n):
alle = false
_check(alle, "T1 Paletten-Namen wie auf der Karte (%s)" % str(namen))
# Farbstatus ohne Auswahl/ohne Feld.
main.interaction.call("select_object", 0)
await process_frame
_check(str(insp.get("_farbe_status").text) == "Farbe: Pack-Farbe",
"T1 Status ohne tint = „Farbe: Pack-Farbe“ (ist „%s“)" % str(insp.get("_farbe_status").text))
_check(insp.get("_farben_grid").get_child_count() == 7, "T1 Farbfeld-Raster zeigt 7 Felder")
func _t2_farbe_doc() -> void:
main.interaction.call("select_object", 0)
await process_frame
var oliv: Color = SvmapTint.PALETTE[1][1]
insp.call("_on_farbe_gewaehlt", oliv, "Oliv")
var t: Variant = _sel_obj(0).get("tint", null)
_check(SvmapTint.gueltig(t), "T2 Klick auf Oliv schreibt tint-Feld ins doc")
var arr: Array = t
_check(absf(float(arr[0]) - oliv.r) < 0.001 and absf(float(arr[1]) - oliv.g) < 0.001
and absf(float(arr[2]) - oliv.b) < 0.001,
"T2 tint = Anzeige-Farbwerte der Palette (%s)" % str(arr))
_check(str(insp.get("_farbe_status").text) == "Farbe: Oliv", "T2 Status zeigt „Farbe: Oliv“")
# Gleiche Farbe erneut → kein Leer-Undo-Schritt.
main.history.clear()
insp.call("_on_farbe_gewaehlt", oliv, "Oliv")
_check(not main.history.can_undo(), "T2 gleiche Farbe erneut = KEIN Leer-Schritt")
# Anderer Farbe → EIN Schritt; Undo stellt die VORHERIGE Farbe (Oliv) her.
insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[6][1], "Weiß")
_check(_undo_steps_to_clear() == 1, "T2 Farbwechsel = GENAU EIN Undo-Schritt")
_check(SvmapTint.gleich(SvmapTint.von(_sel_obj(0)), oliv),
"T2 Undo stellt die vorherige Farbe (Oliv) wieder her")
func _t3_editor_ansicht() -> void:
# Farbe setzen (Objekt ist markiert → orange gewinnt, doc hat das Feld).
main.interaction.call("select_object", 0)
await process_frame
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)")
# Abwählen → Farbe sichtbar: Surface-Override mit albedo = doc-Wert.
main.interaction.call("clear_selection", true)
await process_frame
var ov := mi.get_surface_override_material(0)
_check(ov is BaseMaterial3D, "T3 abgewählt: Surface-Override-Material gesetzt")
if ov is BaseMaterial3D:
var ab: Color = (ov as BaseMaterial3D).albedo_color
_check(SvmapTint.gleich(ab, c), "T3 albedo des Overrides = doc-Farbe (%s vs %s)" % [str(ab), str(c)])
# Platzhalter-Objekt (crate_wood) bekommt die Farbe ebenfalls.
main.interaction.call("select_object", 1)
await process_frame
insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[4][1], "Holzbraun")
main.interaction.call("clear_selection", true)
await process_frame
var mi1 := _marker_mi(1)
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).
main.interaction.call("select_object", 1)
await process_frame
_check(_marker_mi(1).material_override != null, "T3 Auswahl-Orange gewinnt weiter")
func _t4_mehrfach() -> void:
main.interaction.call("clear_selection", true)
main.interaction.call("select_many", [0, 1])
await process_frame
main.history.clear()
insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[0][1], "Wüstensand")
_check(SvmapTint.hat(_sel_obj(0)) and SvmapTint.hat(_sel_obj(1)),
"T4 Mehrfach: beide Objekte haben tint")
_check(SvmapTint.gleich(SvmapTint.von(_sel_obj(0)), SvmapTint.von(_sel_obj(1))),
"T4 Mehrfach: beide dieselbe Farbe")
_check(_undo_steps_to_clear() == 1, "T4 Mehrfach-Farbe = GENAU EIN Undo-Schritt")
# Undo stellt die vorherigen Farben wieder her (T3: Stahlgrau / Holzbraun).
_check(SvmapTint.gleich(SvmapTint.von(_sel_obj(0)), SvmapTint.PALETTE[3][1])
and SvmapTint.gleich(SvmapTint.von(_sel_obj(1)), SvmapTint.PALETTE[4][1]),
"T4 Undo stellt die vorherigen Farben wieder her")
func _t5_reset() -> void:
main.interaction.call("select_many", [0, 1])
await process_frame
insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[2][1], "Tarn-Grün")
insp.call("_on_farbe_reset")
_check(not SvmapTint.hat(_sel_obj(0)) and not SvmapTint.hat(_sel_obj(1)),
"T5 Reset entfernt tint-Felder")
_check(str(insp.get("_farbe_status").text) == "Farbe: Pack-Farbe",
"T5 Status zeigt „Farbe: Pack-Farbe“")
# Farbe setzen und Zurücksetzen sind ZWEI getrennte Nutzeraktionen
# (= 2 Undo-Schritte); die FARBÄNDERUNG selbst ist genau 1 Schritt.
_check(_undo_steps_to_clear() == 2, "T5 Farbe + Reset = 2 getrennte Undo-Schritte")
# Reset ohne vorhandene Farbe = kein Leer-Schritt: Feld neu setzen,
# History leeren, Reset → ohne Änderung kein Commit.
main.interaction.call("select_object", 0)
await process_frame
insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[5][1], "Dunkelbraun")
main.history.clear()
insp.call("_on_farbe_reset")
_check(not SvmapTint.hat(_sel_obj(0)), "T5 Reset entfernt das Dunkelbraun-Feld")
main.history.clear()
insp.call("_on_farbe_reset")
_check(not main.history.can_undo(), "T5 Reset ohne Farbe = KEIN Leer-Schritt")
main.interaction.call("clear_selection", true)
await process_frame
_check(_marker_mi(0).get_surface_override_material(0) == null,
"T5 keine Rest-Overrides nach Reset (Pack-Farbe sichtbar)")
func _t6_roundtrip() -> void:
# tint in doc → save → load: Feld überlebt bitgenau (gerundet).
insp.call("_on_farbe_gewaehlt", SvmapTint.PALETTE[1][1], "Oliv")
var pfad: String = ProjectSettings.globalize_path("user://tint_test.svmap")
var save_res: Dictionary = SvmapIO.save_to_file("user://tint_test.svmap", main.doc)
_check(save_res.ok, "T6 save_to_file ok (%s)" % str(save_res.get("errors", [])))
var geladen: Dictionary = SvmapIO.load_from_file("user://tint_test.svmap")
_check(bool(geladen.get("ok", false)), "T6 load_from_file ok (%s)" % str(geladen.get("errors", [])))
if bool(geladen.get("ok", false)):
var objs: Array = (geladen["data"] as Dictionary)["objects"]
_check(SvmapTint.gleich(SvmapTint.von(objs[0]), SvmapTint.von(_sel_obj(0))),
"T6 tint überlebt den Roundtrip unverändert")
# Alte Karte ohne Feld: Validierung ohne Fehler, View ohne Override.
var alte: Dictionary = main.doc.duplicate(true)
for obj in (alte["objects"] as Array):
SvmapTint.entferne(obj)
SvmapIO.sign_map(alte)
var fehler: PackedStringArray = SvmapIO.validate_map(alte)
_check(fehler.is_empty(), "T6 Karte ohne tint validiert ohne Fehler (%s)" % str(fehler))
if FileAccess.file_exists(pfad):
DirAccess.remove_absolute(pfad)
func _finish() -> void:
print("--------------------------------------------------")
print("CHECKS: %d FAILURES: %d" % [checks, failures])
quit(0 if failures == 0 else 1)

View file

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