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:
+82
-5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user