Material Phase B Stufe 4: PBR auf Rhino-Material applied + Auto-Regen

_ensure_pbr_material baut ein vollständiges Rhino-Material aus dem
Project-Settings-dict:
- ToPhysicallyBased() + BaseColor/Roughness/Metallic/Opacity/OpacityIor
- Diffuse-/Bump-/Transparency-Texture via SetBitmapTexture etc.
- UV-Repeat = 1/uvScaleM
- Cache per Signature (Color+PBR+Texture-Pfade)

_get_all_materials liefert jetzt full-dicts (nicht mehr nur color/hatch/
scale) damit Wand-Regen Zugriff auf PBR + Texturen hat.

Wand-Regen: wenn voll-dict aus Project-Settings vorliegt → PBR-Material,
sonst Fallback auf legacy _ensure_material(hex).

Auto-Regen on Save:
- PBR-Material-Cache + Legacy-Material-Cache invalidieren
- Alle wand_axis im Doc regenerieren (in EINEM Undo-Record)
- User aendert Material-Properties -> existierende Waende updaten sofort

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 17:33:38 +02:00
parent 0bf891641f
commit c63bdd5bc1
2 changed files with 186 additions and 16 deletions
+145 -12
View File
@@ -2208,10 +2208,19 @@ _MATERIAL_LIBRARY = {
def _get_all_materials(doc):
"""Builtin _MATERIAL_LIBRARY + Projekt-Settings-Materialien gemerged.
Returns dict[name] -> {color, hatch, scale}. Projekt-Settings (inkl.
Library-Imports) ueberschreibt builtin bei Namensgleichheit, sodass
der User builtin-Defaults pro Projekt anpassen kann."""
merged = {n: dict(m) for n, m in _MATERIAL_LIBRARY.items()}
Returns dict[name] -> full material dict (color/hatch/scale + PBR +
textures + uvScaleM). Projekt-Settings ueberschreibt builtin bei
Namensgleichheit. Builtin-Materialien bekommen leere PBR-Defaults."""
merged = {}
for n, m in _MATERIAL_LIBRARY.items():
merged[n] = dict(m)
# Builtin: PBR-Defaults wenn nicht gesetzt
merged[n].setdefault("roughness", 0.7)
merged[n].setdefault("reflection", 0.1)
merged[n].setdefault("transparency", 0.0)
merged[n].setdefault("iorN", 1.0)
merged[n].setdefault("uvScaleM", 1.0)
merged[n].setdefault("textures", {})
try:
import rhinopanel
ps = rhinopanel.load_project_settings(doc) if doc else None
@@ -2219,11 +2228,9 @@ def _get_all_materials(doc):
for m in ps.get("materials", []):
n = m.get("name")
if not n: continue
merged[n] = {
"color": m.get("color", "#888888"),
"hatch": m.get("hatch", "Solid"),
"scale": float(m.get("scale", 1.0) or 1.0),
}
# Komplettes dict uebernehmen — _normalize_material hat
# bereits alle Felder validiert.
merged[n] = dict(m)
except Exception as ex:
print("[ELEMENTE] _get_all_materials:", ex)
return merged
@@ -2330,6 +2337,126 @@ def _ensure_material(doc, hex_color):
return -1
def _ensure_pbr_material(doc, mat_dict):
"""Erstellt oder findet ein Rhino-PBR-Material aus dem Project-Settings-
dict (color + roughness + reflection + transparency + iorN + textures +
uvScaleM). Cached pro signature in sc.sticky. Liefert Material-Index
oder -1.
Setzt:
- PhysicallyBased.BaseColor (aus color)
- PhysicallyBased.Roughness, Opacity (= 1-transparency), OpacityIor
- PhysicallyBased.Metallic = reflection (Naeherung)
- SetBitmapTexture / SetBumpTexture / SetTransparencyTexture wenn
Pfade gesetzt
- Texture.Repeat = (1/uvScaleM, 1/uvScaleM)"""
if not isinstance(mat_dict, dict): return -1
color = mat_dict.get("color") or "#888888"
if not isinstance(color, str) or not color.startswith("#") or len(color) < 7:
return -1
# Cache-Key: alle PBR-Felder + Texture-Pfade
texs = mat_dict.get("textures") or {}
def _p(slot):
t = texs.get(slot) if isinstance(texs, dict) else None
return (t or {}).get("path", "") if isinstance(t, dict) else ""
sig = "|".join([
color.lower(),
"r{:.3f}".format(float(mat_dict.get("roughness", 0.7))),
"x{:.3f}".format(float(mat_dict.get("reflection", 0.1))),
"t{:.3f}".format(float(mat_dict.get("transparency", 0.0))),
"i{:.3f}".format(float(mat_dict.get("iorN", 1.0))),
"u{:.3f}".format(float(mat_dict.get("uvScaleM", 1.0))),
"d=" + _p("diffuse"),
"b=" + _p("bump"),
"g=" + _p("roughness"),
"p=" + _p("transparency"),
])
cache = sc.sticky.get("_dossier_pbr_material_cache")
if not isinstance(cache, dict):
cache = {}
sc.sticky["_dossier_pbr_material_cache"] = cache
cached = cache.get(sig)
if cached is not None:
try:
if 0 <= cached < doc.Materials.Count:
m = doc.Materials[cached]
if m is not None and not m.IsDeleted:
return cached
except Exception: pass
del cache[sig]
# Material bauen
try:
import System.Drawing as SD
h = color.lstrip("#")
r = int(h[0:2], 16); g = int(h[2:4], 16); b = int(h[4:6], 16)
mat = Rhino.DocObjects.Material()
mat.Name = "Dossier_PBR_" + (mat_dict.get("name") or "Mat")[:20]
mat.DiffuseColor = SD.Color.FromArgb(255, r, g, b)
# PBR-Properties
try:
mat.ToPhysicallyBased()
pbr = mat.PhysicallyBased
if pbr is not None:
# Color4f (linear) — RGB normalisieren
try:
from Rhino.Display import Color4f
pbr.BaseColor = Color4f(r/255.0, g/255.0, b/255.0, 1.0)
except Exception: pass
try: pbr.Roughness = float(mat_dict.get("roughness", 0.7))
except Exception: pass
# reflection ~ metallic (Naeherung — bei nicht-Metallen
# bleibt Roughness/IoR das Hauptmerkmal). User kann das
# spaeter via separates Metallic-Feld feiner steuern.
try: pbr.Metallic = float(mat_dict.get("reflection", 0.0))
except Exception: pass
trans = float(mat_dict.get("transparency", 0.0))
try: pbr.Opacity = max(0.0, 1.0 - trans)
except Exception: pass
try: pbr.OpacityIor = float(mat_dict.get("iorN", 1.0))
except Exception: pass
except Exception as ex:
print("[ELEMENTE] PBR setup:", ex)
# Texturen + UV-Repeat
try:
uv = float(mat_dict.get("uvScaleM", 1.0)) or 1.0
uv_repeat = 1.0 / uv # uvScaleM = 1m → 1x; 0.5m → 2x Repeat
except Exception: uv_repeat = 1.0
def _apply_tex(slot, setter_name):
p = _p(slot)
if not p: return
try:
setter = getattr(mat, setter_name, None)
if setter is None: return
ok = setter(p)
if not ok: return
# Repeat auf den frisch gesetzten Texture-Slot — alle
# Slot-Indices durchgehen und matchen.
try:
import Rhino.Geometry as rgg
for i in range(mat.GetTextures().Count if False else 100):
try: t = mat.GetTexture(i)
except Exception: break
if t is None: continue
try: t.Repeat = rgg.Vector2d(uv_repeat, uv_repeat)
except Exception: pass
try: mat.SetTexture(t, t.TextureType)
except Exception: pass
except Exception: pass
except Exception as ex:
print("[ELEMENTE] apply tex {}:".format(slot), ex)
_apply_tex("diffuse", "SetBitmapTexture")
_apply_tex("bump", "SetBumpTexture")
_apply_tex("transparency", "SetTransparencyTexture")
# Roughness-Texture: kein direkter Setter im Legacy-Material —
# PhysicallyBased haette eine, ueberspringen wir hier (Phase B+)
idx = doc.Materials.Add(mat)
if idx >= 0:
cache[sig] = idx
return idx
except Exception as ex:
print("[ELEMENTE] _ensure_pbr_material:", ex)
return -1
def _make_wand_layer_breps(axis_curve, layers, dicke, referenz, uk, ok,
miter_start=None, miter_end=None):
"""Baut eine Liste (brep, color_hex, name) pro Schicht. Schicht-Reihen-
@@ -5451,8 +5578,10 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
effective_color = color
target_layer = layer
all_mats = _get_all_materials(doc)
full_mat_dict = None # PBR + Texturen wenn aus Project-Settings
if mat_name and mat_name in all_mats:
effective_color = all_mats[mat_name]["color"]
full_mat_dict = all_mats[mat_name]
effective_color = full_mat_dict.get("color", color)
target_layer = _ensure_material_sublayer(doc, geschoss_name,
mat_name)
if target_layer < 0: target_layer = layer
@@ -5467,8 +5596,12 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
Rhino.DocObjects.ObjectColorSource.ColorFromObject)
attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0)
except Exception: pass
# Faces via Material (DiffuseColor) — getrennt vom ObjectColor.
if effective_color:
# Faces via Material — wenn voll-dict mit PBR vorhanden, PBR-
# Material mit Texturen bauen, sonst einfaches Diffuse-Material.
mat_idx = -1
if full_mat_dict is not None:
mat_idx = _ensure_pbr_material(doc, full_mat_dict)
if mat_idx < 0 and effective_color:
mat_idx = _ensure_material(doc, effective_color)
if mat_idx >= 0:
attrs.MaterialIndex = mat_idx
+37
View File
@@ -706,12 +706,49 @@ class EbenenBridge(panel_base.BaseBridge):
}
save_project_settings(doc2, new_settings)
_broadcast_state(doc2)
# Material-Cache invalidieren (PBR-Cache hashed Color+Texturen+
# PBR-Werte — wenn der User ein Material aendert, muss der
# Cache leer sein, sonst kriegen Waende stale Material-Indizes).
try:
import scriptcontext as sc
sc.sticky["_dossier_pbr_material_cache"] = {}
sc.sticky["_dossier_material_cache"] = {}
except Exception: pass
try:
import scriptcontext as sc
eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._send_state()
except Exception as ex:
print("[PROJECT-SETTINGS] elemente refresh:", ex)
# Alle wand_axis im Doc regenen damit Material-Aenderungen
# (PBR/Texturen/Hatch) auf existing Waende durchschlagen.
try:
import elemente
undo_serial = doc2.BeginUndoRecord("Material-Update Regen")
prev_redraw = doc2.Views.RedrawEnabled
doc2.Views.RedrawEnabled = False
wall_ids = []
for obj in doc2.Objects:
m = elemente._read_meta(obj)
if m and m.get("type") == "wand_axis":
wall_ids.append(m["id"])
# Chain-Anchor regent automatisch alle members — wir koennen
# trotzdem alle einzeln triggern, _REGEN_BUSY-Guard verhindert
# Doppel-Arbeit. Einfacher als Anchor-Election hier.
try:
for wid in wall_ids:
try: elemente._regenerate_element(doc2, wid)
except Exception as ex:
print("[PROJECT-SETTINGS] regen", wid, ex)
finally:
doc2.Views.RedrawEnabled = prev_redraw
try: doc2.EndUndoRecord(undo_serial)
except Exception: pass
try: doc2.Views.Redraw()
except Exception: pass
print("[PROJECT-SETTINGS] {} Waende regenert".format(len(wall_ids)))
except Exception as ex:
print("[PROJECT-SETTINGS] wall regen:", ex)
panel_base.open_satellite_window(
"project_settings",
params=params,