Files
DOSSIER-STANDALONE/src/plan/useWasmPlanRenderer.ts
T
karim b4c4a2cc6b wgpu 22 → 29, glyphon 0.6 → 0.11: requestDevice-Shim entfällt
glyphon 0.11 ist die neueste zu wgpu 29 passende Version (wgpu 30 existiert
bereits, glyphon pinnt aber ^29). Das 22er-Limit maxInterStageShaderComponents
wird von 29 nicht mehr in requiredLimits gesendet — src/engine/requestDeviceShim.ts
komplett entfernt, beide Hook-Importstellen (useWasmPlanRenderer, useWasm3dRenderer)
angepasst. pollster 0.3→0.4, naga 22→29 mitgezogen. Alle Draw-/Text-Pfade
(draw_sequence, glyphon ColorMode::Web, widthScreen-Polylinien, Headless/Golden)
unverändert funktionsfähig.

Verifiziert: cargo check nativ (render2d/render3d/src-tauri) grün, cargo test
render2d 17/17 + Golden bit-exakt, render3d 29/29, wasm32 --features web für
beide grün, npx tsc -b + npm run build grün.
2026-07-03 18:17:41 +02:00

159 lines
5.5 KiB
TypeScript

/**
* React-Hook für den nativen render2d-Renderer als WASM/WebGPU-Viewport.
*
* Spiegelt die Schnittstelle von {@link useGlPlanRenderer} ({ render,
* updateGeometry, ready }), damit PlanView beide Engines ohne weitere Verzweigung
* am selben Canvas + über dieselben Effekte treiben kann. Die eigentliche
* Zeichenlogik lebt in Rust (src-tauri/render2d, Feature `web`); hier wird nur das
* WASM-Modul geladen, der Renderer an das Canvas gebunden und die Szene/ViewBox
* durchgereicht.
*
* Fällt bei fehlendem WebGPU oder Init-Fehler still auf `ready:false` zurück; der
* Aufrufer (PlanView) behält dann den WebGL2-/SVG-Pfad.
*/
import { useCallback, useEffect, useRef, useState } from "react";
import type { Plan } from "./generatePlan";
import { planToRenderScene } from "./toRenderScene";
interface ViewBox {
x: number;
y: number;
w: number;
h: number;
}
/** Minimal-Typ des WASM-Exports (das echte .d.ts entsteht erst mit `build:engine`). */
interface WebPlanRendererLike {
set_scene(json: string): void;
set_view_box(x: number, y: number, w: number, h: number): void;
set_paper_scale(n: number): void;
set_text_scale(n: number): void;
resize(w: number, h: number): void;
render(): void;
}
/**
* WASM-Modul EINMAL laden + initialisieren (memoisiert). Wichtig: `mod.default()`
* darf nur einmal laufen — ein zweiter Init (z.B. durch React-StrictMode-Doppel-
* mount) verwirft die Closures des ersten und wirft „closure invoked recursively
* or after being dropped".
*/
let enginePromise: Promise<any> | null = null;
function loadEngine(): Promise<any> {
if (!enginePromise) {
enginePromise = (async () => {
const mod: any = await import("../engine/pkg/render2d.js");
await mod.default(); // lädt render2d_bg.wasm
return mod;
})();
}
return enginePromise;
}
/**
* Renderer je Canvas EINMAL erzeugen (memoisiert). Ein Canvas kann nur EINEN
* WebGPU-Kontext tragen; ein zweiter `WebPlanRenderer.new` auf demselben Canvas
* (StrictMode-Remount) würde kollidieren. Die Promise wird gecacht, damit der
* Doppelmount denselben Renderer teilt.
*/
const rendererCache = new WeakMap<HTMLCanvasElement, Promise<WebPlanRendererLike>>();
function getRenderer(canvas: HTMLCanvasElement): Promise<WebPlanRendererLike> {
let p = rendererCache.get(canvas);
if (!p) {
p = (async () => {
const mod = await loadEngine();
// Canvas VOR dem Surface-Erzeugen auf die Zielpixel (DPR) bringen.
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.round(rect.width * dpr));
canvas.height = Math.max(1, Math.round(rect.height * dpr));
return (await mod.WebPlanRenderer.new(canvas, undefined)) as WebPlanRendererLike;
})();
rendererCache.set(canvas, p);
// Bei Fehlschlag den Cache leeren, damit ein späterer Versuch neu startet.
p.catch(() => rendererCache.delete(canvas));
}
return p;
}
export function useWasmPlanRenderer(
canvasRef: React.RefObject<HTMLCanvasElement | null>,
enabled: boolean,
) {
const rendererRef = useRef<WebPlanRendererLike | null>(null);
const [ready, setReady] = useState(false);
// WASM-Renderer an das Canvas binden (memoisiert, StrictMode-fest).
useEffect(() => {
if (!enabled || !canvasRef.current) {
setReady(false);
return;
}
let disposed = false;
const canvas = canvasRef.current;
void (async () => {
try {
const renderer = await getRenderer(canvas);
if (disposed) return;
rendererRef.current = renderer;
setReady(true);
} catch (e) {
console.warn("render2d WASM/WebGPU-Init fehlgeschlagen → Fallback", e);
rendererRef.current = null;
setReady(false);
}
})();
return () => {
disposed = true;
setReady(false);
};
}, [enabled, canvasRef]);
/** Szene beim Plan-Wechsel neu setzen (re-tesselliert in Rust, gecacht). */
const updateGeometry = useCallback((plan: Plan) => {
const r = rendererRef.current;
if (!r || !plan) return;
try {
r.set_scene(JSON.stringify(planToRenderScene(plan)));
} catch (e) {
console.warn("render2d set_scene fehlgeschlagen:", e);
}
}, []);
/**
* Einen Frame zeichnen (Canvas-Größe + dpr hier abgleichen). `textScaleN`
* bewusst getrennt von `paperScaleN`: er treibt NUR die Schriftgröße der
* Raumstempel (spiegelt die SVG-Formel — feste Referenzskala 1:100 im
* Anzeigemodus, siehe PlanView.tsx `textScaleForGl`), während `paperScaleN`
* weiter die echten Papier-mm-Strichbreiten der Geometrie treibt.
*/
const render = useCallback(
(viewBox: ViewBox, paperScaleN = 100, textScaleN = 100) => {
const r = rendererRef.current;
const canvas = canvasRef.current;
if (!r || !canvas) return;
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const w = Math.max(1, Math.round(rect.width * dpr));
const h = Math.max(1, Math.round(rect.height * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
try {
r.resize(w, h);
r.set_paper_scale(paperScaleN);
r.set_text_scale(textScaleN);
r.set_view_box(viewBox.x, viewBox.y, viewBox.w, viewBox.h);
r.render();
} catch (e) {
console.warn("render2d render fehlgeschlagen:", e);
}
},
[canvasRef],
);
return { render, updateGeometry, ready };
}