diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index 652c15c..93b8e5b 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -931,8 +931,12 @@ class OberleisteBridge(panel_base.BaseBridge): elif t == "SET_TEXT_SETTINGS": try: import text_create - text_create.save_settings( - Rhino.RhinoDoc.ActiveDoc, p.get("settings") or {}) + doc = Rhino.RhinoDoc.ActiveDoc + patch = p.get("settings") or {} + text_create.save_settings(doc, patch) + # Wenn TextEntities selektiert: gleiche Aenderung direkt + # auf die selektierten Texte applizieren. + text_create.apply_settings_to_selection(doc, patch) except Exception as ex: print("[OBERLEISTE] text settings:", ex) self._send_state(force=True) @@ -1183,15 +1187,19 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception: info["massePresets"] = [] info["masseActiveId"] = None - # Text-Settings + verfuegbare Fonts (Fonts nur einmal initial) + # Text-Settings + verfuegbare Fonts (Fonts nur einmal initial). + # Wenn TextEntity selektiert ist, deren Settings ergaenzen damit + # die UI die Werte des selektierten Textes spiegelt. try: import text_create info["textSettings"] = text_create.load_settings(doc) + info["textSelectionSettings"] = text_create.read_selection_settings(doc) if not getattr(self, "_fonts_sent", False): info["textFonts"] = text_create.available_fonts() self._fonts_sent = True except Exception: info["textSettings"] = {} + info["textSelectionSettings"] = None # Norden-Rotation fuer N/O/S/W-Buttons try: import kamera @@ -1230,6 +1238,9 @@ class OberleisteBridge(panel_base.BaseBridge): _names_tuple, _active_comb, info.get("masseActiveId"), tuple((p.get("id"), p.get("name")) for p in (info.get("massePresets") or [])), + # textSelectionSettings: dict → tuple fuer sig-Vergleich + tuple(sorted((info.get("textSelectionSettings") or {}).items())) if info.get("textSelectionSettings") else None, + info.get("lastSetView"), prompt, ) if not force and sig == self._last_state_sig: diff --git a/rhino/text_create.py b/rhino/text_create.py index 24af4e9..3c97b92 100644 --- a/rhino/text_create.py +++ b/rhino/text_create.py @@ -72,69 +72,20 @@ def available_fonts(): return ["Helvetica", "Arial", "Times New Roman", "Courier New"] -def _floating_input(): - """Modal Eto-Dialog mit TextBox bei der Maus-Position. Returns - eingegebenen Text oder None (Escape/leer).""" - import Eto.Forms as forms - import Eto.Drawing as drawing - - dlg = forms.Dialog() - dlg.Title = "Text" - dlg.Resizable = False - dlg.MinimumSize = drawing.Size(280, 0) - dlg.Padding = drawing.Padding(6) - - tb = forms.TextBox() - tb.PlaceholderText = "Text eingeben — Enter = einfuegen, Esc = abbrechen" - tb.Font = drawing.Font("Helvetica", 13) - tb.Width = 280 - - result = {"text": None, "committed": False} - - def on_keydown(sender, e): - if e.Key == forms.Keys.Enter: - result["text"] = tb.Text or "" - result["committed"] = True - try: dlg.Close() - except Exception: pass - e.Handled = True - elif e.Key == forms.Keys.Escape: - try: dlg.Close() - except Exception: pass - e.Handled = True - - tb.KeyDown += on_keydown - - layout = forms.StackLayout() - layout.Padding = drawing.Padding(0) - layout.Items.Add(tb) - dlg.Content = layout - - # Bei Maus-Cursor positionieren (Position wo User gerade geklickt hat) +def _prompt_for_text(default=""): + """Nativer Rhino-Dialog fuer Text-Eingabe via ShowEditBox. Wirkt + cross-platform (Mac+Windows), kein Eto-Modal-Bug. Returns text oder + None bei Abbruch/leer.""" try: - m = forms.Mouse.Position - dlg.Location = drawing.Point(int(m.X) + 10, int(m.Y) + 12) - except Exception: pass - - # ShowModal blockiert bis Close. Parent = Rhinos Main-Window damit - # der Dialog ueber dem Viewport rendert (Mac). - try: - parent = Rhino.UI.RhinoEtoApp.MainWindow - except Exception: - parent = None - try: - if parent is not None: - dlg.ShowModal(parent) - else: - dlg.ShowModal() + rc, text = Rhino.UI.Dialogs.ShowEditBox( + "Text", "Text:", default or "", False) + if not rc: return None + text = (text or "").strip() + return text or None except Exception as ex: - print("[TEXT] dialog show:", ex) + print("[TEXT] ShowEditBox:", ex) return None - if not result["committed"]: return None - txt = (result["text"] or "").strip() - return txt or None - def _apply_font(te, face, bold, italic): """Setzt Font auf TextEntity. Mehrere Fallback-Pfade fuer @@ -162,6 +113,78 @@ def _apply_font(te, face, bold, italic): return False +def _selected_text_objects(doc): + """Liefert Liste der selektierten TextEntity-Objekte.""" + out = [] + if doc is None: return out + try: + for obj in doc.Objects.GetSelectedObjects(False, False): + try: + if isinstance(obj.Geometry, rg.TextEntity): + out.append(obj) + except Exception: pass + except Exception: pass + return out + + +def apply_settings_to_selection(doc, patch): + """Wendet font/size/bold/italic auf alle selektierten TextEntities an. + Returns Anzahl der geaenderten Objekte.""" + if doc is None or not isinstance(patch, dict): return 0 + selected = _selected_text_objects(doc) + if not selected: return 0 + n = 0 + for obj in selected: + try: + te = obj.Geometry.Duplicate() + 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")): + cur = te.Font + try: cur_face = cur.QuartetName if cur else "Helvetica" + except Exception: cur_face = "Helvetica" + try: cur_bold = bool(cur.Bold) if cur else False + except Exception: cur_bold = False + try: cur_italic = bool(cur.Italic) if cur else False + except Exception: cur_italic = 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)) + doc.Objects.Replace(obj.Id, te) + n += 1 + except Exception as ex: + print("[TEXT] apply selection:", ex) + if n > 0: + try: doc.Views.Redraw() + except Exception: pass + return n + + +def read_selection_settings(doc): + """Wenn TextEntities selektiert: liefert die Settings des ersten als + dict (font/size/bold/italic). Sonst None.""" + sel = _selected_text_objects(doc) + if not sel: return None + try: + te = sel[0].Geometry + font = te.Font + face = font.QuartetName if font else "Helvetica" + bold = bool(font.Bold) if font else False + italic = bool(font.Italic) if font else False + return { + "font": face, + "size": float(te.TextHeight), + "bold": bold, + "italic": italic, + } + except Exception as ex: + print("[TEXT] read selection:", ex) + return None + + def create_text(): """Triggered von der Oberleiste. Vollstaendiger Workflow.""" doc = Rhino.RhinoDoc.ActiveDoc @@ -173,7 +196,7 @@ def create_text(): if rc != Rhino.Commands.Result.Success: return if pt is None: return - text = _floating_input() + text = _prompt_for_text() if not text: return try: diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index 7b04a09..b9c66e1 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -272,6 +272,7 @@ export default function OberleisteApp() { layerCombinations: [], layerCombinationActive: null, massePresets: [], masseActiveId: null, textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false }, + textSelectionSettings: null, textFonts: [], northAngle: 0, lastSetView: null, @@ -779,39 +780,46 @@ export default function OberleisteApp() {
- {/* ====== TEXT-Block 2-Reihen ====== - Reihe 1: Font-Dropdown + Groesse (mm) - Reihe 2: B/I-Toggles + "Text einfuegen"-Button + {/* ====== TEXT-Block (Vectorworks-Stil) ====== + Reihe 1: Font-Dropdown | Size + "m" + Reihe 2: B/I/+ kompakte Segmented-Pill + Wenn TextEntity selektiert: Werte spiegeln Selektion, Aenderungen + applizieren AUF die Selektion + speichern als Default. */} {(() => { - const ts = state.textSettings || {} + const sel = state.textSelectionSettings // null wenn nichts selektiert + const ts = sel || state.textSettings || {} const fonts = state.textFonts || [] const updateTs = (patch) => setTextSettings({ ...ts, ...patch }) - const TEXT_W = 150 + const FONT_W = 130 + const SIZE_W = 60 + const SEG_W = FONT_W + 6 + SIZE_W // B/I/+ Pill spannt ueber beide Spalten in Row 2 + const ACTIVE_BORDER = sel ? 'var(--accent)' : 'var(--border)' return (
{/* Reihe 1, Spalte 1: Font-Dropdown */} updateTs({ font: v })} - width={TEXT_W} - title="Schriftart" + width={FONT_W} + title={sel ? 'Schriftart (auf Selektion appliziert)' : 'Schriftart'} > {fonts.length === 0 && } {fonts.map(f => )} - {/* Reihe 1, Spalte 2: Groesse (mm) */} + {/* Reihe 1, Spalte 2: Size */}
m
- {/* Reihe 2, Spalte 1: B/I-Toggles */} + {/* Reihe 2: B / I / + in einem Segmented-Pill */}
+ {/* B */} + {/* I */} + {/* + Neuen Text einfuegen */} +
- {/* Reihe 2, Spalte 2: "Text einfuegen" Button */} -
) })()}