RTF: pro-Run-Groups + Newlines als \par zwischen Groups
User-Bug: Heading (Helvetica Bold) + Paragraph (Georgia Regular) als
2 Zeilen ergibt in Rhino: alle Text in einer Zeile, mit Bold vom
Heading aber Font+Size vom LETZTEN definierten Paragraph (Georgia).
Root cause: Mein RTF-Generator emitted alle Codes hintereinander ohne
{}-Grouping. RTF-Parser ohne Scope nimmt das letzte \\f und \\fs als
globalen State und appliziert auf ALLES.
Fix _runs_to_rtf:
- Pro Run: text per \\n splitten, zwischen Segmenten \\par emitten
- Jedes nicht-leere Segment wird in {} Group gewrapped mit eigenen
Codes (\\fN \\cfN \\fsN \\b \\i \\ul \\super \\sub)
- Nur AKTIVE Toggles in der Group (kein \\b0/\\i0/\\ulnone mehr —
Default ausserhalb der Group ist plain)
- Resultat: {\\f0\\b Lorem ...}\\par {\\f1 Consectetur ...}
Jede Group ist isoliert, kein Cross-Run-State-Leak
_escape_no_par: \\n bleibt als Literal-Newline durchgelassen (splitting
geschieht im outer loop, nicht im escape).
_commit-Reihenfolge nochmal aufgeraeumt:
1. Plane
2. Defaults (Height, Font, Align) — gilt fuer alles
3. Content (RichText wenn nontrivial, sonst PlainText)
4. Wrap (FormatWidth + TextIsWrapped) — NACH RichText damit nicht
zurueckgesetzt
5. Frame/Mask/DrawForward
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+51
-25
@@ -147,13 +147,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
except Exception: pass
|
||||
te.Plane = plane
|
||||
|
||||
# REIHENFOLGE WICHTIG:
|
||||
# 1. Plain-Text setzen (Initial-Content)
|
||||
# 2. Default-Font / TextHeight / Align setzen (gilt fuer alles)
|
||||
# 3. RichText setzen (ZULETZT — die per-Run-Format-Codes
|
||||
# ueberschreiben dann die Default-Properties)
|
||||
# Andere Reihenfolge → Defaults schiessen die RTF-Runs weg.
|
||||
te.PlainText = text
|
||||
# 1. Defaults (Font, Height, Align) — gilt fuer ALLES
|
||||
try: te.TextHeight = float(st.get("size") or 0.2)
|
||||
except Exception: pass
|
||||
text_create._apply_font(
|
||||
@@ -163,8 +157,9 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
st.get("underline"))
|
||||
text_create._apply_align(te, st.get("align") or "left")
|
||||
|
||||
# RichText (Phase 2) — ZULETZT, ueberschreibt te.Font fuer
|
||||
# alle Runs die eine eigene Schrift haben.
|
||||
# 2. Content — RichText wenn vorhanden, sonst PlainText.
|
||||
# RichText-Runs ueberschreiben Defaults fuer Texte mit
|
||||
# eigener Formatierung.
|
||||
rtf = _runs_to_rtf(
|
||||
runs,
|
||||
st.get("font") or "Helvetica",
|
||||
@@ -174,8 +169,13 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
te.RichText = rtf
|
||||
except Exception as ex:
|
||||
print("[TEXT-EDITOR] RichText set fail:", ex)
|
||||
te.PlainText = text
|
||||
else:
|
||||
te.PlainText = text
|
||||
|
||||
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
||||
# 3. Text-Wrap im Frame — NACH dem Content damit es nicht
|
||||
# durch RichText-Set zurueckgesetzt wird
|
||||
for attr in ("FormatWidth", "TextWidth"):
|
||||
try:
|
||||
setattr(te, attr, width); break
|
||||
except Exception: pass
|
||||
@@ -184,7 +184,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
try: te.TextWrap = True
|
||||
except Exception: pass
|
||||
|
||||
# Frame um den Text + Mask-Margin
|
||||
# 4. Frame um den Text + Mask-Margin
|
||||
frame_kind = (st.get("frame") or "none").lower()
|
||||
try:
|
||||
MF = Rhino.DocObjects.TextMaskFrame
|
||||
@@ -204,7 +204,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
||||
except Exception as ex:
|
||||
print("[TEXT-EDITOR] mask:", ex)
|
||||
|
||||
# Horizontal-to-view (DrawForward = text steht immer zur Kamera)
|
||||
# 5. Horizontal-to-view (DrawForward)
|
||||
try:
|
||||
te.DrawForward = bool(st.get("horizontalToView"))
|
||||
except Exception: pass
|
||||
@@ -323,8 +323,6 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20):
|
||||
parts = ["{\\rtf1\\ansi\\ansicpg1252\\deff0"]
|
||||
parts.append("{\\fonttbl")
|
||||
for i, f in enumerate(fonts):
|
||||
# Rhinos RTF-Parser will fnil/fcharset0 — sonst kann es passieren
|
||||
# dass der font-Eintrag ignoriert wird und ein Default verwendet
|
||||
parts.append("{{\\f{}\\fnil\\fcharset0 {};}}".format(i, f))
|
||||
parts.append("}")
|
||||
if colors:
|
||||
@@ -336,25 +334,53 @@ def _runs_to_rtf(runs, default_font, base_size_m=0.20):
|
||||
|
||||
# Frontend rendert 1m = 100px; Standard-Run-Size ist base_size_m * 100
|
||||
BASE_PX = max(1.0, base_size_m * 100.0)
|
||||
for run in runs:
|
||||
|
||||
def _escape_no_par(s):
|
||||
"""Wie _rtf_escape aber OHNE \\par-Umwandlung von \\n
|
||||
(Newlines werden separat als \\par ZWISCHEN Run-Groups emitted)."""
|
||||
out = []
|
||||
for ch in s:
|
||||
cp = ord(ch)
|
||||
if ch == "\\": out.append("\\\\")
|
||||
elif ch == "{": out.append("\\{")
|
||||
elif ch == "}": out.append("\\}")
|
||||
elif ch == "\n": out.append("\n") # passthrough, wird gesplittet
|
||||
elif cp < 128: out.append(ch)
|
||||
else:
|
||||
v = cp if cp < 0x8000 else cp - 0x10000
|
||||
out.append("\\u{}?".format(v))
|
||||
return "".join(out)
|
||||
|
||||
def _emit_run_group(run, segment):
|
||||
"""Emittiert {codes segment} wenn segment nicht leer."""
|
||||
if not segment: return
|
||||
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
|
||||
if ci > 0: codes.append("\\cf{}".format(ci))
|
||||
# Font-Size: \fs in Halb-Punkten relativ zur TextEntity.TextHeight.
|
||||
# Annahme: \fs20 = 1.0× base. Scaling: \fs = 20 * (run_px / base_px).
|
||||
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))
|
||||
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 = _rtf_escape(run.get("text") or "")
|
||||
parts.append("{} {}".format("".join(codes), body))
|
||||
# Nur AKTIVE Format-Toggles innerhalb der Group — Default ausserhalb
|
||||
if run.get("bold"): codes.append("\\b")
|
||||
if run.get("italic"): codes.append("\\i")
|
||||
if run.get("underline"): codes.append("\\ul")
|
||||
if run.get("sup"): codes.append("\\super")
|
||||
elif run.get("sub"): codes.append("\\sub")
|
||||
parts.append("{{{} {}}}".format("".join(codes), segment))
|
||||
|
||||
# Pro Run: nach Newlines splitten. Jedes Segment ist eine eigene
|
||||
# Group, dazwischen \par fuer den Zeilenumbruch.
|
||||
for run in runs:
|
||||
raw = run.get("text") or ""
|
||||
segments = raw.split("\n")
|
||||
for i, seg in enumerate(segments):
|
||||
if i > 0:
|
||||
parts.append("\\par ")
|
||||
esc = _escape_no_par(seg)
|
||||
if esc:
|
||||
_emit_run_group(run, esc)
|
||||
|
||||
parts.append("}")
|
||||
return "".join(parts)
|
||||
|
||||
Reference in New Issue
Block a user