26c7d9e67d
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>
616 lines
24 KiB
Python
616 lines
24 KiB
Python
#! 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Doppelklick-Hook: wenn User auf einen DOSSIER-Text doppelklickt, oeffnet
|
|
# sich UNSER Editor statt Rhinos Standard-TextEdit.
|
|
|
|
class _DossierTextDoubleClickHook(Rhino.UI.MouseCallback):
|
|
def OnMouseDoubleClick(self, e):
|
|
try:
|
|
doc = Rhino.RhinoDoc.ActiveDoc
|
|
if doc is None: return
|
|
# Welches Objekt ist gerade selektiert? (Rhino's einfache
|
|
# Single-Click hat es ausgewaehlt bevor wir hier sind.)
|
|
target = None
|
|
for obj in doc.Objects.GetSelectedObjects(False, False):
|
|
try:
|
|
if isinstance(obj.Geometry, rg.TextEntity):
|
|
if obj.Attributes.GetUserString("dossier_text") == "1":
|
|
target = obj; break
|
|
except Exception: pass
|
|
if target is not None:
|
|
e.Cancel = True # Rhinos eigener TextEdit-Dialog wird geblockt
|
|
# Defer auf naechsten Idle-Tick — Eto-Form aus MouseCallback
|
|
# heraus zu oeffnen kann zu Re-Entrancy-Problemen fuehren
|
|
sc.sticky["dossier_pending_text_edit"] = str(target.Id)
|
|
except Exception as ex:
|
|
print("[TEXT-HOOK] OnDoubleClick:", ex)
|
|
|
|
|
|
_hook_instance = None
|
|
|
|
|
|
def _ensure_double_click_hook():
|
|
"""Installiert MouseCallback einmalig pro Rhino-Session."""
|
|
global _hook_instance
|
|
if _hook_instance is not None: return
|
|
try:
|
|
_hook_instance = _DossierTextDoubleClickHook()
|
|
_hook_instance.Enabled = True
|
|
# Idle-Handler einmalig registrieren fuer das deferred Opening
|
|
if not sc.sticky.get("dossier_text_idle_registered"):
|
|
Rhino.RhinoApp.Idle += _on_idle_check_pending_edit
|
|
sc.sticky["dossier_text_idle_registered"] = True
|
|
print("[TEXT-HOOK] Doppelklick-Hook installiert")
|
|
except Exception as ex:
|
|
print("[TEXT-HOOK] install:", ex)
|
|
|
|
|
|
def _on_idle_check_pending_edit(sender, e):
|
|
pid = sc.sticky.get("dossier_pending_text_edit")
|
|
if not pid: return
|
|
sc.sticky["dossier_pending_text_edit"] = None
|
|
doc = Rhino.RhinoDoc.ActiveDoc
|
|
if doc is None: return
|
|
try:
|
|
import System
|
|
obj = doc.Objects.FindId(System.Guid(pid))
|
|
if obj is None: return
|
|
open_for_edit(obj)
|
|
except Exception as ex:
|
|
print("[TEXT-HOOK] open for edit:", ex)
|
|
|
|
|
|
class TextEditorBridge(panel_base.BaseBridge):
|
|
def __init__(self, frame_data, settings, fonts,
|
|
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
|
|
self._fonts = fonts
|
|
self._form_ref = None
|
|
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
|
|
|
|
def _on_ready(self):
|
|
doc = Rhino.RhinoDoc.ActiveDoc
|
|
styles = []
|
|
try: styles = text_create.list_styles(doc)
|
|
except Exception: pass
|
|
self.send("INIT", {
|
|
"settings": self._initial_settings,
|
|
"fonts": self._fonts,
|
|
"styles": styles,
|
|
"initialText": self._initial_text,
|
|
"initialRuns": self._initial_runs,
|
|
"initialHtml": self._initial_html,
|
|
"editMode": bool(self._edit_obj_id),
|
|
})
|
|
|
|
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
|
|
import math
|
|
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 {}
|
|
runs = payload.get("runs") # Phase 2: rich-text runs (oder None)
|
|
origin, width, height, _p1, _p2 = self._frame
|
|
|
|
try:
|
|
te = rg.TextEntity()
|
|
# Plane mit optionaler Rotation um Z
|
|
rot_deg = 0.0
|
|
try: rot_deg = float(st.get("rotation") or 0)
|
|
except Exception: pass
|
|
plane = rg.Plane(origin, rg.Vector3d.ZAxis)
|
|
if abs(rot_deg) > 1e-6:
|
|
try:
|
|
plane.Rotate(math.radians(rot_deg), rg.Vector3d.ZAxis, origin)
|
|
except Exception: pass
|
|
te.Plane = plane
|
|
|
|
# RichText (Phase 2) erzeugen wenn Runs nontrivial sind.
|
|
rtf = _runs_to_rtf(
|
|
runs,
|
|
st.get("font") or "Helvetica",
|
|
base_size_m=float(st.get("size") or 0.2)) if runs else None
|
|
if rtf:
|
|
print("[TEXT-EDITOR] RTF len={} preview={!r}".format(
|
|
len(rtf), rtf[:300]))
|
|
|
|
# Defaults (Height + Align gelten immer)
|
|
try: te.TextHeight = float(st.get("size") or 0.2)
|
|
except Exception: pass
|
|
text_create._apply_align(te, st.get("align") or "left")
|
|
text_create._apply_valign(te, st.get("valign") or "top")
|
|
|
|
# Content. Bei RichText KEIN _apply_font — sonst ueberschreibt
|
|
# te.Font die per-Run-Fonts aus der RTF. Stattdessen lassen
|
|
# wir RichText/SetRichText das selber regeln.
|
|
if rtf:
|
|
te.PlainText = text # Initial-Content (RichText ueberschreibt)
|
|
# Bevorzugt SetRichText(rtf, dimstyle) — robusteres API
|
|
applied = False
|
|
try:
|
|
ds = doc.DimStyles.Current
|
|
te.SetRichText(rtf, ds)
|
|
applied = True
|
|
print("[TEXT-EDITOR] SetRichText OK")
|
|
except Exception as ex1:
|
|
print("[TEXT-EDITOR] SetRichText fail:", ex1)
|
|
if not applied:
|
|
try:
|
|
te.RichText = rtf
|
|
applied = True
|
|
print("[TEXT-EDITOR] te.RichText = OK")
|
|
except Exception as ex2:
|
|
print("[TEXT-EDITOR] te.RichText = fail:", ex2)
|
|
if not applied:
|
|
# Letzter Fallback: ohne RTF, mit Toolbar-Defaults
|
|
text_create._apply_font(
|
|
te, st.get("font") or "Helvetica",
|
|
st.get("bold"), st.get("italic"),
|
|
st.get("underline"))
|
|
else:
|
|
te.PlainText = text
|
|
text_create._apply_font(
|
|
te, st.get("font") or "Helvetica",
|
|
st.get("bold"), st.get("italic"),
|
|
st.get("underline"))
|
|
|
|
# 3. Text-Wrap im Frame — NACH dem Content damit es nicht
|
|
# durch RichText-Set zurueckgesetzt wird. Beide Setter
|
|
# versuchen (verschiedene Rhino-Versions-APIs).
|
|
applied_w = None
|
|
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
|
try:
|
|
setattr(te, attr, width)
|
|
applied_w = attr
|
|
break
|
|
except Exception: pass
|
|
try: te.TextIsWrapped = True
|
|
except Exception:
|
|
try: te.TextWrap = True
|
|
except Exception: pass
|
|
print("[TEXT-EDITOR] wrap: width={} applied_attr={}".format(
|
|
width, applied_w))
|
|
|
|
# 4. Frame um den Text + Mask-Margin
|
|
frame_kind = (st.get("frame") or "none").lower()
|
|
try:
|
|
MF = Rhino.DocObjects.TextMaskFrame
|
|
te.MaskFrame = (
|
|
MF.RectFrame if frame_kind == "rect" else
|
|
MF.CapsuleFrame if frame_kind == "capsule" else
|
|
MF.NoFrame
|
|
)
|
|
except Exception as ex:
|
|
print("[TEXT-EDITOR] frame:", ex)
|
|
# Mask: Type entscheidet ob/wie maskiert wird. Margin gilt
|
|
# nur wenn Maske aktiv. Solid-Color erst dann setzen wenn
|
|
# Type=solid (sonst dominiert Viewport-Color).
|
|
try:
|
|
mask_type = (st.get("maskType") or "none").lower()
|
|
mask_m = float(st.get("maskMargin") or 0)
|
|
if mask_type == "none":
|
|
te.MaskEnabled = False
|
|
else:
|
|
te.MaskEnabled = True
|
|
te.MaskOffset = mask_m
|
|
if mask_type == "solid":
|
|
te.MaskUsesViewportColor = False
|
|
mc = st.get("maskColor") or [255, 255, 255]
|
|
try:
|
|
te.MaskColor = System.Drawing.Color.FromArgb(
|
|
int(mc[0]), int(mc[1]), int(mc[2]))
|
|
except Exception as ex:
|
|
print("[TEXT-EDITOR] mask color:", ex)
|
|
else:
|
|
te.MaskUsesViewportColor = True
|
|
except Exception as ex:
|
|
print("[TEXT-EDITOR] mask:", ex)
|
|
|
|
# 5. Horizontal-to-view (DrawForward)
|
|
try:
|
|
te.DrawForward = bool(st.get("horizontalToView"))
|
|
except Exception: pass
|
|
|
|
# 6. Annotation-Scaling (Masstaeblich) — Rhino 8 hat das pro
|
|
# Annotation-Objekt. Property-Name variiert je nach Build,
|
|
# deshalb mehrere Varianten versuchen.
|
|
scale_flag = bool(st.get("scaleWithModel", True))
|
|
applied_scale = None
|
|
for prop in ("AnnotationScalingEnabled",
|
|
"IsAnnotationScalingEnabled",
|
|
"ModelSpaceScalingEnabled"):
|
|
try:
|
|
setattr(te, prop, scale_flag)
|
|
applied_scale = prop
|
|
break
|
|
except Exception: pass
|
|
if applied_scale is None:
|
|
print("[TEXT-EDITOR] AnnotationScaling-Property nicht gefunden")
|
|
|
|
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")
|
|
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.
|
|
if runs:
|
|
try:
|
|
import json
|
|
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:
|
|
try:
|
|
old = doc.Objects.FindId(self._edit_obj_id)
|
|
if old is not None:
|
|
# Plane vom Original uebernehmen wenn nicht rotiert
|
|
# explizit gewuenscht (sonst wandert der Text)
|
|
if abs(rot_deg) < 1e-6:
|
|
te.Plane = old.Geometry.Plane
|
|
doc.Objects.Replace(self._edit_obj_id, te)
|
|
doc.Objects.ModifyAttributes(self._edit_obj_id, attrs, True)
|
|
else:
|
|
doc.Objects.AddText(te, attrs)
|
|
except Exception as ex:
|
|
print("[TEXT-EDITOR] replace:", ex)
|
|
doc.Objects.AddText(te, attrs)
|
|
else:
|
|
doc.Objects.AddText(te, attrs)
|
|
doc.Views.Redraw()
|
|
|
|
# Defaults speichern (ohne color/rotation/frame — die sind
|
|
# situativ, nicht zu uebernehmen)
|
|
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)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase 2: Rich-Text-Runs (vom Frontend HTML-parsing) → Rhino-RTF.
|
|
|
|
def _parse_color_to_rgb(c):
|
|
"""CSS-Farbe ('rgb(r,g,b)' | '#rrggbb' | '#rgb') → (r,g,b) Ints 0-255."""
|
|
if not c: return (0, 0, 0)
|
|
c = c.strip()
|
|
if c.startswith("rgb"):
|
|
import re
|
|
m = re.search(r"(\d+)\D+(\d+)\D+(\d+)", c)
|
|
if m: return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
if c.startswith("#"):
|
|
h = c.lstrip("#")
|
|
if len(h) == 3: h = "".join(ch * 2 for ch in h)
|
|
if len(h) == 6:
|
|
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
|
return (0, 0, 0)
|
|
|
|
|
|
def _rtf_escape(s):
|
|
"""RTF-Escape: \\, {, }, sowie non-ASCII via \\u-Notation."""
|
|
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("\\par\n")
|
|
elif cp < 128: out.append(ch)
|
|
else:
|
|
# Signed 16-bit → Rhinos RTF erwartet das so
|
|
v = cp if cp < 0x8000 else cp - 0x10000
|
|
out.append("\\u{}?".format(v))
|
|
return "".join(out)
|
|
|
|
|
|
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.
|
|
|
|
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
|
|
|
|
# ────────────────────────────────────────────────────────────────
|
|
# PASS 1: Runs verarbeiten + Fonts/Colors sammeln + RTF-Bodies bauen
|
|
# ────────────────────────────────────────────────────────────────
|
|
fonts = [default_font]
|
|
colors = []
|
|
def font_idx(f):
|
|
if not f: return 0
|
|
if f not in fonts: fonts.append(f)
|
|
return fonts.index(f)
|
|
def color_idx(c):
|
|
if not c: return 0
|
|
rgb = _parse_color_to_rgb(c)
|
|
if rgb in colors: return colors.index(rgb) + 1
|
|
colors.append(rgb)
|
|
return len(colors)
|
|
|
|
BASE_PX = max(1.0, base_size_m * 100.0)
|
|
|
|
def _escape_no_par(s):
|
|
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")
|
|
elif cp < 128: out.append(ch)
|
|
else:
|
|
v = cp if cp < 0x8000 else cp - 0x10000
|
|
out.append("\\u{}?".format(v))
|
|
return "".join(out)
|
|
|
|
# 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 ""
|
|
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
|
|
# ────────────────────────────────────────────────────────────────
|
|
parts = ["{\\rtf1\\ansi\\ansicpg1252\\deff0"]
|
|
parts.append("{\\fonttbl")
|
|
for i, f in enumerate(fonts):
|
|
parts.append("{{\\f{}\\fnil\\fcharset0 {};}}".format(i, f))
|
|
parts.append("}")
|
|
if colors:
|
|
parts.append("{\\colortbl;")
|
|
for r, g, b in colors:
|
|
parts.append("\\red{}\\green{}\\blue{};".format(r, g, b))
|
|
parts.append("}")
|
|
parts.append("\\pard")
|
|
parts.extend(body_parts)
|
|
parts.append("}")
|
|
return "".join(parts)
|
|
|
|
|
|
def open_with_frame(p1, p2, origin, width, height):
|
|
"""Frame-Pick-Workflow → Editor oeffnen, neuer Text erstellt."""
|
|
_ensure_double_click_hook()
|
|
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)
|
|
|
|
|
|
def open_for_edit(obj):
|
|
"""Bei Doppelklick auf einen DOSSIER-Text: Editor mit den
|
|
bestehenden Settings + Text-Inhalt oeffnen. COMMIT macht REPLACE."""
|
|
_ensure_double_click_hook()
|
|
doc = Rhino.RhinoDoc.ActiveDoc
|
|
if doc is None or obj is None: return
|
|
te = obj.Geometry
|
|
if not isinstance(te, rg.TextEntity): return
|
|
|
|
# Settings aus dem bestehenden Text lesen
|
|
settings = text_create.load_settings(doc)
|
|
try: settings["size"] = float(te.TextHeight)
|
|
except Exception: pass
|
|
try:
|
|
if te.Font:
|
|
settings["font"] = te.Font.QuartetName
|
|
settings["bold"] = bool(te.Font.Bold)
|
|
settings["italic"] = bool(te.Font.Italic)
|
|
try: settings["underline"] = bool(te.Font.Underlined)
|
|
except Exception: pass
|
|
except Exception: pass
|
|
try:
|
|
h = te.TextHorizontalAlignment
|
|
if h == Rhino.DocObjects.TextHorizontalAlignment.Center:
|
|
settings["align"] = "center"
|
|
elif h == Rhino.DocObjects.TextHorizontalAlignment.Right:
|
|
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"):
|
|
settings["scaleWithModel"] = (flag == "1")
|
|
else:
|
|
for prop in ("AnnotationScalingEnabled",
|
|
"IsAnnotationScalingEnabled",
|
|
"ModelSpaceScalingEnabled"):
|
|
if hasattr(te, prop):
|
|
settings["scaleWithModel"] = bool(getattr(te, prop))
|
|
break
|
|
except Exception: pass
|
|
try:
|
|
v = te.TextVerticalAlignment
|
|
VA = Rhino.DocObjects.TextVerticalAlignment
|
|
if v == VA.Middle: settings["valign"] = "middle"
|
|
elif v == VA.Bottom: settings["valign"] = "bottom"
|
|
else: settings["valign"] = "top"
|
|
except Exception: pass
|
|
try:
|
|
if te.MaskEnabled:
|
|
settings["maskType"] = "solid" if not te.MaskUsesViewportColor else "viewport"
|
|
try: settings["maskMargin"] = float(te.MaskOffset)
|
|
except Exception: pass
|
|
try:
|
|
mc = te.MaskColor
|
|
settings["maskColor"] = [mc.R, mc.G, mc.B]
|
|
except Exception: pass
|
|
else:
|
|
settings["maskType"] = "none"
|
|
except Exception: pass
|
|
|
|
initial_text = ""
|
|
try: initial_text = te.PlainText or ""
|
|
except Exception: pass
|
|
# Runs aus UserString lesen (gespeichert beim letzten Save)
|
|
initial_runs = None
|
|
try:
|
|
import json
|
|
rj = obj.Attributes.GetUserString("dossier_text_runs")
|
|
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
|
|
try:
|
|
bb = te.GetBoundingBox(False)
|
|
if bb.IsValid:
|
|
p2 = rg.Point3d(bb.Max.X, bb.Min.Y, p1.Z)
|
|
origin = rg.Point3d(bb.Min.X, bb.Max.Y, p1.Z)
|
|
width = bb.Max.X - bb.Min.X
|
|
height = bb.Max.Y - bb.Min.Y
|
|
else:
|
|
p2 = p1
|
|
origin = p1
|
|
width = 1.0; height = 0.5
|
|
except Exception:
|
|
p2 = p1; origin = p1; width = 1.0; height = 0.5
|
|
|
|
fonts = text_create.available_fonts()
|
|
bridge = TextEditorBridge(
|
|
(origin, width, height, p1, p2),
|
|
settings, fonts,
|
|
edit_obj_id=obj.Id,
|
|
initial_text=initial_text,
|
|
initial_runs=initial_runs,
|
|
initial_html=initial_html)
|
|
sc.sticky["text_editor_bridge"] = bridge
|
|
|
|
form = panel_base.open_satellite_window(
|
|
"text_editor",
|
|
title="Dossier Text (Bearbeiten)",
|
|
size=(640, 480),
|
|
bridge=bridge,
|
|
topmost=True)
|
|
if form is not None:
|
|
bridge.set_form(form)
|