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:
parent
c8b9c59fda
commit
9fa8029910
8 changed files with 661 additions and 1 deletions
149
src/svmap/svmap_tint.gd
Normal file
149
src/svmap/svmap_tint.gd
Normal 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)
|
||||
1
src/svmap/svmap_tint.gd.uid
Normal file
1
src/svmap/svmap_tint.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cx6cfhyxw42f
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
107
tests/probe_tint_render.gd
Normal file
107
tests/probe_tint_render.gd
Normal 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)
|
||||
1
tests/probe_tint_render.gd.uid
Normal file
1
tests/probe_tint_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://by5iyai3cd08r
|
||||
242
tests/test_map_tint.gd
Normal file
242
tests/test_map_tint.gd
Normal 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)
|
||||
1
tests/test_map_tint.gd.uid
Normal file
1
tests/test_map_tint.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://r6yxgf0a25md
|
||||
Loading…
Reference in a new issue