From 6b3421e7af749c2b13240af80064204c9685a2cf Mon Sep 17 00:00:00 2001 From: karim Date: Thu, 21 May 2026 13:08:51 +0200 Subject: [PATCH] Text-Editor: V-Align + Mask-Type/Color + Case-Transform Erweitert den Editor um die fehlenden Rhino-Text-Optionen: - Vertical Alignment (Top/Middle/Bottom) als 3 Pill-Buttons in Row 2. Backend: text_create._apply_valign mit TextVerticalAlignment-Enum. - Mask-Type Dropdown (None/Viewport/Solid). Bei Solid erscheint ein Mask-Color-Picker. MaskMargin disabled wenn Type=none. Backend: schaltet te.MaskEnabled + te.MaskUsesViewportColor + te.MaskColor entsprechend. - Case-Dropdown in Row 2: upper/lower/capitalize/invert. Wirkt nur auf die Selektion via execCommand insertText. open_for_edit liest valign + maskType/Color/Margin aus bestehendem TextEntity zurueck, damit beim Re-Open der Zustand erhalten bleibt. Co-Authored-By: Claude Opus 4.7 --- rhino/text_create.py | 16 ++++++++ rhino/text_editor.py | 39 ++++++++++++++++++- src/TextEditorApp.jsx | 89 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/rhino/text_create.py b/rhino/text_create.py index 7376f3c..4603f42 100644 --- a/rhino/text_create.py +++ b/rhino/text_create.py @@ -799,6 +799,22 @@ def _apply_align(te, align): return False +def _apply_valign(te, valign): + """Setzt TextVerticalAlignment (Top/Middle/Bottom).""" + try: + m = { + "top": Rhino.DocObjects.TextVerticalAlignment.Top, + "middle": Rhino.DocObjects.TextVerticalAlignment.Middle, + "bottom": Rhino.DocObjects.TextVerticalAlignment.Bottom, + } + if valign in m: + te.TextVerticalAlignment = m[valign] + return True + except Exception as ex: + print("[TEXT] apply valign:", ex) + return False + + def apply_settings_to_selection(doc, patch): """Wendet font/size/bold/italic/align auf alle selektierten TextEntities an. Returns Anzahl der geaenderten Objekte.""" diff --git a/rhino/text_editor.py b/rhino/text_editor.py index d94712d..789f840 100644 --- a/rhino/text_editor.py +++ b/rhino/text_editor.py @@ -162,6 +162,7 @@ class TextEditorBridge(panel_base.BaseBridge): try: te.TextHeight = float(st.get("size") or 0.2) except Exception: pass text_create._apply_align(te, st.get("align") or "left") + text_create._apply_valign(te, st.get("valign") or "top") # Content. Bei RichText KEIN _apply_font — sonst ueberschreibt # te.Font die per-Run-Fonts aus der RTF. Stattdessen lassen @@ -225,12 +226,27 @@ class TextEditorBridge(panel_base.BaseBridge): ) except Exception as ex: print("[TEXT-EDITOR] frame:", ex) + # Mask: Type entscheidet ob/wie maskiert wird. Margin gilt + # nur wenn Maske aktiv. Solid-Color erst dann setzen wenn + # Type=solid (sonst dominiert Viewport-Color). try: + mask_type = (st.get("maskType") or "none").lower() mask_m = float(st.get("maskMargin") or 0) - if mask_m > 0: + if mask_type == "none": + te.MaskEnabled = False + else: te.MaskEnabled = True te.MaskOffset = mask_m - te.MaskUsesViewportColor = True + if mask_type == "solid": + te.MaskUsesViewportColor = False + mc = st.get("maskColor") or [255, 255, 255] + try: + te.MaskColor = System.Drawing.Color.FromArgb( + int(mc[0]), int(mc[1]), int(mc[2])) + except Exception as ex: + print("[TEXT-EDITOR] mask color:", ex) + else: + te.MaskUsesViewportColor = True except Exception as ex: print("[TEXT-EDITOR] mask:", ex) @@ -497,6 +513,25 @@ def open_for_edit(obj): settings["align"] = "right" else: settings["align"] = "left" except Exception: pass + try: + v = te.TextVerticalAlignment + VA = Rhino.DocObjects.TextVerticalAlignment + if v == VA.Middle: settings["valign"] = "middle" + elif v == VA.Bottom: settings["valign"] = "bottom" + else: settings["valign"] = "top" + except Exception: pass + try: + if te.MaskEnabled: + settings["maskType"] = "solid" if not te.MaskUsesViewportColor else "viewport" + try: settings["maskMargin"] = float(te.MaskOffset) + except Exception: pass + try: + mc = te.MaskColor + settings["maskColor"] = [mc.R, mc.G, mc.B] + except Exception: pass + else: + settings["maskType"] = "none" + except Exception: pass initial_text = "" try: initial_text = te.PlainText or "" diff --git a/src/TextEditorApp.jsx b/src/TextEditorApp.jsx index 9da0ccb..189f590 100644 --- a/src/TextEditorApp.jsx +++ b/src/TextEditorApp.jsx @@ -247,6 +247,9 @@ export default function TextEditorApp() { const [frame, setFrame] = useState('none') // none | rect | capsule const [horizontalToView, setHorizontalToView] = useState(false) const [rotation, setRotation] = useState(0) + const [valign, setVAlign] = useState('top') // top | middle | bottom + const [maskType, setMaskType] = useState('none') // none | viewport | solid + const [maskColor, setMaskColor] = useState([255, 255, 255]) const [maskMargin, setMaskMargin] = useState(0) const [symbolsOpen, setSymbolsOpen] = useState(false) const [styles, setStyles] = useState([]) @@ -324,6 +327,9 @@ export default function TextEditorApp() { if (s.italic != null) setItalic(!!s.italic) if (s.underline != null) setUnderline(!!s.underline) if (s.align) setAlign(s.align) + if (s.valign) setVAlign(s.valign) + if (s.maskType) setMaskType(s.maskType) + if (Array.isArray(s.maskColor)) setMaskColor(s.maskColor) // Bei Edit-Mode: bestehenden Text in den Editor laden. Wenn Runs // persistiert sind (= reicher Format-Stand vom letzten Save), // diese als HTML laden — sonst PlainText fallback. @@ -420,6 +426,34 @@ export default function TextEditorApp() { } const clearColor = () => { setColor(null); exec('foreColor', '#000000') } + const onMaskColorPick = (hex) => { + const m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i) + if (m) setMaskColor([parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)]) + if (maskType === 'none') setMaskType('solid') + } + + // Case-Transformationen auf die aktuelle Selektion. Wenn nichts + // markiert ist, no-op (kein All-Text-Modus, sonst zu invasiv). + const transformCase = (mode) => { + restoreSelection() + const sel = window.getSelection() + if (!sel || sel.rangeCount === 0) return + const range = sel.getRangeAt(0) + if (range.collapsed) return + const src = range.toString() + let out = src + if (mode === 'upper') out = src.toUpperCase() + else if (mode === 'lower') out = src.toLowerCase() + else if (mode === 'capitalize') out = src.replace(/\w\S*/g, w => + w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) + else if (mode === 'invert') out = src.split('').map(ch => + ch === ch.toUpperCase() ? ch.toLowerCase() : ch.toUpperCase()).join('') + document.execCommand('insertText', false, out) + editorRef.current?.focus() + } + const rgbToHex = ([r, g, b]) => '#' + + [r, g, b].map(n => Math.max(0, Math.min(255, n|0)).toString(16).padStart(2, '0')).join('') + const onCommit = () => { const el = editorRef.current if (!el) return @@ -432,8 +466,9 @@ export default function TextEditorApp() { text, runs, settings: { - font, size, bold, italic, underline, align, color, - frame, horizontalToView, rotation, maskMargin, + font, size, bold, italic, underline, align, valign, color, + frame, horizontalToView, rotation, + maskType, maskColor, maskMargin, }, }) } @@ -528,24 +563,68 @@ export default function TextEditorApp() {
+ setVAlign('top'))} title="Oben"> + + + setVAlign('middle'))} title="Mittig"> + + + setVAlign('bottom'))} title="Unten"> + + +
x₂ +
+ { if (v) transformCase(v) }} + width={130} + title="Selektion in Gross-/Kleinschrift wandeln" + options={[ + , + , + , + , + , + ]} />
- {/* Toolbar Row 3: Frame / Rotation / Mask / Horizontal-to-view / Symbole */} + {/* Toolbar Row 3: Frame / Mask (Type/Color/Margin) / Rotation / Camera / Symbole */}
( ))} /> + Keine Maske, + , + , + ]} /> + {maskType === 'solid' && ( + + )}
- Mask + opacity: maskType === 'none' ? 0.5 : 1, + }} title="Rand um den Text-Inhalt der mit-maskiert wird (m)"> + Rand setMaskMargin(parseFloat(e.target.value) || 0)}