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
|
except Exception: pass
|
||||||
te.Plane = plane
|
te.Plane = plane
|
||||||
|
|
||||||
# REIHENFOLGE WICHTIG:
|
# 1. Defaults (Font, Height, Align) — gilt fuer ALLES
|
||||||
# 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
|
|
||||||
try: te.TextHeight = float(st.get("size") or 0.2)
|
try: te.TextHeight = float(st.get("size") or 0.2)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
text_create._apply_font(
|
text_create._apply_font(
|
||||||
@@ -163,8 +157,9 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
st.get("underline"))
|
st.get("underline"))
|
||||||
text_create._apply_align(te, st.get("align") or "left")
|
text_create._apply_align(te, st.get("align") or "left")
|
||||||
|
|
||||||
# RichText (Phase 2) — ZULETZT, ueberschreibt te.Font fuer
|
# 2. Content — RichText wenn vorhanden, sonst PlainText.
|
||||||
# alle Runs die eine eigene Schrift haben.
|
# RichText-Runs ueberschreiben Defaults fuer Texte mit
|
||||||
|
# eigener Formatierung.
|
||||||
rtf = _runs_to_rtf(
|
rtf = _runs_to_rtf(
|
||||||
runs,
|
runs,
|
||||||
st.get("font") or "Helvetica",
|
st.get("font") or "Helvetica",
|
||||||
@@ -174,8 +169,13 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
te.RichText = rtf
|
te.RichText = rtf
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT-EDITOR] RichText set fail:", 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:
|
try:
|
||||||
setattr(te, attr, width); break
|
setattr(te, attr, width); break
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
@@ -184,7 +184,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
try: te.TextWrap = True
|
try: te.TextWrap = True
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
# Frame um den Text + Mask-Margin
|
# 4. Frame um den Text + Mask-Margin
|
||||||
frame_kind = (st.get("frame") or "none").lower()
|
frame_kind = (st.get("frame") or "none").lower()
|
||||||
try:
|
try:
|
||||||
MF = Rhino.DocObjects.TextMaskFrame
|
MF = Rhino.DocObjects.TextMaskFrame
|
||||||
@@ -204,7 +204,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT-EDITOR] mask:", ex)
|
print("[TEXT-EDITOR] mask:", ex)
|
||||||
|
|
||||||
# Horizontal-to-view (DrawForward = text steht immer zur Kamera)
|
# 5. Horizontal-to-view (DrawForward)
|
||||||
try:
|
try:
|
||||||
te.DrawForward = bool(st.get("horizontalToView"))
|
te.DrawForward = bool(st.get("horizontalToView"))
|
||||||
except Exception: pass
|
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 = ["{\\rtf1\\ansi\\ansicpg1252\\deff0"]
|
||||||
parts.append("{\\fonttbl")
|
parts.append("{\\fonttbl")
|
||||||
for i, f in enumerate(fonts):
|
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("{{\\f{}\\fnil\\fcharset0 {};}}".format(i, f))
|
||||||
parts.append("}")
|
parts.append("}")
|
||||||
if colors:
|
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
|
# Frontend rendert 1m = 100px; Standard-Run-Size ist base_size_m * 100
|
||||||
BASE_PX = max(1.0, base_size_m * 100.0)
|
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 = []
|
||||||
codes.append("\\f{}".format(font_idx(run.get("font") or default_font)))
|
codes.append("\\f{}".format(font_idx(run.get("font") or default_font)))
|
||||||
ci = color_idx(run.get("color")) if run.get("color") else 0
|
ci = color_idx(run.get("color")) if run.get("color") else 0
|
||||||
if ci > 0: codes.append("\\cf{}".format(ci))
|
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")
|
fsp = run.get("fontSizePx")
|
||||||
if fsp and abs(fsp - BASE_PX) > 0.1:
|
if fsp and abs(fsp - BASE_PX) > 0.1:
|
||||||
rtf_fs = max(2, int(round(20.0 * fsp / BASE_PX)))
|
rtf_fs = max(2, int(round(20.0 * fsp / BASE_PX)))
|
||||||
codes.append("\\fs{}".format(rtf_fs))
|
codes.append("\\fs{}".format(rtf_fs))
|
||||||
codes.append("\\b" if run.get("bold") else "\\b0")
|
# Nur AKTIVE Format-Toggles innerhalb der Group — Default ausserhalb
|
||||||
codes.append("\\i" if run.get("italic") else "\\i0")
|
if run.get("bold"): codes.append("\\b")
|
||||||
codes.append("\\ul" if run.get("underline") else "\\ulnone")
|
if run.get("italic"): codes.append("\\i")
|
||||||
if run.get("sup"): codes.append("\\super")
|
if run.get("underline"): codes.append("\\ul")
|
||||||
elif run.get("sub"): codes.append("\\sub")
|
if run.get("sup"): codes.append("\\super")
|
||||||
else: codes.append("\\nosupersub")
|
elif run.get("sub"): codes.append("\\sub")
|
||||||
body = _rtf_escape(run.get("text") or "")
|
parts.append("{{{} {}}}".format("".join(codes), segment))
|
||||||
parts.append("{} {}".format("".join(codes), body))
|
|
||||||
|
# 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("}")
|
parts.append("}")
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|||||||
Reference in New Issue
Block a user