diff --git a/rhino/elemente.py b/rhino/elemente.py index 573a239..3cf00f4 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -71,31 +71,38 @@ _KEY_OEFF_TUER_TYP = "dossier_oeff_tuer_typ" # "normal" | "wandoeffnu _KEY_OEFF_HINGE_SIDE = "dossier_oeff_hinge_side" # "links" | "rechts" — Bandseite (welche Tuerflueg-Seite) _KEY_OEFF_OPEN_ANGLE = "dossier_oeff_open_angle" # float Grad 0–180 (Plan-Oeffnungswinkel) _KEY_OEFF_SWING_INVERT = "dossier_oeff_swing_invert" # "1"/"0" — flippt die Schwung-Richtung +_KEY_OEFF_STURZ = "dossier_oeff_sturz" # "keine" | "innen" | "aussen" | "beide" — Sturzlinien-Anzeige bei 1:100 Tueren _OEFF_TUER_TYPEN = ("normal", "wandoeffnung") _OEFF_HINGE_SIDES = ("links", "rechts") +_OEFF_STURZ_OPTIONS = ("keine", "innen", "aussen", "beide") -_OEFF_DARSTELLUNGEN = ("einfach", "standard", "detail") +_OEFF_DARSTELLUNGEN = ("auto", "einfach", "standard", "detail") +# 'auto' = folgt der Doc-Level-Modelldarstellung (Oberleiste). Default +# fuer neue Elemente. Explizite Werte ueberstuern den Doc-Level-Wert. +_DARSTELLUNG_DEFAULT_GLOBAL = "einfach" # Doc-Level Default = 1:100 def get_aktive_darstellung(doc): - """Liest die doc-level Darstellungs-Override. Leer → per-object.""" - if doc is None: return "" + """Liest die doc-level Modelldarstellung. Default = einfach (1:100). + 'auto' wird hier nicht akzeptiert — die Doc-Ebene hat IMMER einen + konkreten Wert, an dem sich auto-Elemente orientieren.""" + if doc is None: return _DARSTELLUNG_DEFAULT_GLOBAL try: v = doc.Strings.GetValue(_KEY_AKTIVE_DARSTELLUNG) or "" except Exception: v = "" - return v if v in _OEFF_DARSTELLUNGEN else "" + if v in ("einfach", "standard", "detail"): return v + return _DARSTELLUNG_DEFAULT_GLOBAL def set_aktive_darstellung(doc, value): - """Setzt die doc-level Darstellungs-Override. Leer → clear.""" + """Setzt die doc-level Modelldarstellung. Akzeptiert nur konkrete + Werte (einfach/standard/detail). Leer/None/auto → Default einfach.""" if doc is None: return try: - if value and value in _OEFF_DARSTELLUNGEN: - doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, value) - else: - doc.Strings.Delete(_KEY_AKTIVE_DARSTELLUNG) + v = value if value in ("einfach", "standard", "detail") else _DARSTELLUNG_DEFAULT_GLOBAL + doc.Strings.SetString(_KEY_AKTIVE_DARSTELLUNG, v) except Exception as ex: print("[ELEMENTE] set_aktive_darstellung:", ex) @@ -414,6 +421,30 @@ _OEFF_SIMS_STYLES = { } _OEFF_RAHMEN_POS_OPTIONS = ("aussen", "mid", "innen") +# Pro Oeffnungs-Piece-Kind: eigener Sublayer unter `WAENDE::Öffnungen` + +# zugehoeriges Material (Hex-Diffuse + optional Transparenz). 'name' wird +# als Layer-Name benutzt. 'suffix' wird an den WAENDE-Code +"o" angehaengt +# (z.B. wand_code="20" -> Öffnungen-Ebene "20o" -> Rahmen "20o1") damit der +# Eintrag im dossier_ebenen-Tree eine eindeutige Code-ID bekommt und im +# Ebenen-Panel erscheint. transparency: 0.0–1.0. +_OEFF_PIECE_DEFS = { + "rahmen": {"layer": "Rahmen", "suffix": "1", "color": "#c89a5a", + "transparency": 0.0, "ior": 1.0}, + "glas": {"layer": "Glas", "suffix": "2", "color": "#bcd4e0", + "transparency": 0.88, "ior": 1.5}, + "fluegel": {"layer": "Türblatt", "suffix": "3", "color": "#8a6440", + "transparency": 0.0, "ior": 1.0}, + "sims": {"layer": "Sims", "suffix": "4", "color": "#a8a8a8", + "transparency": 0.0, "ior": 1.0}, + "pane": {"layer": "Pane", "suffix": "5", "color": "#404040", + "transparency": 0.0, "ior": 1.0}, + "schwung": {"layer": "Schwung", "suffix": "6", "color": "#888888", + "transparency": 0.0, "ior": 1.0}, + "sturz": {"layer": "Sturz", "suffix": "7", "color": "#6a6a6a", + "transparency": 0.0, "ior": 1.0, + "linetype": "Dashed"}, +} + # --- Last-Used-Defaults (sticky, session-life) ------------------------------ # Speichert die letzten Werte (Dicke, Referenz, Modus, Neigung), damit der @@ -614,15 +645,180 @@ def _layer_path_volume(doc, geschoss_name): return _layer_path_axis(doc, geschoss_name) -def _layer_path_oeff_swing(doc, geschoss_name): - """Türschwung-Linien — eigener Sublayer (Code 23) damit User die - Schwung-Bögen unabhängig von den Türen-Volumes ausblenden kann.""" - sub = _find_ebene_sublayer_name(doc, ["schwung", "tuerschwung"], - "23", "Türschwung", - default_color="#888888", default_lw=0.13) +def _layer_path_referenz(doc, geschoss_name): + """Sublayer 'Referenzlinien' (Code 19) — eigene Ebene fuer wand_axis + + oeffnung_point Source-Objekte. Getrennt vom Wand-Volumen-Layer (20) + damit der User die Konstruktions-Referenzen ein-/ausblenden kann ohne + die Volumen-Sichtbarkeit zu verlieren. Wird automatisch im Ebenen- + Panel sichtbar (auto-add via _find_ebene_sublayer_name).""" + sub = _find_ebene_sublayer_name(doc, ["referenz", "referenzlinie"], + "19", "Referenzlinien", + default_color="#a0a0a0", default_lw=0.13) return "{}::{}".format(geschoss_name, sub) +def _ensure_oeff_ebenen_in_doc(doc): + """Registriert die Oeffnungen-Ebene + alle Piece-Children im + `dossier_ebenen`-JSON-Tree (als Children der WAENDE-Ebene). Damit + erscheinen die Sublayer im Dossier-Ebenen-Panel und koennen vom User + visible/locked geschaltet werden. + + Returnt den WAENDE-Code (z.B. "20") oder None wenn die WAENDE-Ebene + nicht im JSON-Tree liegt. Triggert build_layers + broadcast_state + wenn etwas geaendert wurde.""" + raw = doc.Strings.GetValue("dossier_ebenen") + try: ebenen = json.loads(raw) if raw else [] + except Exception: ebenen = [] + if not isinstance(ebenen, list): return None + wand_eb = None + for e in ebenen: + if not isinstance(e, dict): continue + nm = (e.get("name") or "").lower() + if "wand" in nm or "wände" in nm or "waende" in nm: + wand_eb = e; break + if wand_eb is None: return None + wand_code = wand_eb.get("code") or "20" + if not isinstance(wand_eb.get("children"), list): + wand_eb["children"] = [] + oeff_code = wand_code + "o" + oeff_eb = next((c for c in wand_eb["children"] if isinstance(c, dict) + and c.get("code") == oeff_code), None) + changed = False + if oeff_eb is None: + oeff_eb = { + "code": oeff_code, "name": "Öffnungen", + "color": "#888888", "lw": 0.13, + "visible": True, "locked": False, "children": [], + } + wand_eb["children"].append(oeff_eb) + changed = True + if not isinstance(oeff_eb.get("children"), list): + oeff_eb["children"] = [] + have_codes = {c.get("code") for c in oeff_eb["children"] + if isinstance(c, dict)} + for kind, spec in _OEFF_PIECE_DEFS.items(): + ccode = oeff_code + spec["suffix"] + if ccode in have_codes: continue + oeff_eb["children"].append({ + "code": ccode, "name": spec["layer"], + "color": spec["color"], "lw": 0.13, + "visible": True, "locked": False, + }) + changed = True + if changed: + try: + doc.Strings.SetString("dossier_ebenen", + json.dumps(ebenen, ensure_ascii=False)) + import layer_builder + z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen") + zlist = json.loads(z_raw) if z_raw else [] + if zlist: layer_builder.build_layers(doc, zlist, ebenen) + import rhinopanel + rhinopanel._broadcast_state(doc) + except Exception as ex: + print("[ELEMENTE] _ensure_oeff_ebenen_in_doc build:", ex) + return wand_code + + +def _layer_path_oeff_kind(doc, geschoss_name, kind): + """Sublayer pro Oeffnungs-Piece-Kind: `::20_Wände::20o_Öffnungen::20oN_`. + Registriert die Ebenen im dossier_ebenen-Tree (damit das Dossier- + Ebenen-Panel sie zeigt) + erstellt die Rhino-Layer-Hierarchie. Liefert + den vollen Layer-Pfad.""" + spec = _OEFF_PIECE_DEFS.get(kind) + if spec is None: + return _layer_path_volume(doc, geschoss_name) + # Sicherstellen dass WAENDE in dossier_ebenen ist (auto-add via + # _find_ebene_sublayer_name) + Code extrahieren + wand_sub_name = _find_ebene_sublayer_name(doc, ["wand", "wände", "waende"], + "20", "Wände", + default_color="#0a0a0a", + default_lw=0.50) + # Oeffnungen + 6 Kind-Sublayer in dossier_ebenen registrieren + wand_code = _ensure_oeff_ebenen_in_doc(doc) or "20" + oeff_code = wand_code + "o" + kind_code = oeff_code + spec["suffix"] + # Rhino-Layer-Namen folgen dem `_`-Schema von layer_builder + full = "{}::{}::{}::{}".format( + geschoss_name, wand_sub_name, + "{}_{}".format(oeff_code, "Öffnungen"), + "{}_{}".format(kind_code, spec["layer"])) + layer_idx = _ensure_layer(doc, full) + # Linetype-Default (z.B. 'Dashed' fuer Sturzlinien) — NUR setzen wenn + # der Layer aktuell auf Continuous (Index 0) steht. Sonst hat der User + # bewusst eine andere Linetype gewaehlt und wir wuerden sie bei jedem + # Regen wegmaecken. + lt_name = spec.get("linetype") + if lt_name and layer_idx >= 0: + try: + layer = doc.Layers[layer_idx] + if layer.LinetypeIndex <= 0: # 0 = Continuous, <0 = unset + lt_idx = doc.Linetypes.Find(lt_name) + if lt_idx > 0: + layer.LinetypeIndex = lt_idx + doc.Layers.Modify(layer, layer_idx, True) + except Exception: pass + return full + + +def _layer_path_oeff_swing(doc, geschoss_name): + """Türschwung-Linien — Sublayer 'Schwung' unter WAENDE::Öffnungen. + Eigene Ebene damit User die Schwung-Bögen unabhaengig ausblenden kann.""" + return _layer_path_oeff_kind(doc, geschoss_name, "schwung") + + +def _ensure_oeff_material(doc, kind): + """Findet oder erstellt ein Material fuer ein Oeffnungs-Piece-Kind. + Unterstuetzt Transparenz (fuer Glas) + Index of Refraction. Cached + pro Kind ueber sc.sticky. Liefert Material-Index oder -1. + + Defensiv geschrieben — bei alten/neuen Rhino-Versionen koennen einzelne + Material-Properties fehlen, wir setzen nur was geht.""" + spec = _OEFF_PIECE_DEFS.get(kind) + if spec is None: return -1 + cache = sc.sticky.get("_dossier_oeff_mat_cache") + if not isinstance(cache, dict): + cache = {} + sc.sticky["_dossier_oeff_mat_cache"] = cache + cached = cache.get(kind) + 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[kind] + try: + import System.Drawing as SD + h = spec["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_Oeff_" + spec["layer"] + mat.DiffuseColor = SD.Color.FromArgb(255, r, g, b) + try: mat.Transparency = float(spec.get("transparency", 0.0)) + except Exception: pass + try: mat.IndexOfRefraction = float(spec.get("ior", 1.0)) + except Exception: pass + # Glas zusaetzlich: TransparentColor leicht bluestichig damit Render + # nicht komplett klar wird (sonst sieht es im Standard-Display wie + # ein Loch aus). Optional weiches Reflektions-Highlight. + if kind == "glas": + try: mat.TransparentColor = SD.Color.FromArgb(255, 220, 235, 245) + except Exception: pass + try: mat.Shine = 0.7 * mat.MaxShine + except Exception: pass + try: mat.Reflectivity = 0.05 + except Exception: pass + idx = doc.Materials.Add(mat) + if idx >= 0: + cache[kind] = idx + return idx + except Exception as ex: + print("[ELEMENTE] _ensure_oeff_material:", ex) + return -1 + + def _layer_path_decke(doc, geschoss_name): """Decken-Outline + Volumen — Sublayer 'DECKEN' (Code 30).""" sub = _find_ebene_sublayer_name(doc, ["decke"], "30", "DECKEN", @@ -1909,7 +2105,7 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, oeff_aussenseite=None, oeff_tuer_rahmen=None, oeff_rahmen_offset=None, oeff_style_id=None, oeff_tuer_typ=None, oeff_hinge_side=None, oeff_open_angle=None, - oeff_swing_invert=None, + oeff_swing_invert=None, oeff_sturz=None, geschoss_end=None, treppe_breite=None, treppe_n=None, treppe_referenz=None, treppe_modus=None, treppe_lauf_d=None, treppe_art=None, @@ -2006,6 +2202,9 @@ def _attach_meta(obj_attrs, wall_id, type_, geschoss, dicke, uk_over, ok_over, try: obj_attrs.SetUserString(_KEY_OEFF_SWING_INVERT, "1" if bool(oeff_swing_invert) else "0") except Exception: pass + if oeff_sturz is not None and oeff_sturz in _OEFF_STURZ_OPTIONS: + try: obj_attrs.SetUserString(_KEY_OEFF_STURZ, oeff_sturz) + except Exception: pass # --- Treppen-Felder --- if geschoss_end is not None: obj_attrs.SetUserString(_KEY_GESCHOSS_END, geschoss_end or "") @@ -2156,8 +2355,8 @@ def _read_meta(obj): ogl = (og_raw == "1") if og_raw in ("0", "1") else is_fenster oref = a.GetUserString(_KEY_OEFF_REFERENZ) or "mid" if oref not in _OEFF_REFERENZ_OPTIONS: oref = "mid" - odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "standard" - if odarst not in _OEFF_DARSTELLUNGEN: odarst = "standard" + odarst = a.GetUserString(_KEY_OEFF_DARSTELLUNG) or "auto" + if odarst not in _OEFF_DARSTELLUNGEN: odarst = "auto" oauss = a.GetUserString(_KEY_OEFF_AUSSENSEITE) or "rechts" if oauss not in _OEFF_AUSSENSEITEN: oauss = "rechts" otrah = a.GetUserString(_KEY_OEFF_TUER_RAHMEN) or "zarge" @@ -2178,6 +2377,8 @@ def _read_meta(obj): if oangle < 0: oangle = 0.0 elif oangle > 180: oangle = 180.0 oswinv = (a.GetUserString(_KEY_OEFF_SWING_INVERT) == "1") + ostz = a.GetUserString(_KEY_OEFF_STURZ) or "beide" + if ostz not in _OEFF_STURZ_OPTIONS: ostz = "beide" # Treppen-Felder gend = a.GetUserString(_KEY_GESCHOSS_END) or "" try: tb = float(a.GetUserString(_KEY_TREPPE_BREITE) or "1.0") @@ -2303,6 +2504,7 @@ def _read_meta(obj): "oeff_hinge_side": ohinge, "oeff_open_angle": oangle, "oeff_swing_invert": oswinv, + "oeff_sturz": ostz, "geschoss_end": gend, "treppe_breite": tb, "treppe_n": tn, @@ -2523,6 +2725,17 @@ def _resolve_rahmen_perp_range(half_d, rahmen_tiefe, rahmen_pos): return lo, hi +def _resolve_oeff_darstellung(oeff_meta): + """Per-Object-Setting wenn != 'auto', sonst Doc-Level-Modelldarstellung. + Ergibt immer einen konkreten Wert (einfach/standard/detail).""" + per_obj = oeff_meta.get("oeff_darstellung", "auto") + if per_obj not in _OEFF_DARSTELLUNGEN: per_obj = "auto" + if per_obj != "auto" and per_obj in ("einfach", "standard", "detail"): + return per_obj + doc = Rhino.RhinoDoc.ActiveDoc + return get_aktive_darstellung(doc) if doc is not None else _DARSTELLUNG_DEFAULT_GLOBAL + + def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke, oeff_meta, base_z): """Generiert die 2D-Plan-Schwung-Linien einer Tuer: @@ -2542,7 +2755,7 @@ def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke, pt, tan, perp = frame breite = float(oeff_meta.get("oeff_breite", 0.9)) rahmen_b = float(oeff_meta.get("oeff_rahmen_b", 0.06)) - darstellung = oeff_meta.get("oeff_darstellung", "standard") + darstellung = _resolve_oeff_darstellung(oeff_meta) aussenseite = oeff_meta.get("oeff_aussenseite", "rechts") aus_sign = +1 if aussenseite == "rechts" else -1 hinge_side = oeff_meta.get("oeff_hinge_side", "links") @@ -2644,11 +2857,72 @@ def _make_tuer_swing_curves(axis_curve, point_on_axis, wall_dicke, return curves +def _make_tuer_sturz_curves(axis_curve, point_on_axis, wall_dicke, + oeff_meta, base_z): + """Sturzlinien (Lintel-Projektion) bei 1:100 Tueren — zeigt die + Aussen-/Innen-Kante des Wand-Sturzes als gestrichelte Linie quer ueber + die Oeffnung. Klassische SIA-Plan-Konvention: 'Was oberhalb der + Schnitt-Ebene liegt, wird gestrichelt projiziert'. + + Steuerung via oeff_meta['oeff_sturz']: + 'keine' → keine Linien + 'innen' → eine Linie an der Wand-Innenseite + 'aussen' → eine Linie an der Wand-Aussenseite + 'beide' → beide Linien (Default) + + Liefert Liste von LineCurves (0, 1 oder 2 Stueck). Plan-Z = base_z + (= OKFF des Geschosses fuer die Tuer).""" + sturz_mode = oeff_meta.get("oeff_sturz", "beide") + if sturz_mode not in _OEFF_STURZ_OPTIONS or sturz_mode == "keine": + return [] + frame = _oeff_axis_frame(axis_curve, point_on_axis) + if frame is None: return [] + pt, tan, _perp = frame + + breite = float(oeff_meta.get("oeff_breite", 1.0)) + aus_sign = +1 if oeff_meta.get("oeff_aussenseite", "rechts") == "rechts" else -1 + half_b = breite * 0.5 + half_d = float(wall_dicke) * 0.5 + z0 = float(base_z) + + # Wand-Achse → 2 Perp-Offsets fuer Aussen-/Innenkante. + # perp_outside_dir = (+aus_sign) * (perp_unit_x, perp_unit_y) + perp_x = -tan.Y + perp_y = +tan.X + outside_perp = +half_d + inside_perp = -half_d + if aus_sign < 0: + outside_perp, inside_perp = inside_perp, outside_perp + + # Pro Seite ein LineCurve: links nach rechts entlang tan, am jeweiligen + # perp-Offset (Aussen oder Innen) der Wand. + def _line_at(perp_off): + pl = rg.Point3d(pt.X + tan.X * (-half_b) + perp_x * perp_off, + pt.Y + tan.Y * (-half_b) + perp_y * perp_off, z0) + pr = rg.Point3d(pt.X + tan.X * (+half_b) + perp_x * perp_off, + pt.Y + tan.Y * (+half_b) + perp_y * perp_off, z0) + try: return rg.LineCurve(pl, pr) + except Exception: return None + + out = [] + if sturz_mode in ("aussen", "beide"): + c = _line_at(outside_perp) + if c is not None: out.append(c) + if sturz_mode in ("innen", "beide"): + c = _line_at(inside_perp) + if c is not None: out.append(c) + return out + + def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base_z): """Baut die einzelnen Brep-Pieces der Oeffnung — Rahmen (single Brep - via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen, - Sims innen. Liefert eine Liste von Breps. Caller persistiert jedes - als eigenes 'oeffnung_volume' Object mit der gleichen Oeffnungs-ID.""" + via Boolean-Differenz), Mittelpfosten (pro Fluegel), Glas, Sims aussen. + Liefert eine Liste von (brep, kind)-Tupeln. Kind ist einer von + 'rahmen' / 'glas' / 'fluegel' / 'sims' / 'pane' — wird vom Caller fuer + Layer-Routing (`_layer_path_oeff_kind`) + Material-Assignment + (`_ensure_oeff_material`) genutzt. Caller persistiert jedes als eigenes + 'oeffnung_volume' Object mit der gleichen Oeffnungs-ID + dossier_oeff_piece + UserString.""" frame = _oeff_axis_frame(axis_curve, point_on_axis) if frame is None: return [] pt, tan, perp = frame @@ -2664,14 +2938,7 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base sims_in_style = oeff_meta.get("oeff_sims_in", "ohne") has_glas = bool(oeff_meta.get("oeff_glas", False)) is_tuer = (oeff_meta.get("oeff_typ") == "tuer") - # Doc-level Darstellungs-Override gewinnt vor per-Object-Setting — - # damit Ausschnitt-Wechsel / Oberleiste-Quick-Switch alle Oeffnungen - # konsistent zwingen koennen. - _doc = Rhino.RhinoDoc.ActiveDoc - global_darst = get_aktive_darstellung(_doc) if _doc is not None else "" - darstellung = global_darst or oeff_meta.get("oeff_darstellung", "standard") - if darstellung not in _OEFF_DARSTELLUNGEN: - darstellung = "standard" + darstellung = _resolve_oeff_darstellung(oeff_meta) # Aussenseite: +1 = aussen ist +Plane.ZAxis (default), -1 = aussen ist # -Plane.ZAxis (Wand "umgedreht"). Wird verwendet um Sims aus/in # konsistent zu platzieren unabhaengig von der Wand-Achsen-Richtung. @@ -2723,7 +2990,11 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base # Rahmen-Mittelebene (= dort wo das Glas im Standard sitzt). Im # 2D-Plan ergibt das eine einzelne Linie quer durch die Oeffnung, # auf dem korrekten Tiefen-Offset. + # Bei Tueren ist diese Pane nicht gewuenscht — eine Tuere im Plan + # zeigt nur den Wand-Cutout + Schwung-Bogen, keine flache Scheibe. if darstellung == "einfach": + if is_tuer: + return [] try: pane_perp = (frame_perp_lo + frame_perp_hi) * 0.5 # Plane.Origin ans Wand-Achsenpunkt verschoben um pane_perp @@ -2739,7 +3010,7 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base rg.Interval(-half_b, +half_b), rg.Interval(z_lo, z_hi)) brep = surf.ToBrep() - return [brep] if brep is not None else [] + return [(brep, "pane")] if brep is not None else [] except Exception as ex: print("[ELEMENTE] einfach pane:", ex) return [] @@ -2768,13 +3039,14 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base diff = rg.Brep.CreateBooleanDifference( [outer_box], [inner_box], 0.001) if diff and len(diff) > 0: - pieces.append(diff[0]) + pieces.append((diff[0], "rahmen")) else: - pieces.append(outer_box) + pieces.append((outer_box, "rahmen")) except Exception as ex: print("[ELEMENTE] Rahmen BoolDiff:", ex) - # --- Mittelpfosten (Fluegel > 1): kleine Stege im inneren Bereich + # --- Mittelpfosten (Fluegel > 1): kleine Stege im inneren Bereich. + # Gleiche Material-Klasse wie Rahmen → 'rahmen'-Kind. if fluegel > 1: span = inner_r - inner_l for i in range(1, fluegel): @@ -2783,7 +3055,7 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base x_hi = x_mid + rahmen_b * 0.5 mp = _make_oeff_box(pt, tan, x_lo, x_hi, payload_z_lo, payload_z_hi, frame_perp_lo, frame_perp_hi) - if mp is not None: pieces.append(mp) + if mp is not None: pieces.append((mp, "rahmen")) # --- INNERE FUELLUNG: Glas (Fenster oder verglaste Tuer) ODER # Tuerblatt (massive Tuere ohne Glas). Beides als Box-Brep pro Fluegel. @@ -2795,7 +3067,10 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base else: fill_t = 0 # nichts (z.B. Fenster ohne Glas) - if fill_t > 0: + # Kind der Fuellung: 'glas' fuer Scheiben (Fenster oder verglaste Tuer), + # 'fluegel' fuer massive Tuerblaetter. + fill_kind = "glas" if has_glas else ("fluegel" if is_tuer else None) + if fill_t > 0 and fill_kind is not None: fill_mid = (frame_perp_lo + frame_perp_hi) * 0.5 # DETAIL (1:20): Doppelverglasung — 2 Scheiben a 6mm, 16mm SZR is_double_glas = (darstellung == "detail" and has_glas and not is_tuer) @@ -2822,12 +3097,12 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base fp = _make_oeff_box(pt, tan, fx_lo, fx_hi, payload_z_lo, payload_z_hi, fl, fh) - if fp is not None: pieces.append(fp) + if fp is not None: pieces.append((fp, fill_kind)) else: fp = _make_oeff_box(pt, tan, inner_l, inner_r, payload_z_lo, payload_z_hi, fl, fh) - if fp is not None: pieces.append(fp) + if fp is not None: pieces.append((fp, fill_kind)) # --- SIMS — nur AUSSEN. aus_sign=+1 -> sims auf +perp, =-1 -> auf # -perp. Drinnen nie. Sim ist gleichzeitig der visuelle Indikator @@ -2843,7 +3118,7 @@ def _make_oeffnung_pieces(axis_curve, point_on_axis, wall_dicke, oeff_meta, base sb = _make_oeff_box(pt, tan, -half_b - s_oh, +half_b + s_oh, s_lo, z_lo, p_lo, p_hi) - if sb is not None: pieces.append(sb) + if sb is not None: pieces.append((sb, "sims")) return pieces @@ -2853,8 +3128,8 @@ SOURCE_TYPES = ("wand_axis", "decke_outline", "dach_outline", "stuetze_point", "traeger_axis", "raum_outline", "decke_aussparung_outline") VOLUME_TYPES = ("wand_volume", "decke_volume", "dach_volume", - "oeffnung_volume", "oeffnung_swing", "treppe_volume", - "stuetze_volume", "traeger_volume", + "oeffnung_volume", "oeffnung_swing", "oeffnung_sturz", + "treppe_volume", "stuetze_volume", "traeger_volume", "raum_stamp", "raum_fill") # Oeffnungs-Cutout: Boolean-Difference aus Wand. Zusaetzlich kriegt die # Oeffnung ihr eigenes Volumen (Rahmen + Sims + Glas) als Sub-Element. @@ -4653,25 +4928,76 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name op_meta["geschoss"], meta["dicke"], "", "", oeff_typ="tuer", oeff_parent=op_meta.get("oeff_parent")) + sw_attrs.SetUserString("dossier_oeff_piece", "schwung") doc.Objects.AddCurve(crv, sw_attrs) + # Sturzlinien (Lintel-Projektion) — nur bei 1:100 Tueren mit + # explizit gesetztem oeff_sturz. Bei standard/detail uebernimmt + # der Rahmen-Brep die Sturz-Darstellung visuell. + old_st = list(_find_objects_by_wall_id(doc, op_meta["id"], + "oeffnung_sturz")) + for o, _m in old_st: + try: doc.Objects.Delete(o.Id, True) + except Exception: pass + if _resolve_oeff_darstellung(op_meta) == "einfach": + sturzes = _make_tuer_sturz_curves(geom, pt_loc, meta["dicke"], + op_meta, op_uk) + if sturzes: + st_layer_idx = _ensure_layer(doc, + _layer_path_oeff_kind(doc, geschoss_name, "sturz")) + for crv in sturzes: + st_attrs = Rhino.DocObjects.ObjectAttributes() + st_attrs.LayerIndex = st_layer_idx + _attach_meta(st_attrs, op_meta["id"], "oeffnung_sturz", + op_meta["geschoss"], meta["dicke"], "", "", + oeff_typ="tuer", + oeff_parent=op_meta.get("oeff_parent"), + oeff_sturz=op_meta.get("oeff_sturz")) + st_attrs.SetUserString("dossier_oeff_piece", "sturz") + doc.Objects.AddCurve(crv, st_attrs) old_objs = list(_find_objects_by_wall_id(doc, op_meta["id"], "oeffnung_volume")) pieces = _make_oeffnung_pieces(geom, pt_loc, meta["dicke"], op_meta, op_uk) - if len(old_objs) == len(pieces) and len(pieces) > 0: - for (old_obj, _old_meta), pbrep in zip(old_objs, pieces): + # Replace-Pfad nur wenn Count UND Kind-Reihenfolge identisch + # geblieben sind — andernfalls stimmt das Layer-/Material-Mapping + # nicht mehr und wir muessen sauber neu anlegen. + can_replace = (len(old_objs) == len(pieces) and len(pieces) > 0) + if can_replace: + for (old_obj, _m), (_pb, k) in zip(old_objs, pieces): + if (old_obj.Attributes.GetUserString("dossier_oeff_piece") + or "") != k: + can_replace = False; break + if can_replace: + for (old_obj, _m), (pbrep, _k) in zip(old_objs, pieces): try: doc.Objects.Replace(old_obj.Id, pbrep) except Exception as ex: print("[ELEMENTE] replace oeff vol:", ex) continue - # Fallback: Anzahl hat sich geaendert → alte loeschen + neue adden. + # Fallback: Anzahl oder Kind hat sich geaendert → alte loeschen + neue adden. for o, _m in old_objs: try: doc.Objects.Delete(o.Id, True) except Exception as ex: print("[ELEMENTE] del old oeff vol:", ex) - for pbrep in pieces: + for (pbrep, kind) in pieces: op_attrs = Rhino.DocObjects.ObjectAttributes() - op_attrs.LayerIndex = op_layer + # Per-Kind Sublayer unter WAENDE::Öffnungen:: + kind_layer = _ensure_layer(doc, + _layer_path_oeff_kind(doc, geschoss_name, kind)) + op_attrs.LayerIndex = kind_layer if kind_layer >= 0 else op_layer + # Per-Kind Material (Holz/Glas/Türblatt/...) — Glas hat + # Transparenz. Edges bleiben schwarz (siehe unten). + mat_idx = _ensure_oeff_material(doc, kind) + if mat_idx >= 0: + op_attrs.MaterialIndex = mat_idx + op_attrs.MaterialSource = ( + Rhino.DocObjects.ObjectMaterialSource.MaterialFromObject) + # Edges/Wireframe immer schwarz, entkoppelt vom Layer/Material. + try: + import System.Drawing as SD + op_attrs.ColorSource = ( + Rhino.DocObjects.ObjectColorSource.ColorFromObject) + op_attrs.ObjectColor = SD.Color.FromArgb(255, 0, 0, 0) + except Exception: pass _attach_meta(op_attrs, op_meta["id"], "oeffnung_volume", op_meta["geschoss"], meta["dicke"], "", "", oeff_typ=op_meta.get("oeff_typ"), @@ -4695,12 +5021,14 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name oeff_tuer_typ=op_meta.get("oeff_tuer_typ"), oeff_hinge_side=op_meta.get("oeff_hinge_side"), oeff_open_angle=op_meta.get("oeff_open_angle"), - oeff_swing_invert=op_meta.get("oeff_swing_invert")) + oeff_swing_invert=op_meta.get("oeff_swing_invert"), + oeff_sturz=op_meta.get("oeff_sturz")) + op_attrs.SetUserString("dossier_oeff_piece", kind) doc.Objects.AddBrep(pbrep, op_attrs) # Source-Layer migrieren + Volumen-Layer-Index ermitteln layer = _ensure_layer(doc, _layer_path_volume(doc, geschoss_name)) - src_layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + src_layer = _ensure_layer(doc, _layer_path_referenz(doc, geschoss_name)) try: if src_layer >= 0 and src_obj.Attributes.LayerIndex != src_layer: new_attrs = src_obj.Attributes.Duplicate() @@ -5312,6 +5640,7 @@ class ElementeBridge(panel_base.BaseBridge): "hingeSide": meta.get("oeff_hinge_side", "links"), "openAngle": meta.get("oeff_open_angle", 90.0), "swingInvert": bool(meta.get("oeff_swing_invert", False)), + "sturz": meta.get("oeff_sturz", "beide"), }) elif meta["type"] == "treppe_axis": gs = _geschoss_by_id(doc, meta["geschoss"]) @@ -5688,7 +6017,7 @@ class ElementeBridge(panel_base.BaseBridge): wall_id = "wall_" + uuid.uuid4().hex[:10] g = _geschoss_by_id(doc, geschoss_id) geschoss_name = g.get("name", "EG") if g else "EG" - axis_layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + axis_layer = _ensure_layer(doc, _layer_path_referenz(doc, geschoss_name)) axis = axis_curve.DuplicateCurve() try: z0 = axis.PointAtStart.Z @@ -6212,7 +6541,7 @@ class ElementeBridge(panel_base.BaseBridge): simsa_def = "standard" if is_fenster else "ohne" glas_def = is_fenster referenz_def = _last("oeff_referenz", "mid") - darst_def = "standard" + darst_def = "auto" tuer_rahmen_def = "zarge" # Pending-Style-ID aus sticky (von Stil-Picker gesetzt). Falls noch # kein Style gepickt, nutzen wir den zuletzt-aktiven fuer diesen typ @@ -6359,7 +6688,7 @@ class ElementeBridge(panel_base.BaseBridge): geschoss = wall_meta["geschoss"] g = _geschoss_by_id(doc, geschoss) geschoss_name = g.get("name", "EG") if g else "EG" - layer = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + layer = _ensure_layer(doc, _layer_path_referenz(doc, geschoss_name)) attrs = Rhino.DocObjects.ObjectAttributes() attrs.LayerIndex = layer @@ -6382,7 +6711,8 @@ class ElementeBridge(panel_base.BaseBridge): oeff_style_id=pending_sid, oeff_tuer_typ="normal", oeff_hinge_side="links", - oeff_open_angle=90.0) + oeff_open_angle=90.0, + oeff_sturz="beide") # Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf # Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit # UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste @@ -7604,7 +7934,7 @@ class ElementeBridge(panel_base.BaseBridge): # Terrain-Daten (XYZ + Grid) holen, sobald Mesh ODER # Hoehenlinien gewuenscht sind — beide nutzen das Grid. need_dem = any(k in kinds for k in - ("terrain", "contours", "contour_tin", "contour_schicht")) + ("terrain", "contours", "contour_tin", "contour_schicht", "contour_patch")) mesh_objects = [] merged_grid = None if need_dem: @@ -7656,13 +7986,14 @@ class ElementeBridge(panel_base.BaseBridge): except Exception as ex: self._push_log("Mesh-Bau fehlgeschlagen: {}".format(ex)) - # Contours sind die Grundlage fuer drei moegliche Outputs: + # Contours sind die Grundlage fuer vier moegliche Outputs: # 'contours' → flache 2D-Curves auf OKFF # 'contour_tin' → TIN-Mesh aus Contour-Vertices # 'contour_schicht' → Planare Flaechen pro Hoehe + # 'contour_patch' → NURBS-Patch-Surface durch alle Konturen # Wir generieren einmal die echten 3D-Curves und teilen - # sie auf die drei Outputs auf. - contour_kinds = ("contours", "contour_tin", "contour_schicht") + # sie auf die Outputs auf. + contour_kinds = ("contours", "contour_tin", "contour_schicht", "contour_patch") need_contours = any(k in kinds for k in contour_kinds) and merged_grid is not None raw_contours = [] if need_contours: @@ -7762,6 +8093,25 @@ class ElementeBridge(panel_base.BaseBridge): len(schicht_objs))) except Exception as ex: self._push_log("Schichtenmodell-Fehler: {}".format(ex)) + + # Patch-Terrain (NURBS-Surface durch alle Hoehenlinien) + if "contour_patch" in kinds and raw_contours: + try: + patch_obj = swisstopo.generate_patch_from_contours( + d, raw_contours, progress=self._push_log) + if patch_obj: + at = patch_obj.Attributes.Duplicate() + at.SetUserString("dossier_swisstopo_kind", "contour_patch") + d.Objects.ModifyAttributes(patch_obj, at, True) + if z_id: + self._move_to_sublayer( + d, [patch_obj], z_id, "80", + tag="contour_patch", + fallback_name="80_swisstopo", + fallback_color="#909090") + except Exception as ex: + self._push_log("Patch-Terrain-Fehler: {}".format(ex)) + # Layer-Move auf aktive Geschoss/80_swisstopo Sublayer if z_id and mesh_objects: sub_name = _find_ebene_sublayer_name( @@ -8833,6 +9183,8 @@ class ElementeBridge(panel_base.BaseBridge): o_angle = max(0.0, min(180.0, o_angle)) o_swinv = bool(p.get("swingInvert", old_meta.get("oeff_swing_invert", False))) + o_sturz = p.get("sturz", old_meta.get("oeff_sturz", "beide")) + if o_sturz not in _OEFF_STURZ_OPTIONS: o_sturz = "beide" if p.get("styleId") and p.get("styleId") != old_meta.get("oeff_style_id"): # Style wurde NEU gewaehlt → seine Werte applizieren styles = list_oeff_styles(doc) @@ -8881,7 +9233,8 @@ class ElementeBridge(panel_base.BaseBridge): oeff_tuer_typ=o_tuer_typ, oeff_hinge_side=o_hinge, oeff_open_angle=o_angle, - oeff_swing_invert=o_swinv) + oeff_swing_invert=o_swinv, + oeff_sturz=o_sturz) axis_obj.Attributes = attrs axis_obj.CommitChanges() parent_id = old_meta.get("oeff_parent", "") @@ -8932,7 +9285,7 @@ class ElementeBridge(panel_base.BaseBridge): g = _geschoss_by_id(doc, geschoss) geschoss_name = g.get("name", "EG") if g else "EG" if old_meta["type"] == "wand_axis": - attrs.LayerIndex = _ensure_layer(doc, _layer_path_axis(doc, geschoss_name)) + attrs.LayerIndex = _ensure_layer(doc, _layer_path_referenz(doc, geschoss_name)) elif old_meta["type"] == "decke_outline": attrs.LayerIndex = _ensure_layer(doc, _layer_path_decke(doc, geschoss_name)) elif old_meta["type"] == "dach_outline": @@ -9953,6 +10306,145 @@ def _sync_orphan_grips(doc): sc.sticky[_SELECT_BUSY] = False +def _migrate_referenz_layer_once(doc): + """One-shot pro Doc-Session: wand_axis + oeffnung_point auf den + neuen Referenzlinien-Sublayer migrieren, und alle versehentlich + versteckten Source-Objekte wieder zeigen. Sticky-Flag pro Doc-Id + damit ein Reopen sauber neu migriert. + + Triggert zudem proaktiv die Registrierung der Referenzlinien-Ebene + im dossier_ebenen-Tree (auch wenn der Doc noch keine wand_axis-Objekte + hat) — dafuer wird _layer_path_referenz fuer jedes existierende + Geschoss aufgerufen, was via _find_ebene_sublayer_name den Auto-Add + + broadcast_state ausloest.""" + if doc is None: return + # Sticky-Version bumped: vorherige Versionen liefen ohne proaktive + # Ebenen-Registrierung — wenn die alten Keys gesetzt sind, wuerde die + # neue Logik nie greifen. v3 = aktuelle Implementierung. + try: key = "_dossier_referenz_migration_v3_" + str(doc.RuntimeSerialNumber) + except Exception: key = "_dossier_referenz_migration_v3_default" + if sc.sticky.get(key): return + sc.sticky[key] = True + n_moved = 0 + n_shown = 0 + n_geschosse = 0 + by_geschoss = {} + try: + # Proaktive Registrierung: fuer jedes Geschoss die Referenzlinien- + # Layer anlegen. Damit erscheint die Ebene im Panel auch wenn noch + # keine Achsen migriert werden muessen. _layer_path_referenz ruft + # intern _find_ebene_sublayer_name, das die Referenzlinien-Ebene + # in dossier_ebenen einfuegt + broadcast_state ausloest. + geschosse = _load_geschosse(doc) + for g in geschosse: + if not isinstance(g, dict): continue + g_id = g.get("id") + g_name = g.get("name") + # Nur echte Geschosse (mit id) — nicht generische Ebenen-Eintraege + # die _load_geschosse als Fallback liefert wenn dossier_zeichnungs- + # ebenen leer ist. + if not g_id or not g_name: continue + try: + ref_idx = _ensure_layer(doc, + _layer_path_referenz(doc, g_name)) + if ref_idx >= 0: + by_geschoss[g_name] = ref_idx + n_geschosse += 1 + except Exception as ex: + print("[ELEMENTE] referenz-ebene fuer {}: {}".format(g_name, ex)) + for obj in list(doc.Objects): + try: + meta = _read_meta(obj) + if not meta: continue + t = meta.get("type") + if t not in ("wand_axis", "oeffnung_point"): continue + # Unhide + if obj.IsHidden: + if doc.Objects.Show(obj.Id, True): n_shown += 1 + # Layer-Migration auf Referenzlinien + g_id = meta.get("geschoss") + if not g_id: continue + g = _geschoss_by_id(doc, g_id) + if not g: continue + g_name = g.get("name") or "EG" + ref_idx = by_geschoss.get(g_name) + if ref_idx is None: + ref_idx = _ensure_layer(doc, + _layer_path_referenz(doc, g_name)) + by_geschoss[g_name] = ref_idx + if ref_idx < 0: continue + if obj.Attributes.LayerIndex != ref_idx: + a = obj.Attributes.Duplicate() + a.LayerIndex = ref_idx + if doc.Objects.ModifyAttributes(obj, a, True): + n_moved += 1 + except Exception: pass + print("[ELEMENTE] Referenz-Migration (v3): {} Geschoss(e) registriert, " + "{} Objekte bewegt, {} eingeblendet".format( + n_geschosse, n_moved, n_shown)) + # Finaler Broadcast — falls _find_ebene_sublayer_name den Eintrag + # nicht neu angelegt hat (z.B. weil er schon existiert) wird das + # Panel sonst nicht neu gerendert. + try: + import rhinopanel + rhinopanel._broadcast_state(doc) + except Exception: pass + except Exception as ex: + print("[ELEMENTE] _migrate_referenz_layer_once:", ex) + + +def _ensure_oeff_ebenen_once(doc): + """Idle-Tick One-Shot: registriert Oeffnungen-Subtree (Öffnungen + + 6 Piece-Kinder) in dossier_ebenen + migriert existierende + oeffnung_volume / oeffnung_swing Objekte auf die neuen code- + praefigierten Sublayer. Damit auch User die schon vorher Tueren/ + Fenster gezeichnet haben die Layer im Panel sehen + die alten + plain-Namen-Layer leeren wir damit der Layer-Manager sauber bleibt.""" + if doc is None: return + try: key = "_dossier_oeff_subtree_migration_" + str(doc.RuntimeSerialNumber) + except Exception: key = "_dossier_oeff_subtree_migration_default" + if sc.sticky.get(key): return + sc.sticky[key] = True + try: + # 1) Subtree in dossier_ebenen registrieren — feuert build_layers + # + broadcast_state intern + _ensure_oeff_ebenen_in_doc(doc) + # 2) Existierende Objekte auf die neuen Layer ziehen + n_moved = 0 + by_target = {} # (geschoss_name, kind) -> layer_idx + for obj in list(doc.Objects): + try: + meta = _read_meta(obj) + if not meta: continue + t = meta.get("type") + if t not in ("oeffnung_volume", "oeffnung_swing"): continue + kind = obj.Attributes.GetUserString("dossier_oeff_piece") + if not kind: continue + if kind not in _OEFF_PIECE_DEFS: continue + g_id = meta.get("geschoss") + if not g_id: continue + g = _geschoss_by_id(doc, g_id) + if not g: continue + g_name = g.get("name") or "EG" + cache_k = (g_name, kind) + target_idx = by_target.get(cache_k) + if target_idx is None: + target_idx = _ensure_layer(doc, + _layer_path_oeff_kind(doc, g_name, kind)) + by_target[cache_k] = target_idx + if target_idx < 0: continue + if obj.Attributes.LayerIndex != target_idx: + a = obj.Attributes.Duplicate() + a.LayerIndex = target_idx + if doc.Objects.ModifyAttributes(obj, a, True): + n_moved += 1 + except Exception: pass + if n_moved: + print("[ELEMENTE] Oeffnungen-Subtree-Migration: {} Objekte auf neue Sublayer".format(n_moved)) + except Exception as ex: + print("[ELEMENTE] _ensure_oeff_ebenen_once:", ex) + + def _on_idle_selection(sender, e): """Pollt periodisch die Selektion + verarbeitet Pending-Regenerate-Queue. @@ -9974,6 +10466,15 @@ def _on_idle_selection(sender, e): doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return + # One-shot Migration: alte Source-Curves (wand_axis, oeffnung_point) + # auf den neuen Referenzlinien-Layer schieben + alle versehentlich + # versteckten wieder zeigen (Cleanup nach der abgebrochenen + # Hide-on-Select-Implementation). + _migrate_referenz_layer_once(doc) + # Oeffnungen-Tree in dossier_ebenen anlegen falls noch nicht vorhanden + # (sonst erscheinen die neuen Sublayer nicht im Ebenen-Panel). + _ensure_oeff_ebenen_once(doc) + # 1) Pending Regenerations abarbeiten — debounct (50 ms Ruhe). # Kein EnableDrawing-Suspend mehr (das hat User-Feedback langsamer # gemacht und konnte das Volumen "verschwinden" lassen wenn der diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index 442c2a9..768ddbd 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -1087,6 +1087,32 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception as ex: print("[OBERLEISTE] open layer-combinations:", ex) + # --- Anordnen (DisplayOrder Z-Stack) ---------------------------- + # Nutzt Rhinos native _BringToFront / _BringForward / _SendBackward + # / _SendToBack. Diese setzen Attributes.DisplayOrder — keine + # Geometrie-Aenderung, kein Z-Offset. Selection-Check verhindert + # nervigen "Select objects"-Prompt wenn der User den Button leer + # drueckt. + elif t == "ARRANGE": + cmd = { + "front": "_BringToFront", + "forward": "_BringForward", + "backward": "_SendBackward", + "back": "_SendToBack", + }.get(p.get("dir")) + if cmd: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is not None: + try: + sel = list(doc.Objects.GetSelectedObjects(False, False)) + except Exception: sel = [] + if sel: + try: + Rhino.RhinoApp.SendKeystrokes("\x1b", False) + Rhino.RhinoApp.RunScript(cmd, False) + except Exception as ex: + print("[OBERLEISTE] arrange {}: {}".format(cmd, ex)) + # --- Command-Line Integration ----------------------------------- elif t == "RUN_COMMAND": cmd = (p.get("cmd") or "").strip() diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 0e35134..f4592e7 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -1148,10 +1148,22 @@ class EbenenBridge(panel_base.BaseBridge): return try: ebenen = json.loads(raw) - for e in ebenen: - if e.get("code") == code: - e[field] = value - break + # Rekursive Suche — Sub-Ebenen (z.B. WAENDE→Öffnungen→Sturz mit + # Code 20o7) liegen mehrere Ebenen tief. Frueher nur Top-Level + # iteriert → Style-Changes an nested Sublayer wurden nicht + # persistiert und kamen beim naechsten broadcast als alte Werte + # zurueck. + def _set_in_tree(lst): + for e in lst: + if not isinstance(e, dict): continue + if e.get("code") == code: + e[field] = value + return True + kids = e.get("children") + if isinstance(kids, list) and _set_in_tree(kids): + return True + return False + _set_in_tree(ebenen) doc.Strings.SetString("dossier_ebenen", json.dumps(ebenen, ensure_ascii=False)) _broadcast_state(doc) except Exception as ex: diff --git a/rhino/swisstopo.py b/rhino/swisstopo.py index df91b5f..8b9e09f 100644 --- a/rhino/swisstopo.py +++ b/rhino/swisstopo.py @@ -844,6 +844,71 @@ def generate_schichtenmodell(doc, contour_curves, progress=None): return created +def generate_patch_from_contours(doc, contour_curves, progress=None): + """Erzeugt eine NURBS-Patch-Flaeche durch alle Hoehenlinien — der + kanonische Rhino-Workflow fuer Terrain aus Konturen (Brep.CreatePatch + fittet eine Surface durch Curves + Points). Loft funktioniert hier + nicht, weil benachbarte Konturen unterschiedliche Topologie haben + (Inseln, Halbinseln, geschlossen vs. offen am Rand). + + UV-Spans werden aus dem Bounding-Box-Aspect-Ratio abgeleitet, so + dass ein rechteckiger Site mehr Spans in der laengeren Richtung + bekommt. + + Liefert das erstellte RhinoObject oder None.""" + import System + if not contour_curves: return None + valid = [c for c in contour_curves if c is not None] + if len(valid) < 2: + if progress: progress("Patch braucht mindestens 2 Hoehenlinien") + return None + bb = rg.BoundingBox.Unset + for c in valid: + bb_c = c.GetBoundingBox(True) + if not bb_c.IsValid: continue + if not bb.IsValid: + bb = bb_c + else: + bb.Union(bb_c) + if not bb.IsValid: + if progress: progress("Patch: ungueltige Bounding-Box") + return None + dx = bb.Max.X - bb.Min.X + dy = bb.Max.Y - bb.Min.Y + base_span = 40 + if dx >= dy and dy > 1e-6: + aspect = dx / dy + u_spans = int(round(base_span * math.sqrt(aspect))) + v_spans = base_span + elif dx > 1e-6: + aspect = dy / dx + u_spans = base_span + v_spans = int(round(base_span * math.sqrt(aspect))) + else: + u_spans = v_spans = base_span + u_spans = max(8, min(200, u_spans)) + v_spans = max(8, min(200, v_spans)) + if progress: + progress("Patch aus {} Hoehenlinien ({}x{} UV-Spans)...".format( + len(valid), u_spans, v_spans)) + geom_list = System.Collections.Generic.List[rg.GeometryBase]() + for c in valid: geom_list.Add(c) + try: + brep = rg.Brep.CreatePatch( + geom_list, u_spans, v_spans, doc.ModelAbsoluteTolerance) + if brep is None: + if progress: progress("Patch fehlgeschlagen (None zurueck)") + return None + gid = doc.Objects.AddBrep(brep) + if gid and gid != System.Guid.Empty: + obj = doc.Objects.Find(gid) + if progress: progress("→ Patch-Terrain erzeugt") + return obj + except Exception as ex: + if progress: progress("Patch-Fehler: {}".format(ex)) + return None + + def generate_contour_curves(grid, shift_lv95, m_to_unit, interval=2.0, progress=None): """Generiert Hoehenlinien (Contour-Curves) aus dem Terrain-Grid via diff --git a/src/AusschnittSettingsApp.jsx b/src/AusschnittSettingsApp.jsx index a0a1291..5362b39 100644 --- a/src/AusschnittSettingsApp.jsx +++ b/src/AusschnittSettingsApp.jsx @@ -90,13 +90,13 @@ export default function AusschnittSettingsApp() { + hint="SIA-400 Detaillierungsgrad fuer diesen Ausschnitt — leer = beim Restore nicht aendern"> - {suffix && {suffix}} + {suffix && ( + + {suffix} + + )} ) } diff --git a/src/ElementeApp.jsx b/src/ElementeApp.jsx index 3f06510..1b37951 100644 --- a/src/ElementeApp.jsx +++ b/src/ElementeApp.jsx @@ -1777,9 +1777,10 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
onUpdate({ darstellung: v })} - title="Detaillierungsgrad — beeinflusst die generierte Geometrie"> + title="Detaillierungsgrad — Auto folgt der Modelldarstellung in der Topbar"> + @@ -1874,6 +1875,24 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
)} + {!isFenster && (oeff.tuerTyp || 'normal') === 'normal' && ( +
+ + Sturz + + +
+ )} +
Breite setDarstellung(v)} - title="Darstellungs-Override fuer Fenster/Tueren (SIA-400 LoD)" + title="Modelldarstellung — Default fuer Fenster/Tueren auf 'Auto'. Einzelobjekt-Override im Properties-Panel." width={PRESET_W} > - @@ -631,6 +631,65 @@ export default function OberleisteApp() {
+ {/* ====== ANORDNEN (2D-Z-Stack via Rhino-DisplayOrder) ====== + 2x2-Grid (quadratisch): oben Aufwaerts-Aktionen, unten Abwaerts. + Reihe 1: Vorderste | 1 hoch (vertical_align_top, expand_less) + Reihe 2: 1 runter | Hinterste (expand_more, vertical_align_bottom) + Selection-Check + Rhino-DisplayOrder im Backend, keine Z-Offsets. */} + {(() => { + const CELL = 26 // quadratisch: 2 * CELL Breite, ~2 * BAR_H Hoehe + const Btn = ({ icon, dir, title, isFirst }) => ( + + ) + const rowStyle = { + display: 'inline-flex', width: CELL * 2, + height: BAR_H + 2, boxSizing: 'border-box', + border: '1px solid var(--border)', borderRadius: 999, + overflow: 'hidden', flexShrink: 0, + } + return ( +
+
+ + +
+
+ + +
+
+ ) + })()} + +
+ {/* ====== TEXT-Block (Vectorworks-Stil) ====== Reihe 1: Style ▼ | Font ▼ | Size ▼ Reihe 2: [B][I][U] | [L][C][R] | [+] diff --git a/src/SwisstopoApp.jsx b/src/SwisstopoApp.jsx index 36eb398..4189ed2 100644 --- a/src/SwisstopoApp.jsx +++ b/src/SwisstopoApp.jsx @@ -64,6 +64,7 @@ export default function SwisstopoApp() { const [getContours, setGetContours] = useState(false) const [getContourTin,setGetContourTin]= useState(false) const [getContourSchicht, setGetContourSchicht] = useState(false) + const [getContourPatch, setGetContourPatch] = useState(false) const [contourInt, setContourInt] = useState('2.0') // TLM3D deaktiviert: swisstopo liefert nur GDB/SHP/GPKG — kein DXF. // Rhino kann das nicht nativ importieren; OSM-Importer ist die Alternative @@ -134,7 +135,7 @@ export default function SwisstopoApp() { const handleImport = () => { if (!center) { addLog('Bitte zuerst einen Standort wählen'); return } - if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getTlm) { + if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getContourPatch && !getTlm) { addLog('Mindestens eine Datenquelle wählen'); return } setLogs([]) @@ -147,6 +148,7 @@ export default function SwisstopoApp() { if (getContours) kinds.push('contours') if (getContourTin) kinds.push('contour_tin') if (getContourSchicht)kinds.push('contour_schicht') + if (getContourPatch) kinds.push('contour_patch') if (getTlm) kinds.push('tlm') const tlmList = Object.entries(tlmKinds).filter(([, v]) => v).map(([k]) => k) send('RUN_IMPORT', { @@ -344,7 +346,16 @@ export default function SwisstopoApp() { Schichtenmodell aus Höhenlinien - {(getContours || getContourTin || getContourSchicht) && ( + + + + {(getContours || getContourTin || getContourSchicht || getContourPatch) && ( + {/* Chevron sitzt visuell weiter rechts (marginLeft) — marginRight + kompensiert das, damit die nachfolgenden Elemente (Auge, Code, + Farbe, Name) nicht mitrutschen. Spacer fuer kinderlose Zeilen + spiegelt dasselbe Offset, sonst springt die Eye-Spalte zwischen + Parent- und Leaf-Zeilen. */} {hasChildren ? ( ) : ( - + )}