From 9fa80299101264140b66922995049b59896179c5 Mon Sep 17 00:00:00 2001 From: "Hermes (scopedvr-dev)" Date: Mon, 19 Oct 2026 20:54:34 +0000 Subject: [PATCH] =?UTF-8?q?Farbe=20je=20Objekt=20(t=5Fb0edb807):=20Inspekt?= =?UTF-8?q?or-Abschnitt=20'Farbe=20/=20Material'=20mit=207er-Palette=20(W?= =?UTF-8?q?=C3=BCstensand/Oliv/Tarn-Gr=C3=BCn/Stahlgrau/Holzbraun/Dunkelbr?= =?UTF-8?q?aun/Wei=C3=9F)=20+=20'Zur=C3=BCcksetzen=20(Pack-Farbe)';=20Mehr?= =?UTF-8?q?fachauswahl=20=3D=20EIN=20Undo-Schritt;=20Speichern=20als=20OPT?= =?UTF-8?q?IONALES=20Objekt-Feld=20tint:[r,g,b]=20(Anzeige-sRGB,=204=20Ste?= =?UTF-8?q?llen=20=E2=80=94=20R=C3=BCckw=C3=A4rtskompatibel,=20Karten=20oh?= =?UTF-8?q?ne=20Feld=20unver=C3=A4ndert).=20Neuer=20Helfer=20src/svmap/svm?= =?UTF-8?q?ap=5Ftint.gd=20(1:1-Spiegel=20im=20Client,=20gemeinsame=20Seman?= =?UTF-8?q?tik=20Editor+Spiel):=20albedo=5Fcolor-ERSATZ=20via=20Surface-Ov?= =?UTF-8?q?erride-Kopien=20=E2=80=94=20Original-Materialien=20bleiben=20un?= =?UTF-8?q?angetastet,=20Auswahl-Orange=20(material=5Foverride)=20gewinnt?= =?UTF-8?q?=20weiter;=20set=5Fselected=5Fmulti=20re-synced=20Tint=20beim?= =?UTF-8?q?=20Abw=C3=A4hlen,=20tint=5Frefresh=20f=C3=BCr=20Inspector-?= =?UTF-8?q?=C3=84nderungen=20ohne=20Rebuild.=20Map-View-Bauer=20wendet=20t?= =?UTF-8?q?int=20beim=20Aufbau=20an=20(echte=20GLBs=20UND=20Platzhalter).?= =?UTF-8?q?=20tests/test=5Fmap=5Ftint.gd=20neu=20(31=20Checks=20gr=C3=BCn)?= =?UTF-8?q?,=20Editor-Gesamtsuite=2018/18=20(gui=5Fsmoke-F5-Artefakt=20per?= =?UTF-8?q?=20Gegenprobe=20entkr=C3=A4ftet,=20updater=20falsch-rot=20im=20?= =?UTF-8?q?Sammel-Filter).=20Rauheit/Metallisch=20bewusst=20weggelassen=20?= =?UTF-8?q?(Begr=C3=BCndung=20im=20Handoff/BEWEIS.md).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/svmap/svmap_tint.gd | 149 +++++++++++++++++++ src/svmap/svmap_tint.gd.uid | 1 + src/ui/inspector_panel.gd | 117 +++++++++++++++ src/viewport/map_view_builder.gd | 44 +++++- tests/probe_tint_render.gd | 107 ++++++++++++++ tests/probe_tint_render.gd.uid | 1 + tests/test_map_tint.gd | 242 +++++++++++++++++++++++++++++++ tests/test_map_tint.gd.uid | 1 + 8 files changed, 661 insertions(+), 1 deletion(-) create mode 100644 src/svmap/svmap_tint.gd create mode 100644 src/svmap/svmap_tint.gd.uid create mode 100644 tests/probe_tint_render.gd create mode 100644 tests/probe_tint_render.gd.uid create mode 100644 tests/test_map_tint.gd create mode 100644 tests/test_map_tint.gd.uid diff --git a/src/svmap/svmap_tint.gd b/src/svmap/svmap_tint.gd new file mode 100644 index 0000000..501bb06 --- /dev/null +++ b/src/svmap/svmap_tint.gd @@ -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) diff --git a/src/svmap/svmap_tint.gd.uid b/src/svmap/svmap_tint.gd.uid new file mode 100644 index 0000000..db1fa23 --- /dev/null +++ b/src/svmap/svmap_tint.gd.uid @@ -0,0 +1 @@ +uid://cx6cfhyxw42f diff --git a/src/ui/inspector_panel.gd b/src/ui/inspector_panel.gd index 579a4af..821eaec 100644 --- a/src/ui/inspector_panel.gd +++ b/src/ui/inspector_panel.gd @@ -154,6 +154,9 @@ var _info: Label var _hint: Label var _reset_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 _built := false @@ -227,6 +230,31 @@ func setup(p_main) -> void: btn_row.add_child(_ground_btn) _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 = Label.new() _info.text = "" @@ -248,6 +276,92 @@ func _field(text: String) -> Label: 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, min_v := -1000000.0, max_v := 1000000.0) -> DragValue: var w := DragValue.new() @@ -383,6 +497,9 @@ func refresh() -> void: else: w.set_shown(_get_value(field)) + # Farbsektion (Karte t_b0edb807): aktueller Farbstand der Auswahl. + _farbe_status.text = _farbe_text(objs) + # Info-Block (nur lesen). 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) diff --git a/src/viewport/map_view_builder.gd b/src/viewport/map_view_builder.gd index fbc7911..a707741 100644 --- a/src/viewport/map_view_builder.gd +++ b/src/viewport/map_view_builder.gd @@ -249,6 +249,12 @@ func _build_objects(objects: Array, manifest: Dictionary) -> void: marker.set_meta("glb", echt) object_markers.append(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. @@ -266,12 +272,48 @@ func set_selected(idx: int) -> void: func set_selected_multi(indices: Array) -> void: selected_indices = indices.duplicate() selected_index = indices.back() if not indices.is_empty() else -1 + 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)) - 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: diff --git a/tests/probe_tint_render.gd b/tests/probe_tint_render.gd new file mode 100644 index 0000000..be90212 --- /dev/null +++ b/tests/probe_tint_render.gd @@ -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) diff --git a/tests/probe_tint_render.gd.uid b/tests/probe_tint_render.gd.uid new file mode 100644 index 0000000..acd2973 --- /dev/null +++ b/tests/probe_tint_render.gd.uid @@ -0,0 +1 @@ +uid://by5iyai3cd08r diff --git a/tests/test_map_tint.gd b/tests/test_map_tint.gd new file mode 100644 index 0000000..b566ea1 --- /dev/null +++ b/tests/test_map_tint.gd @@ -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) diff --git a/tests/test_map_tint.gd.uid b/tests/test_map_tint.gd.uid new file mode 100644 index 0000000..f992bb9 --- /dev/null +++ b/tests/test_map_tint.gd.uid @@ -0,0 +1 @@ +uid://r6yxgf0a25md