#! 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. def list_styles(doc): if doc is None: return [] try: raw = doc.Strings.GetValue(_STYLES_KEY) if not raw: return [] 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 _apply_font(te, face, bold, italic): """Setzt Font auf TextEntity. FromQuartetProperties zuerst (sauberer — konstruiert Font-Objekt direkt mit (face, bold, italic), unabhaengig von einem evtl. schon im FontTable existierenden Eintrag mit gleichem Namen aber anderen Flags). Fallback FindOrCreate. `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) # Suffix-Stripping (haeufige Endungen die manche Fonts in der QuartetName # haben — wir wollen die Base-Family) 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={}".format(face, bold, italic)) # Pfad 1: FromQuartetProperties (direkter, keine FontTable-Cache-Bugs) 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 2: 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 n = 0 for obj in selected: try: te = obj.Geometry.Duplicate() if "size" in patch: try: te.TextHeight = float(patch["size"]) except Exception: pass # Font: bei jeder Aenderung neu setzen (face+bold+italic kombiniert) if any(k in patch for k in ("font", "bold", "italic")): cur = te.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 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 _apply_font(te, face, bool(bold), bool(italic)) if "align" in patch and patch["align"] in _ALIGNS: _apply_align(te, patch["align"]) # underline: derzeit nicht auf TextEntity-Font-API mappbar. # Wird in den Settings gespeichert, aber visuell (noch) nicht angewendet. 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 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": False, # derzeit nicht lesbar "align": align, } except Exception as ex: print("[TEXT] read selection:", ex) return None def create_text(): """Triggered von der Oberleiste. Vollstaendiger Workflow.""" doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return settings = load_settings(doc) rc, pt = Rhino.Input.RhinoGet.GetPoint( "Position fuer Text", False) if rc != Rhino.Commands.Result.Success: return if pt is None: return text = _prompt_for_text() if not text: return try: te = rg.TextEntity() te.Plane = rg.Plane(pt, rg.Vector3d.ZAxis) te.PlainText = text try: te.TextHeight = float(settings.get("size", 0.20)) except Exception: pass _apply_font(te, settings.get("font") or "Helvetica", settings.get("bold"), settings.get("italic")) _apply_align(te, settings.get("align") or "left") doc.Objects.AddText(te) doc.Views.Redraw() except Exception as ex: print("[TEXT] create:", ex)