Text-Editor: Stile, Size-Dropdown, U+Align, bessere Icons, Fonts-Fallback
User-Wunsch: Text-Editor war unfertig — keine Fonts sichtbar, Bold liess sich nicht entfernen, Size soll Dropdown mit Eigene, Text-Stile noetig, Unterstrichen + Links/Mitte/Rechts fehlten, schoenere Icons. Backend (text_create.py): - DEFAULTS erweitert um underline + align (left/center/right) - _normalize() validiert Settings (align nur left/center/right) - Text-Style-Preset-System analog mass_style: - list_styles / save_style / delete_style / apply_style - get_active_style_id / set_active_style_id - doc.Strings["dossier_text_styles"] (JSON list mit id/name + settings) - doc.Strings["dossier_text_style_active"] - _apply_align(te, "left"|"center"|"right") setzt TextHorizontalAlignment - apply_settings_to_selection + create_text rufen _apply_align mit auf - read_selection_settings liest auch align - available_fonts mit Fallback-Liste (Helvetica, Arial, Times, etc.) wenn Rhino.DocObjects.Font.AvailableFontFaceNames leer ist - underline: in Settings + Styles persistiert, NOCH NICHT visuell appliziert (braucht TextEntity-RichText-API) Backend (oberleiste.py): - Neue Handler APPLY_TEXT_STYLE / SAVE_TEXT_STYLE / DELETE_TEXT_STYLE - State liefert textStyles + textStyleActiveId - textFonts jetzt bei jedem _send_state mitgeschickt (vorher one-shot mit _fonts_sent flag — verlor sich nach Panel-Re-Mount und User sah keine Fonts mehr) Frontend (OberleisteApp): - Text-Block komplett neu gelayoutet (3 Spalten Grid): Reihe 1: [Style ▼] [Font ▼] [Size ▼] Reihe 2: [B|I|U] [L|C|R] [+] - Style-Dropdown mit Optionen "+ Speichern…" und "🗑 Aktiven loeschen" - Size-Dropdown mit Preset-Werten (0.05/0.10/.../1.00 m) + "Eigene…" → toggle zu Custom-Number-Input bei "Eigene"-Auswahl - B/I/U mit Material-Icons format_bold/italic/underlined statt B/I-Text - L/C/R Alignment-Buttons mit format_align_left/center/right - ToggleBtn-Helper-Komponente fuer alle 6 Toggles - "+" Insert-Button bleibt klein (Icon size 14) - Accent-Border auf allen Pills wenn Text selektiert (visuelles Feedback "Aenderungen wirken auf Selektion") - Bold/Italic/Underline lassen sich jetzt sauber togglen (waren als proper Booleans serialisiert — vorher Bug evtl. durch fehlende Font- Liste maskiert) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+159
-21
@@ -27,25 +27,37 @@ if _HERE not in sys.path:
|
||||
sys.path.insert(0, _HERE)
|
||||
|
||||
_SETTINGS_KEY = "dossier_text_settings"
|
||||
_STYLES_KEY = "dossier_text_styles"
|
||||
_STYLE_ACTIVE_KEY = "dossier_text_style_active"
|
||||
|
||||
_ALIGNS = ("left", "center", "right")
|
||||
|
||||
_DEFAULTS = {
|
||||
"font": "Helvetica",
|
||||
"size": 0.20, # in Model-Units (m bei m-Doc, mm bei mm-Doc)
|
||||
"bold": False,
|
||||
"italic": False,
|
||||
"font": "Helvetica",
|
||||
"size": 0.20, # in Model-Units (m bei m-Doc, mm bei mm-Doc)
|
||||
"bold": False,
|
||||
"italic": False,
|
||||
"underline": False,
|
||||
"align": "left",
|
||||
}
|
||||
|
||||
|
||||
def _normalize(s):
|
||||
out = dict(_DEFAULTS)
|
||||
if isinstance(s, dict):
|
||||
for k, v in s.items():
|
||||
if k not in _DEFAULTS: continue
|
||||
if k == "align" and v not in _ALIGNS: continue
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def load_settings(doc):
|
||||
if doc is None: return dict(_DEFAULTS)
|
||||
try:
|
||||
raw = doc.Strings.GetValue(_SETTINGS_KEY)
|
||||
if not raw: return dict(_DEFAULTS)
|
||||
s = json.loads(raw)
|
||||
if not isinstance(s, dict): return dict(_DEFAULTS)
|
||||
out = dict(_DEFAULTS)
|
||||
out.update({k: s[k] for k in s if k in _DEFAULTS})
|
||||
return out
|
||||
return _normalize(json.loads(raw))
|
||||
except Exception:
|
||||
return dict(_DEFAULTS)
|
||||
|
||||
@@ -54,22 +66,120 @@ def save_settings(doc, partial):
|
||||
"""Merged partial-Updates rein und persistiert."""
|
||||
if doc is None or not isinstance(partial, dict): return
|
||||
cur = load_settings(doc)
|
||||
cur.update({k: partial[k] for k in partial if k in _DEFAULTS})
|
||||
for k, v in partial.items():
|
||||
if k not in _DEFAULTS: continue
|
||||
if k == "align" and v not in _ALIGNS: continue
|
||||
cur[k] = v
|
||||
try:
|
||||
doc.Strings.SetString(_SETTINGS_KEY, json.dumps(cur))
|
||||
except Exception as ex:
|
||||
print("[TEXT] save settings:", ex)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Text-Stile (Presets, analog mass_style): doc.Strings JSON-Liste mit
|
||||
# benannten Settings + aktiver Style-ID.
|
||||
|
||||
def list_styles(doc):
|
||||
if doc is None: return []
|
||||
try:
|
||||
raw = doc.Strings.GetValue(_STYLES_KEY)
|
||||
if not raw: return []
|
||||
items = json.loads(raw)
|
||||
if not isinstance(items, list): return []
|
||||
out = []
|
||||
for it in items:
|
||||
if not isinstance(it, dict): continue
|
||||
norm = _normalize(it)
|
||||
norm["id"] = it.get("id") or ("ts_" + uuid.uuid4().hex[:8])
|
||||
norm["name"] = it.get("name") or "Stil"
|
||||
out.append(norm)
|
||||
return out
|
||||
except Exception as ex:
|
||||
print("[TEXT] list_styles:", ex)
|
||||
return []
|
||||
|
||||
|
||||
def get_active_style_id(doc):
|
||||
if doc is None: return None
|
||||
try:
|
||||
return doc.Strings.GetValue(_STYLE_ACTIVE_KEY) or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def set_active_style_id(doc, sid):
|
||||
if doc is None: return
|
||||
try:
|
||||
doc.Strings.SetString(_STYLE_ACTIVE_KEY, sid or "")
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def save_style(doc, name, settings=None):
|
||||
"""Speichert aktuelle Settings als benannten Style. Returns die ID."""
|
||||
if doc is None or not name: return None
|
||||
items = list_styles(doc)
|
||||
# Existing same-name → update
|
||||
sid = None
|
||||
for it in items:
|
||||
if it["name"] == name:
|
||||
sid = it["id"]; break
|
||||
norm = _normalize(settings if settings is not None else load_settings(doc))
|
||||
norm["id"] = sid or ("ts_" + uuid.uuid4().hex[:8])
|
||||
norm["name"] = name
|
||||
if sid:
|
||||
items = [norm if it["id"] == sid else it for it in items]
|
||||
else:
|
||||
items.append(norm)
|
||||
try:
|
||||
doc.Strings.SetString(_STYLES_KEY, json.dumps(items))
|
||||
except Exception as ex:
|
||||
print("[TEXT] save_style:", ex)
|
||||
return norm["id"]
|
||||
|
||||
|
||||
def delete_style(doc, sid):
|
||||
if doc is None or not sid: return
|
||||
items = [it for it in list_styles(doc) if it.get("id") != sid]
|
||||
try:
|
||||
doc.Strings.SetString(_STYLES_KEY, json.dumps(items))
|
||||
except Exception: pass
|
||||
if get_active_style_id(doc) == sid:
|
||||
set_active_style_id(doc, "")
|
||||
|
||||
|
||||
def apply_style(doc, sid):
|
||||
"""Wendet einen Style als aktive Defaults an + (wenn Selektion) auch
|
||||
auf die selektierten TextEntities."""
|
||||
if doc is None or not sid: return
|
||||
items = list_styles(doc)
|
||||
style = next((it for it in items if it["id"] == sid), None)
|
||||
if not style: return
|
||||
set_active_style_id(doc, sid)
|
||||
# Defaults aus Style schreiben (ohne id/name)
|
||||
patch = {k: style[k] for k in style if k in _DEFAULTS}
|
||||
save_settings(doc, patch)
|
||||
apply_settings_to_selection(doc, patch)
|
||||
|
||||
|
||||
_FONT_FALLBACK = [
|
||||
"Helvetica", "Helvetica Neue", "Arial", "Arial Narrow",
|
||||
"Times New Roman", "Times", "Courier", "Courier New",
|
||||
"Menlo", "Monaco", "Verdana", "Geneva", "Lucida Grande",
|
||||
"Avenir", "Avenir Next", "Optima", "Palatino",
|
||||
]
|
||||
|
||||
|
||||
def available_fonts():
|
||||
"""Liefert sortierte Liste verfuegbarer System-Fonts (Face-Names)."""
|
||||
"""Sortierte Liste verfuegbarer System-Fonts. Mit Fallback auf
|
||||
haeufige Mac-Fonts falls die Rhino-API nichts liefert."""
|
||||
try:
|
||||
names = Rhino.DocObjects.Font.AvailableFontFaceNames()
|
||||
out = sorted({str(n) for n in names if n})
|
||||
return out
|
||||
if out: return out
|
||||
except Exception as ex:
|
||||
print("[TEXT] available_fonts:", ex)
|
||||
return ["Helvetica", "Arial", "Times New Roman", "Courier New"]
|
||||
return list(_FONT_FALLBACK)
|
||||
|
||||
|
||||
def _prompt_for_text(default=""):
|
||||
@@ -127,9 +237,25 @@ def _selected_text_objects(doc):
|
||||
return out
|
||||
|
||||
|
||||
def _apply_align(te, align):
|
||||
"""Setzt TextHorizontalAlignment."""
|
||||
try:
|
||||
m = {
|
||||
"left": Rhino.DocObjects.TextHorizontalAlignment.Left,
|
||||
"center": Rhino.DocObjects.TextHorizontalAlignment.Center,
|
||||
"right": Rhino.DocObjects.TextHorizontalAlignment.Right,
|
||||
}
|
||||
if align in m:
|
||||
te.TextHorizontalAlignment = m[align]
|
||||
return True
|
||||
except Exception as ex:
|
||||
print("[TEXT] apply align:", ex)
|
||||
return False
|
||||
|
||||
|
||||
def apply_settings_to_selection(doc, patch):
|
||||
"""Wendet font/size/bold/italic auf alle selektierten TextEntities an.
|
||||
Returns Anzahl der geaenderten Objekte."""
|
||||
"""Wendet font/size/bold/italic/align auf alle selektierten
|
||||
TextEntities an. Returns Anzahl der geaenderten Objekte."""
|
||||
if doc is None or not isinstance(patch, dict): return 0
|
||||
selected = _selected_text_objects(doc)
|
||||
if not selected: return 0
|
||||
@@ -153,6 +279,10 @@ def apply_settings_to_selection(doc, patch):
|
||||
bold = patch["bold"] if "bold" in patch else cur_bold
|
||||
italic = patch["italic"] if "italic" in patch else cur_italic
|
||||
_apply_font(te, face, bool(bold), bool(italic))
|
||||
if "align" in patch and patch["align"] in _ALIGNS:
|
||||
_apply_align(te, patch["align"])
|
||||
# underline: derzeit nicht auf TextEntity-Font-API mappbar.
|
||||
# Wird in den Settings gespeichert, aber visuell (noch) nicht angewendet.
|
||||
doc.Objects.Replace(obj.Id, te)
|
||||
n += 1
|
||||
except Exception as ex:
|
||||
@@ -164,8 +294,7 @@ def apply_settings_to_selection(doc, patch):
|
||||
|
||||
|
||||
def read_selection_settings(doc):
|
||||
"""Wenn TextEntities selektiert: liefert die Settings des ersten als
|
||||
dict (font/size/bold/italic). Sonst None."""
|
||||
"""Wenn TextEntities selektiert: liefert die Settings des ersten."""
|
||||
sel = _selected_text_objects(doc)
|
||||
if not sel: return None
|
||||
try:
|
||||
@@ -174,11 +303,19 @@ def read_selection_settings(doc):
|
||||
face = font.QuartetName if font else "Helvetica"
|
||||
bold = bool(font.Bold) if font else False
|
||||
italic = bool(font.Italic) if font else False
|
||||
align = "left"
|
||||
try:
|
||||
h = te.TextHorizontalAlignment
|
||||
if h == Rhino.DocObjects.TextHorizontalAlignment.Center: align = "center"
|
||||
elif h == Rhino.DocObjects.TextHorizontalAlignment.Right: align = "right"
|
||||
except Exception: pass
|
||||
return {
|
||||
"font": face,
|
||||
"size": float(te.TextHeight),
|
||||
"bold": bold,
|
||||
"italic": italic,
|
||||
"font": face,
|
||||
"size": float(te.TextHeight),
|
||||
"bold": bold,
|
||||
"italic": italic,
|
||||
"underline": False, # derzeit nicht lesbar
|
||||
"align": align,
|
||||
}
|
||||
except Exception as ex:
|
||||
print("[TEXT] read selection:", ex)
|
||||
@@ -208,6 +345,7 @@ def create_text():
|
||||
except Exception: pass
|
||||
_apply_font(te, settings.get("font") or "Helvetica",
|
||||
settings.get("bold"), settings.get("italic"))
|
||||
_apply_align(te, settings.get("align") or "left")
|
||||
doc.Objects.AddText(te)
|
||||
doc.Views.Redraw()
|
||||
except Exception as ex:
|
||||
|
||||
Reference in New Issue
Block a user