diff --git a/rhino/text_create.py b/rhino/text_create.py index 8877a81..f4732c0 100644 --- a/rhino/text_create.py +++ b/rhino/text_create.py @@ -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