diff --git a/rhino/text_create.py b/rhino/text_create.py index 2ffd42b..0a22040 100644 --- a/rhino/text_create.py +++ b/rhino/text_create.py @@ -197,6 +197,320 @@ def _prompt_for_text(default=""): 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 = "x²" + 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 @@ -548,40 +862,69 @@ def read_selection_settings(doc): def create_text(): - """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.""" + """DOSSIER Custom Text-Workflow: + 1. Frame ziehen (Live-Rechteck-Vorschau) + 2. _dossier_text_editor (eigener Editor mit Toolbar, Sonderzeichen, + Farbe, Sub/Superscript) oeffnet sich neben dem Frame + 3. TextEntity wird im Frame mit allen gewaehlten Settings erstellt + und mit UserString "dossier_text=1" getagged (fuer evtl. spaeteren + Double-Click-Hook auf unseren Editor) + """ + import System doc = Rhino.RhinoDoc.ActiveDoc if doc is None: return settings = load_settings(doc) + fonts = available_fonts() frame = _pick_text_frame() if frame is None: return p1, p2, origin, width, height = frame - text = _inline_editor(p1, p2) + new_state = _dossier_text_editor(p1, p2, settings, fonts) + if not new_state: return + text = (new_state.get("text") or "").strip() if not text: return + # Defaults aus Editor uebernehmen (ohne color) + save_settings(doc, { + "font": new_state.get("font"), + "size": new_state.get("size"), + "bold": new_state.get("bold"), + "italic": new_state.get("italic"), + "underline": new_state.get("underline"), + "align": new_state.get("align"), + }) + try: te = rg.TextEntity() te.Plane = rg.Plane(origin, rg.Vector3d.ZAxis) te.PlainText = text - try: - te.TextHeight = float(settings.get("size", 0.20)) + try: te.TextHeight = float(new_state.get("size") or 0.2) except Exception: pass - _apply_font(te, settings.get("font") or "Helvetica", - settings.get("bold"), settings.get("italic"), - settings.get("underline")) - _apply_align(te, settings.get("align") or "left") + _apply_font(te, new_state.get("font") or "Helvetica", + new_state.get("bold"), new_state.get("italic"), + new_state.get("underline")) + _apply_align(te, new_state.get("align") or "left") for attr in ("FormatWidth", "TextWidth", "MaskWidth"): - try: - setattr(te, attr, width); break + try: setattr(te, attr, width); break except Exception: pass try: te.TextIsWrapped = True except Exception: try: te.TextWrap = True except Exception: pass - doc.Objects.AddText(te) + + # Object-Attribute: Farbe wenn explizit gesetzt + DOSSIER-Tag + attrs = Rhino.DocObjects.ObjectAttributes() + col = new_state.get("color") + if col is not None: + try: + attrs.ColorSource = Rhino.DocObjects.ObjectColorSource.ColorFromObject + attrs.ObjectColor = System.Drawing.Color.FromArgb( + int(col[0]), int(col[1]), int(col[2])) + except Exception as ex: + print("[TEXT] color attr:", ex) + attrs.SetUserString("dossier_text", "1") + doc.Objects.AddText(te, attrs) doc.Views.Redraw() except Exception as ex: print("[TEXT] create:", ex)