Text-Editor: Rhino RTF-Dialekt + Round-Trip + Oberleiste-Sync
Lange Iteration mit dem Rhino TextEntity-RTF-Parser (siehe MEMORY:
rhino_textentity_rtf_limits.md). Finale Form:
- RTF-Body: per-Segment {\fN\cfN\b\i\ulnone seg}-Groups, \par
zwischen Groups als Linebreak, { }-Space-Group fuer Leerzeilen
(Rhino collapsed sonst aufeinanderfolgende \par). \fs (Font-Size)
ist NICHT unterstuetzt → eine Size pro TextEntity (global).
- htmlToRuns: emittiert \n VOR Block-Elementen wenn schon Content
davor — fixt nested <div>A<div>B</div></div> die sonst A+B ohne
Trenner als ein Run liefern.
- Round-Trip-Erhaltung: editor.innerHTML 1:1 als UserString
"dossier_text_html" persistiert, beim Reopen direkt gesetzt
(kein runsToHtml-Konvertieren das Zeilen verlieren kann).
- Oberleiste-Editing: in-place modify von obj.Geometry + Commit-
Changes statt Duplicate+Replace (Mac Rhino gibt False zurueck
bei RichText-Klonen). Plus _patch_rtf_b_i_ul: regex-flippt
\b/\b0, \i/\i0, \ul/\ulnone global in der RTF damit Bold/Italic/
Underline OFF in der Oberleiste auch wirklich auf DOSSIER-Texte
greift (per-Segment-Codes wuerden te.Font-Aenderung sonst
uebersteuern).
- Stil-ID am Text persistiert + von read_selection_settings
zurueckgelesen → Stil-Dropdown spiegelt Selektion.
- Editor neu: V-Align (Top/Middle/Bottom), Mask-Type (None/Viewport/
Solid) mit Farb-Picker, Case-Transform (upper/lower/capitalize/
invert), Masstaeblich-Toggle (AnnotationScalingEnabled),
Symbol-Popover, Frame-Optionen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+67
-61
@@ -84,7 +84,8 @@ def _on_idle_check_pending_edit(sender, e):
|
||||
|
||||
class TextEditorBridge(panel_base.BaseBridge):
|
||||
def __init__(self, frame_data, settings, fonts,
|
||||
edit_obj_id=None, initial_text="", initial_runs=None):
|
||||
edit_obj_id=None, initial_text="", initial_runs=None,
|
||||
initial_html=None):
|
||||
panel_base.BaseBridge.__init__(self, "text_editor")
|
||||
self._frame = frame_data # (origin, width, height, p1, p2)
|
||||
self._initial_settings = settings
|
||||
@@ -93,6 +94,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit gesetzt
|
||||
self._initial_text = initial_text
|
||||
self._initial_runs = initial_runs # rich-format-Runs falls vorhanden
|
||||
self._initial_html = initial_html # 1:1 Editor-HTML beim Reopen
|
||||
|
||||
def set_form(self, form):
|
||||
self._form_ref = form
|
||||
@@ -108,6 +110,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
"styles": styles,
|
||||
"initialText": self._initial_text,
|
||||
"initialRuns": self._initial_runs,
|
||||
"initialHtml": self._initial_html,
|
||||
"editMode": bool(self._edit_obj_id),
|
||||
})
|
||||
|
||||
@@ -282,6 +285,10 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
print("[TEXT-EDITOR] color:", ex)
|
||||
attrs.SetUserString("dossier_text", "1")
|
||||
attrs.SetUserString("dossier_text_scaled", "1" if scale_flag else "0")
|
||||
sid = st.get("styleId")
|
||||
if sid:
|
||||
try: attrs.SetUserString("dossier_text_style_id", sid)
|
||||
except Exception: pass
|
||||
# Runs als JSON persistieren — beim Re-Open kann der Editor
|
||||
# die ganze Struktur (Fonts/Sizes/Styles pro Segment) wieder
|
||||
# herstellen statt nur PlainText zu zeigen.
|
||||
@@ -291,6 +298,15 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
attrs.SetUserString("dossier_text_runs", json.dumps(runs))
|
||||
except Exception as ex:
|
||||
print("[TEXT-EDITOR] runs persist:", ex)
|
||||
# Editor-innerHTML 1:1 persistieren — beim Reopen wird der
|
||||
# exakte Editor-Zustand wiederhergestellt, kein Round-Trip
|
||||
# ueber runs (was Zeilen zusammen ziehen kann).
|
||||
html = payload.get("html")
|
||||
if html:
|
||||
try:
|
||||
attrs.SetUserString("dossier_text_html", html)
|
||||
except Exception as ex:
|
||||
print("[TEXT-EDITOR] html persist:", ex)
|
||||
|
||||
# Edit-Mode: bestehenden TextEntity ersetzen statt neu hinzu
|
||||
if self._edit_obj_id is not None:
|
||||
@@ -366,17 +382,13 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20):
|
||||
"""Konvertiert Format-Runs in Rhinos RTF-Dialekt. Runs ist Liste von
|
||||
dicts mit Keys text/font/bold/italic/underline/sup/sub/color/fontSizePx.
|
||||
base_size_m: TextEntity.TextHeight (in m). Frontend rendert 1m = 100px,
|
||||
also entspricht base_size_m * 100 dem "Standard" \\fs20 in RTF."""
|
||||
also entspricht base_size_m * 100 dem "Standard" \\fs20 in RTF.
|
||||
|
||||
Wir emittieren IMMER RTF wenn Runs vorliegen — auch wenn die Runs
|
||||
auf den ersten Blick "trivial" aussehen. So bleibt das Format
|
||||
stabil ueber Re-Edits hinweg und es gibt keinen impliziten Fallback
|
||||
auf _apply_font (= alles auf eine Schrift)."""
|
||||
if not runs: return None
|
||||
# Triviale Runs (alle plain, ein Font) → kein RTF noetig
|
||||
nontrivial = False
|
||||
for r in runs:
|
||||
if r.get("bold") or r.get("italic") or r.get("underline") \
|
||||
or r.get("sup") or r.get("sub") or r.get("color") \
|
||||
or r.get("fontSizePx") \
|
||||
or (r.get("font") and r["font"] != default_font):
|
||||
nontrivial = True; break
|
||||
if not nontrivial: return None
|
||||
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# PASS 1: Runs verarbeiten + Fonts/Colors sammeln + RTF-Bodies bauen
|
||||
@@ -410,57 +422,39 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20):
|
||||
out.append("\\u{}?".format(v))
|
||||
return "".join(out)
|
||||
|
||||
# Ansatz: jedes Text-Segment in eigene {}-Group mit lokalen Codes.
|
||||
# Zwischen Segmenten (und ueber Run-Grenzen hinweg) \\par fuer
|
||||
# Paragraph-Breaks. So bleibt Per-Run-Formatting (Groups isolieren
|
||||
# State) UND Mehrzeiligkeit (\\par ist Rhinos echter Linebreak).
|
||||
body_parts = []
|
||||
pending_pars = 0 # Wieviele \\par muessen noch vor naechstem Segment
|
||||
|
||||
def _emit_group(run, seg):
|
||||
codes = []
|
||||
codes.append("\\f{}".format(font_idx(run.get("font") or default_font)))
|
||||
ci = color_idx(run.get("color")) if run.get("color") else 0
|
||||
codes.append("\\cf{}".format(ci) if ci > 0 else "\\cf0")
|
||||
fsp = run.get("fontSizePx")
|
||||
if fsp and abs(fsp - BASE_PX) > 0.1:
|
||||
rtf_fs = max(2, int(round(20.0 * fsp / BASE_PX)))
|
||||
codes.append("\\fs{}".format(rtf_fs))
|
||||
else:
|
||||
codes.append("\\fs20")
|
||||
codes.append("\\b" if run.get("bold") else "\\b0")
|
||||
codes.append("\\i" if run.get("italic") else "\\i0")
|
||||
codes.append("\\ul" if run.get("underline") else "\\ulnone")
|
||||
if run.get("sup"): codes.append("\\super")
|
||||
elif run.get("sub"): codes.append("\\sub")
|
||||
else: codes.append("\\nosupersub")
|
||||
body_parts.append("{{{} {}}}".format("".join(codes), _escape_no_par(seg)))
|
||||
|
||||
def _flush_pars(n):
|
||||
# 1 \\par = einfacher Zeilenumbruch. N>1 \\par hintereinander
|
||||
# wuerde Rhino zu einem einzigen Umbruch collapsen — fuer
|
||||
# jede zusaetzliche Leerzeile schieben wir einen Space-Group
|
||||
# dazwischen, damit der leere Paragraph Inhalt hat.
|
||||
if n <= 0: return
|
||||
body_parts.append("\\par ")
|
||||
for _ in range(n - 1):
|
||||
body_parts.append("{ }\\par ")
|
||||
|
||||
first_emitted = False
|
||||
# Rhinos TextEntity-RTF: per-Segment in {}-Group, \par zwischen
|
||||
# Groups als Linebreak. Diese Form hat in commit 3bd949e fuer
|
||||
# Newlines funktioniert. Fuer Leerzeilen: zusaetzliche {\par}-Group
|
||||
# zwischen den regulaeren Segmenten — leere Group mit eigenem \par
|
||||
# umgeht den Multi-\par-Collapse.
|
||||
lines = [[]]
|
||||
for run in runs:
|
||||
raw = run.get("text") or ""
|
||||
segments = raw.split("\n")
|
||||
for i, seg in enumerate(segments):
|
||||
if i > 0:
|
||||
pending_pars += 1
|
||||
if seg:
|
||||
if first_emitted:
|
||||
_flush_pars(pending_pars)
|
||||
pending_pars = 0
|
||||
_emit_group(run, seg)
|
||||
first_emitted = True
|
||||
if first_emitted and pending_pars > 0:
|
||||
_flush_pars(pending_pars)
|
||||
parts_in_run = raw.split("\n")
|
||||
for j, part in enumerate(parts_in_run):
|
||||
if j > 0:
|
||||
lines.append([])
|
||||
if part:
|
||||
lines[-1].append((run, part))
|
||||
|
||||
body_parts = []
|
||||
for li, line in enumerate(lines):
|
||||
if li > 0:
|
||||
body_parts.append("\\par ")
|
||||
if not line:
|
||||
# Leere Zeile: Space-Group damit der Paragraph Inhalt hat
|
||||
# (sonst collapsed Rhino zwei \par auf einen Linebreak).
|
||||
body_parts.append("{ }")
|
||||
continue
|
||||
for (run, seg) in line:
|
||||
codes = []
|
||||
codes.append("\\f{}".format(font_idx(run.get("font") or default_font)))
|
||||
ci = color_idx(run.get("color")) if run.get("color") else 0
|
||||
codes.append("\\cf{}".format(ci) if ci > 0 else "\\cf0")
|
||||
codes.append("\\b" if run.get("bold") else "\\b0")
|
||||
codes.append("\\i" if run.get("italic") else "\\i0")
|
||||
codes.append("\\ul" if run.get("underline") else "\\ulnone")
|
||||
body_parts.append("{{{} {}}}".format("".join(codes), _escape_no_par(seg)))
|
||||
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# PASS 2: RTF-Header mit JETZT vollstaendigen Tables + Body
|
||||
@@ -530,6 +524,10 @@ def open_for_edit(obj):
|
||||
settings["align"] = "right"
|
||||
else: settings["align"] = "left"
|
||||
except Exception: pass
|
||||
try:
|
||||
sid = obj.Attributes.GetUserString("dossier_text_style_id")
|
||||
if sid: settings["styleId"] = sid
|
||||
except Exception: pass
|
||||
try:
|
||||
flag = obj.Attributes.GetUserString("dossier_text_scaled")
|
||||
if flag in ("0", "1"):
|
||||
@@ -573,6 +571,13 @@ def open_for_edit(obj):
|
||||
if rj: initial_runs = json.loads(rj)
|
||||
except Exception as ex:
|
||||
print("[TEXT-EDITOR] read runs:", ex)
|
||||
# Editor-innerHTML (Round-Trip-Konservierung): wenn vorhanden,
|
||||
# wird der Editor exakt mit diesem HTML geoeffnet
|
||||
initial_html = None
|
||||
try:
|
||||
h = obj.Attributes.GetUserString("dossier_text_html")
|
||||
if h: initial_html = h
|
||||
except Exception: pass
|
||||
|
||||
# Frame aus dem Text-BBox ableiten (fuer Dialog-Positionierung)
|
||||
p1 = te.Plane.Origin
|
||||
@@ -596,7 +601,8 @@ def open_for_edit(obj):
|
||||
settings, fonts,
|
||||
edit_obj_id=obj.Id,
|
||||
initial_text=initial_text,
|
||||
initial_runs=initial_runs)
|
||||
initial_runs=initial_runs,
|
||||
initial_html=initial_html)
|
||||
sc.sticky["text_editor_bridge"] = bridge
|
||||
|
||||
form = panel_base.open_satellite_window(
|
||||
|
||||
Reference in New Issue
Block a user