From 85011cb8efa077c00776df5180ddac79bf8bd12f Mon Sep 17 00:00:00 2001 From: Karim Date: Sun, 5 Jul 2026 17:57:17 +0200 Subject: [PATCH] Ribbon: TopBar-Ansichtssteuerung in Ansichten-Tab gemergt; TopBar auf schmale globale Leiste reduziert --- src/App.tsx | 102 +++++++------ src/styles.css | 9 ++ src/ui/TopBar.tsx | 286 +++++++++++++++-------------------- src/ui/ribbon/RibbonBar.tsx | 15 +- src/ui/ribbon/ribbonItems.ts | 17 ++- 5 files changed, 206 insertions(+), 223 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 31ee012..6146ed2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -69,7 +69,7 @@ import type { Pick3dHit } from "./viewport/Wasm3DViewport"; import { ResourceManager } from "./ui/ResourceManager"; import type { ResourceManagerHandlers } from "./ui/ResourceManager"; import { HatchSwatch } from "./ui/hatchPreview"; -import { TopBar } from "./ui/TopBar"; +import { TopBar, ViewRibbonTab } from "./ui/TopBar"; import { RibbonBar } from "./ui/ribbon/RibbonBar"; import { Dropdown } from "./ui/Dropdown"; import type { DropdownItem } from "./ui/Dropdown"; @@ -3191,55 +3191,15 @@ export default function App() { e.target.value = ""; }} /> + {/* Schmale globale Leiste: Marke/Ressourcen · Text-Formatierung · + Datei/Export/Einstellungen. Ansichts-/Zoom-/Darstellungs-Steuerung ist + in den „Ansichten"-Ribbon-Tab gewandert (ViewRibbonTab, unten). */} setExportOpen(true)} onExportDxf={() => setExportDxfOpen(true)} onExportSchedule={onExportSchedule} - renderMode={renderMode} - onRenderMode={setRenderMode} - renderModeEnabled={viewType === "perspektive"} - planColorMode={planColorMode} - onPlanColorMode={setPlanColorMode} - combos={{ - // Ebenen-Kombinationen: Speichern = Snapshot des Layer-Baums → - // localStorage; Laden = Map lesen → Sichtbarkeits-Flags anwenden. - listLayerCombos, - saveLayerCombo: (name) => - saveLayerCombo(name, snapshotLayerVisibility()), - loadLayerCombo: (name) => { - const codes = loadLayerCombo(name); - if (codes) applyLayerVisibility(codes); - }, - deleteLayerCombo, - // Zeichnungs-Kombinationen: analog über drawingLevels[].visible. - listDrawingCombos, - saveDrawingCombo: (name) => - saveDrawingCombo(name, snapshotDrawingVisibility()), - loadDrawingCombo: (name) => { - const ids = loadDrawingCombo(name); - if (ids) applyDrawingVisibility(ids); - }, - deleteDrawingCombo, - }} resourcesOpen={resourcesOpen} onToggleResources={() => setResourcesOpen((v) => !v)} onOpenSettings={() => setSettingsOpen(true)} @@ -3255,15 +3215,63 @@ export default function App() { textTarget={textTarget} /> - {/* Ribbon-Oberleiste (2D · 3D · BIM · Ansichten) — additiv unter der TopBar. - Teilt sich denselben Aktivierungs-Pfad wie die Werkzeug-Sidebar; die - Sidebar bleibt vorerst erhalten (Plan-Doc Phase 3 entfernt sie). */} + {/* Ribbon-Oberleiste (2D · 3D · BIM · Ansichten). Der „Ansichten"-Tab trägt + die aus der TopBar gemergte Ansichts-/Zoom-/Darstellungs-Steuerung + (ViewRibbonTab); die Werkzeug-Sidebar bleibt vorerst erhalten. */} + saveLayerCombo(name, snapshotLayerVisibility()), + loadLayerCombo: (name) => { + const codes = loadLayerCombo(name); + if (codes) applyLayerVisibility(codes); + }, + deleteLayerCombo, + // Zeichnungs-Kombinationen: analog über drawingLevels[].visible. + listDrawingCombos, + saveDrawingCombo: (name) => + saveDrawingCombo(name, snapshotDrawingVisibility()), + loadDrawingCombo: (name) => { + const ids = loadDrawingCombo(name); + if (ids) applyDrawingVisibility(ids); + }, + deleteDrawingCombo, + }} + /> + } />
diff --git a/src/styles.css b/src/styles.css index 067c69d..574c036 100644 --- a/src/styles.css +++ b/src/styles.css @@ -5472,3 +5472,12 @@ body { font-style: italic; padding: 0 8px; } +/* „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. */ +.ribbon-views { + display: flex; + align-items: center; + gap: 10px; + min-height: 48px; +} diff --git a/src/ui/TopBar.tsx b/src/ui/TopBar.tsx index 439e664..ea7c450 100644 --- a/src/ui/TopBar.tsx +++ b/src/ui/TopBar.tsx @@ -84,49 +84,12 @@ const ZOOM_CLUSTER_WIDTH = 140; export interface TopBarProps { project: Project; - // Ansicht. - /** Ob der Ansichts-Umschalter sinnvoll ist (nur für Geschosse). */ - viewToggleEnabled: boolean; - viewType: ViewType; - onViewType: (v: ViewType) => void; + // Ansichts-/Zoom-/Darstellungs-Steuerung ist in den „Ansichten"-Ribbon-Tab + // gewandert (siehe ViewRibbonTab). Die TopBar ist nur noch die schmale globale + // Leiste: Marke/Ressourcen · Text-Formatierung · Datei/Export/Einstellungen. - // 3D-Ansicht: kanonischer Blickwinkel + Kamera-Sichtwinkel (FOV). Wirkt nur in - // der Perspektive; ein Preset-Klick wechselt nötigenfalls auch dorthin. - view3d: View3d; - onView3d: (v: View3d) => void; - /** Sichtwinkel der Perspektivkamera in Grad (Kamera-Einstellungen). */ - fov: number; - onFov: (n: number) => void; - - // Zeichenwerkzeuge (Auswahl/Wand/Linie …), Wandtyp-Wahl und Fang-Menü leben - // jetzt in der Werkzeug-Palette (ToolsPanel), nicht mehr in der Oberleiste. - - // Detailgrad. - detail: DetailLevel; - onDetail: (d: DetailLevel) => void; - /** Detailgrad wirkt nur, wenn ein Plan gezeichnet wird (sonst deaktiviert). */ - detailEnabled: boolean; - - // Massstab + Zoom. - /** - * Gewählter (angewandter) Papier-Massstab-Nenner (1:N) — steuert das Dropdown - * und „100 %". Setzt beim Wählen den Plan-Zoom auf exakt 1:N. - */ - scaleDenominator: number; - /** Einen Papier-Massstab anwenden (Preset/frei): setzt N + Plan-Zoom. */ - onApplyScale: (n: number) => void; - /** - * LIVE Papier-Massstab-Nenner aus der aktuellen View-Transform (ändert sich - * beim Wheel-Zoom) oder `null`, wenn keine Plan-Ansicht aktiv ist. - */ - liveScale: number | null; - /** Live-Zoom in Prozent (aus der Plan-View-Transform). */ - zoomPercent: number; - /** Zoom-Aktionen — nur wirksam, wenn eine Plan-Ansicht aktiv ist. */ + /** Ob Zoom/Plan aktiv ist — steuert nur noch, ob Export im Datei-Menü geht. */ zoomEnabled: boolean; - onFit: () => void; - onFitSelection: () => void; - onZoom100: () => void; /** Öffnet den PDF-Export-Dialog (nur im Grundriss sinnvoll). */ onExportPdf: () => void; /** Öffnet den DXF-Export-Dialog (nur im Grundriss sinnvoll). */ @@ -134,21 +97,6 @@ export interface TopBarProps { /** Lädt die Bauteilliste des Projekts als CSV herunter. */ onExportSchedule: () => void; - // Darstellungsart — kontextabhängig: Perspektive nutzt `renderMode`, - // Grundriss nutzt `planColorMode`. Genau einer der beiden ist je View aktiv. - renderMode: RenderMode; - onRenderMode: (m: RenderMode) => void; - /** Render-Modus nur in der Perspektive sinnvoll (sonst deaktiviert). */ - renderModeEnabled: boolean; - - // Farb-Modus des Grundrisses (Farbig ↔ Schwarz-Weiss). Nur im Grundriss. - planColorMode: PlanColorMode; - onPlanColorMode: (m: PlanColorMode) => void; - - // Sichtbarkeits-Kombinationen (Ebenen-/Zeichnungskombinationen). - /** Handler-Satz für die beiden Kombinations-Dropdowns. */ - combos: ComboMenuHandlers; - // Ressourcen-Fenster. resourcesOpen: boolean; onToggleResources: () => void; @@ -958,8 +906,45 @@ function AppMenu({ ); } -export function TopBar({ - project, +/** + * Props des „Ansichten"-Tabs — die Ansichts-/Zoom-/Darstellungs-Steuerung, die + * früher in der Mitte der TopBar sass. Eine Teilmenge von {@link TopBarProps}. + */ +export interface ViewRibbonTabProps { + viewToggleEnabled: boolean; + viewType: ViewType; + onViewType: (v: ViewType) => void; + view3d: View3d; + onView3d: (v: View3d) => void; + fov: number; + onFov: (n: number) => void; + detail: DetailLevel; + onDetail: (d: DetailLevel) => void; + detailEnabled: boolean; + scaleDenominator: number; + onApplyScale: (n: number) => void; + liveScale: number | null; + zoomPercent: number; + zoomEnabled: boolean; + onFit: () => void; + onFitSelection: () => void; + onZoom100: () => void; + renderMode: RenderMode; + onRenderMode: (m: RenderMode) => void; + renderModeEnabled: boolean; + planColorMode: PlanColorMode; + onPlanColorMode: (m: PlanColorMode) => void; + combos: ComboMenuHandlers; +} + +/** + * „Ansichten"-Tab des Ribbons — trägt die aus der TopBar gemergten Mittelcluster: + * 2D/3D-Ansichts-Raster (+ Kamera-FOV), Sichtbarkeits-Kombinationen, Detailgrad, + * Massstab/Zoom-Cluster und Darstellungsart. EIN Ribbon für alles; Handler/Setter + * kommen unverändert von App (gleiche Props wie zuvor an die TopBar). Wird von + * `RibbonBar` als Inhalt des „Ansichten"-Tabs gerendert. + */ +export function ViewRibbonTab({ viewToggleEnabled, viewType, onViewType, @@ -978,44 +963,13 @@ export function TopBar({ onFit, onFitSelection, onZoom100, - onExportPdf, - onExportDxf, - onExportSchedule, renderMode, onRenderMode, renderModeEnabled, planColorMode, onPlanColorMode, combos, - resourcesOpen, - onToggleResources, - onOpenSettings, - layoutMenu, - onImport, - onSaveProject, - onOpenProject, - textTarget, -}: TopBarProps) { - // Dezente Scrollleiste: die Oberleiste scrollt horizontal (overflow-x). Statt - // der prägnanten Standard-Leiste blenden wir eine eigene, sehr ruhige Leiste - // NUR während des Scrollens ein (Klasse `is-scrolling`, ~900 ms nachlaufend). - const headerRef = useRef(null); - useEffect(() => { - const el = headerRef.current; - if (!el) return; - let timer: ReturnType | undefined; - const onScroll = () => { - el.classList.add("is-scrolling"); - if (timer) clearTimeout(timer); - timer = setTimeout(() => el.classList.remove("is-scrolling"), 900); - }; - el.addEventListener("scroll", onScroll, { passive: true }); - return () => { - el.removeEventListener("scroll", onScroll); - if (timer) clearTimeout(timer); - }; - }, []); - +}: ViewRibbonTabProps) { // „frei" wird über einen Prompt erfasst; akzeptiert „1:35" ODER „35". const onScaleChange = (value: string) => { if (value === "__custom__") { @@ -1030,14 +984,7 @@ export function TopBar({ const n = Number(value); if (Number.isFinite(n) && n > 0) onApplyScale(n); }; - - // Aktueller Nenner steht evtl. nicht in den Presets (frei gesetzt) → dann - // einen Eintrag „1:N" voranstellen, damit das Select den Wert anzeigt. const inPresets = SCALE_PRESETS.includes(scaleDenominator); - - // Optionsliste des Massstab-Dropdowns: ggf. der frei gesetzte aktuelle Wert - // (damit er sichtbar bleibt), dann die Presets, zuletzt „frei …" als - // Sonder-Wert, der über onScaleChange den Prompt öffnet (Funktion unverändert). const scaleOptions: DropdownOption[] = [ ...(inPresets ? [] @@ -1047,41 +994,8 @@ export function TopBar({ ]; return ( -
- {/* Marke (DOSSIER-Wortmarke) + Settings/Ressourcen-Icons daneben, - gestapelt wie im Rhino-Plugin (ToolbarApp.jsx: Wortmarke links, - rechts daneben ein schmaler Icon-Stapel). */} -
-
- - DOSSIER. - - {t("brand.tag")} -
-
- -
-
- - - - {/* Ansichten als EIN kompaktes Icon-Grid (Vectorworks/DOSSIER-Stil): der - 2D-Grundriss UND die 3D-Blicke sind eine durchgängige Icon-Serie — - „Grundriss" schaltet auf den 2D-Plan, die übrigen auf die jeweilige - 3D-Ansicht (Perspektive wird dabei automatisch aktiviert). 4 oben / - 3 unten. Tooltip nennt jede Ansicht im Klartext. */} - {/* Ansichts-Icons bleiben immer sichtbar; auf Nicht-Geschoss-Ebenen - (Schnitt/Ansicht/2D-Zeichnung) sind sie deaktiviert, weil dort keine - 2D/3D-Umschaltung greift — die Serie verschwindet nicht mehr ganz. */} +
+ {/* Ansichten als EIN kompaktes Icon-Grid (2D-Grundriss + 3D-Blicke). */}
+ ); +} + +export function TopBar({ + project, + zoomEnabled, + onExportPdf, + onExportDxf, + onExportSchedule, + resourcesOpen, + onToggleResources, + onOpenSettings, + layoutMenu, + onImport, + onSaveProject, + onOpenProject, + textTarget, +}: TopBarProps) { + // Dezente Scrollleiste: die Oberleiste scrollt horizontal (overflow-x). Statt + // der prägnanten Standard-Leiste blenden wir eine eigene, sehr ruhige Leiste + // NUR während des Scrollens ein (Klasse `is-scrolling`, ~900 ms nachlaufend). + const headerRef = useRef(null); + useEffect(() => { + const el = headerRef.current; + if (!el) return; + let timer: ReturnType | undefined; + const onScroll = () => { + el.classList.add("is-scrolling"); + if (timer) clearTimeout(timer); + timer = setTimeout(() => el.classList.remove("is-scrolling"), 900); + }; + el.addEventListener("scroll", onScroll, { passive: true }); + return () => { + el.removeEventListener("scroll", onScroll); + if (timer) clearTimeout(timer); + }; + }, []); + + return ( +
+ {/* Marke (DOSSIER-Wortmarke) + Settings/Ressourcen-Icons daneben, + gestapelt wie im Rhino-Plugin (ToolbarApp.jsx: Wortmarke links, + rechts daneben ein schmaler Icon-Stapel). */} +
+
+ + DOSSIER. + + {t("brand.tag")} +
+
+ +
+
+ {/* Text-Gruppe (kontextabhängig) — Stil/Schrift/Grösse + B/I/U · L/C/R + + Zeilenhöhe. Formatiert das aktuelle Text-Ziel (Raumstempel) live. Bleibt + in der schmalen globalen Leiste, weil sie eine kontextuelle Formatier- + Steuerung ist (kein Ansichts-/Ausgabe-Element). */} + + {/* Rechte Gruppe: Layout-Menü · Projektname · Datei-Burger (ganz rechts, GTK-Stil) — Ressourcen sitzt jetzt bei der Marke (s. o.). */}
diff --git a/src/ui/ribbon/RibbonBar.tsx b/src/ui/ribbon/RibbonBar.tsx index 1f4a4b3..4f18a2c 100644 --- a/src/ui/ribbon/RibbonBar.tsx +++ b/src/ui/ribbon/RibbonBar.tsx @@ -12,6 +12,7 @@ // Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). import { useState } from "react"; +import type { ReactNode } from "react"; import { t } from "../../i18n"; import { ToolIcon } from "../../panels/ToolsPanel"; import { getTool } from "../../tools/tools"; @@ -32,6 +33,13 @@ export interface RibbonBarProps { onSelectTool: (id: ToolId) => void; /** Engine-Befehl starten (Ändern-Gruppe u. a.). */ onRunCommand: (name: string) => void; + /** + * Reicher Inhalt des „Ansichten"-Tabs (aus der TopBar gemergte Ansichts-/Zoom-/ + * Darstellungs-Steuerung, `ViewRibbonTab`). Von App geliefert, damit dessen + * Setter/State dort bleiben — analog dazu, wie die TopBar das Layout-Menü als + * Node bekommt. Fehlt er, zeigt der Tab den Leer-Hinweis. + */ + viewsContent?: ReactNode; } /** Ein einzelner Ribbon-Button (Werkzeug ODER Befehl). */ @@ -104,9 +112,12 @@ export function RibbonBar(props: RibbonBarProps) { ))}
- {/* Gruppen des aktiven Tabs */} + {/* Gruppen des aktiven Tabs. Der „Ansichten"-Tab trägt reichen Custom- + Inhalt (ViewRibbonTab) statt der datengetriebenen Item-Gruppen. */}
- {current.groups.length === 0 ? ( + {current.id === "views" && props.viewsContent ? ( + props.viewsContent + ) : current.groups.length === 0 ? (
{t("ribbon.empty")}
) : ( current.groups.map((g) => ( diff --git a/src/ui/ribbon/ribbonItems.ts b/src/ui/ribbon/ribbonItems.ts index f210230..7a37df0 100644 --- a/src/ui/ribbon/ribbonItems.ts +++ b/src/ui/ribbon/ribbonItems.ts @@ -41,9 +41,10 @@ const t = (id: ToolId): RibbonItem => ({ kind: "tool", id }); const c = (name: string): RibbonItem => ({ kind: "command", name }); /** - * Die Ribbon-Tabs. Phase 1 füllt 2D (Zeichnen + Ändern) und BIM (Bauteile) mit - * den vorhandenen Werkzeugen/Befehlen; 3D und Ansichten bleiben vorerst leer - * (Platzhalter, in einer späteren Phase gefüllt — siehe Plan-Doc). + * Die Ribbon-Tabs (Reihenfolge 2D·3D·BIM·Ansichten). 2D (Zeichnen + Ändern) und + * BIM (Bauteile) sind datengetrieben aus Werkzeugen/Befehlen; „Ansichten" trägt + * reichen Custom-Inhalt (aus der TopBar gemergt, siehe `ViewRibbonTab` — die + * Registry lässt seine `groups` leer). 3D ist vorerst leer (spätere Phase). */ export const RIBBON_TABS: RibbonTab[] = [ { @@ -74,6 +75,11 @@ export const RIBBON_TABS: RibbonTab[] = [ }, ], }, + { + id: "3d", + labelKey: "ribbon.tab.3d", + groups: [], + }, { id: "bim", labelKey: "ribbon.tab.bim", @@ -91,11 +97,6 @@ export const RIBBON_TABS: RibbonTab[] = [ }, ], }, - { - id: "3d", - labelKey: "ribbon.tab.3d", - groups: [], - }, { id: "views", labelKey: "ribbon.tab.views",