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:
+143
-83
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user