diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index c50e76d..63b7268 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -906,6 +906,22 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception as ex: print("[OBERLEISTE] open about:", ex) + # --- Text-Erstellung (Floating-Input) --------------------------- + elif t == "CREATE_TEXT": + try: + import text_create + text_create.create_text() + except Exception as ex: + print("[OBERLEISTE] create text:", ex) + elif t == "SET_TEXT_SETTINGS": + try: + import text_create + text_create.save_settings( + Rhino.RhinoDoc.ActiveDoc, p.get("settings") or {}) + except Exception as ex: + print("[OBERLEISTE] text settings:", ex) + self._send_state(force=True) + # --- Masse (Mass-Style) ----------------------------------------- elif t == "SET_MASSE_ACTIVE": try: @@ -1152,6 +1168,15 @@ class OberleisteBridge(panel_base.BaseBridge): except Exception: info["massePresets"] = [] info["masseActiveId"] = None + # Text-Settings + verfuegbare Fonts (Fonts nur einmal initial) + try: + import text_create + info["textSettings"] = text_create.load_settings(doc) + if not getattr(self, "_fonts_sent", False): + info["textFonts"] = text_create.available_fonts() + self._fonts_sent = True + except Exception: + info["textSettings"] = {} # Command-Line State prompt = _get_command_prompt() info["cmdPrompt"] = prompt diff --git a/rhino/text_create.py b/rhino/text_create.py new file mode 100644 index 0000000..24af4e9 --- /dev/null +++ b/rhino/text_create.py @@ -0,0 +1,191 @@ +#! python 3 +# -*- coding: utf-8 -*- +""" +text_create.py +Text-Erstellungs-Workflow mit Floating-Input-Box statt Rhino-Dialog. + +Flow: + 1. User klickt Topbar-Button "Text einfuegen" + 2. RhinoGet.GetPoint(): User picked Position im Viewport + 3. Eto.Dialog mit TextBox erscheint neben Maus-Cursor + 4. User tippt, Enter = commit, Escape = abbrechen + 5. TextEntity wird mit Topbar-Settings (Font/Size/Bold/Italic) + am gepickten Punkt erstellt + +Settings werden pro Dokument in doc.Strings["dossier_text_settings"] +persistiert (JSON: font/size/bold/italic). +""" +import os +import sys +import json +import Rhino +import Rhino.Geometry as rg +import scriptcontext as sc + +_HERE = os.path.dirname(os.path.abspath(__file__)) +if _HERE not in sys.path: + sys.path.insert(0, _HERE) + +_SETTINGS_KEY = "dossier_text_settings" + +_DEFAULTS = { + "font": "Helvetica", + "size": 0.20, # in Model-Units (m bei m-Doc, mm bei mm-Doc) + "bold": False, + "italic": False, +} + + +def load_settings(doc): + if doc is None: return dict(_DEFAULTS) + try: + raw = doc.Strings.GetValue(_SETTINGS_KEY) + if not raw: return dict(_DEFAULTS) + s = json.loads(raw) + if not isinstance(s, dict): return dict(_DEFAULTS) + out = dict(_DEFAULTS) + out.update({k: s[k] for k in s if k in _DEFAULTS}) + return out + except Exception: + return dict(_DEFAULTS) + + +def save_settings(doc, partial): + """Merged partial-Updates rein und persistiert.""" + if doc is None or not isinstance(partial, dict): return + cur = load_settings(doc) + cur.update({k: partial[k] for k in partial if k in _DEFAULTS}) + try: + doc.Strings.SetString(_SETTINGS_KEY, json.dumps(cur)) + except Exception as ex: + print("[TEXT] save settings:", ex) + + +def available_fonts(): + """Liefert sortierte Liste verfuegbarer System-Fonts (Face-Names).""" + try: + names = Rhino.DocObjects.Font.AvailableFontFaceNames() + out = sorted({str(n) for n in names if n}) + return out + except Exception as ex: + print("[TEXT] available_fonts:", ex) + 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) + 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() + except Exception as ex: + print("[TEXT] dialog show:", 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 + verschiedene RhinoCommon-Versionen.""" + doc = Rhino.RhinoDoc.ActiveDoc + # Pfad 1: FindOrCreate im FontTable + zuweisen + try: + font_idx = doc.Fonts.FindOrCreate(face, bool(bold), bool(italic)) + if font_idx >= 0: + font = doc.Fonts[font_idx] + if font is not None: + te.Font = font + return True + except Exception as ex: + print("[TEXT] font path 1:", ex) + # Pfad 2: statische FromQuartetProperties + try: + font = Rhino.DocObjects.Font.FromQuartetProperties( + face, bool(bold), bool(italic)) + if font is not None: + te.Font = font + return True + except Exception as ex: + print("[TEXT] font path 2:", ex) + return False + + +def create_text(): + """Triggered von der Oberleiste. Vollstaendiger Workflow.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + settings = load_settings(doc) + + rc, pt = Rhino.Input.RhinoGet.GetPoint( + "Position fuer Text", False) + if rc != Rhino.Commands.Result.Success: return + if pt is None: return + + text = _floating_input() + if not text: return + + try: + te = rg.TextEntity() + te.Plane = rg.Plane(pt, rg.Vector3d.ZAxis) + te.PlainText = text + try: + 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")) + doc.Objects.AddText(te) + doc.Views.Redraw() + except Exception as ex: + print("[TEXT] create:", ex) diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index 464d5e0..0552a02 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -11,7 +11,7 @@ import { deleteLayerCombination, openLayerCombinationsDialog, openDossierSettings, openKameraPanel, setMasseActive, openMasseSettings, - openAbout, + openAbout, createText, setTextSettings, } from './lib/rhinoBridge' const PRESETS = [ @@ -305,6 +305,8 @@ export default function OberleisteApp() { overridesActivePreset: null, overridesPresets: [], layerCombinations: [], layerCombinationActive: null, massePresets: [], masseActiveId: null, + textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false }, + textFonts: [], }) const [appliedScale, setAppliedScale] = useState(null) const appliedScaleRef = useRef(null) @@ -759,6 +761,146 @@ export default function OberleisteApp() { ) })()} +
+ + {/* ====== TEXT-Block 2-Reihen ====== + Reihe 1: Font-Dropdown + Groesse (mm) + Reihe 2: B/I-Toggles + "Text einfuegen"-Button + */} + {(() => { + const ts = state.textSettings || {} + const fonts = state.textFonts || [] + const updateTs = (patch) => setTextSettings({ ...ts, ...patch }) + const TEXT_W = 150 + return ( +
+ {/* Reihe 1, Spalte 1: Font-Dropdown */} + updateTs({ font: v })} + width={TEXT_W} + title="Schriftart" + > + {fonts.length === 0 && } + {fonts.map(f => )} + + {/* Reihe 1, Spalte 2: Groesse (mm) */} +
+ { + const v = parseFloat(e.target.value) + if (!isNaN(v) && v > 0) updateTs({ size: v }) + }} + style={{ + flex: 1, minWidth: 0, + background: 'transparent', border: 'none', outline: 'none', + color: 'var(--text-primary)', + fontSize: 11, fontFamily: 'DM Mono, monospace', + padding: 0, textAlign: 'right', + appearance: 'auto', + }} + title="Texthoehe in Model-Units" + /> + m +
+ {/* Reihe 2, Spalte 1: B/I-Toggles */} +
+ + +
+ {/* Reihe 2, Spalte 2: "Text einfuegen" Button */} + +
+ ) + })()} + {/* Snap-Toggles (Ortho/Grid/OSnap) sind in Rhinos eigener Footer-Bar schon vorhanden — hier rausgenommen um Doppelung zu vermeiden. */} diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index a79df68..ed79704 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -171,6 +171,8 @@ export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) } export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) } export function openAbout() { send('OPEN_ABOUT', {}) } +export function createText() { send('CREATE_TEXT', {}) } +export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) } // --- Masse (in Oberleiste + Satellite-Fenster MasseSettings) --- // Topbar: aktives Mass setzen + Settings-Fenster oeffnen