Project-Settings Material-List/Detail + PBR-UI (Phase B Stufe 2+3)

UI:
- Voreinstellungen: InlineNumberField (Label links, schmaler Number-Input
  rechts mit Einheit-Suffix) statt full-width pills
- Materialien-Tab: List/Detail-Layout im ArchiCAD-Stil
  - Links (240px): suchbare Material-Liste mit Color-Swatch + Source-Badge
  - Rechts: Detail-Panel mit collapsible Sections
- Slider-Component (Pill-Track mit accent-fill + Wert rechts)
- TextureSlot-Component (Preview-Tile + Filename + Picker- + Clear-Button)

Schema:
- Material erweitert: roughness, reflection, transparency, iorN, uvScaleM
- Texturen: diffuse / bump / roughness / transparency (je {path, ...})
- _normalize_material clamped 0..1, IoR 1.0..2.5

Backend:
- Neuer Handler PICK_TEXTURE_FILE oeffnet Eto.OpenFileDialog fuer
  .jpg/.png/.tif/.bmp/.tga, antwortet mit TEXTURE_PICKED {slot, path}

Hinweis: PBR-Werte + Texturen landen aktuell nur in doc.Strings —
Anwendung auf Rhinos Render-Material kommt in Stufe 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 17:11:23 +02:00
parent f760d1c54b
commit 0bf891641f
3 changed files with 561 additions and 144 deletions
+82 -5
View File
@@ -141,21 +141,62 @@ _PROJECT_SETTINGS_DEFAULTS = {
def _normalize_material(m):
"""Garantiert Material-Schema: name + color + hatch + scale + source
+ libraryId. source: 'local' | 'library' | 'builtin'. libraryId: UUID
wenn aus Library, sonst null. Felder fehlen bei alten Eintraegen — wir
setzen Defaults damit Frontend nicht null-checken muss."""
"""Garantiert Material-Schema. Felder:
- Identitaet: name, source ('local'|'library'|'builtin'), libraryId
- Section-Hatch (2D): color, hatch, scale
- PBR (3D-Render): roughness (0..1), reflection (0..1),
transparency (0..1), iorN (1.0..2.5)
- UV: uvScaleM (= 1 Welt-Meter ≙ wieviel der Textur)
- Texturen: textures = { diffuse, bump, roughness, transparency }
pro Slot {path: absolute string} oder null. Strength fuer Bump."""
if not isinstance(m, dict): return None
textures = m.get("textures") or {}
if not isinstance(textures, dict): textures = {}
def _tex(slot):
t = textures.get(slot)
if not isinstance(t, dict): return None
p = t.get("path")
if not p: return None
out = {"path": str(p)}
# Bump hat zusaetzlich strength (-1..1, default 0.5)
if slot == "bump":
try: out["strength"] = float(t.get("strength", 0.5))
except Exception: out["strength"] = 0.5
return out
return {
"name": m.get("name") or "Unbenannt",
"color": m.get("color") or "#888888",
"hatch": m.get("hatch") or "Solid",
"scale": float(m.get("scale", 1.0) or 1.0),
"source": m.get("source") or "local",
"libraryId": m.get("libraryId"), # None wenn nicht aus Library
"libraryId": m.get("libraryId"),
# PBR (3D-Render) — alle 0..1 ausser iorN
"roughness": _clamp01(m.get("roughness", 0.7)),
"reflection": _clamp01(m.get("reflection", 0.1)),
"transparency": _clamp01(m.get("transparency", 0.0)),
"iorN": _clamp(m.get("iorN", 1.0), 1.0, 2.5),
# UV-Skalierung (1 m = uvScaleM Textur-Tiles)
"uvScaleM": float(m.get("uvScaleM", 1.0) or 1.0),
# Texturen
"textures": {
"diffuse": _tex("diffuse"),
"bump": _tex("bump"),
"roughness": _tex("roughness"),
"transparency": _tex("transparency"),
},
}
def _clamp01(v):
try: return max(0.0, min(1.0, float(v)))
except Exception: return 0.0
def _clamp(v, lo, hi):
try: return max(lo, min(hi, float(v)))
except Exception: return lo
def load_project_settings(doc):
"""Liefert die Project-Settings als dict — mit Defaults-Merge wenn
Felder fehlen. Garantiert dass `defaults` und `materials` immer da
@@ -617,6 +658,12 @@ class EbenenBridge(panel_base.BaseBridge):
try: self._open_library()
except Exception as ex:
print("[EBENEN] open library:", ex)
elif t == "PICK_TEXTURE_FILE":
# Oeffnet macOS-File-Picker fuer Bild-Dateien. Antwort an
# Frontend via TEXTURE_PICKED-Message.
try: self._pick_texture_file(p)
except Exception as ex:
print("[EBENEN] pick texture:", ex)
# ---- Helpers ----
@@ -672,6 +719,36 @@ class EbenenBridge(panel_base.BaseBridge):
size=(440, 540),
on_save=on_save)
def _pick_texture_file(self, payload):
"""Oeffnet macOS-File-Picker (via Eto.Forms.OpenFileDialog) und
schickt den ausgewaehlten Pfad zurueck ans Frontend.
Payload: {slot: 'diffuse'|'bump'|...} — slot wird mit zurueckgegeben
damit das Frontend weiss welches Slot-Field aktualisieren."""
slot = payload.get("slot") or "diffuse"
try:
import Eto.Forms as forms
dlg = forms.OpenFileDialog()
dlg.Title = "Textur waehlen ({})".format(slot)
dlg.MultiSelect = False
f = forms.FileFilter("Bilder",
".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp", ".tga")
dlg.Filters.Add(f)
dlg.Filters.Add(forms.FileFilter("Alle", ".*"))
try:
parent_form = sc.sticky.get("_dossier_active_settings_form")
except Exception:
parent_form = None
res = (dlg.ShowDialog(parent_form) if parent_form
else dlg.ShowDialog(None))
if str(res) != "Ok":
self.send("TEXTURE_PICKED", {"slot": slot, "path": None})
return
path = dlg.FileName or ""
self.send("TEXTURE_PICKED", {"slot": slot, "path": path})
except Exception as ex:
print("[EBENEN] pick texture:", ex)
self.send("TEXTURE_PICKED", {"slot": slot, "path": None})
def _open_library(self):
"""Oeffnet den Library-Browser als Satellite. Bridge bleibt offen
damit User mehrere Items hintereinander importieren kann; nach