diff --git a/rhino/elemente.py b/rhino/elemente.py index 7786aec..d7f1f57 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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,13 +5596,17 @@ 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 - attrs.MaterialSource = ( - Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject) + if mat_idx >= 0: + attrs.MaterialIndex = mat_idx + attrs.MaterialSource = ( + Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject) _attach_meta(attrs, element_id, "wand_volume", meta["geschoss"], meta["dicke"], meta["uk_override"], meta["ok_override"], diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index b08a2ff..e809971 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -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,