React WYSIWYG Text-Editor (Topmost Satellite-WebView) — Phase 1
User-Wunsch: eigener WYSIWYG-Editor im React/Topbar-GUI-Stil. Topmost. Verschiedene Schriftarten/-Dicken sichtbar im Editor selbst. Neues Backend (rhino/text_editor.py): - TextEditorBridge mit Frame-Daten im Konstruktor, INIT-Push mit Settings + Font-Liste, COMMIT erstellt TextEntity, CANCEL schliesst - open_with_frame(p1, p2, origin, width, height): oeffnet Satellite- Window mit mode='text_editor' + topmost=True - panel_base.open_satellite_window: neuer Parameter topmost (default False) der form.Topmost setzt text_create.create_text: ruft jetzt text_editor.open_with_frame nach dem Frame-Pick. Eto-basierter _dossier_text_editor bleibt im Modul als Fallback aber wird nicht mehr verwendet. Neues Frontend (src/TextEditorApp.jsx, mode='text_editor'): - Layout im DOSSIER-Topbar-Stil (dunkle Pills, accent on hover) - Pill-Helper-Komponente fuer alle Toggle/Action-Buttons - Dropdown-Helper fuer Font + Size - Toolbar Row 1: Font-Dropdown | Size-Dropdown | Color-Picker | Layer-Reset - Toolbar Row 2: B/I/U mit Material-Icons | L/C/R Align | x²/x₂ Sup/Sub - Sonderzeichen-Palette: 41 Unicode-Symbole (Architektur/Math/Pfeile/ Auszeichnungen), Klick inserted am Cursor - WYSIWYG-Editor: contentEditable div mit fontFamily=ausgewaehlt, textAlign=ausgewaehlt — Format-Toolbar wirkt via document.execCommand (bold/italic/underline/justifyLeft/Center/Right/superscript/subscript/ fontName/foreColor) - "Einfuegen" sendet COMMIT mit text (innerText) + settings - "Abbrechen" CANCEL → Bridge schliesst Form Phase 1 Limitation: rendert in Rhino als PlainText mit den globalen Settings (font/size/bold/italic/align/color) — verschiedene Schriftarten INNERHALB des Texts sind im Editor sichtbar aber nicht im finalen Rhino-TextEntity. Phase 2: HTML → Rhino RichText/RTF Mapping. rhinoBridge.js: send() jetzt exportiert (war intern) damit TextEditorApp generisch COMMIT/CANCEL senden kann. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -367,7 +367,8 @@ def attach_webview(panel, bridge, mode):
|
|||||||
# --- Satelliten-Fenster (echtes Rhino-Fenster mit eingebetteter WebView) ----
|
# --- Satelliten-Fenster (echtes Rhino-Fenster mit eingebetteter WebView) ----
|
||||||
|
|
||||||
def open_satellite_window(mode, params=None, title=None, size=(420, 560),
|
def open_satellite_window(mode, params=None, title=None, size=(420, 560),
|
||||||
on_save=None, on_cancel=None, bridge=None):
|
on_save=None, on_cancel=None, bridge=None,
|
||||||
|
topmost=False):
|
||||||
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form) mit eingebetteter WebView.
|
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form) mit eingebetteter WebView.
|
||||||
Die WebView laedt die React-App mit dem gegebenen `mode` und `params`.
|
Die WebView laedt die React-App mit dem gegebenen `mode` und `params`.
|
||||||
|
|
||||||
@@ -389,7 +390,7 @@ def open_satellite_window(mode, params=None, title=None, size=(420, 560),
|
|||||||
form.ClientSize = drawing.Size(int(size[0]), int(size[1]))
|
form.ClientSize = drawing.Size(int(size[0]), int(size[1]))
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
form.Resizable = True
|
form.Resizable = True
|
||||||
form.Topmost = False
|
form.Topmost = bool(topmost)
|
||||||
|
|
||||||
wv = forms.WebView()
|
wv = forms.WebView()
|
||||||
|
|
||||||
|
|||||||
+7
-55
@@ -862,69 +862,21 @@ def read_selection_settings(doc):
|
|||||||
|
|
||||||
|
|
||||||
def create_text():
|
def create_text():
|
||||||
"""DOSSIER Custom Text-Workflow:
|
"""DOSSIER Custom Text-Workflow (React WYSIWYG-Editor):
|
||||||
1. Frame ziehen (Live-Rechteck-Vorschau)
|
1. Frame ziehen (Live-Rechteck-Vorschau)
|
||||||
2. _dossier_text_editor (eigener Editor mit Toolbar, Sonderzeichen,
|
2. text_editor.open_with_frame oeffnet das React-WYSIWYG-Fenster
|
||||||
Farbe, Sub/Superscript) oeffnet sich neben dem Frame
|
(Topmost, neben dem Frame). Editor handlet die Eingabe + erstellt
|
||||||
3. TextEntity wird im Frame mit allen gewaehlten Settings erstellt
|
die TextEntity bei COMMIT.
|
||||||
und mit UserString "dossier_text=1" getagged (fuer evtl. spaeteren
|
|
||||||
Double-Click-Hook auf unseren Editor)
|
|
||||||
"""
|
"""
|
||||||
import System
|
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
settings = load_settings(doc)
|
|
||||||
fonts = available_fonts()
|
|
||||||
|
|
||||||
frame = _pick_text_frame()
|
frame = _pick_text_frame()
|
||||||
if frame is None: return
|
if frame is None: return
|
||||||
p1, p2, origin, width, height = frame
|
p1, p2, origin, width, height = frame
|
||||||
|
|
||||||
new_state = _dossier_text_editor(p1, p2, settings, fonts)
|
|
||||||
if not new_state: return
|
|
||||||
text = (new_state.get("text") or "").strip()
|
|
||||||
if not text: return
|
|
||||||
|
|
||||||
# Defaults aus Editor uebernehmen (ohne color)
|
|
||||||
save_settings(doc, {
|
|
||||||
"font": new_state.get("font"),
|
|
||||||
"size": new_state.get("size"),
|
|
||||||
"bold": new_state.get("bold"),
|
|
||||||
"italic": new_state.get("italic"),
|
|
||||||
"underline": new_state.get("underline"),
|
|
||||||
"align": new_state.get("align"),
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
te = rg.TextEntity()
|
import text_editor
|
||||||
te.Plane = rg.Plane(origin, rg.Vector3d.ZAxis)
|
text_editor.open_with_frame(p1, p2, origin, width, height)
|
||||||
te.PlainText = text
|
|
||||||
try: te.TextHeight = float(new_state.get("size") or 0.2)
|
|
||||||
except Exception: pass
|
|
||||||
_apply_font(te, new_state.get("font") or "Helvetica",
|
|
||||||
new_state.get("bold"), new_state.get("italic"),
|
|
||||||
new_state.get("underline"))
|
|
||||||
_apply_align(te, new_state.get("align") or "left")
|
|
||||||
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
|
||||||
try: setattr(te, attr, width); break
|
|
||||||
except Exception: pass
|
|
||||||
try: te.TextIsWrapped = True
|
|
||||||
except Exception:
|
|
||||||
try: te.TextWrap = True
|
|
||||||
except Exception: pass
|
|
||||||
|
|
||||||
# Object-Attribute: Farbe wenn explizit gesetzt + DOSSIER-Tag
|
|
||||||
attrs = Rhino.DocObjects.ObjectAttributes()
|
|
||||||
col = new_state.get("color")
|
|
||||||
if col is not None:
|
|
||||||
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:
|
except Exception as ex:
|
||||||
print("[TEXT] color attr:", ex)
|
print("[TEXT] open editor:", ex)
|
||||||
attrs.SetUserString("dossier_text", "1")
|
|
||||||
doc.Objects.AddText(te, attrs)
|
|
||||||
doc.Views.Redraw()
|
|
||||||
except Exception as ex:
|
|
||||||
print("[TEXT] create:", ex)
|
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#! 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
|
||||||
|
|
||||||
|
|
||||||
|
class TextEditorBridge(panel_base.BaseBridge):
|
||||||
|
def __init__(self, frame_data, settings, fonts):
|
||||||
|
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
|
||||||
|
|
||||||
|
def set_form(self, form):
|
||||||
|
self._form_ref = form
|
||||||
|
|
||||||
|
def _on_ready(self):
|
||||||
|
self.send("INIT", {
|
||||||
|
"settings": self._initial_settings,
|
||||||
|
"fonts": self._fonts,
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
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 {}
|
||||||
|
origin, width, height, _p1, _p2 = self._frame
|
||||||
|
|
||||||
|
try:
|
||||||
|
te = rg.TextEntity()
|
||||||
|
te.Plane = rg.Plane(origin, rg.Vector3d.ZAxis)
|
||||||
|
te.PlainText = text
|
||||||
|
try: te.TextHeight = float(st.get("size") or 0.2)
|
||||||
|
except Exception: pass
|
||||||
|
text_create._apply_font(
|
||||||
|
te,
|
||||||
|
st.get("font") or "Helvetica",
|
||||||
|
st.get("bold"), st.get("italic"),
|
||||||
|
st.get("underline"))
|
||||||
|
text_create._apply_align(te, st.get("align") or "left")
|
||||||
|
for attr in ("FormatWidth", "TextWidth", "MaskWidth"):
|
||||||
|
try:
|
||||||
|
setattr(te, attr, width); break
|
||||||
|
except Exception: pass
|
||||||
|
try: te.TextIsWrapped = True
|
||||||
|
except Exception:
|
||||||
|
try: te.TextWrap = True
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
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")
|
||||||
|
doc.Objects.AddText(te, attrs)
|
||||||
|
doc.Views.Redraw()
|
||||||
|
|
||||||
|
# Defaults speichern (ohne color)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def open_with_frame(p1, p2, origin, width, height):
|
||||||
|
"""Aufgerufen aus text_create.create_text() nach Frame-Pick.
|
||||||
|
Oeffnet das React-WYSIWYG-Editor-Fenster (Topmost) neben dem Frame.
|
||||||
|
Non-blocking — Bridge handlet die Eingabe + erstellt TextEntity bei
|
||||||
|
COMMIT.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import Icon from './components/Icon'
|
||||||
|
import { onMessage, notifyReady, send } from './lib/rhinoBridge'
|
||||||
|
|
||||||
|
const SYMBOLS = [
|
||||||
|
'∅', 'Ø', '⌀', '°', '±', '×', '÷',
|
||||||
|
'²', '³', '½', '¼', '¾', '⅓', '⅔',
|
||||||
|
'≤', '≥', '≠', '≈', '∞', '√', '∆', 'π', 'µ',
|
||||||
|
'←', '→', '↑', '↓', '↔', '↕',
|
||||||
|
'•', '·', '▪', '◆', '★', '☆', '✓', '✗',
|
||||||
|
'§', '¶', '©', '®', '™',
|
||||||
|
]
|
||||||
|
const SIZE_PRESETS = [0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.50, 0.70, 1.00]
|
||||||
|
|
||||||
|
const BAR_H = 22
|
||||||
|
|
||||||
|
function Pill({ children, onClick, active, disabled, title, style }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (disabled || active) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (active) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
height: BAR_H, minWidth: BAR_H,
|
||||||
|
padding: '0 10px',
|
||||||
|
background: active ? 'var(--accent)' : 'var(--bg-input)',
|
||||||
|
color: active ? 'var(--bg-panel)' : 'var(--text-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999,
|
||||||
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
gap: 4, fontSize: 11,
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: disabled ? 0.5 : 1,
|
||||||
|
appearance: 'none', WebkitAppearance: 'none',
|
||||||
|
lineHeight: 1, boxSizing: 'border-box',
|
||||||
|
transition: 'background 0.15s, border-color 0.15s',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>{children}</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dropdown({ value, onChange, options, width, title }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center',
|
||||||
|
height: BAR_H + 2, width, boxSizing: 'border-box',
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999,
|
||||||
|
overflow: 'hidden', flexShrink: 0,
|
||||||
|
}} title={title}>
|
||||||
|
<select value={value || ''}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%', height: '100%',
|
||||||
|
background: 'transparent', color: 'var(--text-primary)',
|
||||||
|
border: 'none', outline: 'none',
|
||||||
|
padding: '0 18px 0 10px', fontSize: 11,
|
||||||
|
appearance: 'none', WebkitAppearance: 'none',
|
||||||
|
backgroundImage: 'var(--select-arrow)',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundPosition: 'right 6px center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}>
|
||||||
|
{options}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TextEditorApp() {
|
||||||
|
const [fonts, setFonts] = useState([])
|
||||||
|
const [font, setFont] = useState('Helvetica')
|
||||||
|
const [size, setSize] = useState(0.20)
|
||||||
|
const [bold, setBold] = useState(false)
|
||||||
|
const [italic, setItalic] = useState(false)
|
||||||
|
const [underline, setUnderline] = useState(false)
|
||||||
|
const [align, setAlign] = useState('left')
|
||||||
|
const [color, setColor] = useState(null) // [r,g,b] oder null
|
||||||
|
const editorRef = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onMessage('INIT', (data) => {
|
||||||
|
setFonts(data.fonts || [])
|
||||||
|
const s = data.settings || {}
|
||||||
|
if (s.font) setFont(s.font)
|
||||||
|
if (s.size != null) setSize(s.size)
|
||||||
|
if (s.bold != null) setBold(!!s.bold)
|
||||||
|
if (s.italic != null) setItalic(!!s.italic)
|
||||||
|
if (s.underline != null) setUnderline(!!s.underline)
|
||||||
|
if (s.align) setAlign(s.align)
|
||||||
|
// Editor-Default-Font setzen
|
||||||
|
setTimeout(() => editorRef.current?.focus(), 100)
|
||||||
|
})
|
||||||
|
notifyReady()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const exec = (cmd, value) => {
|
||||||
|
try {
|
||||||
|
document.execCommand(cmd, false, value)
|
||||||
|
editorRef.current?.focus()
|
||||||
|
} catch (e) { console.error('execCommand', cmd, e) }
|
||||||
|
}
|
||||||
|
const insertText = (s) => {
|
||||||
|
try {
|
||||||
|
document.execCommand('insertText', false, s)
|
||||||
|
editorRef.current?.focus()
|
||||||
|
} catch (e) { console.error('insertText', e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleBold = () => { setBold(b => !b); exec('bold') }
|
||||||
|
const toggleItalic = () => { setItalic(b => !b); exec('italic') }
|
||||||
|
const toggleUnderline = () => { setUnderline(b => !b); exec('underline') }
|
||||||
|
const doAlign = (a) => { setAlign(a)
|
||||||
|
exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') }
|
||||||
|
const doSup = () => exec('superscript')
|
||||||
|
const doSub = () => exec('subscript')
|
||||||
|
const onColorPick = (hex) => {
|
||||||
|
const m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
|
||||||
|
if (m) setColor([parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)])
|
||||||
|
exec('foreColor', hex)
|
||||||
|
}
|
||||||
|
const clearColor = () => { setColor(null); exec('foreColor', '#000000') }
|
||||||
|
|
||||||
|
const onCommit = () => {
|
||||||
|
const text = editorRef.current?.innerText || ''
|
||||||
|
if (!text.trim()) return
|
||||||
|
send('COMMIT', {
|
||||||
|
text,
|
||||||
|
settings: { font, size, bold, italic, underline, align, color },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onCancel = () => send('CANCEL', {})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
padding: 10, height: '100vh',
|
||||||
|
display: 'flex', flexDirection: 'column', gap: 8,
|
||||||
|
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||||
|
background: 'var(--bg-panel)',
|
||||||
|
boxSizing: 'border-box', overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
{/* Toolbar Row 1: Font | Size | Color | Layer-Reset */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Dropdown value={font} onChange={(v) => { setFont(v); exec('fontName', v) }}
|
||||||
|
width={150} title="Schrift"
|
||||||
|
options={
|
||||||
|
fonts.length === 0
|
||||||
|
? <option value="">— Fonts laden —</option>
|
||||||
|
: fonts.map(f => <option key={f} value={f}>{f}</option>)
|
||||||
|
} />
|
||||||
|
<Dropdown value={String(size)}
|
||||||
|
onChange={(v) => setSize(parseFloat(v))}
|
||||||
|
width={90} title="Texthöhe (m)"
|
||||||
|
options={SIZE_PRESETS.map(s => (
|
||||||
|
<option key={s} value={String(s)}>{s.toFixed(2)} m</option>
|
||||||
|
))}
|
||||||
|
/>
|
||||||
|
<label style={{
|
||||||
|
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||||
|
height: BAR_H + 2, padding: '0 8px',
|
||||||
|
background: 'var(--bg-input)', border: '1px solid var(--border)',
|
||||||
|
borderRadius: 999, cursor: 'pointer', flexShrink: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}} title="Textfarbe">
|
||||||
|
<input type="color"
|
||||||
|
style={{ width: 16, height: 16, border: 'none', padding: 0, background: 'transparent' }}
|
||||||
|
onChange={(e) => onColorPick(e.target.value)} />
|
||||||
|
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Farbe</span>
|
||||||
|
</label>
|
||||||
|
<Pill onClick={clearColor} title="Layer-Farbe verwenden">Layer</Pill>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toolbar Row 2: B I U + Align + Sup/Sub */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<Pill active={bold} onClick={toggleBold} title="Fett">
|
||||||
|
<Icon name="format_bold" size={13} />
|
||||||
|
</Pill>
|
||||||
|
<Pill active={italic} onClick={toggleItalic} title="Kursiv">
|
||||||
|
<Icon name="format_italic" size={13} />
|
||||||
|
</Pill>
|
||||||
|
<Pill active={underline} onClick={toggleUnderline} title="Unterstrichen">
|
||||||
|
<Icon name="format_underlined" size={13} />
|
||||||
|
</Pill>
|
||||||
|
<div style={{ width: 6 }} />
|
||||||
|
<Pill active={align === 'left'} onClick={() => doAlign('left')} title="Links">
|
||||||
|
<Icon name="format_align_left" size={13} />
|
||||||
|
</Pill>
|
||||||
|
<Pill active={align === 'center'} onClick={() => doAlign('center')} title="Mittig">
|
||||||
|
<Icon name="format_align_center" size={13} />
|
||||||
|
</Pill>
|
||||||
|
<Pill active={align === 'right'} onClick={() => doAlign('right')} title="Rechts">
|
||||||
|
<Icon name="format_align_right" size={13} />
|
||||||
|
</Pill>
|
||||||
|
<div style={{ width: 6 }} />
|
||||||
|
<Pill onClick={doSup} title="Hochstellen (x²)" style={{ fontFamily: 'serif' }}>x²</Pill>
|
||||||
|
<Pill onClick={doSub} title="Tiefstellen (x₂)" style={{ fontFamily: 'serif' }}>x₂</Pill>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Symbol-Palette */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
<span style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||||
|
textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
||||||
|
Sonderzeichen
|
||||||
|
</span>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||||
|
{SYMBOLS.map(s => (
|
||||||
|
<button key={s} onClick={() => insertText(s)} title={`Einfügen: ${s}`}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: 28, height: 22,
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 13, lineHeight: 1,
|
||||||
|
cursor: 'pointer', padding: 0,
|
||||||
|
transition: 'background 0.12s, border-color 0.12s',
|
||||||
|
}}>{s}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* WYSIWYG Editor */}
|
||||||
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
contentEditable
|
||||||
|
suppressContentEditableWarning
|
||||||
|
spellCheck={false}
|
||||||
|
style={{
|
||||||
|
flex: 1, minHeight: 100,
|
||||||
|
padding: 12,
|
||||||
|
background: 'var(--bg-input)',
|
||||||
|
color: 'var(--text-primary)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 6,
|
||||||
|
fontFamily: font,
|
||||||
|
fontSize: 14, lineHeight: 1.5,
|
||||||
|
outline: 'none',
|
||||||
|
overflowY: 'auto',
|
||||||
|
textAlign: align,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Bottom Buttons */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
|
||||||
|
<Pill onClick={onCancel}>Abbrechen</Pill>
|
||||||
|
<button onClick={onCommit}
|
||||||
|
style={{
|
||||||
|
height: BAR_H + 2, padding: '0 14px',
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: 'var(--bg-panel)',
|
||||||
|
border: '1px solid var(--accent)',
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 11, fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
appearance: 'none', WebkitAppearance: 'none',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}>Einfügen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ function _asciiEscape(s) {
|
|||||||
'\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4))
|
'\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4))
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(type, payload = {}) {
|
export function send(type, payload = {}) {
|
||||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||||
const json = _asciiEscape(JSON.stringify({ type, payload }))
|
const json = _asciiEscape(JSON.stringify({ type, payload }))
|
||||||
if (json.length <= CHUNK_SIZE) {
|
if (json.length <= CHUNK_SIZE) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import OsmApp from './OsmApp.jsx'
|
|||||||
import KameraApp from './KameraApp.jsx'
|
import KameraApp from './KameraApp.jsx'
|
||||||
import MasseSettingsApp from './MasseSettingsApp.jsx'
|
import MasseSettingsApp from './MasseSettingsApp.jsx'
|
||||||
import AboutApp from './AboutApp.jsx'
|
import AboutApp from './AboutApp.jsx'
|
||||||
|
import TextEditorApp from './TextEditorApp.jsx'
|
||||||
import GestaltungApp from './GestaltungApp.jsx'
|
import GestaltungApp from './GestaltungApp.jsx'
|
||||||
import AusschnitteApp from './AusschnitteApp.jsx'
|
import AusschnitteApp from './AusschnitteApp.jsx'
|
||||||
import MassstabApp from './MassstabApp.jsx'
|
import MassstabApp from './MassstabApp.jsx'
|
||||||
@@ -46,6 +47,7 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
|
|||||||
: mode === 'kamera' ? KameraApp
|
: mode === 'kamera' ? KameraApp
|
||||||
: mode === 'masse_settings' ? MasseSettingsApp
|
: mode === 'masse_settings' ? MasseSettingsApp
|
||||||
: mode === 'about' ? AboutApp
|
: mode === 'about' ? AboutApp
|
||||||
|
: mode === 'text_editor' ? TextEditorApp
|
||||||
: App
|
: App
|
||||||
|
|
||||||
window.onerror = function (msg, src, line, col, err) {
|
window.onerror = function (msg, src, line, col, err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user