render2d im Browser: WASM/WebGPU-Viewport hinter ?engine=wasm

Integrations-Spike: die native 2D-Engine (src-tauri/render2d) laeuft jetzt als
WASM/WebGPU-Canvas in der App-UI, mit derselben Szene (planToRenderScene) und
ViewBox wie der WebGL2-Pfad; das SVG-Overlay (Text/Griffe/Auswahl) bleibt drueber.

- render2d: neues Feature "web" (wasm-bindgen/web-sys), Fassade WebPlanRenderer
  (new/set_scene/set_view_box/set_paper_scale/resize/render) auf Canvas-Surface.
  Geteilte GPU-Schicht (gpu.rs), native Fensterschicht (winit) unberuehrt.
- Font: cosmic-text findet auf wasm keine Systemfonts -> Inter-Bytes eingebettet
  (assets/Inter.ttf), Renderer.load_font + text_family aus der geladenen Familie.
- Frontend: useWasmPlanRenderer (gleiche Schnittstelle wie useGlPlanRenderer);
  PlanView waehlt die Engine per ?engine=wasm + navigator.gpu, sonst WebGL2/SVG.
- Build: npm-Script build:engine (wasm-pack, target web -> src/engine/pkg,
  gitignored). chromium-shell.sh mit WebGPU-Flags (--enable-unsafe-webgpu/Vulkan).
- Kompat-Shim: wgpu 22 sendet das spec-entfernte Limit maxInterStageShaderComponents,
  das neuere Chromium ablehnen -> im Hook vor requestDevice entfernt.

Verifiziert: cargo test -p render2d gruen (18), tsc gruen, wasm-Build gruen;
Headless-Chromium (?engine=wasm) zeichnet den Grundriss (Waende/Schraffur/
Tuerschwenk/Treppe/Raumtext) im WASM-Viewport, Fallback ohne Flag unveraendert.
This commit is contained in:
2026-07-02 22:30:47 +02:00
parent 2a2b3b294c
commit a3b25da96d
12 changed files with 607 additions and 19 deletions
+38 -4
View File
@@ -130,8 +130,10 @@ struct TextState {
/// Schrift des Plans: dieselbe Familie wie der Browser-Text (`--font` in
/// styles.css beginnt mit "Inter"); cosmic-text laedt die Systemfonts und
/// faellt bei fehlender Familie auf sans-serif zurueck.
const TEXT_FAMILY: &str = "Inter";
/// faellt bei fehlender Familie auf sans-serif zurueck. Standardwert des
/// `Renderer.text_family`-Feldes — im Web-Pfad (kein System-Font-Zugriff) wird
/// er auf die Familie der eingebetteten Font-Bytes umgestellt (siehe `load_font`).
const DEFAULT_TEXT_FAMILY: &str = "Inter";
/// Der native 2D-Renderer: haelt beide Pipelines, den (dynamischen) Uniform-
/// Puffer und die aktuell hochgeladene Szene. Pan/Zoom aktualisiert nur die
@@ -158,6 +160,12 @@ pub struct Renderer {
msaa_size: (u32, u32),
/// Glyphen-Textpass (lazily beim ersten Frame mit Texten erstellt).
text: Option<TextState>,
/// Schriftfamilie fuer die Textzeilen (Default `DEFAULT_TEXT_FAMILY`). Wird im
/// Web-Pfad auf die Familie der eingebetteten Font-Bytes umgestellt.
text_family: String,
/// Noch nicht in die FontSystem-DB geladene Font-Bytes (Web-Pfad: es gibt
/// keine Systemfonts). Werden beim ersten `ensure_text` eingespielt.
pending_font: Option<Vec<u8>>,
/// Loeschfarbe (Zeichenblatt), Default hell wie im WebGL-Pfad (#f0f0f0).
pub clear_color: wgpu::Color,
/// Papier-Massstab-Nenner (1:N) fuer echte mm-Strichbreiten.
@@ -392,6 +400,8 @@ impl Renderer {
msaa_view: None,
msaa_size: (0, 0),
text: None,
text_family: DEFAULT_TEXT_FAMILY.to_string(),
pending_font: None,
// #f0f0f0 (== --sheet). 0.941 entspricht dem WebGL-clearColor.
clear_color: wgpu::Color {
r: 0.941,
@@ -458,6 +468,13 @@ impl Renderer {
});
}
/// Reicht Font-Bytes (TrueType/OpenType) nach, die beim ersten `ensure_text`
/// in die FontSystem-DB geladen werden. Noetig im Web-Pfad, wo cosmic-text
/// KEINE Systemfonts findet — ohne das bliebe der Grundriss textlos.
pub fn load_font(&mut self, bytes: Vec<u8>) {
self.pending_font = Some(bytes);
}
/// Erstellt den Glyphen-Textpass beim ersten Bedarf (FontSystem laedt die
/// Systemfonts einmalig). Der TextRenderer MUSS mit demselben
/// MultisampleState wie die Geometrie-Pipelines gebaut werden (MSAA 4x),
@@ -466,6 +483,20 @@ impl Renderer {
if self.text.is_some() {
return;
}
let mut font_system = glyphon::FontSystem::new();
// Nachgereichte Font-Bytes (Web-Pfad) in die DB laden und die tatsaechlich
// registrierte Familie als Text-Familie uebernehmen (die Inter-Variable-
// Datei meldet sich z.B. als "Inter Variable", nicht "Inter").
if let Some(bytes) = self.pending_font.take() {
let db = font_system.db_mut();
let before: Vec<_> = db.faces().map(|f| f.id).collect();
db.load_font_data(bytes);
if let Some(face) = db.faces().find(|f| !before.contains(&f.id)) {
if let Some((name, _)) = face.families.first() {
self.text_family = name.clone();
}
}
}
let cache = glyphon::Cache::new(device);
let viewport = glyphon::Viewport::new(device, &cache);
let mut atlas = glyphon::TextAtlas::new(device, queue, &cache, self.format);
@@ -480,7 +511,7 @@ impl Renderer {
None,
);
self.text = Some(TextState {
font_system: glyphon::FontSystem::new(),
font_system,
swash_cache: glyphon::SwashCache::new(),
viewport,
atlas,
@@ -615,6 +646,9 @@ impl Renderer {
.map_or(false, |s| !s.texts.is_empty());
if has_texts || self.text.is_some() {
self.ensure_text(device, queue);
// Familie VOR dem mutablen Borrow von self.text kopieren (ensure_text
// hat sie ggf. auf die geladene Font-Familie umgestellt).
let family = self.text_family.clone();
let ts = self.text.as_mut().expect("ensure_text");
let cvb = corrected_view_box(view_box, vw, vh);
let meet = meet_scale(view_box, vw, vh);
@@ -629,7 +663,7 @@ impl Renderer {
let mut placed: Vec<Placed> = Vec::new();
if let Some(scene) = &self.scene {
let attrs = glyphon::Attrs::new()
.family(glyphon::Family::Name(TEXT_FAMILY));
.family(glyphon::Family::Name(&family));
for t in &scene.texts {
let font_px = t.size_mm * mm_px;
// Unlesbar kleine Glyphen ueberspringen (spart Shaping/Atlas).