Ribbon Phase 4: modulare Custom-Bar als eigener Tab (Eigene) mit +-Picker, localStorage-persistiert

This commit is contained in:
2026-07-05 20:57:13 +02:00
parent 12441d36fb
commit e82d4b084c
7 changed files with 163 additions and 5 deletions
+2 -1
View File
@@ -39,7 +39,7 @@
## ⏭️ Als Nächstes
- [ ] **Ribbon-UI + modulare Bars** (Nutzer-Vision 2026-07-05, bestätigt: Tab-Schema **2D·3D·BIM·Ansichten**). Voller Plan + datengetriebene Architektur: **[docs/design/ribbon-ui-plan.md](docs/design/ribbon-ui-plan.md)**. Ribbon-Oberleiste mit Tabs ersetzt die Werkzeug-Sidebar; datengetriebene Registry (RibbonItem = tool|command|action) ermöglicht auch eine **modulare Custom-Bar** (gleiche Items, vom Nutzer gewählt). Attribute bekommen volle Höhe, Objektinfo darunter gemergt; XYZ-Box oben rechts bleibt. **Phasen:** ~~(1) Gerüst + 2D/BIM-Tab~~ ✅ (`9d6e86d`), ~~(2) TopBar → Ansichten-Tab mergen~~ ✅ (`85011cb`), ~~(2b) Tabs in die TopBar-Zeile~~ ✅ (`456ebc8`), ~~(2c) OCS-Chrome (kleine Wortmarke + Quick-Access-Icons statt Burger, Zeile 26px)~~ ✅ (`4e3b074`), ~~(2d) Text + Ansichten auf eine Leiste, „Ansichten" als Standard-Tab zuerst~~ ✅ (`fe22cbf`), (3) **teilweise** ✅: Werkzeug-Sidebar aus Default-Layout raus + Attribute volle linke Höhe + Wandtyp-/Deckentyp-Picker ins Attribute-Panel verschoben (Nutzer-Entscheid), `LAYOUT_VERSION`→8; **offen:** Objektinfo unter Attribute mergen (großer Panel-Umbau, XYZ-Box-Platzierung — visuell iterieren), (4) modulare Custom-Bar. **Offen:** 3D-Tab füllen (aktuell leer; ggf. 3D-spezifisch statt Doppelung mit Ansichten), Band-Höhe/Abstände + 26px-Zeile visuell im Tauri abnehmen, Dauer-Zoom-Anzeige (Statusleiste?) klären. ✅ Vorarbeit: Eigenschaften-Grid im OCS-Stil (`00733d8`), Kreis+Bogen-Werkzeuge (`e454eab`/`bd2b12b`). **Nächster Schritt: im Tauri visuell prüfen (Band-Höhe, Icons, Aktiv-Highlight), dann Phase 2/3.**
- [ ] **Ribbon-UI + modulare Bars** (Nutzer-Vision 2026-07-05, bestätigt: Tab-Schema **2D·3D·BIM·Ansichten**). Voller Plan + datengetriebene Architektur: **[docs/design/ribbon-ui-plan.md](docs/design/ribbon-ui-plan.md)**. Ribbon-Oberleiste mit Tabs ersetzt die Werkzeug-Sidebar; datengetriebene Registry (RibbonItem = tool|command|action) ermöglicht auch eine **modulare Custom-Bar** (gleiche Items, vom Nutzer gewählt). Attribute bekommen volle Höhe, Objektinfo darunter gemergt; XYZ-Box oben rechts bleibt. **Phasen:** ~~(1) Gerüst + 2D/BIM-Tab~~ ✅ (`9d6e86d`), ~~(2) TopBar → Ansichten-Tab mergen~~ ✅ (`85011cb`), ~~(2b) Tabs in die TopBar-Zeile~~ ✅ (`456ebc8`), ~~(2c) OCS-Chrome (kleine Wortmarke + Quick-Access-Icons statt Burger, Zeile 26px)~~ ✅ (`4e3b074`), ~~(2d) Text + Ansichten auf eine Leiste, „Ansichten" als Standard-Tab zuerst~~ ✅ (`fe22cbf`), (3) **teilweise** ✅: Werkzeug-Sidebar aus Default-Layout raus + Attribute volle linke Höhe + Wandtyp-/Deckentyp-Picker ins Attribute-Panel verschoben (Nutzer-Entscheid), `LAYOUT_VERSION`→8; **offen:** Objektinfo unter Attribute mergen (großer Panel-Umbau, XYZ-Box-Platzierung — visuell iterieren), ~~(4) modulare Custom-Bar~~ ✅ (Tab „Eigene" + -Picker, localStorage-persistiert). **Offen:** 3D-Tab füllen (aktuell leer; ggf. 3D-spezifisch statt Doppelung mit Ansichten), Band-Höhe/Abstände + 26px-Zeile visuell im Tauri abnehmen, Dauer-Zoom-Anzeige (Statusleiste?) klären. ✅ Vorarbeit: Eigenschaften-Grid im OCS-Stil (`00733d8`), Kreis+Bogen-Werkzeuge (`e454eab`/`bd2b12b`). **Nächster Schritt: im Tauri visuell prüfen (Band-Höhe, Icons, Aktiv-Highlight), dann Phase 2/3.**
- [x] ~~**SPIKE — Bild-Texturen in `render3d`**~~**erledigt `0ca3b1d`** (verifiziert 2026-07-05): `RenderStyle::Textured` real, prozedurales 256×256-Schachbrett (kein Asset/`image`-Crate), UVs planar in Metern, Textur-Bind-Group group 1, `MESH_TEXTURED_WGSL` (gleiche Beleuchtung, Albedo aus `textureSample`), `spike3d` per `T` umschaltbar. Alt-Vertexpfad `[pos,normal,color]` bitgleich (Regressionstest). `cargo test` 58 grün (59 mit `--features render`, inkl. naga-Test); `spike3d`-Build sauber; keine neuen Deps, Default-Build unverändert. Auftrag: [SPIKE_TEXTUR_render3d.md](SPIKE_TEXTUR_render3d.md). **Lücken bis „richtig gutes" Texturing → siehe 3D-REST unten.**
@@ -107,6 +107,7 @@
_Nur jüngste Session; ältere Historie siehe `git log` und HANDOVER-Narrative._
- [x] 2026-07-05 **Ribbon Phase 4: modulare Custom-Bar** (Tab „Eigene") — datengetrieben auf der bestehenden Registry: neuer `custom`-Tab, `CustomBar` in `RibbonBar.tsx` rendert die vom Nutzer gewählten Items + einen „+ Hinzufügen"-Picker (Dropdown mit Häkchen, listet ALLE Werkzeuge/Befehle aus `ALL_RIBBON_ITEMS`, Klick = an/abwählen). `itemKey`/`itemFromKey` für stabile Persistenz; State in App (`customItems`) via localStorage (`dossier.ribbon.customItems`). Gleiche `RibbonButton`-Aktivierung wie normale Tabs (kein Sonderweg). tsc + Suite 334 grün.
- [x] 2026-07-05 **Ribbon Phase 3 (Teil): Werkzeug-Sidebar raus**`DEFAULT_LEFT_GROUPS` nur noch `attributes` (volle linke Höhe), `LAYOUT_VERSION` 7→8 (gespeicherte Layouts fallen auf neuen Default zurück → sichtbar). Wandtyp-/Deckentyp-Picker von `ToolsPanel` ins `AttributesPanel` verschoben (Nutzer-Entscheid „ins Attribute-Panel") — erscheint bei aktivem Wand-/Decken-Werkzeug (Sektion „Neues Bauteil", auch ohne Auswahl), Host-State unverändert. `ToolsPanel` bleibt registriert (per Fenster-Menü andockbar), `Dropdown`-Import dort entfernt. tsc + Suite 334 grün. **Offen:** Objektinfo unter Attribute mergen.
- [x] 2026-07-05 **Fix: gezeichnete/importierte Kreise+Bögen im WebGL unsichtbar** — der WebGL-Compiler (`glPlan/glPlanCompile.ts`, Default-Renderer) dispatchte `polygon`/`line`/`arc`, aber NICHT `drawingCircle`/`drawingArc` → seit `4ac99d3` (Kreise als echtes `drawingCircle`-Primitiv statt 64-Eck-Polygon) fielen sie im GL still raus (SVG-/WASM-Pfad hatten sie, GL nicht). Zweig ergänzt: bildschirm-adaptive Tessellierung wie beim `arc`-Primitiv (Kreis geschlossen + optionale Vollton-Füllung, Bogen a0..a1 offen). tsc + Suite 334 grün. **Nutzer-Report** („zeichne Kreis, bleibt nicht sichtbar").
- [x] 2026-07-05 **Textur-Spike verifiziert erledigt** (`0ca3b1d`) — bei der Backlog-Abarbeitung festgestellt, dass die render3d-Textur-Spike (`RenderStyle::Textured`, Schachbrett, planare Meter-UVs, `MESH_TEXTURED_WGSL` group 1, `spike3d`-`T`-Toggle) bereits vollständig umgesetzt + committet war; `cargo test` 58/59 grün, keine neuen Deps, Alt-Pfad bitgleich. „Als Nächstes"-Eintrag war veraltet → abgehakt; PBR-Restlückenliste (~1829 PT) in 3D-REST übernommen.
+39
View File
@@ -230,6 +230,31 @@ function promptCount(): number | null {
return Number.isFinite(n) && n >= 1 ? n : null;
}
/** localStorage-Schlüssel der modularen Custom-Bar (Ribbon-Tab „Eigene"). */
const RIBBON_CUSTOM_KEY = "dossier.ribbon.customItems";
/** Persistierte Custom-Bar-Item-Schlüssel lesen (leer bei Fehler/Erststart). */
function loadCustomItems(): string[] {
if (typeof window === "undefined") return [];
try {
const raw = window.localStorage.getItem(RIBBON_CUSTOM_KEY);
const arr = raw ? JSON.parse(raw) : null;
return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : [];
} catch {
return [];
}
}
/** Custom-Bar-Item-Schlüssel persistieren (Fehler still schlucken). */
function saveCustomItems(items: string[]): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(RIBBON_CUSTOM_KEY, JSON.stringify(items));
} catch {
// localStorage nicht verfügbar (privater Modus o. ä.) → ignorieren.
}
}
/** Umriss (bbox) aller Drawing2D-Geometrien in Modell-Metern (null = leer). */
function drawingsBBox(
drawings: Drawing2D[],
@@ -360,6 +385,18 @@ export default function App() {
// weil die Tab-Reiter in der TopBar-Zeile sitzen und der Ribbon-Inhalt darunter
// — beide teilen sich denselben aktiven Tab.
const [ribbonTab, setRibbonTab] = useState<RibbonTabId>("views");
// Modulare Custom-Bar (Ribbon-Tab „Eigene"): persistierte Item-Schlüssel. Der
// Picker im Tab wählt Werkzeuge/Befehle an/ab (localStorage-persistiert).
const [customItems, setCustomItems] = useState<string[]>(loadCustomItems);
const onToggleCustomItem = (key: string) => {
setCustomItems((prev) => {
const next = prev.includes(key)
? prev.filter((k) => k !== key)
: [...prev, key];
saveCustomItems(next);
return next;
});
};
const [activeWallTypeId, setActiveWallTypeId] = useState<string>(
project.wallTypes[0].id,
);
@@ -3231,6 +3268,8 @@ export default function App() {
toolsEnabled={activeLevel.kind === "floor"}
onSelectTool={onSelectTool}
onRunCommand={onRunCommand}
customItems={customItems}
onToggleCustomItem={onToggleCustomItem}
tabContent={{
views: (
<ViewRibbonTab
+4
View File
@@ -125,9 +125,13 @@ export const de = {
"ribbon.tab.3d": "3D",
"ribbon.tab.bim": "BIM",
"ribbon.tab.views": "Ansichten",
"ribbon.tab.custom": "Eigene",
"ribbon.group.draw": "Zeichnen",
"ribbon.group.modify": "Ändern",
"ribbon.group.components": "Bauteile",
"ribbon.group.custom": "Eigene Leiste",
"ribbon.custom.add": " Hinzufügen",
"ribbon.custom.empty": "Elemente über Hinzufügen wählen",
"ribbon.empty": "Noch keine Werkzeuge in diesem Tab",
"tool.select.hint": "Auswahl — Elemente klicken/aufziehen",
"tool.window.hint": "Fenster: Wand wählen, dann Position setzen",
+4
View File
@@ -124,9 +124,13 @@ export const en: Record<TranslationKey, string> = {
"ribbon.tab.3d": "3D",
"ribbon.tab.bim": "BIM",
"ribbon.tab.views": "Views",
"ribbon.tab.custom": "Custom",
"ribbon.group.draw": "Draw",
"ribbon.group.modify": "Modify",
"ribbon.group.components": "Components",
"ribbon.group.custom": "Custom bar",
"ribbon.custom.add": " Add",
"ribbon.custom.empty": "Pick elements via “+ Add”",
"ribbon.empty": "No tools in this tab yet",
"tool.select.hint": "Select — click/drag elements",
"tool.window.hint": "Window: pick a wall, then set the position",
+12
View File
@@ -5529,6 +5529,18 @@ body {
font-style: italic;
padding: 0 8px;
}
/* Custom-Bar-Picker („+ Hinzufügen") — kompakter, unauffälliger Trigger, der
sich neben die Custom-Buttons stellt. */
.ribbon-add.tb-dd-trigger {
align-self: center;
height: 24px;
font-size: 11px;
color: var(--muted);
border-style: dashed;
}
.ribbon-add.tb-dd-trigger:hover {
color: var(--label);
}
/* „Ansichten"-Tab: die aus der TopBar gemergten Cluster (View-Grid, Kombis,
Detail, Zoom, Darstellung) in EINER Zeile — nutzt die bestehenden `.tb-*`-
Stile unverändert weiter. */
+70 -3
View File
@@ -17,10 +17,19 @@ import { ToolIcon } from "../../panels/ToolsPanel";
import { getTool } from "../../tools/tools";
import type { ToolId } from "../../tools/types";
import { getCommand } from "../../commands/registry";
import { RIBBON_TABS } from "./ribbonItems";
import { Dropdown } from "../Dropdown";
import type { DropdownItem } from "../Dropdown";
import { RIBBON_TABS, ALL_RIBBON_ITEMS, itemKey, itemFromKey } from "./ribbonItems";
import type { RibbonItem, RibbonTabId } from "./ribbonItems";
import { CommandIcon } from "./CommandIcon";
/** Aufgelöste Beschriftung eines Items (Werkzeug bzw. Befehl). */
function itemLabel(item: RibbonItem): string {
if (item.kind === "tool") return t(getTool(item.id).labelKey);
const cmd = getCommand(item.name);
return cmd ? t(cmd.labelKey) : item.name;
}
export interface RibbonBarProps {
/** Aktiver Tab (State liegt in App, geteilt mit den Reitern in der TopBar). */
tab: RibbonTabId;
@@ -41,6 +50,10 @@ export interface RibbonBarProps {
* Gruppen zeigt den Leer-Hinweis.
*/
tabContent?: Partial<Record<RibbonTabId, ReactNode>>;
/** Persistierte Item-Schlüssel der modularen Custom-Bar (Tab „Eigene"). */
customItems: string[];
/** Ein Item in der Custom-Bar an-/abwählen (Picker im „Eigene"-Tab). */
onToggleCustomItem: (key: string) => void;
}
/** Aktivierungs-/Highlight-Kontext, den jeder Button-Typ braucht. */
@@ -120,6 +133,53 @@ export function RibbonTabs({
);
}
/**
* Modulare Custom-Bar (Tab Eigene") rendert die vom Nutzer gewählten Items
* plus einen "-Picker, der ALLE verfügbaren Werkzeuge/Befehle mit Häkchen für
* die bereits enthaltenen listet (Klick = an-/abwählen). Gleiche Item-Typen +
* gleiche Aktivierung wie die normalen Tabs kein Sonderweg.
*/
function CustomBar({
customItems,
onToggle,
ctx,
}: {
customItems: string[];
onToggle: (key: string) => void;
ctx: ButtonCtx;
}) {
const pickerItems: DropdownItem[] = ALL_RIBBON_ITEMS.map((it) => {
const key = itemKey(it);
return {
label: itemLabel(it),
active: customItems.includes(key),
onSelect: () => onToggle(key),
};
});
const items = customItems
.map(itemFromKey)
.filter((it): it is RibbonItem => it !== null);
return (
<div className="ribbon-group">
<div className="ribbon-items">
{items.length === 0 && (
<div className="ribbon-empty">{t("ribbon.custom.empty")}</div>
)}
{items.map((item, i) => (
<RibbonButton key={`${itemKey(item)}-${i}`} item={item} ctx={ctx} />
))}
<Dropdown
items={pickerItems}
placeholder={t("ribbon.custom.add")}
title={t("ribbon.custom.add")}
triggerClassName="ribbon-add"
/>
</div>
<div className="ribbon-group-title">{t("ribbon.group.custom")}</div>
</div>
);
}
export function RibbonBar(props: RibbonBarProps) {
const current = RIBBON_TABS.find((tt) => tt.id === props.tab) ?? RIBBON_TABS[0];
const custom = props.tabContent?.[current.id];
@@ -134,9 +194,16 @@ export function RibbonBar(props: RibbonBarProps) {
return (
<div className="ribbon">
{/* Inhalt des aktiven Tabs. Tabs mit reichem Custom-Inhalt (Ansichten",
Text") rendern diesen statt der datengetriebenen Item-Gruppen. */}
Text") rendern diesen; der „Eigene"-Tab die modulare Custom-Bar; sonst
die datengetriebenen Item-Gruppen. */}
<div className="ribbon-body">
{custom ? (
{current.id === "custom" ? (
<CustomBar
customItems={props.customItems}
onToggle={props.onToggleCustomItem}
ctx={ctx}
/>
) : custom ? (
custom
) : current.groups.length === 0 ? (
<div className="ribbon-empty">{t("ribbon.empty")}</div>
+32 -1
View File
@@ -28,7 +28,7 @@ export interface RibbonGroup {
}
/** Kennung eines Ribbon-Tabs (fest, für Tab-State + i18n). */
export type RibbonTabId = "views" | "2d" | "3d" | "bim";
export type RibbonTabId = "views" | "2d" | "3d" | "bim" | "custom";
/** Ein Ribbon-Tab: Beschriftung + gruppierte Elemente. */
export interface RibbonTab {
@@ -105,4 +105,35 @@ export const RIBBON_TABS: RibbonTab[] = [
},
],
},
{
// Modulare Custom-Bar (Phase 4): der Nutzer stellt sich die Elemente selbst
// zusammen (persistiert). Gerendert aus `customItems`, nicht aus `groups`.
id: "custom",
labelKey: "ribbon.tab.custom",
groups: [],
},
];
/** Stabiler Schlüssel eines Items für die Persistenz der Custom-Bar. */
export function itemKey(item: RibbonItem): string {
return item.kind === "tool" ? `tool:${item.id}` : `command:${item.name}`;
}
/** Parst einen Item-Schlüssel zurück (oder null bei unbekanntem Präfix). */
export function itemFromKey(key: string): RibbonItem | null {
const i = key.indexOf(":");
if (i < 0) return null;
const kind = key.slice(0, i);
const rest = key.slice(i + 1);
if (kind === "tool") return { kind: "tool", id: rest as ToolId };
if (kind === "command") return { kind: "command", name: rest };
return null;
}
/**
* Alle datengetriebenen Items (aus den Werkzeug-/Befehls-Tabs) die wählbare
* Menge für den Custom-Bar-Picker. views"/„custom" tragen keine `groups`.
*/
export const ALL_RIBBON_ITEMS: RibbonItem[] = RIBBON_TABS.flatMap((tab) =>
tab.groups.flatMap((g) => g.items),
);