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:
+34
-8
@@ -934,12 +934,35 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
patch = p.get("settings") or {}
|
patch = p.get("settings") or {}
|
||||||
text_create.save_settings(doc, patch)
|
text_create.save_settings(doc, patch)
|
||||||
# Wenn TextEntities selektiert: gleiche Aenderung direkt
|
|
||||||
# auf die selektierten Texte applizieren.
|
|
||||||
text_create.apply_settings_to_selection(doc, patch)
|
text_create.apply_settings_to_selection(doc, patch)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[OBERLEISTE] text settings:", ex)
|
print("[OBERLEISTE] text settings:", ex)
|
||||||
self._send_state(force=True)
|
self._send_state(force=True)
|
||||||
|
elif t == "APPLY_TEXT_STYLE":
|
||||||
|
try:
|
||||||
|
import text_create
|
||||||
|
text_create.apply_style(
|
||||||
|
Rhino.RhinoDoc.ActiveDoc, p.get("id"))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] apply text style:", ex)
|
||||||
|
self._send_state(force=True)
|
||||||
|
elif t == "SAVE_TEXT_STYLE":
|
||||||
|
try:
|
||||||
|
import text_create
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
sid = text_create.save_style(doc, p.get("name") or "Stil")
|
||||||
|
if sid: text_create.set_active_style_id(doc, sid)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] save text style:", ex)
|
||||||
|
self._send_state(force=True)
|
||||||
|
elif t == "DELETE_TEXT_STYLE":
|
||||||
|
try:
|
||||||
|
import text_create
|
||||||
|
text_create.delete_style(
|
||||||
|
Rhino.RhinoDoc.ActiveDoc, p.get("id"))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] delete text style:", ex)
|
||||||
|
self._send_state(force=True)
|
||||||
|
|
||||||
# --- Masse (Mass-Style) -----------------------------------------
|
# --- Masse (Mass-Style) -----------------------------------------
|
||||||
elif t == "SET_MASSE_ACTIVE":
|
elif t == "SET_MASSE_ACTIVE":
|
||||||
@@ -1187,19 +1210,22 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
except Exception:
|
except Exception:
|
||||||
info["massePresets"] = []
|
info["massePresets"] = []
|
||||||
info["masseActiveId"] = None
|
info["masseActiveId"] = None
|
||||||
# Text-Settings + verfuegbare Fonts (Fonts nur einmal initial).
|
# Text-Settings + verfuegbare Fonts + Styles. Fonts werden bei
|
||||||
# Wenn TextEntity selektiert ist, deren Settings ergaenzen damit
|
# jedem _send_state mitgeschickt damit nach Re-Mount (z.B. Panel-
|
||||||
# die UI die Werte des selektierten Textes spiegelt.
|
# Andocken) die Liste nicht leer ist.
|
||||||
try:
|
try:
|
||||||
import text_create
|
import text_create
|
||||||
info["textSettings"] = text_create.load_settings(doc)
|
info["textSettings"] = text_create.load_settings(doc)
|
||||||
info["textSelectionSettings"] = text_create.read_selection_settings(doc)
|
info["textSelectionSettings"] = text_create.read_selection_settings(doc)
|
||||||
if not getattr(self, "_fonts_sent", False):
|
info["textFonts"] = text_create.available_fonts()
|
||||||
info["textFonts"] = text_create.available_fonts()
|
info["textStyles"] = text_create.list_styles(doc)
|
||||||
self._fonts_sent = True
|
info["textStyleActiveId"] = text_create.get_active_style_id(doc)
|
||||||
except Exception:
|
except Exception:
|
||||||
info["textSettings"] = {}
|
info["textSettings"] = {}
|
||||||
info["textSelectionSettings"] = None
|
info["textSelectionSettings"] = None
|
||||||
|
info["textFonts"] = []
|
||||||
|
info["textStyles"] = []
|
||||||
|
info["textStyleActiveId"] = None
|
||||||
# Norden-Rotation fuer N/O/S/W-Buttons
|
# Norden-Rotation fuer N/O/S/W-Buttons
|
||||||
try:
|
try:
|
||||||
import kamera
|
import kamera
|
||||||
|
|||||||
+159
-21
@@ -27,25 +27,37 @@ if _HERE not in sys.path:
|
|||||||
sys.path.insert(0, _HERE)
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
_SETTINGS_KEY = "dossier_text_settings"
|
_SETTINGS_KEY = "dossier_text_settings"
|
||||||
|
_STYLES_KEY = "dossier_text_styles"
|
||||||
|
_STYLE_ACTIVE_KEY = "dossier_text_style_active"
|
||||||
|
|
||||||
|
_ALIGNS = ("left", "center", "right")
|
||||||
|
|
||||||
_DEFAULTS = {
|
_DEFAULTS = {
|
||||||
"font": "Helvetica",
|
"font": "Helvetica",
|
||||||
"size": 0.20, # in Model-Units (m bei m-Doc, mm bei mm-Doc)
|
"size": 0.20, # in Model-Units (m bei m-Doc, mm bei mm-Doc)
|
||||||
"bold": False,
|
"bold": False,
|
||||||
"italic": 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):
|
def load_settings(doc):
|
||||||
if doc is None: return dict(_DEFAULTS)
|
if doc is None: return dict(_DEFAULTS)
|
||||||
try:
|
try:
|
||||||
raw = doc.Strings.GetValue(_SETTINGS_KEY)
|
raw = doc.Strings.GetValue(_SETTINGS_KEY)
|
||||||
if not raw: return dict(_DEFAULTS)
|
if not raw: return dict(_DEFAULTS)
|
||||||
s = json.loads(raw)
|
return _normalize(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
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return dict(_DEFAULTS)
|
return dict(_DEFAULTS)
|
||||||
|
|
||||||
@@ -54,22 +66,120 @@ def save_settings(doc, partial):
|
|||||||
"""Merged partial-Updates rein und persistiert."""
|
"""Merged partial-Updates rein und persistiert."""
|
||||||
if doc is None or not isinstance(partial, dict): return
|
if doc is None or not isinstance(partial, dict): return
|
||||||
cur = load_settings(doc)
|
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:
|
try:
|
||||||
doc.Strings.SetString(_SETTINGS_KEY, json.dumps(cur))
|
doc.Strings.SetString(_SETTINGS_KEY, json.dumps(cur))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT] save settings:", 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():
|
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:
|
try:
|
||||||
names = Rhino.DocObjects.Font.AvailableFontFaceNames()
|
names = Rhino.DocObjects.Font.AvailableFontFaceNames()
|
||||||
out = sorted({str(n) for n in names if n})
|
out = sorted({str(n) for n in names if n})
|
||||||
return out
|
if out: return out
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT] available_fonts:", ex)
|
print("[TEXT] available_fonts:", ex)
|
||||||
return ["Helvetica", "Arial", "Times New Roman", "Courier New"]
|
return list(_FONT_FALLBACK)
|
||||||
|
|
||||||
|
|
||||||
def _prompt_for_text(default=""):
|
def _prompt_for_text(default=""):
|
||||||
@@ -127,9 +237,25 @@ def _selected_text_objects(doc):
|
|||||||
return out
|
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):
|
def apply_settings_to_selection(doc, patch):
|
||||||
"""Wendet font/size/bold/italic auf alle selektierten TextEntities an.
|
"""Wendet font/size/bold/italic/align auf alle selektierten
|
||||||
Returns Anzahl der geaenderten Objekte."""
|
TextEntities an. Returns Anzahl der geaenderten Objekte."""
|
||||||
if doc is None or not isinstance(patch, dict): return 0
|
if doc is None or not isinstance(patch, dict): return 0
|
||||||
selected = _selected_text_objects(doc)
|
selected = _selected_text_objects(doc)
|
||||||
if not selected: return 0
|
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
|
bold = patch["bold"] if "bold" in patch else cur_bold
|
||||||
italic = patch["italic"] if "italic" in patch else cur_italic
|
italic = patch["italic"] if "italic" in patch else cur_italic
|
||||||
_apply_font(te, face, bool(bold), bool(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)
|
doc.Objects.Replace(obj.Id, te)
|
||||||
n += 1
|
n += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -164,8 +294,7 @@ def apply_settings_to_selection(doc, patch):
|
|||||||
|
|
||||||
|
|
||||||
def read_selection_settings(doc):
|
def read_selection_settings(doc):
|
||||||
"""Wenn TextEntities selektiert: liefert die Settings des ersten als
|
"""Wenn TextEntities selektiert: liefert die Settings des ersten."""
|
||||||
dict (font/size/bold/italic). Sonst None."""
|
|
||||||
sel = _selected_text_objects(doc)
|
sel = _selected_text_objects(doc)
|
||||||
if not sel: return None
|
if not sel: return None
|
||||||
try:
|
try:
|
||||||
@@ -174,11 +303,19 @@ def read_selection_settings(doc):
|
|||||||
face = font.QuartetName if font else "Helvetica"
|
face = font.QuartetName if font else "Helvetica"
|
||||||
bold = bool(font.Bold) if font else False
|
bold = bool(font.Bold) if font else False
|
||||||
italic = bool(font.Italic) 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 {
|
return {
|
||||||
"font": face,
|
"font": face,
|
||||||
"size": float(te.TextHeight),
|
"size": float(te.TextHeight),
|
||||||
"bold": bold,
|
"bold": bold,
|
||||||
"italic": italic,
|
"italic": italic,
|
||||||
|
"underline": False, # derzeit nicht lesbar
|
||||||
|
"align": align,
|
||||||
}
|
}
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT] read selection:", ex)
|
print("[TEXT] read selection:", ex)
|
||||||
@@ -208,6 +345,7 @@ def create_text():
|
|||||||
except Exception: pass
|
except Exception: pass
|
||||||
_apply_font(te, settings.get("font") or "Helvetica",
|
_apply_font(te, settings.get("font") or "Helvetica",
|
||||||
settings.get("bold"), settings.get("italic"))
|
settings.get("bold"), settings.get("italic"))
|
||||||
|
_apply_align(te, settings.get("align") or "left")
|
||||||
doc.Objects.AddText(te)
|
doc.Objects.AddText(te)
|
||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
|||||||
+202
-119
@@ -12,6 +12,7 @@ import {
|
|||||||
openDossierSettings, openKameraPanel,
|
openDossierSettings, openKameraPanel,
|
||||||
setMasseActive, openMasseSettings,
|
setMasseActive, openMasseSettings,
|
||||||
openAbout, createText, setTextSettings,
|
openAbout, createText, setTextSettings,
|
||||||
|
applyTextStyle, saveTextStyle, deleteTextStyle,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const PRESETS = [
|
const PRESETS = [
|
||||||
@@ -271,9 +272,12 @@ export default function OberleisteApp() {
|
|||||||
overridesActivePreset: null, overridesPresets: [],
|
overridesActivePreset: null, overridesPresets: [],
|
||||||
layerCombinations: [], layerCombinationActive: null,
|
layerCombinations: [], layerCombinationActive: null,
|
||||||
massePresets: [], masseActiveId: null,
|
massePresets: [], masseActiveId: null,
|
||||||
textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false },
|
textSettings: { font: 'Helvetica', size: 0.20, bold: false, italic: false,
|
||||||
|
underline: false, align: 'left' },
|
||||||
textSelectionSettings: null,
|
textSelectionSettings: null,
|
||||||
textFonts: [],
|
textFonts: [],
|
||||||
|
textStyles: [],
|
||||||
|
textStyleActiveId: null,
|
||||||
northAngle: 0,
|
northAngle: 0,
|
||||||
lastSetView: null,
|
lastSetView: null,
|
||||||
})
|
})
|
||||||
@@ -282,6 +286,7 @@ export default function OberleisteApp() {
|
|||||||
const [draft, setDraft] = useState('')
|
const [draft, setDraft] = useState('')
|
||||||
const [customMode, setCustomMode] = useState(false) // Dropdown -> Custom-Input switch
|
const [customMode, setCustomMode] = useState(false) // Dropdown -> Custom-Input switch
|
||||||
const customInputRef = useRef(null)
|
const customInputRef = useRef(null)
|
||||||
|
const [textSizeCustom, setTextSizeCustom] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onMessage('STATE', (s) => {
|
onMessage('STATE', (s) => {
|
||||||
@@ -781,151 +786,229 @@ export default function OberleisteApp() {
|
|||||||
<div style={sep} />
|
<div style={sep} />
|
||||||
|
|
||||||
{/* ====== TEXT-Block (Vectorworks-Stil) ======
|
{/* ====== TEXT-Block (Vectorworks-Stil) ======
|
||||||
Reihe 1: Font-Dropdown | Size + "m"
|
Reihe 1: Style ▼ | Font ▼ | Size ▼
|
||||||
Reihe 2: B/I/+ kompakte Segmented-Pill
|
Reihe 2: [B][I][U] | [L][C][R] | [+]
|
||||||
Wenn TextEntity selektiert: Werte spiegeln Selektion, Aenderungen
|
Wenn TextEntity selektiert: Werte spiegeln Selektion, Aenderungen
|
||||||
applizieren AUF die Selektion + speichern als Default.
|
gehen direkt rauf und werden zusaetzlich als Default gespeichert.
|
||||||
*/}
|
*/}
|
||||||
{(() => {
|
{(() => {
|
||||||
const sel = state.textSelectionSettings // null wenn nichts selektiert
|
const sel = state.textSelectionSettings
|
||||||
const ts = sel || state.textSettings || {}
|
const ts = sel || state.textSettings || {}
|
||||||
const fonts = state.textFonts || []
|
const fonts = state.textFonts || []
|
||||||
|
const styles = state.textStyles || []
|
||||||
|
const activeStyleId = state.textStyleActiveId
|
||||||
const updateTs = (patch) => setTextSettings({ ...ts, ...patch })
|
const updateTs = (patch) => setTextSettings({ ...ts, ...patch })
|
||||||
const FONT_W = 130
|
const STYLE_W = 110
|
||||||
const SIZE_W = 60
|
const FONT_W = 130
|
||||||
const SEG_W = FONT_W + 6 + SIZE_W // B/I/+ Pill spannt ueber beide Spalten in Row 2
|
const SIZE_W = 80
|
||||||
const ACTIVE_BORDER = sel ? 'var(--accent)' : 'var(--border)'
|
const ACTIVE_BORDER = sel ? 'var(--accent)' : 'var(--border)'
|
||||||
|
const SIZE_PRESETS = [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.50, 0.70, 1.00]
|
||||||
|
|
||||||
|
// Toggle-Button-Helper fuer B/I/U/Align
|
||||||
|
const ToggleBtn = ({ active, onClick, title, children, borderLeft, lastInRow }) => (
|
||||||
|
<button onClick={onClick} title={title}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (active) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.color = 'var(--accent-light)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (active) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.color = 'var(--text-primary)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: '100%',
|
||||||
|
background: active ? 'var(--accent)' : 'var(--bg-input)',
|
||||||
|
color: active ? 'var(--bg-panel)' : 'var(--text-primary)',
|
||||||
|
border: 'none',
|
||||||
|
borderLeft: borderLeft ? '1px solid var(--border)' : 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
appearance: 'none', WebkitAppearance: 'none',
|
||||||
|
lineHeight: 1, padding: 0,
|
||||||
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
transition: 'background 0.15s, color 0.15s',
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid', gridTemplateColumns: `${FONT_W}px ${SIZE_W}px`,
|
display: 'grid',
|
||||||
|
gridTemplateColumns: `${STYLE_W}px ${FONT_W}px ${SIZE_W}px`,
|
||||||
gap: '4px 6px', alignItems: 'center', flexShrink: 0,
|
gap: '4px 6px', alignItems: 'center', flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
{/* Reihe 1, Spalte 1: Font-Dropdown */}
|
{/* === Reihe 1 === */}
|
||||||
|
{/* Style-Dropdown */}
|
||||||
|
<BarCombo
|
||||||
|
value={activeStyleId || ''}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v === '__save__') {
|
||||||
|
const n = (window.prompt('Name für neuen Text-Stil:', 'Stil') || '').trim()
|
||||||
|
if (n) saveTextStyle(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (v === '__delete__') {
|
||||||
|
if (activeStyleId &&
|
||||||
|
window.confirm(`Stil "${styles.find(s => s.id === activeStyleId)?.name}" löschen?`))
|
||||||
|
deleteTextStyle(activeStyleId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (v) applyTextStyle(v)
|
||||||
|
}}
|
||||||
|
width={STYLE_W}
|
||||||
|
title="Text-Stil — gespeicherte Settings-Presets"
|
||||||
|
>
|
||||||
|
<option value="">— Stil —</option>
|
||||||
|
{styles.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
<option value="__save__">+ Speichern…</option>
|
||||||
|
{activeStyleId && <option value="__delete__">🗑 Aktiven löschen</option>}
|
||||||
|
</BarCombo>
|
||||||
|
{/* Font-Dropdown */}
|
||||||
<BarCombo
|
<BarCombo
|
||||||
icon="text_fields"
|
|
||||||
value={ts.font || ''}
|
value={ts.font || ''}
|
||||||
onChange={(v) => updateTs({ font: v })}
|
onChange={(v) => updateTs({ font: v })}
|
||||||
width={FONT_W}
|
width={FONT_W}
|
||||||
title={sel ? 'Schriftart (auf Selektion appliziert)' : 'Schriftart'}
|
title={sel ? 'Schriftart (auf Selektion appliziert)' : 'Schriftart'}
|
||||||
>
|
>
|
||||||
{fonts.length === 0 && <option value="">—</option>}
|
{fonts.length === 0 && <option value="">— Fonts laden —</option>}
|
||||||
{fonts.map(f => <option key={f} value={f}>{f}</option>)}
|
{fonts.map(f => <option key={f} value={f}>{f}</option>)}
|
||||||
</BarCombo>
|
</BarCombo>
|
||||||
{/* Reihe 1, Spalte 2: Size */}
|
{/* Size-Dropdown mit "Eigene" / Custom-Input */}
|
||||||
<div style={{
|
{textSizeCustom ? (
|
||||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
<div style={{
|
||||||
height: BAR_H + 2, padding: '0 8px', boxSizing: 'border-box',
|
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||||
background: 'var(--bg-input)',
|
height: BAR_H + 2, padding: '0 8px', boxSizing: 'border-box',
|
||||||
border: '1px solid ' + ACTIVE_BORDER,
|
background: 'var(--bg-input)',
|
||||||
borderRadius: 999,
|
border: '1px solid ' + ACTIVE_BORDER,
|
||||||
flexShrink: 0, width: SIZE_W,
|
borderRadius: 999,
|
||||||
transition: 'border-color 0.15s',
|
flexShrink: 0, width: SIZE_W,
|
||||||
}}>
|
}}>
|
||||||
<input
|
<input
|
||||||
type="number" step="0.01" min="0.01"
|
type="number" step="0.01" min="0.01" autoFocus
|
||||||
value={ts.size != null ? ts.size : 0.2}
|
value={ts.size != null ? ts.size : 0.2}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const v = parseFloat(e.target.value)
|
const v = parseFloat(e.target.value)
|
||||||
if (!isNaN(v) && v > 0) updateTs({ size: v })
|
if (!isNaN(v) && v > 0) updateTs({ size: v })
|
||||||
}}
|
}}
|
||||||
style={{
|
onBlur={() => setTextSizeCustom(false)}
|
||||||
flex: 1, minWidth: 0,
|
onKeyDown={(e) => {
|
||||||
background: 'transparent', border: 'none', outline: 'none',
|
if (e.key === 'Enter' || e.key === 'Escape') {
|
||||||
color: 'var(--text-primary)',
|
e.target.blur()
|
||||||
fontSize: 11, fontFamily: 'DM Mono, monospace',
|
}
|
||||||
padding: 0, textAlign: 'right', appearance: 'auto',
|
}}
|
||||||
|
style={{
|
||||||
|
flex: 1, minWidth: 0,
|
||||||
|
background: 'transparent', border: 'none', outline: 'none',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
fontSize: 11, fontFamily: 'DM Mono, monospace',
|
||||||
|
padding: 0, textAlign: 'right', appearance: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}>m</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<BarCombo
|
||||||
|
value={String(ts.size != null ? ts.size : 0.2)}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v === '__custom__') { setTextSizeCustom(true); return }
|
||||||
|
const n = parseFloat(v)
|
||||||
|
if (!isNaN(n) && n > 0) updateTs({ size: n })
|
||||||
}}
|
}}
|
||||||
|
width={SIZE_W}
|
||||||
title={sel ? 'Texthoehe (auf Selektion appliziert)' : 'Texthoehe (m)'}
|
title={sel ? 'Texthoehe (auf Selektion appliziert)' : 'Texthoehe (m)'}
|
||||||
/>
|
>
|
||||||
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}>m</span>
|
{SIZE_PRESETS.map(s => (
|
||||||
</div>
|
<option key={s} value={String(s)}>{s.toFixed(2)} m</option>
|
||||||
{/* Reihe 2: B / I / + in einem Segmented-Pill */}
|
))}
|
||||||
|
{ts.size != null && !SIZE_PRESETS.some(s => Math.abs(s - ts.size) < 0.001) && (
|
||||||
|
<option value={String(ts.size)}>{ts.size.toFixed(2)} m</option>
|
||||||
|
)}
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
<option value="__custom__">Eigene…</option>
|
||||||
|
</BarCombo>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* === Reihe 2 === */}
|
||||||
|
{/* B/I/U Segmented */}
|
||||||
<div style={{
|
<div style={{
|
||||||
gridColumn: '1 / span 2',
|
display: 'inline-flex',
|
||||||
display: 'inline-flex', justifySelf: 'start',
|
border: '1px solid ' + ACTIVE_BORDER, borderRadius: 999,
|
||||||
border: '1px solid ' + ACTIVE_BORDER,
|
overflow: 'hidden', flexShrink: 0, width: STYLE_W,
|
||||||
borderRadius: 999, overflow: 'hidden', flexShrink: 0,
|
height: BAR_H + 2, boxSizing: 'border-box',
|
||||||
width: SEG_W, height: BAR_H + 2, boxSizing: 'border-box',
|
|
||||||
transition: 'border-color 0.15s',
|
transition: 'border-color 0.15s',
|
||||||
}}>
|
}}>
|
||||||
{/* B */}
|
<ToggleBtn active={!!ts.bold}
|
||||||
<button
|
|
||||||
onClick={() => updateTs({ bold: !ts.bold })}
|
onClick={() => updateTs({ bold: !ts.bold })}
|
||||||
onMouseEnter={(e) => {
|
title={(sel ? 'Fett auf Selektion' : 'Fett') + ' (Default)'}>
|
||||||
if (ts.bold) return
|
<Icon name="format_bold" size={13} />
|
||||||
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
</ToggleBtn>
|
||||||
e.currentTarget.style.color = 'var(--accent-light)'
|
<ToggleBtn active={!!ts.italic} borderLeft
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
if (ts.bold) return
|
|
||||||
e.currentTarget.style.background = 'var(--bg-input)'
|
|
||||||
e.currentTarget.style.color = 'var(--text-primary)'
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
flex: 1, height: '100%',
|
|
||||||
background: ts.bold ? 'var(--accent)' : 'var(--bg-input)',
|
|
||||||
color: ts.bold ? 'var(--bg-panel)' : 'var(--text-primary)',
|
|
||||||
border: 'none', cursor: 'pointer',
|
|
||||||
fontWeight: 700, fontSize: 11,
|
|
||||||
appearance: 'none', WebkitAppearance: 'none',
|
|
||||||
lineHeight: 1, padding: 0,
|
|
||||||
transition: 'background 0.15s, color 0.15s',
|
|
||||||
}}
|
|
||||||
title={(sel ? 'Fett auf Selektion' : 'Fett') + ' (Default)'}
|
|
||||||
>B</button>
|
|
||||||
{/* I */}
|
|
||||||
<button
|
|
||||||
onClick={() => updateTs({ italic: !ts.italic })}
|
onClick={() => updateTs({ italic: !ts.italic })}
|
||||||
onMouseEnter={(e) => {
|
title={(sel ? 'Kursiv auf Selektion' : 'Kursiv') + ' (Default)'}>
|
||||||
if (ts.italic) return
|
<Icon name="format_italic" size={13} />
|
||||||
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
</ToggleBtn>
|
||||||
e.currentTarget.style.color = 'var(--accent-light)'
|
<ToggleBtn active={!!ts.underline} borderLeft
|
||||||
}}
|
onClick={() => updateTs({ underline: !ts.underline })}
|
||||||
onMouseLeave={(e) => {
|
title="Unterstrichen (Settings — Rendering kommt mit RichText-Support)">
|
||||||
if (ts.italic) return
|
<Icon name="format_underlined" size={13} />
|
||||||
e.currentTarget.style.background = 'var(--bg-input)'
|
</ToggleBtn>
|
||||||
e.currentTarget.style.color = 'var(--text-primary)'
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
flex: 1, height: '100%',
|
|
||||||
background: ts.italic ? 'var(--accent)' : 'var(--bg-input)',
|
|
||||||
color: ts.italic ? 'var(--bg-panel)' : 'var(--text-primary)',
|
|
||||||
border: 'none', borderLeft: '1px solid var(--border)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontStyle: 'italic', fontSize: 11,
|
|
||||||
appearance: 'none', WebkitAppearance: 'none',
|
|
||||||
lineHeight: 1, padding: 0,
|
|
||||||
transition: 'background 0.15s, color 0.15s',
|
|
||||||
}}
|
|
||||||
title={(sel ? 'Kursiv auf Selektion' : 'Kursiv') + ' (Default)'}
|
|
||||||
>I</button>
|
|
||||||
{/* + Neuen Text einfuegen */}
|
|
||||||
<button
|
|
||||||
onClick={() => createText()}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
|
||||||
e.currentTarget.style.color = 'var(--accent-light)'
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.background = 'var(--bg-input)'
|
|
||||||
e.currentTarget.style.color = 'var(--text-primary)'
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
flex: 1, height: '100%',
|
|
||||||
background: 'var(--bg-input)',
|
|
||||||
color: 'var(--text-primary)',
|
|
||||||
border: 'none', borderLeft: '1px solid var(--border)',
|
|
||||||
cursor: 'pointer',
|
|
||||||
appearance: 'none', WebkitAppearance: 'none',
|
|
||||||
lineHeight: 1, padding: 0,
|
|
||||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
transition: 'background 0.15s, color 0.15s',
|
|
||||||
}}
|
|
||||||
title="Neuen Text einfuegen — Position picken, Text eingeben"
|
|
||||||
>
|
|
||||||
<Icon name="add" size={13} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* L/C/R Align Segmented */}
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
border: '1px solid ' + ACTIVE_BORDER, borderRadius: 999,
|
||||||
|
overflow: 'hidden', flexShrink: 0, width: FONT_W,
|
||||||
|
height: BAR_H + 2, boxSizing: 'border-box',
|
||||||
|
transition: 'border-color 0.15s',
|
||||||
|
}}>
|
||||||
|
<ToggleBtn active={(ts.align || 'left') === 'left'}
|
||||||
|
onClick={() => updateTs({ align: 'left' })}
|
||||||
|
title={(sel ? 'Linksbuendig auf Selektion' : 'Linksbuendig')}>
|
||||||
|
<Icon name="format_align_left" size={13} />
|
||||||
|
</ToggleBtn>
|
||||||
|
<ToggleBtn active={ts.align === 'center'} borderLeft
|
||||||
|
onClick={() => updateTs({ align: 'center' })}
|
||||||
|
title={(sel ? 'Zentriert auf Selektion' : 'Zentriert')}>
|
||||||
|
<Icon name="format_align_center" size={13} />
|
||||||
|
</ToggleBtn>
|
||||||
|
<ToggleBtn active={ts.align === 'right'} borderLeft
|
||||||
|
onClick={() => updateTs({ align: 'right' })}
|
||||||
|
title={(sel ? 'Rechtsbuendig auf Selektion' : 'Rechtsbuendig')}>
|
||||||
|
<Icon name="format_align_right" size={13} />
|
||||||
|
</ToggleBtn>
|
||||||
|
</div>
|
||||||
|
{/* + Neuen Text einfuegen */}
|
||||||
|
<button
|
||||||
|
onClick={() => createText()}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||||
|
e.currentTarget.style.color = 'var(--accent-light)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
|
e.currentTarget.style.color = 'var(--text-primary)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: SIZE_W, height: BAR_H + 2, boxSizing: 'border-box',
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999,
|
||||||
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
appearance: 'none', WebkitAppearance: 'none',
|
||||||
|
lineHeight: 1, padding: 0, flexShrink: 0,
|
||||||
|
transition: 'background 0.15s, color 0.15s, border-color 0.15s',
|
||||||
|
}}
|
||||||
|
title="Neuen Text einfuegen — Position picken, Text eingeben"
|
||||||
|
>
|
||||||
|
<Icon name="add" size={14} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
|
|||||||
export function openAbout() { send('OPEN_ABOUT', {}) }
|
export function openAbout() { send('OPEN_ABOUT', {}) }
|
||||||
export function createText() { send('CREATE_TEXT', {}) }
|
export function createText() { send('CREATE_TEXT', {}) }
|
||||||
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
|
export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) }
|
||||||
|
export function applyTextStyle(id) { send('APPLY_TEXT_STYLE', { id }) }
|
||||||
|
export function saveTextStyle(name) { send('SAVE_TEXT_STYLE', { name }) }
|
||||||
|
export function deleteTextStyle(id) { send('DELETE_TEXT_STYLE', { id }) }
|
||||||
|
|
||||||
// --- Masse (in Oberleiste + Satellite-Fenster MasseSettings) ---
|
// --- Masse (in Oberleiste + Satellite-Fenster MasseSettings) ---
|
||||||
// Topbar: aktives Mass setzen + Settings-Fenster oeffnen
|
// Topbar: aktives Mass setzen + Settings-Fenster oeffnen
|
||||||
|
|||||||
Reference in New Issue
Block a user