React WYSIWYG Text-Editor (Topmost Satellite-WebView) — Phase 1

User-Wunsch: eigener WYSIWYG-Editor im React/Topbar-GUI-Stil. Topmost.
Verschiedene Schriftarten/-Dicken sichtbar im Editor selbst.

Neues Backend (rhino/text_editor.py):
- TextEditorBridge mit Frame-Daten im Konstruktor, INIT-Push mit
  Settings + Font-Liste, COMMIT erstellt TextEntity, CANCEL schliesst
- open_with_frame(p1, p2, origin, width, height): oeffnet Satellite-
  Window mit mode='text_editor' + topmost=True
- panel_base.open_satellite_window: neuer Parameter topmost (default
  False) der form.Topmost setzt

text_create.create_text: ruft jetzt text_editor.open_with_frame nach
dem Frame-Pick. Eto-basierter _dossier_text_editor bleibt im Modul als
Fallback aber wird nicht mehr verwendet.

Neues Frontend (src/TextEditorApp.jsx, mode='text_editor'):
- Layout im DOSSIER-Topbar-Stil (dunkle Pills, accent on hover)
- Pill-Helper-Komponente fuer alle Toggle/Action-Buttons
- Dropdown-Helper fuer Font + Size
- Toolbar Row 1: Font-Dropdown | Size-Dropdown | Color-Picker | Layer-Reset
- Toolbar Row 2: B/I/U mit Material-Icons | L/C/R Align | x²/x₂ Sup/Sub
- Sonderzeichen-Palette: 41 Unicode-Symbole (Architektur/Math/Pfeile/
  Auszeichnungen), Klick inserted am Cursor
- WYSIWYG-Editor: contentEditable div mit fontFamily=ausgewaehlt,
  textAlign=ausgewaehlt — Format-Toolbar wirkt via document.execCommand
  (bold/italic/underline/justifyLeft/Center/Right/superscript/subscript/
  fontName/foreColor)
- "Einfuegen" sendet COMMIT mit text (innerText) + settings
- "Abbrechen" CANCEL → Bridge schliesst Form

Phase 1 Limitation: rendert in Rhino als PlainText mit den globalen
Settings (font/size/bold/italic/align/color) — verschiedene Schriftarten
INNERHALB des Texts sind im Editor sichtbar aber nicht im finalen
Rhino-TextEntity. Phase 2: HTML → Rhino RichText/RTF Mapping.

rhinoBridge.js: send() jetzt exportiert (war intern) damit
TextEditorApp generisch COMMIT/CANCEL senden kann.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 01:28:26 +02:00
parent b047d0aa4b
commit ab0ecfbf14
6 changed files with 425 additions and 58 deletions
+3 -2
View File
@@ -367,7 +367,8 @@ def attach_webview(panel, bridge, mode):
# --- Satelliten-Fenster (echtes Rhino-Fenster mit eingebetteter WebView) ----
def open_satellite_window(mode, params=None, title=None, size=(420, 560),
on_save=None, on_cancel=None, bridge=None):
on_save=None, on_cancel=None, bridge=None,
topmost=False):
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form) mit eingebetteter WebView.
Die WebView laedt die React-App mit dem gegebenen `mode` und `params`.
@@ -389,7 +390,7 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
form.ClientSize = drawing.Size(int(size[0]), int(size[1]))
except Exception: pass
form.Resizable = True
form.Topmost = False
form.Topmost = bool(topmost)
wv = forms.WebView()
+7 -55
View File
@@ -862,69 +862,21 @@ def read_selection_settings(doc):
def create_text():
"""DOSSIER Custom Text-Workflow:
"""DOSSIER Custom Text-Workflow (React WYSIWYG-Editor):
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)
2. text_editor.open_with_frame oeffnet das React-WYSIWYG-Fenster
(Topmost, neben dem Frame). Editor handlet die Eingabe + erstellt
die TextEntity bei COMMIT.
"""
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
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(new_state.get("size") or 0.2)
except Exception: pass
_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
except Exception: pass
try: te.TextIsWrapped = True
except Exception:
try: te.TextWrap = True
except Exception: pass
# 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()
import text_editor
text_editor.open_with_frame(p1, p2, origin, width, height)
except Exception as ex:
print("[TEXT] create:", ex)
print("[TEXT] open editor:", ex)
+131
View File
@@ -0,0 +1,131 @@
#! python 3
# -*- coding: utf-8 -*-
"""
text_editor.py
React-WYSIWYG-Editor in Satellite-WebView (Topmost). User picked Frame
in create_text(), dann oeffnet sich dieser Editor neben dem Frame.
TextEditorBridge haelt Frame-Daten + Settings, auf COMMIT erstellt es
die TextEntity und schliesst das Fenster.
"""
import os
import sys
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)
import panel_base
import text_create
class TextEditorBridge(panel_base.BaseBridge):
def __init__(self, frame_data, settings, fonts):
panel_base.BaseBridge.__init__(self, "text_editor")
self._frame = frame_data # (origin, width, height, p1, p2)
self._initial_settings = settings
self._fonts = fonts
self._form_ref = None
def set_form(self, form):
self._form_ref = form
def _on_ready(self):
self.send("INIT", {
"settings": self._initial_settings,
"fonts": self._fonts,
})
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "COMMIT":
self._commit(p)
try: self._form_ref.Close()
except Exception: pass
elif t == "CANCEL":
try: self._form_ref.Close()
except Exception: pass
def _commit(self, payload):
import System
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None or self._frame is None: return
text = (payload.get("text") or "").strip()
if not text: return
st = payload.get("settings") or {}
origin, width, height, _p1, _p2 = self._frame
try:
te = rg.TextEntity()
te.Plane = rg.Plane(origin, rg.Vector3d.ZAxis)
te.PlainText = text
try: te.TextHeight = float(st.get("size") or 0.2)
except Exception: pass
text_create._apply_font(
te,
st.get("font") or "Helvetica",
st.get("bold"), st.get("italic"),
st.get("underline"))
text_create._apply_align(te, st.get("align") or "left")
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
try:
setattr(te, attr, width); break
except Exception: pass
try: te.TextIsWrapped = True
except Exception:
try: te.TextWrap = True
except Exception: pass
attrs = Rhino.DocObjects.ObjectAttributes()
col = st.get("color") # [r,g,b] oder None
if col is not None and len(col) >= 3:
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-EDITOR] color:", ex)
attrs.SetUserString("dossier_text", "1")
doc.Objects.AddText(te, attrs)
doc.Views.Redraw()
# Defaults speichern (ohne color)
text_create.save_settings(doc, {
"font": st.get("font"),
"size": st.get("size"),
"bold": st.get("bold"),
"italic": st.get("italic"),
"underline": st.get("underline"),
"align": st.get("align"),
})
except Exception as ex:
print("[TEXT-EDITOR] commit:", ex)
def open_with_frame(p1, p2, origin, width, height):
"""Aufgerufen aus text_create.create_text() nach Frame-Pick.
Oeffnet das React-WYSIWYG-Editor-Fenster (Topmost) neben dem Frame.
Non-blocking — Bridge handlet die Eingabe + erstellt TextEntity bei
COMMIT.
"""
doc = Rhino.RhinoDoc.ActiveDoc
settings = text_create.load_settings(doc)
fonts = text_create.available_fonts()
bridge = TextEditorBridge((origin, width, height, p1, p2),
settings, fonts)
sc.sticky["text_editor_bridge"] = bridge
form = panel_base.open_satellite_window(
"text_editor",
title="Dossier Text",
size=(640, 480),
bridge=bridge,
topmost=True)
if form is not None:
bridge.set_form(form)