Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung (konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text- Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback, falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform) fuer fluessige Interaktion ohne React-Re-Render je Frame. Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM (Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext- Import, Tauri-Compute-Boundary-PoC).
7.9 KiB
Oberleiste – Angleichung an DOSSIER (Umsetzungs-Spezifikation)
Verbindliche Vorlage für den Umbau der Top-Bar (src/ui/TopBar.tsx,
src/styles.css, Verdrahtung in src/App.tsx). Quelle: das DOSSIER-Rhino-
Plugin (ToolbarApp.jsx, components/BarControls.jsx, TextEditorApp.jsx).
Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
0. Grundprimitive (neu, DOSSIER-konform)
Alle Leisten-Controls teilen dieselbe Höhe und Pillenform.
BAR_H = 22pxBasis-Höhe. Segmentierte PillenBAR_H + 2 = 24px(box-sizing:border-box, 1px Rand inbegriffen).- Pille:
border:1px solid var(--border),border-radius:999px,background:var(--input). Hover (interaktiv):border-color:var(--accent-border),background:var(--accent-dim). - Aktiver Zustand (Toggle AN / aktive Segmentzelle):
background:var(--accent),color:#fff.
BarCombo (Pillen-Dropdown)
Wir haben bereits src/ui/Dropdown.tsx. Der Dropdown-Trigger MUSS optisch der
Pille entsprechen (Höhe 24, radius 999, obige Farben). Ein optionales Icon sitzt
LINKS ausserhalb der Pille (18px breit, var(--muted)), ein optionaler
Zahnrad-Knopf („settings") sitzt rechts innerhalb der Pille. Prüfen, ob
Dropdown bereits so aussieht; falls nicht → Trigger-CSS angleichen (Klasse
tb-dd-trigger). KEINE zweite Dropdown-Implementierung bauen.
Segmentpille (3er/4er)
Aussencontainer display:inline-flex; height:24px; border:1px solid var(--border); border-radius:999px; overflow:hidden. Zellen ohne eigenen Radius; interne Trenner
über border-left:1px solid var(--border) (erste Zelle ohne). Aktive Zelle
var(--accent)/#fff, inaktiv var(--input)/var(--ink), Hover
var(--accent-dim)/var(--accent). Genutzt für: Ansichts-Icons, Zoom (%/fit/center),
B/I/U, L/C/R.
BarButton (quadratischer Icon-Knopf)
22×22, border-radius:999px, sonst wie Pille. Aktiv = Akzentfüllung, Icon #fff.
1. Reihenfolge der Gruppen (links → rechts)
- Marke (bestehend, unverändert).
- Ansichts-Gruppe (bestehend
view-grid; Zellen auf Segmentpillen-Look bringen). - Sichtbarkeits-Kombinationen (bestehend,
BarCombo-Look). - Detailgrad + Massstab (gestapelt,
BarCombo-Look). - Massstab/Zoom-Cluster NEU (siehe §2) — ersetzt die heutige Gruppe mit der DOPPELTEN Zoom-Anzeige.
- Darstellungsart (bestehend,
BarCombo). - Text-Gruppe NEU (siehe §3) — die zentrale neue Leiste.
- Referenzlinien / Linien-Modus (bestehend).
- Rechts: Layout · Ressourcen · Projektname.
2. Massstab/Zoom-Cluster (ersetzt Doppel-Zoom-Bug)
HEUTE FALSCH: In TopBar.tsx wird tb-zoom (Zoom %) ZWEIMAL gerendert
(einmal im tb-zoomstack, einmal darunter als eigener <span>). Die zweite,
lose <span className="tb-zoom">…%</span> ersatzlos ENTFERNEN.
NEUES Layout — 2×2-Raster (display:grid; grid-template-columns:auto auto; gap:4px 6px; align-items:center):
- Spalte 1, beide Zeilen (
grid-row:1 / span 2): EINE kombinierte Stat-Pille,width:70px, HöheBAR_H*2+6 = 50px,border-radius:14px(NICHT 999),border:1px solid var(--border),background:var(--input), Innen zwei Zeilen mittig, getrennt durch 1px-Linie (var(--border)):- oben: Live-Massstab
1:N(Akzentfarbe,var(--font-mono), 11px, 700) - unten: Zoom
NN%(var(--ink-2), mono, 11px) - „am Massstab" (Zoom==gewählter Massstab): Pille
background:var(--accent-dim),border-color:var(--accent), Textvar(--accent). - Nicht-Plan-Ansicht: beide Werte „—".
- oben: Live-Massstab
- Spalte 2, Zeile 1: Massstab-Dropdown (
BarCombo, ~140px, mono) + Print/PDF bleibt separat. (Massstab-Dropdown ist der bestehendescaleOptions-Dropdown.) - Spalte 2, Zeile 2: Zoom-Segmentpille mit 3 Zellen —
%(=onZoom100, Label „1:1"/100 %),fit_screen(=onFit),center_focus_strong(=onFitSelection). Material-Icons. Daneben ggf. Referenzlinien-BarButton.
Export-Knöpfe (PDF/DXF) wandern in eine eigene kleine BarButton-Reihe rechts vom
Cluster (Icons picture_as_pdf / download) ODER bleiben Pillen — Hauptsache
NICHT mehr Teil des Zoom-Blocks, damit der Cluster ruhig bleibt.
3. Text-Gruppe in der Oberleiste (NEU – Kern dieser Aufgabe)
Immer sichtbar. 3×2-Raster (grid-template-columns:110px 130px 80px; gap:4px 6px).
Setzt Defaults für neuen Text UND formatiert die aktuelle Auswahl live.
Zeile 1:
- Stil-Preset
BarCombo(110px): Optionen ausDEFAULT_PRESETS(Titel/Untertitel/Label/Notiz) + „— Stil —". - Font
BarCombo(130px): Systemfont-Liste (mind. Helvetica, Arial, Inter, Times New Roman, Georgia, Courier New).applyMark(doc,range,'font',v). - Grösse
BarCombo(80px): Presets in pt[8,9,10,11,12,14,18,24,36,48]- „Eigene…" → Zahl-Input-Pille.
applyMark(...,'sizePt',n).
- „Eigene…" → Zahl-Input-Pille.
Zeile 2:
- B/I/U Segmentpille (110px, Icons
format_bold/format_italic/format_underlined):toggleMark(doc,range,'bold'|'italic'|'underline'). Aktiv-Zustand ausisMarkActive(doc,range,mark). - L/C/R Segmentpille (130px, Icons
format_align_left/_center/_right): setztparagraph.alignim Bereich. - „+"-Text-Button (80px, BarButton/Pille, Icon
add, Label „Text"): startet das Text-Werkzeug (neues Textobjekt platzieren). Falls das Text-Annotation- Werkzeug noch nicht existiert, Button vorerstdisabledmit Tooltip (kein stiller No-Op) — aber Verdrahtung vorbereiten.
Auswahl-Bewusstsein (WICHTIG): Prop textTarget (oder aus App-State): entweder
null (nichts Text-artiges selektiert → Controls setzen nur Defaults, Ränder
normal) ODER { doc: RichTextDoc, range: TextRange|null, apply: (doc)=>void }
für den aktuell selektierten Raumstempel/Text. Ist textTarget != null, tragen
die Zeile-2-Pillen border-color:var(--accent) (Akzent-Glow), und alle Aktionen
wirken auf textTarget.doc via textTarget.apply(newDoc). Ohne aktive Range
(nur Objekt selektiert, kein Editor offen) wirkt Formatierung auf das GANZE Doc.
Quelle der textTarget-Daten: der Raum-Agent exponiert Stempel-Doc + Setter
(setRoomStampDoc(roomId, doc)) im App-State (siehe Peer-Absprache). App leitet
für den selektierten Raum {doc: room.stampDoc, range: activeStampRange, apply: d => setRoomStampDoc(room.id, d)} an die Text-Gruppe.
4. Text-Inhalt bearbeiten: kleines Fenster (kein Footer)
Doppelklick auf einen Raumstempel/ein Textobjekt öffnet ein schwebendes
Dialog-Fenster (nicht den Footer, kein Panel-Aufklappen). Umsetzung: neue
Komponente src/ui/TextEditorDialog.tsx — ein zentriertes/абgesetztes Fenster
(~560×420, --shadow-3, border-radius:8px, Titel „Text bearbeiten",
Kopf mit Schliessen-✕), Body = der bestehende src/text/RichTextEditor.tsx
(er bringt seine eigene Mini-Toolbar mit — das ist hier ok, weil es ein eigenes
Fenster ist), Fuss = „Abbrechen" / „Übernehmen". „Übernehmen" ruft
setRoomStampDoc(roomId, editedDoc).
Der Doppelklick-Handler lebt in App (Plan-View/Viewport → onDoubleClick auf
Stempel-Hit → openTextEditor(roomId)), NICHT im Footer. Falls der Raum-Agent
den Footer benutzt hat: diesen Pfad entfernen und durch den Dialog ersetzen.
5. Farb-/Stil-Tokens
Bestehende CSS-Variablen weiterverwenden (--panel,--input,--border,
--accent,--accent-dim,--accent-border,--ink,--ink-2,--muted,
--font-mono,--shadow-1..3). KEINE neuen Farbwerte hart kodieren. Falls ein
Token fehlt (z. B. --accent-border), prüfen und ggf. aus bestehenden ableiten.
6. i18n
Neue Keys in de.ts UND en.ts: text.style, text.font, text.size,
text.size.custom, text.bold/italic/underline, text.align.left/center/right,
text.add, text.add.hint, text.editTitle, text.apply, text.cancel,
text.selectedHint. Presets-Namen über bestehende rt.*/Preset-Keys, sofern da.
7. Gate (Pflicht)
rm -f tsconfig.tsbuildinfo && npx tsc -b grün, npm run build grün, keine
AI-Spuren (grep auf Claude/Anthropic/AI/Generated/Co-Authored), Boot-Probe
(node scripts/probe.mjs) ohne Konsolenfehler, Screenshot der Oberleiste.
KEIN Commit.