Files
DOSSIER/rhino/text_create.py
T
karim e7a1753519 Text-Editor: Default-Stile + Stil-Picker im Dialog
User-Wunsch: vorgespeicherte Stile (Heading, Paragraph Helvetica/Georgia)
direkt im Editor anwendbar.

Backend (text_create.py):
- _DEFAULT_STYLES: 7 sinnvolle Architektur-Defaults — Titel (0.40m bold),
  Heading 1 (0.30m bold), Heading 2 (0.20m bold), Paragraph Helvetica
  (0.15m), Paragraph Georgia (0.15m Georgia), Notiz (0.10m italic),
  Bildlegende (0.08m italic)
- list_styles: seedet die Defaults beim ersten Zugriff falls noch keine
  Styles im Doc existieren (analog mass_style)
- Bestehende save_style/delete_style/apply_style funktionieren weiter

Backend (text_editor.py):
- INIT-Payload erweitert um styles[] (Liste aller verfuegbaren Stile
  mit id/name/font/size/bold/italic/underline/align)

Frontend (TextEditorApp.jsx):
- Neuer Stil-Picker als erstes Dropdown in Toolbar-Row 1 (150px)
- Optionen: "— Stil wählen —" + alle verfuegbaren Stile
- onChange: applyStyle(style) — setzt Toolbar-State + appliziert via
  execCommand auf die aktuelle Selektion im WYSIWYG-Editor (oder als
  Default fuer kommendes Tippen wenn keine Selektion)
- queryCommandState-Check fuer Bold/Italic/Underline damit nur toggled
  wird wenn nicht schon im gewuenschten State

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:38:23 +02:00

912 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#! 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 = ""
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)