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
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _frame_editor_dialog(initial=""):
|
def _inline_editor(p1, p2, initial=""):
|
||||||
"""InDesign-Stil Editor: multi-line TextArea in Eto-Dialog. User tippt
|
"""Inline-Editor: chromeloses Eto.Form ueber dem gepickten Frame im
|
||||||
rein, OK → Text-String zurueck. Esc/Cancel → None."""
|
Viewport positioniert. Cmd+Enter / Ctrl+Enter = commit, Esc = abbrechen.
|
||||||
|
Returns Text-String oder None."""
|
||||||
import Eto.Forms as forms
|
import Eto.Forms as forms
|
||||||
import Eto.Drawing as drawing
|
import Eto.Drawing as drawing
|
||||||
dlg = forms.Dialog()
|
try:
|
||||||
dlg.Title = "Text einfuegen"
|
view = Rhino.RhinoDoc.ActiveDoc.Views.ActiveView
|
||||||
dlg.Resizable = True
|
vp = view.ActiveViewport
|
||||||
dlg.MinimumSize = drawing.Size(360, 180)
|
# WorldToScreen → (bool, int_x, int_y) viewport-lokale Pixel
|
||||||
dlg.Padding = drawing.Padding(10)
|
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 = forms.TextArea()
|
||||||
ta.AcceptsReturn = True
|
ta.AcceptsReturn = True
|
||||||
ta.AcceptsTab = False
|
ta.AcceptsTab = False
|
||||||
ta.Wrap = True
|
ta.Wrap = True
|
||||||
ta.Text = initial or ""
|
ta.Text = initial or ""
|
||||||
ta.Font = drawing.Font("Helvetica", 13)
|
try: ta.Font = drawing.Font("Helvetica", 13)
|
||||||
ta.Width = 380
|
except Exception: pass
|
||||||
ta.Height = 160
|
try: ta.BackgroundColor = drawing.Color.FromArgb(245, 245, 245)
|
||||||
|
except Exception: pass
|
||||||
|
ta.ShowBorder = False
|
||||||
|
|
||||||
result = {"text": None, "committed": False}
|
result = {"text": None, "committed": False}
|
||||||
|
|
||||||
ok_btn = forms.Button()
|
def on_keydown(sender, e):
|
||||||
ok_btn.Text = "Einfuegen"
|
try:
|
||||||
cancel_btn = forms.Button()
|
is_cmd = (e.Modifiers == forms.Keys.Application or
|
||||||
cancel_btn.Text = "Abbrechen"
|
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):
|
ta.KeyDown += on_keydown
|
||||||
result["text"] = ta.Text or ""
|
form.Content = ta
|
||||||
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
|
try: form.Show()
|
||||||
cancel_btn.Click += on_cancel
|
except Exception as ex:
|
||||||
|
print("[TEXT] inline editor show:", ex)
|
||||||
# Layout: TextArea ueber volle Breite, Buttons rechtsbuendig unten
|
return None
|
||||||
layout = forms.DynamicLayout()
|
try: ta.Focus()
|
||||||
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
|
except Exception: pass
|
||||||
|
|
||||||
try:
|
# Warten bis User Esc oder Cmd+Enter drueckt
|
||||||
parent = Rhino.UI.RhinoEtoApp.MainWindow
|
while True:
|
||||||
if parent is not None: dlg.ShowModal(parent)
|
try:
|
||||||
else: dlg.ShowModal()
|
if form.Closed: break
|
||||||
except Exception as ex:
|
except Exception:
|
||||||
print("[TEXT] frame editor:", ex)
|
break
|
||||||
return None
|
try: Rhino.RhinoApp.Wait()
|
||||||
|
except Exception: break
|
||||||
|
|
||||||
if not result["committed"]: return None
|
if not result["committed"]: return None
|
||||||
return (result["text"] or "").strip() or None
|
return (result["text"] or "").strip() or None
|
||||||
|
|
||||||
|
|
||||||
def _pick_text_frame():
|
def _pick_text_frame():
|
||||||
"""Picked 2 Ecken eines Text-Feldes (Rechteck in der XY-Ebene des
|
"""Picked 2 Ecken eines Text-Feldes mit Live-Rechteck-Vorschau
|
||||||
ersten Punkts). Returns (origin_pt, width, height) oder None."""
|
(wie Rhinos _Rectangle). Returns (p1, p2, origin, width, height)
|
||||||
|
oder None bei Abbruch."""
|
||||||
try:
|
try:
|
||||||
|
import System
|
||||||
gp = Rhino.Input.Custom.GetPoint()
|
gp = Rhino.Input.Custom.GetPoint()
|
||||||
gp.SetCommandPrompt("Erste Ecke des Text-Feldes")
|
gp.SetCommandPrompt("Erste Ecke des Text-Feldes")
|
||||||
gp.Get()
|
gp.Get()
|
||||||
@@ -281,8 +303,24 @@ def _pick_text_frame():
|
|||||||
gp2.SetCommandPrompt("Gegenueberliegende Ecke")
|
gp2.SetCommandPrompt("Gegenueberliegende Ecke")
|
||||||
try: gp2.SetBasePoint(p1, True)
|
try: gp2.SetBasePoint(p1, True)
|
||||||
except Exception: pass
|
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()
|
gp2.Get()
|
||||||
if gp2.CommandResult() != Rhino.Commands.Result.Success: return None
|
if gp2.CommandResult() != Rhino.Commands.Result.Success: return None
|
||||||
p2 = gp2.Point()
|
p2 = gp2.Point()
|
||||||
@@ -293,35 +331,49 @@ def _pick_text_frame():
|
|||||||
height = max_y - min_y
|
height = max_y - min_y
|
||||||
if width < 1e-6 or height < 1e-6:
|
if width < 1e-6 or height < 1e-6:
|
||||||
print("[TEXT] frame zu klein"); return None
|
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)
|
origin = rg.Point3d(min_x, max_y, p1.Z)
|
||||||
return (origin, width, height)
|
return (p1, p2, origin, width, height)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT] pick frame:", ex)
|
print("[TEXT] pick frame:", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _apply_font(te, face, bold, italic):
|
def _apply_font(te, face, bold, italic, underline=False):
|
||||||
"""Setzt Font auf TextEntity. FromQuartetProperties zuerst (sauberer —
|
"""Setzt Font auf TextEntity. Mehrere Konstruktor-Pfade fuer
|
||||||
konstruiert Font-Objekt direkt mit (face, bold, italic), unabhaengig
|
verschiedene RhinoCommon-Versionen:
|
||||||
von einem evtl. schon im FontTable existierenden Eintrag mit gleichem
|
- 5-arg Font(face,bold,italic,underline,strike) — unterstuetzt underline
|
||||||
Namen aber anderen Flags). Fallback FindOrCreate.
|
- 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."""
|
QuartetNames wie "Helvetica-Bold" nicht den Quartet-Lookup blockieren."""
|
||||||
face = str(face or "Helvetica").strip()
|
face = str(face or "Helvetica").strip()
|
||||||
bold = bool(bold)
|
bold = bool(bold)
|
||||||
italic = bool(italic)
|
italic = bool(italic)
|
||||||
# Suffix-Stripping (haeufige Endungen die manche Fonts in der QuartetName
|
underline = bool(underline)
|
||||||
# haben — wir wollen die Base-Family)
|
|
||||||
for suffix in ("-BoldItalic", "-BoldOblique", "-Bold", "-Italic",
|
for suffix in ("-BoldItalic", "-BoldOblique", "-Bold", "-Italic",
|
||||||
"-Oblique", " Bold Italic", " Bold Oblique",
|
"-Oblique", " Bold Italic", " Bold Oblique",
|
||||||
" Bold", " Italic", " Oblique"):
|
" Bold", " Italic", " Oblique"):
|
||||||
if face.endswith(suffix):
|
if face.endswith(suffix):
|
||||||
face = face[:-len(suffix)].strip()
|
face = face[:-len(suffix)].strip(); break
|
||||||
break
|
print("[TEXT] _apply_font face={!r} bold={} italic={} underline={}".format(
|
||||||
print("[TEXT] _apply_font face={!r} bold={} italic={}".format(face, bold, italic))
|
face, bold, italic, underline))
|
||||||
# Pfad 1: FromQuartetProperties (direkter, keine FontTable-Cache-Bugs)
|
# 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:
|
try:
|
||||||
font = Rhino.DocObjects.Font.FromQuartetProperties(face, bold, italic)
|
font = Rhino.DocObjects.Font.FromQuartetProperties(face, bold, italic)
|
||||||
if font is not None:
|
if font is not None:
|
||||||
@@ -329,7 +381,7 @@ def _apply_font(te, face, bold, italic):
|
|||||||
return True
|
return True
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT] FromQuartet:", ex)
|
print("[TEXT] FromQuartet:", ex)
|
||||||
# Pfad 2: doc.Fonts.FindOrCreate
|
# Pfad 4: doc.Fonts.FindOrCreate
|
||||||
try:
|
try:
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
font_idx = doc.Fonts.FindOrCreate(face, bold, italic)
|
font_idx = doc.Fonts.FindOrCreate(face, bold, italic)
|
||||||
@@ -386,8 +438,14 @@ def apply_settings_to_selection(doc, patch):
|
|||||||
if "size" in patch:
|
if "size" in patch:
|
||||||
try: te.TextHeight = float(patch["size"])
|
try: te.TextHeight = float(patch["size"])
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
# Font: bei jeder Aenderung neu setzen (face+bold+italic kombiniert)
|
# Font: bei jeder Aenderung neu setzen. Vorher PlainText-
|
||||||
if any(k in patch for k in ("font", "bold", "italic")):
|
# 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
|
cur = te.Font
|
||||||
try: cur_face = cur.QuartetName if cur else "Helvetica"
|
try: cur_face = cur.QuartetName if cur else "Helvetica"
|
||||||
except Exception: cur_face = "Helvetica"
|
except Exception: cur_face = "Helvetica"
|
||||||
@@ -395,14 +453,15 @@ def apply_settings_to_selection(doc, patch):
|
|||||||
except Exception: cur_bold = False
|
except Exception: cur_bold = False
|
||||||
try: cur_italic = bool(cur.Italic) if cur else False
|
try: cur_italic = bool(cur.Italic) if cur else False
|
||||||
except Exception: cur_italic = 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
|
face = patch.get("font") or cur_face
|
||||||
bold = patch["bold"] if "bold" in patch else cur_bold
|
bold = patch["bold"] if "bold" in patch else cur_bold
|
||||||
italic = patch["italic"] if "italic" in patch else cur_italic
|
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:
|
if "align" in patch and patch["align"] in _ALIGNS:
|
||||||
_apply_align(te, patch["align"])
|
_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)
|
doc.Objects.Replace(obj.Id, te)
|
||||||
n += 1
|
n += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -423,6 +482,8 @@ def read_selection_settings(doc):
|
|||||||
face = font.QuartetName if font else "Helvetica"
|
face = font.QuartetName if font else "Helvetica"
|
||||||
bold = bool(font.Bold) if font else False
|
bold = bool(font.Bold) if font else False
|
||||||
italic = bool(font.Italic) 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"
|
align = "left"
|
||||||
try:
|
try:
|
||||||
h = te.TextHorizontalAlignment
|
h = te.TextHorizontalAlignment
|
||||||
@@ -434,7 +495,7 @@ def read_selection_settings(doc):
|
|||||||
"size": float(te.TextHeight),
|
"size": float(te.TextHeight),
|
||||||
"bold": bold,
|
"bold": bold,
|
||||||
"italic": italic,
|
"italic": italic,
|
||||||
"underline": False, # derzeit nicht lesbar
|
"underline": underline,
|
||||||
"align": align,
|
"align": align,
|
||||||
}
|
}
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -443,18 +504,18 @@ def read_selection_settings(doc):
|
|||||||
|
|
||||||
|
|
||||||
def create_text():
|
def create_text():
|
||||||
"""InDesign-Stil: User picked 2 Ecken (Frame) → Multi-Line-Editor-
|
"""InDesign-Stil: User zieht Frame (Live-Rechteck-Vorschau) → Inline-
|
||||||
Dialog → TextEntity wird im Frame mit Text-Wrap erstellt. Frame-Breite
|
Editor poppt direkt ueber dem Frame im Viewport → tippen → Cmd+Enter
|
||||||
bestimmt den Word-Wrap."""
|
fuegt TextEntity ein. Esc bricht ab."""
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
settings = load_settings(doc)
|
settings = load_settings(doc)
|
||||||
|
|
||||||
frame = _pick_text_frame()
|
frame = _pick_text_frame()
|
||||||
if frame is None: return
|
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
|
if not text: return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -465,10 +526,9 @@ def create_text():
|
|||||||
te.TextHeight = float(settings.get("size", 0.20))
|
te.TextHeight = float(settings.get("size", 0.20))
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
_apply_font(te, settings.get("font") or "Helvetica",
|
_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")
|
_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"):
|
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
||||||
try:
|
try:
|
||||||
setattr(te, attr, width); break
|
setattr(te, attr, width); break
|
||||||
|
|||||||
Reference in New Issue
Block a user