diff --git a/rhino/text_editor.py b/rhino/text_editor.py index 0a4254d..fe15332 100644 --- a/rhino/text_editor.py +++ b/rhino/text_editor.py @@ -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)