#! python 3 # -*- coding: utf-8 -*- """ text_create.py Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog. Flow: 1. User klickt Topbar-Button "Text einfuegen" 2. RhinoGet.GetPoint(): User picked Position im Viewport 3. Eto.Dialog mit TextBox erscheint neben Maus-Cursor 4. User tippt, Enter = commit, Escape = abbrechen 5. TextEntity wird mit Topbar-Settings (Font/Size/Bold/Italic) am gepickten Punkt erstellt Settings werden pro Dokument in doc.Strings["dossier_text_settings"] persistiert (JSON: font/size/bold/italic). """ import os import sys import json import Rhino import Rhino.Geometry as rg import scriptcontext as sc _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) _SETTINGS_KEY = "dossier_text_settings" _STYLES_KEY = "dossier_text_styles" _STYLE_ACTIVE_KEY = "dossier_text_style_active" _ALIGNS = ("left", "center", "right") _DEFAULTS = { "font": "Helvetica", "size": 0.20, # in Model-Units (m bei m-Doc, mm bei mm-Doc) "bold": False, "italic": False, "underline": False, "align": "left", } def _normalize(s): out = dict(_DEFAULTS) if isinstance(s, dict): for k, v in s.items(): if k not in _DEFAULTS: continue if k == "align" and v not in _ALIGNS: continue out[k] = v return out def load_settings(doc): if doc is None: return dict(_DEFAULTS) try: raw = doc.Strings.GetValue(_SETTINGS_KEY) if not raw: return dict(_DEFAULTS) return _normalize(json.loads(raw)) except Exception: return dict(_DEFAULTS) def save_settings(doc, partial): """Merged partial-Updates rein und persistiert.""" if doc is None or not isinstance(partial, dict): return cur = load_settings(doc) for k, v in partial.items(): if k not in _DEFAULTS: continue if k == "align" and v not in _ALIGNS: continue cur[k] = v try: doc.Strings.SetString(_SETTINGS_KEY, json.dumps(cur)) except Exception as ex: print("[TEXT] save settings:", ex) # --------------------------------------------------------------------------- # Text-Stile (Presets, analog mass_style): doc.Strings JSON-Liste mit # benannten Settings + aktiver Style-ID. # Default-Stile fuer Architektur-Workflow — werden bei erstem list_styles # automatisch erzeugt wenn das Doc noch keine eigenen hat. _DEFAULT_STYLES = [ {"name": "Titel", "font": "Helvetica", "size": 0.40, "bold": True, "italic": False, "underline": False, "align": "left"}, {"name": "Heading 1", "font": "Helvetica", "size": 0.30, "bold": True, "italic": False, "underline": False, "align": "left"}, {"name": "Heading 2", "font": "Helvetica", "size": 0.20, "bold": True, "italic": False, "underline": False, "align": "left"}, {"name": "Paragraph (Helvetica)", "font": "Helvetica", "size": 0.15, "bold": False, "italic": False, "underline": False, "align": "left"}, {"name": "Paragraph (Georgia)", "font": "Georgia", "size": 0.15, "bold": False, "italic": False, "underline": False, "align": "left"}, {"name": "Notiz", "font": "Helvetica", "size": 0.10, "bold": False, "italic": True, "underline": False, "align": "left"}, {"name": "Bildlegende", "font": "Helvetica", "size": 0.08, "bold": False, "italic": True, "underline": False, "align": "left"}, ] def list_styles(doc): if doc is None: return [] try: raw = doc.Strings.GetValue(_STYLES_KEY) if not raw: # Seed Defaults bei erstem Zugriff seeded = [_normalize(s) for s in _DEFAULT_STYLES] for i, s in enumerate(seeded): s["id"] = "ts_default_" + str(i) s["name"] = _DEFAULT_STYLES[i]["name"] try: doc.Strings.SetString(_STYLES_KEY, json.dumps(seeded)) except Exception: pass return seeded items = json.loads(raw) if not isinstance(items, list): return [] out = [] for it in items: if not isinstance(it, dict): continue norm = _normalize(it) norm["id"] = it.get("id") or ("ts_" + uuid.uuid4().hex[:8]) norm["name"] = it.get("name") or "Stil" out.append(norm) return out except Exception as ex: print("[TEXT] list_styles:", ex) return [] def get_active_style_id(doc): if doc is None: return None try: return doc.Strings.GetValue(_STYLE_ACTIVE_KEY) or None except Exception: return None def set_active_style_id(doc, sid): if doc is None: return try: doc.Strings.SetString(_STYLE_ACTIVE_KEY, sid or "") except Exception: pass def save_style(doc, name, settings=None): """Speichert aktuelle Settings als benannten Style. Returns die ID.""" if doc is None or not name: return None items = list_styles(doc) # Existing same-name → update sid = None for it in items: if it["name"] == name: sid = it["id"]; break norm = _normalize(settings if settings is not None else load_settings(doc)) norm["id"] = sid or ("ts_" + uuid.uuid4().hex[:8]) norm["name"] = name if sid: items = [norm if it["id"] == sid else it for it in items] else: items.append(norm) try: doc.Strings.SetString(_STYLES_KEY, json.dumps(items)) except Exception as ex: print("[TEXT] save_style:", ex) return norm["id"] def delete_style(doc, sid): if doc is None or not sid: return items = [it for it in list_styles(doc) if it.get("id") != sid] try: doc.Strings.SetString(_STYLES_KEY, json.dumps(items)) except Exception: pass if get_active_style_id(doc) == sid: set_active_style_id(doc, "") def apply_style(doc, sid): """Wendet einen Style als aktive Defaults an + (wenn Selektion) auch auf die selektierten TextEntities.""" if doc is None or not sid: return items = list_styles(doc) style = next((it for it in items if it["id"] == sid), None) if not style: return set_active_style_id(doc, sid) # Defaults aus Style schreiben (ohne id/name) patch = {k: style[k] for k in style if k in _DEFAULTS} save_settings(doc, patch) apply_settings_to_selection(doc, patch) _FONT_FALLBACK = [ "Helvetica", "Helvetica Neue", "Arial", "Arial Narrow", "Times New Roman", "Times", "Courier", "Courier New", "Menlo", "Monaco", "Verdana", "Geneva", "Lucida Grande", "Avenir", "Avenir Next", "Optima", "Palatino", ] def available_fonts(): """Sortierte Liste verfuegbarer System-Fonts. Mit Fallback auf haeufige Mac-Fonts falls die Rhino-API nichts liefert.""" try: names = Rhino.DocObjects.Font.AvailableFontFaceNames() out = sorted({str(n) for n in names if n}) if out: return out except Exception as ex: print("[TEXT] available_fonts:", ex) return list(_FONT_FALLBACK) def _prompt_for_text(default=""): """Nativer Rhino-Dialog fuer Text-Eingabe via ShowEditBox. Wirkt cross-platform (Mac+Windows), kein Eto-Modal-Bug. Returns text oder None bei Abbruch/leer.""" try: rc, text = Rhino.UI.Dialogs.ShowEditBox( "Text", "Text:", default or "", False) if not rc: return None text = (text or "").strip() return text or None except Exception as ex: print("[TEXT] ShowEditBox:", ex) return None def _dossier_text_editor(p1, p2, settings, fonts, initial=""): """DOSSIER Custom Text-Editor: eigene Toolbar (Font, Size, Farbe, B/I/U, Align), Symbol-Palette mit Sonderzeichen, Multi-Line-TextArea. Returns dict {text, font, size, bold, italic, underline, align, color} oder None bei Abbruch. color ist (r,g,b)-Tuple oder None (= Layer-Farbe). """ import Eto.Forms as forms import Eto.Drawing as drawing import System # Frame-Position fuer Dialog-Positionierung (rechts neben dem Frame) sx = sy = None try: view = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView vp = view.ActiveViewport c1 = vp.WorldToClient(p1) c2 = vp.WorldToClient(p2) view_rect = view.ScreenRectangle fx = view_rect.X + int(min(c1.X, c2.X)) fy = view_rect.Y + int(min(c1.Y, c2.Y)) fw = abs(int(c2.X - c1.X)) sx = int(fx + fw + 20) sy = int(fy) except Exception as ex: print("[TEXT] viewport-coords:", ex) # Editor-State (mutable durch Closures) st = { "text": initial or "", "font": settings.get("font") or "Helvetica", "size": float(settings.get("size") or 0.2), "bold": bool(settings.get("bold")), "italic": bool(settings.get("italic")), "underline": bool(settings.get("underline")), "align": settings.get("align") or "left", "color": None, # None = Layer-Farbe } result = {"committed": False, "state": None} dlg = forms.Dialog() dlg.Title = "Dossier Text" dlg.Resizable = True dlg.Padding = drawing.Padding(10) try: dlg.MinimumSize = drawing.Size(580, 440) except Exception: pass if sx is not None and sy is not None: try: dlg.Location = drawing.Point(sx, sy) except Exception: pass # === TextArea zuerst (wird von Symbol-Inserts angesprochen) === ta = forms.TextArea() ta.AcceptsReturn = True ta.AcceptsTab = False ta.Wrap = True ta.Text = st["text"] try: ta.Font = drawing.Font(st["font"], 14) except Exception: pass try: ta.Size = drawing.Size(560, 240) except Exception: pass def _update_ta_font(): try: ta.Font = drawing.Font(st["font"], 14) except Exception: pass # === Toolbar Row 1: Font / Size / Color === font_dd = forms.DropDown() for f in fonts: font_dd.Items.Add(f) try: for i, f in enumerate(fonts): if f == st["font"]: font_dd.SelectedIndex = i; break except Exception: pass def on_font_change(s, e): try: idx = font_dd.SelectedIndex if idx >= 0 and idx < len(fonts): st["font"] = fonts[idx] _update_ta_font() except Exception: pass font_dd.SelectedIndexChanged += on_font_change size_input = forms.NumericStepper() size_input.MinValue = 0.001 size_input.MaxValue = 100.0 size_input.Increment = 0.05 size_input.DecimalPlaces = 3 size_input.Value = st["size"] def on_size_change(s, e): try: st["size"] = float(size_input.Value) except Exception: pass size_input.ValueChanged += on_size_change color_picker = forms.ColorPicker() try: color_picker.AllowAlpha = False except Exception: pass try: color_picker.Value = drawing.Colors.Black except Exception: pass def on_color_change(s, e): try: c = color_picker.Value st["color"] = (int(c.Rb), int(c.Gb), int(c.Bb)) except Exception: pass color_picker.ValueChanged += on_color_change color_reset = forms.Button() color_reset.Text = "Layer" color_reset.ToolTip = "Farbe der Ebene benutzen" def on_color_reset(s, e): st["color"] = None try: color_picker.Value = drawing.Colors.Black except Exception: pass color_reset.Click += on_color_reset # === Toolbar Row 2: B I U + Align === bold_check = forms.CheckBox() bold_check.Text = "B" bold_check.Checked = st["bold"] bold_check.CheckedChanged += lambda s, e: st.update(bold=bool(bold_check.Checked)) italic_check = forms.CheckBox() italic_check.Text = "I" italic_check.Checked = st["italic"] italic_check.CheckedChanged += lambda s, e: st.update(italic=bool(italic_check.Checked)) underline_check = forms.CheckBox() underline_check.Text = "U" underline_check.Checked = st["underline"] underline_check.CheckedChanged += lambda s, e: st.update(underline=bool(underline_check.Checked)) align_left = forms.RadioButton() align_left.Text = "L" align_center = forms.RadioButton(align_left) align_center.Text = "C" align_right = forms.RadioButton(align_left) align_right.Text = "R" if st["align"] == "center": align_center.Checked = True elif st["align"] == "right": align_right.Checked = True else: align_left.Checked = True align_left.CheckedChanged += lambda s, e: (st.update(align="left") if align_left.Checked else None) align_center.CheckedChanged += lambda s, e: (st.update(align="center") if align_center.Checked else None) align_right.CheckedChanged += lambda s, e: (st.update(align="right") if align_right.Checked else None) # === Symbol-Palette (Unicode-Buttons, inserten am Cursor) === # Architektur, Mathe, Pfeile, Auszeichnungen SYMBOLS = [ '∅', 'Ø', '⌀', '°', '±', '×', '÷', '²', '³', '½', '¼', '¾', '⅓', '⅔', '≤', '≥', '≠', '≈', '∞', '√', '∆', 'π', 'µ', '←', '→', '↑', '↓', '↔', '↕', '•', '·', '▪', '◆', '★', '☆', '✓', '✗', '§', '¶', '©', '®', '™', ] def insert_at_cursor(s_char): try: pos = ta.CaretIndex txt = ta.Text or "" ta.Text = txt[:pos] + s_char + txt[pos:] try: ta.CaretIndex = pos + len(s_char) except Exception: pass try: ta.Focus() except Exception: pass except Exception as ex: print("[TEXT] insert:", ex) def make_sym_handler(s_char): return lambda sender, e: insert_at_cursor(s_char) sym_panel = forms.DynamicLayout() sym_panel.Spacing = drawing.Size(2, 2) # 7 Symbole pro Reihe PER_ROW = 12 for row_start in range(0, len(SYMBOLS), PER_ROW): sym_panel.BeginHorizontal() for sym in SYMBOLS[row_start:row_start + PER_ROW]: btn = forms.Button() btn.Text = sym btn.MinimumSize = drawing.Size(28, 24) btn.ToolTip = "Einfuegen: " + sym btn.Click += make_sym_handler(sym) sym_panel.Add(btn) sym_panel.Add(None, True, False) sym_panel.EndHorizontal() # === Hochstellen / Tiefstellen (Unicode-Konvertierung der Selektion # bzw. der zuletzt eingegebenen Zahl. Einfacher Quick-Insert: ²³ etc.) SUP_MAP = {'0':'⁰','1':'¹','2':'²','3':'³','4':'⁴','5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹','+':'⁺','-':'⁻','=':'⁼'} SUB_MAP = {'0':'₀','1':'₁','2':'₂','3':'₃','4':'₄','5':'₅','6':'₆','7':'₇','8':'₈','9':'₉','+':'₊','-':'₋','=':'₌'} def convert_selection(mapping): try: sel = ta.SelectedText or "" if not sel: return converted = "".join(mapping.get(c, c) for c in sel) txt = ta.Text or "" start = ta.Selection.Start ta.Text = txt[:start] + converted + txt[start + len(sel):] try: ta.Focus() except Exception: pass except Exception as ex: print("[TEXT] convert:", ex) sup_btn = forms.Button() sup_btn.Text = "x²" sup_btn.ToolTip = "Markierte Ziffern hochstellen" sup_btn.Click += lambda s, e: convert_selection(SUP_MAP) sub_btn = forms.Button() sub_btn.Text = "x₂" sub_btn.ToolTip = "Markierte Ziffern tiefstellen" sub_btn.Click += lambda s, e: convert_selection(SUB_MAP) # === Buttons unten === ok_btn = forms.Button() ok_btn.Text = "Einfuegen" cancel_btn = forms.Button() cancel_btn.Text = "Abbrechen" def on_ok(s, e): st["text"] = ta.Text or "" result["committed"] = True result["state"] = dict(st) try: dlg.Close() except Exception: pass def on_cancel(s, e): try: dlg.Close() except Exception: pass ok_btn.Click += on_ok cancel_btn.Click += on_cancel # Cmd/Ctrl+Enter im TextArea = OK def on_ta_keydown(sender, e): try: is_cmd = (e.Modifiers == forms.Keys.Application or e.Modifiers == forms.Keys.Control) except Exception: is_cmd = False if e.Key == forms.Keys.Enter and is_cmd: on_ok(sender, e); e.Handled = True elif e.Key == forms.Keys.Escape: on_cancel(sender, e); e.Handled = True ta.KeyDown += on_ta_keydown # === Layout zusammenbauen === def lbl(text, width=None): l = forms.Label() l.Text = text try: l.VerticalAlignment = forms.VerticalAlignment.Center except Exception: pass return l layout = forms.DynamicLayout() layout.Spacing = drawing.Size(8, 8) # Row 1: Font + Size + Color layout.BeginHorizontal() layout.Add(lbl("Schrift")) layout.Add(font_dd, True, False) layout.Add(lbl("Grösse")) layout.Add(size_input) layout.Add(lbl("Farbe")) layout.Add(color_picker) layout.Add(color_reset) layout.EndHorizontal() # Row 2: B I U + Align + Sup/Sub layout.BeginHorizontal() layout.Add(bold_check) layout.Add(italic_check) layout.Add(underline_check) layout.Add(lbl(" ")) layout.Add(align_left) layout.Add(align_center) layout.Add(align_right) layout.Add(lbl(" ")) layout.Add(sup_btn) layout.Add(sub_btn) layout.Add(None, True, False) layout.EndHorizontal() # Symbol-Palette layout.BeginVertical() layout.AddRow(lbl("Sonderzeichen")) layout.AddRow(sym_panel) layout.EndVertical() # TextArea layout.BeginVertical() layout.AddRow(ta) layout.EndVertical() # Buttons unten layout.BeginHorizontal() layout.Add(None, True, False) layout.Add(cancel_btn) layout.Add(ok_btn) layout.EndHorizontal() dlg.Content = layout try: dlg.DefaultButton = ok_btn dlg.AbortButton = cancel_btn except Exception: pass try: parent = Rhino.UI.RhinoEtoApp.MainWindow if parent is not None: dlg.ShowModal(parent) else: dlg.ShowModal() except Exception as ex: print("[TEXT] dialog:", ex) return None if not result["committed"]: return None return result["state"] def _inline_editor(p1, p2, initial=""): """Editor-Dialog mit Multi-Line-TextArea, positioniert NEBEN dem gepickten Frame (statt zentriert). Eto.Dialog.ShowModal — reliable auf Mac/Win. Returns Text-String oder None.""" import Eto.Forms as forms import Eto.Drawing as drawing # Frame-Position im Screen ermitteln (fuer Dialog-Positionierung) sx = sy = None sw = max(360, 280) sh = max(180, 150) try: view = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView vp = view.ActiveViewport # WorldToClient → System.Drawing.Point (viewport-lokale Pixel) c1 = vp.WorldToClient(p1) c2 = vp.WorldToClient(p2) view_rect = view.ScreenRectangle # absolute Viewport-Position fx = view_rect.X + int(min(c1.X, c2.X)) fy = view_rect.Y + int(min(c1.Y, c2.Y)) fw = abs(int(c2.X - c1.X)) fh = abs(int(c2.Y - c1.Y)) sx = int(fx + fw + 20) sy = int(fy) sw = max(360, fw) sh = max(180, fh) except Exception as ex: print("[TEXT] viewport-coords:", ex) dlg = forms.Dialog() dlg.Title = "Text einfuegen" dlg.Resizable = True dlg.Padding = drawing.Padding(8) try: dlg.MinimumSize = drawing.Size(360, 180) except Exception: pass try: dlg.ClientSize = drawing.Size(int(sw), int(sh)) except Exception: pass # Position neben dem Frame (falls verfuegbar) if sx is not None and sy is not None: try: dlg.Location = drawing.Point(sx, sy) except Exception: pass ta = forms.TextArea() ta.AcceptsReturn = True ta.AcceptsTab = False ta.Wrap = True ta.Text = initial or "" try: ta.Font = drawing.Font("Helvetica", 13) except Exception: pass # Explizit Groesse setzen — DynamicLayout expandiert TextArea sonst # nicht zuverlaessig (zeigte sich als 1-Zeilen-Streifen) try: ta.Size = drawing.Size(int(sw) - 20, max(120, int(sh) - 70)) except Exception: pass result = {"text": None, "committed": False} ok_btn = forms.Button() ok_btn.Text = "Einfuegen" cancel_btn = forms.Button() cancel_btn.Text = "Abbrechen" def on_ok(s, e): result["text"] = ta.Text or "" result["committed"] = True try: dlg.Close() except Exception: pass def on_cancel(s, e): try: dlg.Close() except Exception: pass ok_btn.Click += on_ok cancel_btn.Click += on_cancel # Cmd/Ctrl+Enter Shortcut im TextArea def on_keydown(sender, e): try: is_cmd = (e.Modifiers == forms.Keys.Application or e.Modifiers == forms.Keys.Control) except Exception: is_cmd = False if e.Key == forms.Keys.Enter and is_cmd: on_ok(sender, e); e.Handled = True elif e.Key == forms.Keys.Escape: on_cancel(sender, e); e.Handled = True ta.KeyDown += on_keydown layout = forms.DynamicLayout() layout.Spacing = drawing.Size(6, 6) layout.BeginVertical() layout.AddRow(ta) layout.EndVertical() layout.BeginVertical() layout.BeginHorizontal() layout.Add(None, True, False) layout.Add(cancel_btn) layout.Add(ok_btn) layout.EndHorizontal() layout.EndVertical() dlg.Content = layout try: dlg.DefaultButton = ok_btn dlg.AbortButton = cancel_btn except Exception: pass try: parent = Rhino.UI.RhinoEtoApp.MainWindow if parent is not None: dlg.ShowModal(parent) else: dlg.ShowModal() except Exception as ex: print("[TEXT] dialog show:", ex) return None if not result["committed"]: return None return (result["text"] or "").strip() or None def _pick_text_frame(): """Picked 2 Ecken eines Text-Feldes mit Live-Rechteck-Vorschau (wie Rhinos _Rectangle). Returns (p1, p2, origin, width, height) oder None bei Abbruch.""" try: import System gp = Rhino.Input.Custom.GetPoint() gp.SetCommandPrompt("Erste Ecke des Text-Feldes") gp.Get() if gp.CommandResult() != Rhino.Commands.Result.Success: return None p1 = gp.Point() gp2 = Rhino.Input.Custom.GetPoint() gp2.SetCommandPrompt("Gegenueberliegende Ecke") try: gp2.SetBasePoint(p1, True) except Exception: pass # Live Rechteck-Preview via DynamicDraw rect_color = System.Drawing.Color.FromArgb(180, 95, 168, 150) # petrol def _draw_rect(sender, e): try: pt = e.CurrentPoint z = p1.Z corners = [ p1, Rhino.Geometry.Point3d(pt.X, p1.Y, z), Rhino.Geometry.Point3d(pt.X, pt.Y, z), Rhino.Geometry.Point3d(p1.X, pt.Y, z), p1, ] e.Display.DrawDottedPolyline(corners, rect_color, False) except Exception: pass gp2.DynamicDraw += _draw_rect gp2.Get() if gp2.CommandResult() != Rhino.Commands.Result.Success: return None p2 = gp2.Point() min_x = min(p1.X, p2.X); max_x = max(p1.X, p2.X) min_y = min(p1.Y, p2.Y); max_y = max(p1.Y, p2.Y) width = max_x - min_x height = max_y - min_y if width < 1e-6 or height < 1e-6: print("[TEXT] frame zu klein"); return None origin = rg.Point3d(min_x, max_y, p1.Z) return (p1, p2, origin, width, height) except Exception as ex: print("[TEXT] pick frame:", ex) return None def _apply_font(te, face, bold, italic, underline=False): """Setzt Font auf TextEntity. Mehrere Konstruktor-Pfade fuer verschiedene RhinoCommon-Versionen: - 5-arg Font(face,bold,italic,underline,strike) — unterstuetzt underline - FromQuartetProperties — keine underline - FontTable.FindOrCreate — keine underline face wird normalisiert (Bold/Italic-Suffixe entfernen) damit QuartetNames wie "Helvetica-Bold" nicht den Quartet-Lookup blockieren.""" face = str(face or "Helvetica").strip() bold = bool(bold) italic = bool(italic) underline = bool(underline) for suffix in ("-BoldItalic", "-BoldOblique", "-Bold", "-Italic", "-Oblique", " Bold Italic", " Bold Oblique", " Bold", " Italic", " Oblique"): if face.endswith(suffix): face = face[:-len(suffix)].strip(); break print("[TEXT] _apply_font face={!r} bold={} italic={} underline={}".format( face, bold, italic, underline)) # Pfad 1: 5-arg Font-Konstruktor mit echten Enums (Python.NET 3 erlaubt # keinen impliziten bool→Enum-Cast mehr). Underline-Support nur hier. try: FW = Rhino.DocObjects.Font.FontWeight FS = Rhino.DocObjects.Font.FontStyle weight = FW.Bold if bold else FW.Normal style = FS.Italic if italic else FS.Upright font = Rhino.DocObjects.Font(face, weight, style, underline, False) if font is not None: te.Font = font return True except Exception as ex: if underline: print("[TEXT] Font(5-arg) fail:", ex) # Pfad 2: 3-arg Font-Konstruktor try: font = Rhino.DocObjects.Font(face, bold, italic) if font is not None: te.Font = font return True except Exception: pass # Pfad 3: FromQuartetProperties try: font = Rhino.DocObjects.Font.FromQuartetProperties(face, bold, italic) if font is not None: te.Font = font return True except Exception as ex: print("[TEXT] FromQuartet:", ex) # Pfad 4: doc.Fonts.FindOrCreate try: doc = Rhino.RhinoDoc.ActiveDoc font_idx = doc.Fonts.FindOrCreate(face, bold, italic) if font_idx >= 0: font = doc.Fonts[font_idx] if font is not None: te.Font = font return True except Exception as ex: print("[TEXT] FindOrCreate:", ex) return False def _selected_text_objects(doc): """Liefert Liste der selektierten TextEntity-Objekte.""" out = [] if doc is None: return out try: for obj in doc.Objects.GetSelectedObjects(False, False): try: if isinstance(obj.Geometry, rg.TextEntity): out.append(obj) except Exception: pass except Exception: pass return out def _apply_align(te, align): """Setzt TextHorizontalAlignment.""" try: m = { "left": Rhino.DocObjects.TextHorizontalAlignment.Left, "center": Rhino.DocObjects.TextHorizontalAlignment.Center, "right": Rhino.DocObjects.TextHorizontalAlignment.Right, } if align in m: te.TextHorizontalAlignment = m[align] return True except Exception as ex: print("[TEXT] apply align:", ex) return False def apply_settings_to_selection(doc, patch): """Wendet font/size/bold/italic/align auf alle selektierten TextEntities an. Returns Anzahl der geaenderten Objekte.""" if doc is None or not isinstance(patch, dict): return 0 selected = _selected_text_objects(doc) if not selected: return 0 import System n = 0 for obj in selected: try: old = obj.Geometry # Aktuelle Werte lesen (vor Modifikation) cur = old.Font try: cur_face = cur.QuartetName if cur else "Helvetica" except Exception: cur_face = "Helvetica" try: cur_bold = bool(cur.Bold) if cur else False except Exception: cur_bold = False try: cur_italic = bool(cur.Italic) if cur else False except Exception: cur_italic = False try: cur_underline = bool(cur.Underlined) if cur else False except Exception: cur_underline = False # Neue Werte aus Patch + Fallback auf aktuell face = patch.get("font") or cur_face bold = patch["bold"] if "bold" in patch else cur_bold italic = patch["italic"] if "italic" in patch else cur_italic underline = patch["underline"] if "underline" in patch else cur_underline size = float(patch["size"]) if "size" in patch else float(old.TextHeight) align = patch["align"] if patch.get("align") in _ALIGNS else None # FRESH TextEntity bauen statt Duplicate-Modify. Bypassed # Probleme wo te.Font-Setter wegen Rich-Text-Runs oder # DimensionStyle-Override nicht greift. te = rg.TextEntity() te.Plane = old.Plane try: te.PlainText = old.PlainText except Exception: pass te.TextHeight = size # DimensionStyle entkoppeln damit unser Font nicht von Style # ueberschrieben wird. try: te.DimensionStyleId = System.Guid.Empty except Exception: pass _apply_font(te, face, bool(bold), bool(italic), bool(underline)) # Alignment: aus Patch oder vom alten Entity uebernehmen if align: _apply_align(te, align) else: try: te.TextHorizontalAlignment = old.TextHorizontalAlignment except Exception: pass doc.Objects.Replace(obj.Id, te) n += 1 except Exception as ex: print("[TEXT] apply selection:", ex) if n > 0: try: doc.Views.Redraw() except Exception: pass return n def read_selection_settings(doc): """Wenn TextEntities selektiert: liefert die Settings des ersten.""" sel = _selected_text_objects(doc) if not sel: return None try: te = sel[0].Geometry font = te.Font face = font.QuartetName if font else "Helvetica" bold = bool(font.Bold) if font else False italic = bool(font.Italic) if font else False try: underline = bool(font.Underlined) if font else False except Exception: underline = False align = "left" try: h = te.TextHorizontalAlignment if h == Rhino.DocObjects.TextHorizontalAlignment.Center: align = "center" elif h == Rhino.DocObjects.TextHorizontalAlignment.Right: align = "right" except Exception: pass return { "font": face, "size": float(te.TextHeight), "bold": bold, "italic": italic, "underline": underline, "align": align, } except Exception as ex: print("[TEXT] read selection:", ex) return None def create_text(): """DOSSIER Custom Text-Workflow (React WYSIWYG-Editor): 1. Frame ziehen (Live-Rechteck-Vorschau) 2. text_editor.open_with_frame oeffnet das React-WYSIWYG-Fenster (Topmost, neben dem Frame). Editor handlet die Eingabe + erstellt die TextEntity bei COMMIT. """ doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return frame = _pick_text_frame() if frame is None: return p1, p2, origin, width, height = frame try: import text_editor text_editor.open_with_frame(p1, p2, origin, width, height) except Exception as ex: print("[TEXT] open editor:", ex)