diff --git a/src/App.tsx b/src/App.tsx index 659cd89..968aa48 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -65,7 +65,7 @@ import { TextEditorDialog } from "./ui/TextEditorDialog"; import { RoomStampEditor } from "./panels/RoomStampEditor"; import { defaultRoomStamp } from "./model/roomStamp"; import type { DetailLevel, RenderMode, View3d, ViewType } from "./ui/TopBar"; -import { StatusBar } from "./ui/StatusBar"; +import { StatusBar, type RendererMode } from "./ui/StatusBar"; import { getTool } from "./tools/tools"; import { applyAngleConstraint, computeSnap } from "./tools/snapping"; import { @@ -2187,6 +2187,20 @@ export default function App() { // aktiv. Sonst sind Cursor/Zoom bedeutungslos (PlanView ist nicht montiert → // das Handle ist null, Aktionen sind No-Ops). const planActive = activeLevel.kind === "floor" && viewType === "grundriss"; + // Plan-Renderer-Umschalter (Statusleiste): reines Ableiten/Setzen des `engine`- + // URL-Params (siehe PlanView.tsx `wantWasm`); die Renderer-Auswahl selbst bleibt + // dort. Ein Wechsel lädt die Seite neu — kein Live-Remount der Renderer-Hooks. + const rendererMode: RendererMode = + new URLSearchParams(window.location.search).get("engine") === "wasm" + ? "wasm" + : "webgl2"; + const webGpuAvailable = typeof navigator !== "undefined" && "gpu" in navigator; + const setRendererMode = (mode: RendererMode) => { + const url = new URL(window.location.href); + if (mode === "wasm") url.searchParams.set("engine", "wasm"); + else url.searchParams.delete("engine"); + window.location.assign(url.toString()); + }; // Übernimmt ein Zeichenwerkzeug ODER eine laufende Transformation die linke // Maustaste der Plan-Ansicht? Dann liegt der Pointer-Input beim Tool/Transform // (statt Auswahl/Marquee), und die Editier-Griffe sind ausgeblendet. @@ -2932,6 +2946,9 @@ export default function App() { zoomPercent={planActive ? zoomPercent : null} activeFloor={activeLevel.name} activeLayer={activeLayerName} + rendererMode={rendererMode} + webGpuAvailable={webGpuAvailable} + onRendererModeChange={setRendererMode} /> {/* Schwebendes Ressourcen-Fenster ÜBER allem (hoher z-index, abgedunkelter diff --git a/src/i18n/de.ts b/src/i18n/de.ts index e9db958..b38a75f 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -186,6 +186,10 @@ export const de = { "status.floor": "Geschoss", "status.layer": "Ebene", "status.none": "—", + "status.renderer": "Renderer", + "status.rendererWebgl": "WebGL2", + "status.rendererWasm": "Engine", + "status.rendererWasmUnavailable": "WebGPU nicht verfügbar", // ── Navigator-Panels ───────────────────────────────────────────────────── "nav.drawingLevels": "Zeichnungsebenen", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 6e14e5a..341840c 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -185,6 +185,10 @@ export const en: Record = { "status.floor": "Floor", "status.layer": "Layer", "status.none": "—", + "status.renderer": "Renderer", + "status.rendererWebgl": "WebGL2", + "status.rendererWasm": "Engine", + "status.rendererWasmUnavailable": "WebGPU unavailable", // Navigator panels. "nav.drawingLevels": "Drawing levels", diff --git a/src/styles.css b/src/styles.css index c3c7726..6b6276c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -760,6 +760,49 @@ body { background: var(--border); } +/* Renderer-Umschalter (WebGL2 / Engine) in der Statusleiste — kleine + Segmentpille, an die 22px-Leiste angepasst (siehe .tb-seg für das größere + Vorbild in der Oberleiste). */ +.sb-renderer-toggle { + display: inline-flex; + align-items: stretch; + height: 16px; + border: 1px solid var(--border); + border-radius: 999px; + overflow: hidden; + background: var(--input); +} +.sb-renderer-btn { + display: inline-flex; + align-items: center; + border: none; + border-left: 1px solid var(--border); + background: transparent; + color: var(--muted); + font-family: inherit; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.02em; + padding: 0 8px; + cursor: pointer; + transition: background 0.14s, color 0.14s; +} +.sb-renderer-btn:first-child { + border-left: none; +} +.sb-renderer-btn:hover:not(:disabled) { + background: var(--accent-dim); + color: var(--accent); +} +.sb-renderer-btn.active { + background: var(--accent); + color: #fff; +} +.sb-renderer-btn:disabled { + opacity: 0.4; + cursor: default; +} + /* ── Command-Line (Rhino-artig) — über der Statusleiste ────────────────────── Dunkle, ruhige Leiste mit Prompt links, klickbaren Inline-Optionen und einer monospace-Texteingabe rechts; das Autocomplete-Dropdown öffnet nach oben. */ diff --git a/src/ui/StatusBar.tsx b/src/ui/StatusBar.tsx index 85ec699..b247960 100644 --- a/src/ui/StatusBar.tsx +++ b/src/ui/StatusBar.tsx @@ -11,6 +11,9 @@ import type { Vec2 } from "../model/types"; import { t } from "../i18n"; +/** Plan-Renderer-Wahl, gespiegelt aus dem `?engine`-URL-Param (siehe PlanView). */ +export type RendererMode = "webgl2" | "wasm"; + export interface StatusBarProps { /** Kontextueller Werkzeug-Hinweis links (z. B. „Auswahl"). */ hint: string; @@ -29,6 +32,15 @@ export interface StatusBarProps { activeFloor: string; /** Name der aktiven Ebene (Kategorie) oder „—". */ activeLayer: string; + /** Aktuell gewählter Plan-Renderer (aus dem `?engine`-URL-Param abgeleitet). */ + rendererMode: RendererMode; + /** Ist `navigator.gpu` vorhanden? Steuert, ob „Engine" wählbar ist. */ + webGpuAvailable: boolean; + /** + * Renderer wechseln (setzt/entfernt den `engine`-URL-Param und lädt neu — kein + * Live-Remount der Renderer-Hooks). + */ + onRendererModeChange: (mode: RendererMode) => void; } /** Ein Wert-Feld im rechten Cluster: kleine Beschriftung + Wert. */ @@ -57,6 +69,9 @@ export function StatusBar({ zoomPercent, activeFloor, activeLayer, + rendererMode, + webGpuAvailable, + onRendererModeChange, }: StatusBarProps) { const coords = cursor ? `${fmt(cursor.x)}, ${fmt(cursor.y)}` @@ -69,6 +84,30 @@ export function StatusBar({ {/* Rechts: abgeleiteter Wert-Cluster. */}
+ + {t("status.renderer")} + + + + + +