Text: Rechteck-Preview + Inline-Viewport-Editor + Underline-Versuch

User-Wunsch:
- Rechteck als Frame statt 2-Punkte-Linie picken
- Direkt im Viewport schreiben statt zentrierter Dialog
- Bold/Italic/Underline echt anwendbar

Aenderungen:

_pick_text_frame: DynamicDraw-Event auf GetPoint zeichnet Live-
Rechteck-Vorschau in petrol-accent waehrend User die 2. Ecke picked
(analog Rhinos _Rectangle). Returns jetzt (p1, p2, origin, w, h).

_inline_editor (neu, ersetzt _frame_editor_dialog): chromeloses
Eto.Form (WindowStyle.None_) absolut positioniert ueber dem gepickten
Frame im Viewport via vp.WorldToScreen + view.ScreenRectangle. TextArea
fuellt den Frame. Cmd+Enter / Ctrl+Enter = commit, Esc = abbrechen.
Look-and-feel: schreibst direkt "im" Feld auf der Arbeitsflaeche.

_apply_font: erweitert um 5-arg Font(face,bold,italic,underline,strike)
Konstruktor als Pfad 1 (falls Rhino-Version das unterstuetzt). Fallback
3-arg Font, FromQuartetProperties, FindOrCreate.

apply_settings_to_selection: vor jedem Font-Set ein PlainText-Reset
damit eventuelle Rich-Text-Formatierungs-Runs nicht das neue te.Font
ueberschreiben — vermutlicher Bold-Toggle-Bug-Fix. Underline jetzt im
patch-Loop mit drin.

read_selection_settings: liest auch font.Underlined wenn vorhanden.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 01:03:25 +02:00
parent e9f0e255a0
commit 9256e5866e
+143 -83
View File
@@ -197,80 +197,102 @@ def _prompt_for_text(default=""):
return None
def _frame_editor_dialog(initial=""):
"""InDesign-Stil Editor: multi-line TextArea in Eto-Dialog. User tippt
rein, OK → Text-String zurueck. Esc/Cancel → None."""
def _inline_editor(p1, p2, initial=""):
"""Inline-Editor: chromeloses Eto.Form ueber dem gepickten Frame im
Viewport positioniert. Cmd+Enter / Ctrl+Enter = commit, Esc = abbrechen.
Returns Text-String oder None."""
import Eto.Forms as forms
import Eto.Drawing as drawing
dlg = forms.Dialog()
dlg.Title = "Text einfuegen"
dlg.Resizable = True
dlg.MinimumSize = drawing.Size(360, 180)
dlg.Padding = drawing.Padding(10)
try:
view = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
vp = view.ActiveViewport
# WorldToScreen → (bool, int_x, int_y) viewport-lokale Pixel
ok1, x1, y1 = vp.WorldToScreen(p1)
ok2, x2, y2 = vp.WorldToScreen(p2)
view_rect = view.ScreenRectangle # absolute Viewport-Position
except Exception as ex:
print("[TEXT] viewport-coords:", ex)
return None
if not (ok1 and ok2):
return None
# Frame-Rect in absoluten Screen-Pixeln
sx = view_rect.X + min(x1, x2)
sy = view_rect.Y + min(y1, y2)
sw = max(60, abs(x2 - x1))
sh = max(28, abs(y2 - y1))
form = forms.Form()
try: form.WindowStyle = forms.WindowStyle.None_
except Exception:
try: form.WindowStyle = getattr(forms.WindowStyle, "None")
except Exception: pass
try: form.Topmost = True
except Exception: pass
form.Resizable = False
form.ClientSize = drawing.Size(int(sw), int(sh))
try:
form.Location = drawing.Point(int(sx), int(sy))
except Exception: pass
ta = forms.TextArea()
ta.AcceptsReturn = True
ta.AcceptsTab = False
ta.Wrap = True
ta.Text = initial or ""
ta.Font = drawing.Font("Helvetica", 13)
ta.Width = 380
ta.Height = 160
try: ta.Font = drawing.Font("Helvetica", 13)
except Exception: pass
try: ta.BackgroundColor = drawing.Color.FromArgb(245, 245, 245)
except Exception: pass
ta.ShowBorder = False
result = {"text": None, "committed": False}
ok_btn = forms.Button()
ok_btn.Text = "Einfuegen"
cancel_btn = forms.Button()
cancel_btn.Text = "Abbrechen"
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:
result["text"] = ta.Text or ""
result["committed"] = True
try: form.Close()
except Exception: pass
e.Handled = True
elif e.Key == forms.Keys.Escape:
try: form.Close()
except Exception: pass
e.Handled = True
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
ta.KeyDown += on_keydown
form.Content = ta
ok_btn.Click += on_ok
cancel_btn.Click += on_cancel
# Layout: TextArea ueber volle Breite, Buttons rechtsbuendig unten
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
try: form.Show()
except Exception as ex:
print("[TEXT] inline editor show:", ex)
return None
try: ta.Focus()
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] frame editor:", ex)
return None
# Warten bis User Esc oder Cmd+Enter drueckt
while True:
try:
if form.Closed: break
except Exception:
break
try: Rhino.RhinoApp.Wait()
except Exception: break
if not result["committed"]: return None
return (result["text"] or "").strip() or None
def _pick_text_frame():
"""Picked 2 Ecken eines Text-Feldes (Rechteck in der XY-Ebene des
ersten Punkts). Returns (origin_pt, width, height) oder None."""
"""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()
@@ -281,8 +303,24 @@ def _pick_text_frame():
gp2.SetCommandPrompt("Gegenueberliegende Ecke")
try: gp2.SetBasePoint(p1, True)
except Exception: pass
try: gp2.DrawLineFromPoint(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()
@@ -293,35 +331,49 @@ def _pick_text_frame():
height = max_y - min_y
if width < 1e-6 or height < 1e-6:
print("[TEXT] frame zu klein"); return None
# Origin = obere linke Ecke (Text laeuft von dort nach unten)
origin = rg.Point3d(min_x, max_y, p1.Z)
return (origin, width, height)
return (p1, p2, origin, width, height)
except Exception as ex:
print("[TEXT] pick frame:", 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.
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
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)
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={}".format(face, bold, italic))
# Pfad 1: FromQuartetProperties (direkter, keine FontTable-Cache-Bugs)
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 underline+strikethrough)
try:
font = Rhino.DocObjects.Font(face, bold, italic, 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:
@@ -329,7 +381,7 @@ def _apply_font(te, face, bold, italic):
return True
except Exception as ex:
print("[TEXT] FromQuartet:", ex)
# Pfad 2: doc.Fonts.FindOrCreate
# Pfad 4: doc.Fonts.FindOrCreate
try:
doc = Rhino.RhinoDoc.ActiveDoc
font_idx = doc.Fonts.FindOrCreate(face, bold, italic)
@@ -386,8 +438,14 @@ def apply_settings_to_selection(doc, patch):
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")):
# Font: bei jeder Aenderung neu setzen. Vorher PlainText-
# Reset damit eventuelle RichText-Formatierungs-Runs nicht
# das neue te.Font ueberschreiben.
if any(k in patch for k in ("font", "bold", "italic", "underline")):
try:
plain = te.PlainText
te.PlainText = plain # reset zu plain-mode
except Exception: pass
cur = te.Font
try: cur_face = cur.QuartetName if cur else "Helvetica"
except Exception: cur_face = "Helvetica"
@@ -395,14 +453,15 @@ def apply_settings_to_selection(doc, patch):
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
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))
underline = patch["underline"] if "underline" in patch else cur_underline
_apply_font(te, face, bool(bold), bool(italic), bool(underline))
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:
@@ -423,6 +482,8 @@ def read_selection_settings(doc):
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
@@ -434,7 +495,7 @@ def read_selection_settings(doc):
"size": float(te.TextHeight),
"bold": bold,
"italic": italic,
"underline": False, # derzeit nicht lesbar
"underline": underline,
"align": align,
}
except Exception as ex:
@@ -443,18 +504,18 @@ def read_selection_settings(doc):
def create_text():
"""InDesign-Stil: User picked 2 Ecken (Frame) → Multi-Line-Editor-
Dialog → TextEntity wird im Frame mit Text-Wrap erstellt. Frame-Breite
bestimmt den Word-Wrap."""
"""InDesign-Stil: User zieht Frame (Live-Rechteck-Vorschau) → Inline-
Editor poppt direkt ueber dem Frame im Viewport → tippen → Cmd+Enter
fuegt TextEntity ein. Esc bricht ab."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
settings = load_settings(doc)
frame = _pick_text_frame()
if frame is None: return
origin, width, height = frame
p1, p2, origin, width, height = frame
text = _frame_editor_dialog()
text = _inline_editor(p1, p2)
if not text: return
try:
@@ -465,10 +526,9 @@ def create_text():
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"))
settings.get("bold"), settings.get("italic"),
settings.get("underline"))
_apply_align(te, settings.get("align") or "left")
# Text-Wrap an der Frame-Breite (Property-Namen variieren je nach
# RhinoCommon-Version — best-effort, schluckt Fehler).
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
try:
setattr(te, attr, width); break