Doppelklick-Hook auf DOSSIER-Texte + Size-Mapping in RTF
User: Doppelklick auf DOSSIER-Text oeffnet weiterhin Rhinos Editor. Verschiedene Groessen im Editor erscheinen nicht in Rhino. Doppelklick-Hook (rhino/text_editor.py): - _DossierTextDoubleClickHook subklassiert Rhino.UI.MouseCallback. OnMouseDoubleClick prueft selektierte TextEntities auf UserString "dossier_text"="1" und cancelled das Event (= blockt Rhinos Standard-TextEdit-Dialog), setzt sticky["dossier_pending_text_edit"] mit der Obj-ID - _on_idle_check_pending_edit (RhinoApp.Idle event): nimmt sticky-ID auf naechstem Idle-Tick und ruft open_for_edit(obj) — defer noetig weil Eto-Form aus MouseCallback heraus oeffnen Re-Entrancy macht - _ensure_double_click_hook() installiert Hook + Idle-Handler einmalig pro Rhino-Session (idempotent) - startup.py ruft das jetzt direkt nach Modul-Load auf Edit-Mode (open_for_edit): - Liest aus bestehendem TextEntity die Settings (Font, Size, Bold, Italic, Underline, Align) + PlainText - Frame fuer Dialog-Positionierung aus BBox abgeleitet - TextEditorBridge mit edit_obj_id + initial_text gestartet - INIT-Payload um initialText + editMode erweitert - COMMIT: bei edit_obj_id gesetzt → doc.Objects.Replace statt AddText. Plane wird vom Original uebernommen wenn keine explizite Rotation, damit der Text an seinem Platz bleibt Frontend (TextEditorApp.jsx): - Bei INIT mit initialText: editor.innerText wird damit befuellt - htmlToRuns extrahiert font-size in Pixel pro Run (inline style oder computed style != base) - baseCtx _basePx aus computed style des Editor-divs Size-Mapping (rhino/text_editor.py _runs_to_rtf): - fontSizePx in Runs triggert non-trivial (RTF wird generiert) - Pro Run: \fs in Halb-Punkten = 20 * (run_px / 14_base_px) round - 14px = \fs20 (1.0× TextEntity.TextHeight) - 21px = \fs30 (1.5×) - 28px = \fs40 (2.0×) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,12 @@ def _load_all(sender, e):
|
|||||||
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
|
print("[STARTUP] {} ({}) OK".format(mod_id, py_name))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
|
print("[STARTUP] {} ({}) FEHLER: {}".format(mod_id, py_name, ex))
|
||||||
|
# Text-Editor Doppelklick-Hook fuer DOSSIER-Texte
|
||||||
|
try:
|
||||||
|
import text_editor
|
||||||
|
text_editor._ensure_double_click_hook()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[STARTUP] text_editor hook:", ex)
|
||||||
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
|
# DOSSIERUI Window-Layout — Hinweis fuer manuelles Laden
|
||||||
_hint_dossier_ui()
|
_hint_dossier_ui()
|
||||||
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
|
# Startup-Timing-Summary 3 Sekunden spaeter (nachdem alle async Idle-
|
||||||
|
|||||||
@@ -906,6 +906,7 @@ def create_text():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import text_editor
|
import text_editor
|
||||||
|
text_editor._ensure_double_click_hook()
|
||||||
text_editor.open_with_frame(p1, p2, origin, width, height)
|
text_editor.open_with_frame(p1, p2, origin, width, height)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT] open editor:", ex)
|
print("[TEXT] open editor:", ex)
|
||||||
|
|||||||
+168
-10
@@ -21,13 +21,77 @@ import panel_base
|
|||||||
import text_create
|
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):
|
class TextEditorBridge(panel_base.BaseBridge):
|
||||||
def __init__(self, frame_data, settings, fonts):
|
def __init__(self, frame_data, settings, fonts,
|
||||||
|
edit_obj_id=None, initial_text=""):
|
||||||
panel_base.BaseBridge.__init__(self, "text_editor")
|
panel_base.BaseBridge.__init__(self, "text_editor")
|
||||||
self._frame = frame_data # (origin, width, height, p1, p2)
|
self._frame = frame_data # (origin, width, height, p1, p2)
|
||||||
self._initial_settings = settings
|
self._initial_settings = settings
|
||||||
self._fonts = fonts
|
self._fonts = fonts
|
||||||
self._form_ref = None
|
self._form_ref = None
|
||||||
|
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit gesetzt
|
||||||
|
self._initial_text = initial_text
|
||||||
|
|
||||||
def set_form(self, form):
|
def set_form(self, form):
|
||||||
self._form_ref = form
|
self._form_ref = form
|
||||||
@@ -38,9 +102,11 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
try: styles = text_create.list_styles(doc)
|
try: styles = text_create.list_styles(doc)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
self.send("INIT", {
|
self.send("INIT", {
|
||||||
"settings": self._initial_settings,
|
"settings": self._initial_settings,
|
||||||
"fonts": self._fonts,
|
"fonts": self._fonts,
|
||||||
"styles": styles,
|
"styles": styles,
|
||||||
|
"initialText": self._initial_text,
|
||||||
|
"editMode": bool(self._edit_obj_id),
|
||||||
})
|
})
|
||||||
|
|
||||||
def handle(self, data):
|
def handle(self, data):
|
||||||
@@ -145,7 +211,25 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT-EDITOR] color:", ex)
|
print("[TEXT-EDITOR] color:", ex)
|
||||||
attrs.SetUserString("dossier_text", "1")
|
attrs.SetUserString("dossier_text", "1")
|
||||||
doc.Objects.AddText(te, attrs)
|
|
||||||
|
# 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()
|
doc.Views.Redraw()
|
||||||
|
|
||||||
# Defaults speichern (ohne color/rotation/frame — die sind
|
# Defaults speichern (ohne color/rotation/frame — die sind
|
||||||
@@ -207,6 +291,7 @@ def _runs_to_rtf(runs, default_font):
|
|||||||
for r in runs:
|
for r in runs:
|
||||||
if r.get("bold") or r.get("italic") or r.get("underline") \
|
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("sup") or r.get("sub") or r.get("color") \
|
||||||
|
or r.get("fontSizePx") \
|
||||||
or (r.get("font") and r["font"] != default_font):
|
or (r.get("font") and r["font"] != default_font):
|
||||||
nontrivial = True; break
|
nontrivial = True; break
|
||||||
if not nontrivial: return None
|
if not nontrivial: return None
|
||||||
@@ -237,11 +322,19 @@ def _runs_to_rtf(runs, default_font):
|
|||||||
parts.append("}")
|
parts.append("}")
|
||||||
parts.append("\\pard")
|
parts.append("\\pard")
|
||||||
|
|
||||||
|
# Editor Default-Font-Size in px (siehe TextEditorApp Editor-div: 14px)
|
||||||
|
BASE_PX = 14
|
||||||
for run in runs:
|
for run in runs:
|
||||||
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")
|
||||||
|
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("\\b" if run.get("bold") else "\\b0")
|
||||||
codes.append("\\i" if run.get("italic") else "\\i0")
|
codes.append("\\i" if run.get("italic") else "\\i0")
|
||||||
codes.append("\\ul" if run.get("underline") else "\\ulnone")
|
codes.append("\\ul" if run.get("underline") else "\\ulnone")
|
||||||
@@ -256,11 +349,8 @@ def _runs_to_rtf(runs, default_font):
|
|||||||
|
|
||||||
|
|
||||||
def open_with_frame(p1, p2, origin, width, height):
|
def open_with_frame(p1, p2, origin, width, height):
|
||||||
"""Aufgerufen aus text_create.create_text() nach Frame-Pick.
|
"""Frame-Pick-Workflow → Editor oeffnen, neuer Text erstellt."""
|
||||||
Oeffnet das React-WYSIWYG-Editor-Fenster (Topmost) neben dem Frame.
|
_ensure_double_click_hook()
|
||||||
Non-blocking — Bridge handlet die Eingabe + erstellt TextEntity bei
|
|
||||||
COMMIT.
|
|
||||||
"""
|
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
settings = text_create.load_settings(doc)
|
settings = text_create.load_settings(doc)
|
||||||
fonts = text_create.available_fonts()
|
fonts = text_create.available_fonts()
|
||||||
@@ -276,3 +366,71 @@ def open_with_frame(p1, p2, origin, width, height):
|
|||||||
topmost=True)
|
topmost=True)
|
||||||
if form is not None:
|
if form is not None:
|
||||||
bridge.set_form(form)
|
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
|
||||||
|
|
||||||
|
initial_text = ""
|
||||||
|
try: initial_text = te.PlainText or ""
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|||||||
+21
-6
@@ -108,9 +108,6 @@ function htmlToRuns(rootEl) {
|
|||||||
// Computed style oder inline style.
|
// Computed style oder inline style.
|
||||||
const cs = window.getComputedStyle ? window.getComputedStyle(node) : null
|
const cs = window.getComputedStyle ? window.getComputedStyle(node) : null
|
||||||
if (node.style.fontFamily) nc.font = node.style.fontFamily.replace(/['"]/g, '').split(',')[0].trim()
|
if (node.style.fontFamily) nc.font = node.style.fontFamily.replace(/['"]/g, '').split(',')[0].trim()
|
||||||
else if (cs?.fontFamily && cs.fontFamily !== ctx.font && cs.fontFamily) {
|
|
||||||
// nur uebernehmen wenn explizit anders
|
|
||||||
}
|
|
||||||
if (node.style.color) nc.color = node.style.color
|
if (node.style.color) nc.color = node.style.color
|
||||||
if (node.style.fontWeight) {
|
if (node.style.fontWeight) {
|
||||||
const fw = node.style.fontWeight
|
const fw = node.style.fontWeight
|
||||||
@@ -118,6 +115,14 @@ function htmlToRuns(rootEl) {
|
|||||||
}
|
}
|
||||||
if (node.style.fontStyle === 'italic') nc.italic = true
|
if (node.style.fontStyle === 'italic') nc.italic = true
|
||||||
if (node.style.textDecoration?.includes('underline')) nc.underline = true
|
if (node.style.textDecoration?.includes('underline')) nc.underline = true
|
||||||
|
// Font-Size: aus inline-style oder computed style (Pixel)
|
||||||
|
if (node.style.fontSize) {
|
||||||
|
const m = node.style.fontSize.match(/(\d+\.?\d*)px/)
|
||||||
|
if (m) nc.fontSizePx = parseFloat(m[1])
|
||||||
|
} else if (cs?.fontSize) {
|
||||||
|
const px = parseFloat(cs.fontSize)
|
||||||
|
if (px && px !== ctx._basePx) nc.fontSizePx = px
|
||||||
|
}
|
||||||
// Legacy <font> Element von execCommand
|
// Legacy <font> Element von execCommand
|
||||||
if (tag === 'font') {
|
if (tag === 'font') {
|
||||||
const c = node.getAttribute('color'); if (c) nc.color = c
|
const c = node.getAttribute('color'); if (c) nc.color = c
|
||||||
@@ -130,8 +135,10 @@ function htmlToRuns(rootEl) {
|
|||||||
flush('\n', ctx)
|
flush('\n', ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const basePx = parseFloat(window.getComputedStyle(rootEl).fontSize) || 14
|
||||||
const baseCtx = { font: null, color: null, bold: false, italic: false,
|
const baseCtx = { font: null, color: null, bold: false, italic: false,
|
||||||
underline: false, sup: false, sub: false }
|
underline: false, sup: false, sub: false,
|
||||||
|
fontSizePx: null, _basePx: basePx }
|
||||||
for (const child of rootEl.childNodes) walk(child, baseCtx)
|
for (const child of rootEl.childNodes) walk(child, baseCtx)
|
||||||
// Trailing-Newline weg
|
// Trailing-Newline weg
|
||||||
if (runs.length && runs[runs.length-1].text === '\n') runs.pop()
|
if (runs.length && runs[runs.length-1].text === '\n') runs.pop()
|
||||||
@@ -221,8 +228,16 @@ export default function TextEditorApp() {
|
|||||||
if (s.italic != null) setItalic(!!s.italic)
|
if (s.italic != null) setItalic(!!s.italic)
|
||||||
if (s.underline != null) setUnderline(!!s.underline)
|
if (s.underline != null) setUnderline(!!s.underline)
|
||||||
if (s.align) setAlign(s.align)
|
if (s.align) setAlign(s.align)
|
||||||
// Editor-Default-Font setzen
|
// Bei Edit-Mode: bestehenden Text in den Editor laden
|
||||||
setTimeout(() => editorRef.current?.focus(), 100)
|
const initialText = data.initialText || ''
|
||||||
|
setTimeout(() => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
if (initialText) {
|
||||||
|
editorRef.current.innerText = initialText
|
||||||
|
}
|
||||||
|
editorRef.current.focus()
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
})
|
})
|
||||||
notifyReady()
|
notifyReady()
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
Reference in New Issue
Block a user