UI: Renderer-Umschalter (WebGL2 / eigene Engine) in der Statusleiste

This commit is contained in:
2026-07-02 23:35:01 +02:00
parent 623b243306
commit 0e839b50d6
5 changed files with 108 additions and 1 deletions
+18 -1
View File
@@ -65,7 +65,7 @@ import { TextEditorDialog } from "./ui/TextEditorDialog";
import { RoomStampEditor } from "./panels/RoomStampEditor"; import { RoomStampEditor } from "./panels/RoomStampEditor";
import { defaultRoomStamp } from "./model/roomStamp"; import { defaultRoomStamp } from "./model/roomStamp";
import type { DetailLevel, RenderMode, View3d, ViewType } from "./ui/TopBar"; 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 { getTool } from "./tools/tools";
import { applyAngleConstraint, computeSnap } from "./tools/snapping"; import { applyAngleConstraint, computeSnap } from "./tools/snapping";
import { import {
@@ -2187,6 +2187,20 @@ export default function App() {
// aktiv. Sonst sind Cursor/Zoom bedeutungslos (PlanView ist nicht montiert → // aktiv. Sonst sind Cursor/Zoom bedeutungslos (PlanView ist nicht montiert →
// das Handle ist null, Aktionen sind No-Ops). // das Handle ist null, Aktionen sind No-Ops).
const planActive = activeLevel.kind === "floor" && viewType === "grundriss"; 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 // Übernimmt ein Zeichenwerkzeug ODER eine laufende Transformation die linke
// Maustaste der Plan-Ansicht? Dann liegt der Pointer-Input beim Tool/Transform // Maustaste der Plan-Ansicht? Dann liegt der Pointer-Input beim Tool/Transform
// (statt Auswahl/Marquee), und die Editier-Griffe sind ausgeblendet. // (statt Auswahl/Marquee), und die Editier-Griffe sind ausgeblendet.
@@ -2932,6 +2946,9 @@ export default function App() {
zoomPercent={planActive ? zoomPercent : null} zoomPercent={planActive ? zoomPercent : null}
activeFloor={activeLevel.name} activeFloor={activeLevel.name}
activeLayer={activeLayerName} activeLayer={activeLayerName}
rendererMode={rendererMode}
webGpuAvailable={webGpuAvailable}
onRendererModeChange={setRendererMode}
/> />
{/* Schwebendes Ressourcen-Fenster ÜBER allem (hoher z-index, abgedunkelter {/* Schwebendes Ressourcen-Fenster ÜBER allem (hoher z-index, abgedunkelter
+4
View File
@@ -186,6 +186,10 @@ export const de = {
"status.floor": "Geschoss", "status.floor": "Geschoss",
"status.layer": "Ebene", "status.layer": "Ebene",
"status.none": "—", "status.none": "—",
"status.renderer": "Renderer",
"status.rendererWebgl": "WebGL2",
"status.rendererWasm": "Engine",
"status.rendererWasmUnavailable": "WebGPU nicht verfügbar",
// ── Navigator-Panels ───────────────────────────────────────────────────── // ── Navigator-Panels ─────────────────────────────────────────────────────
"nav.drawingLevels": "Zeichnungsebenen", "nav.drawingLevels": "Zeichnungsebenen",
+4
View File
@@ -185,6 +185,10 @@ export const en: Record<TranslationKey, string> = {
"status.floor": "Floor", "status.floor": "Floor",
"status.layer": "Layer", "status.layer": "Layer",
"status.none": "—", "status.none": "—",
"status.renderer": "Renderer",
"status.rendererWebgl": "WebGL2",
"status.rendererWasm": "Engine",
"status.rendererWasmUnavailable": "WebGPU unavailable",
// Navigator panels. // Navigator panels.
"nav.drawingLevels": "Drawing levels", "nav.drawingLevels": "Drawing levels",
+43
View File
@@ -760,6 +760,49 @@ body {
background: var(--border); 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 /* Command-Line (Rhino-artig) über der Statusleiste
Dunkle, ruhige Leiste mit Prompt links, klickbaren Inline-Optionen und einer Dunkle, ruhige Leiste mit Prompt links, klickbaren Inline-Optionen und einer
monospace-Texteingabe rechts; das Autocomplete-Dropdown öffnet nach oben. */ monospace-Texteingabe rechts; das Autocomplete-Dropdown öffnet nach oben. */
+39
View File
@@ -11,6 +11,9 @@
import type { Vec2 } from "../model/types"; import type { Vec2 } from "../model/types";
import { t } from "../i18n"; import { t } from "../i18n";
/** Plan-Renderer-Wahl, gespiegelt aus dem `?engine`-URL-Param (siehe PlanView). */
export type RendererMode = "webgl2" | "wasm";
export interface StatusBarProps { export interface StatusBarProps {
/** Kontextueller Werkzeug-Hinweis links (z. B. „Auswahl"). */ /** Kontextueller Werkzeug-Hinweis links (z. B. „Auswahl"). */
hint: string; hint: string;
@@ -29,6 +32,15 @@ export interface StatusBarProps {
activeFloor: string; activeFloor: string;
/** Name der aktiven Ebene (Kategorie) oder „—". */ /** Name der aktiven Ebene (Kategorie) oder „—". */
activeLayer: string; 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. */ /** Ein Wert-Feld im rechten Cluster: kleine Beschriftung + Wert. */
@@ -57,6 +69,9 @@ export function StatusBar({
zoomPercent, zoomPercent,
activeFloor, activeFloor,
activeLayer, activeLayer,
rendererMode,
webGpuAvailable,
onRendererModeChange,
}: StatusBarProps) { }: StatusBarProps) {
const coords = cursor const coords = cursor
? `${fmt(cursor.x)}, ${fmt(cursor.y)}` ? `${fmt(cursor.x)}, ${fmt(cursor.y)}`
@@ -69,6 +84,30 @@ export function StatusBar({
{/* Rechts: abgeleiteter Wert-Cluster. */} {/* Rechts: abgeleiteter Wert-Cluster. */}
<div className="sb-cluster"> <div className="sb-cluster">
<span className="sb-field">
<span className="sb-key">{t("status.renderer")}</span>
<span className="sb-renderer-toggle" role="group" aria-label={t("status.renderer")}>
<button
type="button"
className={`sb-renderer-btn${rendererMode === "webgl2" ? " active" : ""}`}
aria-pressed={rendererMode === "webgl2"}
onClick={() => onRendererModeChange("webgl2")}
>
{t("status.rendererWebgl")}
</button>
<button
type="button"
className={`sb-renderer-btn${rendererMode === "wasm" ? " active" : ""}`}
aria-pressed={rendererMode === "wasm"}
disabled={!webGpuAvailable}
title={webGpuAvailable ? undefined : t("status.rendererWasmUnavailable")}
onClick={() => onRendererModeChange("wasm")}
>
{t("status.rendererWasm")}
</button>
</span>
</span>
<Sep />
<Field label={t("status.xy")} value={coords} /> <Field label={t("status.xy")} value={coords} />
<Sep /> <Sep />
<Field label={t("status.unit")} value={unit} /> <Field label={t("status.unit")} value={unit} />