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:
Generated
+92
-1
@@ -31,7 +31,8 @@
|
|||||||
"puppeteer": "^25.2.1",
|
"puppeteer": "^25.2.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.4.8",
|
||||||
"vitest": "^4.1.9"
|
"vitest": "^4.1.9",
|
||||||
|
"wasm-pack": "^0.15.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -750,6 +751,19 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -2206,6 +2220,16 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chromium-bidi": {
|
"node_modules/chromium-bidi": {
|
||||||
"version": "16.0.1",
|
"version": "16.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
|
||||||
@@ -2986,6 +3010,29 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mitt": {
|
"node_modules/mitt": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
@@ -3577,6 +3624,33 @@
|
|||||||
"url": "https://github.com/fontello/svg2ttf?sponsor=1"
|
"url": "https://github.com/fontello/svg2ttf?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar": {
|
||||||
|
"version": "7.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz",
|
||||||
|
"integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
|
"chownr": "^3.0.0",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"minizlib": "^3.1.0",
|
||||||
|
"yallist": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar/node_modules/yallist": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-segmentation": {
|
"node_modules/text-segmentation": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
@@ -3968,6 +4042,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wasm-pack": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.15.0.tgz",
|
||||||
|
"integrity": "sha512-DdqtGWc3+iFx+7lL7QU5LBWs7qMnwQSWxF0htSfE15sNa3roVwHjAkTm2JXgueU2GGfSwNqbq2EzyC2b/biKDA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tar": "^7.5.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"wasm-pack": "run.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webdriver-bidi-protocol": {
|
"node_modules/webdriver-bidi-protocol": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
|
||||||
|
|||||||
+4
-2
@@ -13,7 +13,8 @@
|
|||||||
"tauri:dev": "tauri dev",
|
"tauri:dev": "tauri dev",
|
||||||
"tauri:build": "tauri build",
|
"tauri:build": "tauri build",
|
||||||
"dump:native": "node scripts/dump-native-scene.mjs",
|
"dump:native": "node scripts/dump-native-scene.mjs",
|
||||||
"shell": "scripts/chromium-shell.sh"
|
"shell": "scripts/chromium-shell.sh",
|
||||||
|
"build:engine": "wasm-pack build src-tauri/render2d --release --target web --out-dir ../../src/engine/pkg --out-name render2d --no-default-features --features web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mlightcad/libredwg-web": "^0.7.7",
|
"@mlightcad/libredwg-web": "^0.7.7",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"puppeteer": "^25.2.1",
|
"puppeteer": "^25.2.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.4.8",
|
||||||
"vitest": "^4.1.9"
|
"vitest": "^4.1.9",
|
||||||
|
"wasm-pack": "^0.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,11 +77,19 @@ trap cleanup EXIT
|
|||||||
|
|
||||||
mkdir -p "${PROFILE_DIR}"
|
mkdir -p "${PROFILE_DIR}"
|
||||||
|
|
||||||
echo "chromium-shell: öffne ${URL} in ${BROWSER_BIN} (App-Modus) …"
|
# WebGPU: Auf Linux liegt WebGPU in Chromium noch hinter Flags. Für den
|
||||||
|
# render2d-WASM-Viewport (`?engine=wasm`, useWasmPlanRenderer) muss `navigator.gpu`
|
||||||
|
# vorhanden sein — daher `--enable-unsafe-webgpu` (aktiviert die API auch ohne
|
||||||
|
# volle Konformität) plus `--enable-features=Vulkan` (WebGPU nutzt unter Linux das
|
||||||
|
# Vulkan-Backend). Ohne `?engine=wasm` sind die Flags folgenlos (der Standard-
|
||||||
|
# WebGL2-Pfad läuft unverändert weiter).
|
||||||
|
echo "chromium-shell: öffne ${URL} in ${BROWSER_BIN} (App-Modus, WebGPU aktiv) …"
|
||||||
"${BROWSER_BIN}" \
|
"${BROWSER_BIN}" \
|
||||||
--app="${URL}" \
|
--app="${URL}" \
|
||||||
--user-data-dir="${PROFILE_DIR}" \
|
--user-data-dir="${PROFILE_DIR}" \
|
||||||
--window-size=1600,1000 \
|
--window-size=1600,1000 \
|
||||||
|
--enable-unsafe-webgpu \
|
||||||
|
--enable-features=Vulkan \
|
||||||
>/tmp/cad-chromium-shell-browser.log 2>&1
|
>/tmp/cad-chromium-shell-browser.log 2>&1
|
||||||
|
|
||||||
# Läuft weiter, bis der Browserprozess (das App-Fenster) beendet wird; dann
|
# Läuft weiter, bis der Browserprozess (das App-Fenster) beendet wird; dann
|
||||||
|
|||||||
Generated
+15
@@ -618,6 +618,16 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -3648,12 +3658,17 @@ name = "render2d"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
"console_error_panic_hook",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"glyphon",
|
"glyphon",
|
||||||
|
"js-sys",
|
||||||
"naga",
|
"naga",
|
||||||
"pollster",
|
"pollster",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Nativer wgpu-2D-Renderer fuer den CAD-Grundriss (Tessellierung + Pipelines)"
|
description = "Nativer wgpu-2D-Renderer fuer den CAD-Grundriss (Tessellierung + Pipelines)"
|
||||||
|
|
||||||
|
# cdylib: von wasm-pack (Feature "web") fuer das .wasm-Modul benoetigt. rlib:
|
||||||
|
# damit die Crate weiterhin als Pfad-Abhaengigkeit (cad-tauri, Feature native2d)
|
||||||
|
# und im Test-/Bin-Build (spike) nutzbar bleibt. Der cdylib-Artefakt-Build auf
|
||||||
|
# nativen Zielen ist harmlos (leere Export-Oberflaeche ohne Feature "web").
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
# Bewusst NICHT Teil des src-tauri-Workspaces: die Tessellierungs-Bibliothek soll
|
# Bewusst NICHT Teil des src-tauri-Workspaces: die Tessellierungs-Bibliothek soll
|
||||||
# unabhaengig von Tauri baubar/testbar bleiben (headless, ohne Webview-Toolchain).
|
# unabhaengig von Tauri baubar/testbar bleiben (headless, ohne Webview-Toolchain).
|
||||||
# Ausschluss laeuft ueber `exclude = ["render2d"]` im uebergeordneten
|
# Ausschluss laeuft ueber `exclude = ["render2d"]` im uebergeordneten
|
||||||
@@ -31,8 +38,25 @@ render = ["dep:wgpu", "dep:bytemuck", "dep:pollster", "dep:glyphon"]
|
|||||||
# Display-Session zur visuellen Verifikation.
|
# Display-Session zur visuellen Verifikation.
|
||||||
window = ["render", "dep:winit", "dep:env_logger"]
|
window = ["render", "dep:winit", "dep:env_logger"]
|
||||||
|
|
||||||
|
# Browser-Bindings (wasm32 + WebGPU): dieselbe GPU-Schicht (`render`) hinter einer
|
||||||
|
# wasm-bindgen-Fassade (`web.rs`), die eine HTML-Canvas als wgpu-Surface bespielt.
|
||||||
|
# Baut NUR fuer target wasm32-unknown-unknown sinnvoll (wgpu waehlt dort das
|
||||||
|
# WebGPU-Backend). Die native Fensterschicht (`window`) bleibt davon unberuehrt.
|
||||||
|
web = [
|
||||||
|
"render",
|
||||||
|
"dep:wasm-bindgen",
|
||||||
|
"dep:wasm-bindgen-futures",
|
||||||
|
"dep:web-sys",
|
||||||
|
"dep:js-sys",
|
||||||
|
"dep:console_error_panic_hook",
|
||||||
|
"dep:serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
# JSON-Szene (dieselbe Ableitung wie der native Push): im Web-Pfad zur Laufzeit
|
||||||
|
# geparst; optional, nur mit Feature "web".
|
||||||
|
serde_json = { version = "1", optional = true }
|
||||||
|
|
||||||
# --- GPU (nur mit Feature "render") ------------------------------------------
|
# --- GPU (nur mit Feature "render") ------------------------------------------
|
||||||
# Standard-Features von wgpu (wgsl, webgpu, dx12, metal). Die Linux-Backends
|
# Standard-Features von wgpu (wgsl, webgpu, dx12, metal). Die Linux-Backends
|
||||||
@@ -49,6 +73,20 @@ glyphon = { version = "0.6", optional = true }
|
|||||||
winit = { version = "0.30", optional = true }
|
winit = { version = "0.30", optional = true }
|
||||||
env_logger = { version = "0.11", optional = true }
|
env_logger = { version = "0.11", optional = true }
|
||||||
|
|
||||||
|
# --- Browser/WASM (nur mit Feature "web") ------------------------------------
|
||||||
|
# wasm-bindgen-Fassade + Canvas-Zugriff. Versionen bewusst breit ("0.2"/"0.3"),
|
||||||
|
# damit wasm-pack die passende wasm-bindgen-CLI selbst zieht.
|
||||||
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
|
wasm-bindgen-futures = { version = "0.4", optional = true }
|
||||||
|
js-sys = { version = "0.3", optional = true }
|
||||||
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
|
web-sys = { version = "0.3", optional = true, features = [
|
||||||
|
"HtmlCanvasElement",
|
||||||
|
"Window",
|
||||||
|
"Document",
|
||||||
|
"console",
|
||||||
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
# naga validiert die WGSL-Quellen headless (Parser + Validator) im Test, ohne
|
# naga validiert die WGSL-Quellen headless (Parser + Validator) im Test, ohne
|
||||||
|
|||||||
Binary file not shown.
@@ -130,8 +130,10 @@ struct TextState {
|
|||||||
|
|
||||||
/// Schrift des Plans: dieselbe Familie wie der Browser-Text (`--font` in
|
/// Schrift des Plans: dieselbe Familie wie der Browser-Text (`--font` in
|
||||||
/// styles.css beginnt mit "Inter"); cosmic-text laedt die Systemfonts und
|
/// styles.css beginnt mit "Inter"); cosmic-text laedt die Systemfonts und
|
||||||
/// faellt bei fehlender Familie auf sans-serif zurueck.
|
/// faellt bei fehlender Familie auf sans-serif zurueck. Standardwert des
|
||||||
const TEXT_FAMILY: &str = "Inter";
|
/// `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-
|
/// Der native 2D-Renderer: haelt beide Pipelines, den (dynamischen) Uniform-
|
||||||
/// Puffer und die aktuell hochgeladene Szene. Pan/Zoom aktualisiert nur die
|
/// Puffer und die aktuell hochgeladene Szene. Pan/Zoom aktualisiert nur die
|
||||||
@@ -158,6 +160,12 @@ pub struct Renderer {
|
|||||||
msaa_size: (u32, u32),
|
msaa_size: (u32, u32),
|
||||||
/// Glyphen-Textpass (lazily beim ersten Frame mit Texten erstellt).
|
/// Glyphen-Textpass (lazily beim ersten Frame mit Texten erstellt).
|
||||||
text: Option<TextState>,
|
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).
|
/// Loeschfarbe (Zeichenblatt), Default hell wie im WebGL-Pfad (#f0f0f0).
|
||||||
pub clear_color: wgpu::Color,
|
pub clear_color: wgpu::Color,
|
||||||
/// Papier-Massstab-Nenner (1:N) fuer echte mm-Strichbreiten.
|
/// Papier-Massstab-Nenner (1:N) fuer echte mm-Strichbreiten.
|
||||||
@@ -392,6 +400,8 @@ impl Renderer {
|
|||||||
msaa_view: None,
|
msaa_view: None,
|
||||||
msaa_size: (0, 0),
|
msaa_size: (0, 0),
|
||||||
text: None,
|
text: None,
|
||||||
|
text_family: DEFAULT_TEXT_FAMILY.to_string(),
|
||||||
|
pending_font: None,
|
||||||
// #f0f0f0 (== --sheet). 0.941 entspricht dem WebGL-clearColor.
|
// #f0f0f0 (== --sheet). 0.941 entspricht dem WebGL-clearColor.
|
||||||
clear_color: wgpu::Color {
|
clear_color: wgpu::Color {
|
||||||
r: 0.941,
|
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
|
/// Erstellt den Glyphen-Textpass beim ersten Bedarf (FontSystem laedt die
|
||||||
/// Systemfonts einmalig). Der TextRenderer MUSS mit demselben
|
/// Systemfonts einmalig). Der TextRenderer MUSS mit demselben
|
||||||
/// MultisampleState wie die Geometrie-Pipelines gebaut werden (MSAA 4x),
|
/// MultisampleState wie die Geometrie-Pipelines gebaut werden (MSAA 4x),
|
||||||
@@ -466,6 +483,20 @@ impl Renderer {
|
|||||||
if self.text.is_some() {
|
if self.text.is_some() {
|
||||||
return;
|
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 cache = glyphon::Cache::new(device);
|
||||||
let viewport = glyphon::Viewport::new(device, &cache);
|
let viewport = glyphon::Viewport::new(device, &cache);
|
||||||
let mut atlas = glyphon::TextAtlas::new(device, queue, &cache, self.format);
|
let mut atlas = glyphon::TextAtlas::new(device, queue, &cache, self.format);
|
||||||
@@ -480,7 +511,7 @@ impl Renderer {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
self.text = Some(TextState {
|
self.text = Some(TextState {
|
||||||
font_system: glyphon::FontSystem::new(),
|
font_system,
|
||||||
swash_cache: glyphon::SwashCache::new(),
|
swash_cache: glyphon::SwashCache::new(),
|
||||||
viewport,
|
viewport,
|
||||||
atlas,
|
atlas,
|
||||||
@@ -615,6 +646,9 @@ impl Renderer {
|
|||||||
.map_or(false, |s| !s.texts.is_empty());
|
.map_or(false, |s| !s.texts.is_empty());
|
||||||
if has_texts || self.text.is_some() {
|
if has_texts || self.text.is_some() {
|
||||||
self.ensure_text(device, queue);
|
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 ts = self.text.as_mut().expect("ensure_text");
|
||||||
let cvb = corrected_view_box(view_box, vw, vh);
|
let cvb = corrected_view_box(view_box, vw, vh);
|
||||||
let meet = meet_scale(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();
|
let mut placed: Vec<Placed> = Vec::new();
|
||||||
if let Some(scene) = &self.scene {
|
if let Some(scene) = &self.scene {
|
||||||
let attrs = glyphon::Attrs::new()
|
let attrs = glyphon::Attrs::new()
|
||||||
.family(glyphon::Family::Name(TEXT_FAMILY));
|
.family(glyphon::Family::Name(&family));
|
||||||
for t in &scene.texts {
|
for t in &scene.texts {
|
||||||
let font_px = t.size_mm * mm_px;
|
let font_px = t.size_mm * mm_px;
|
||||||
// Unlesbar kleine Glyphen ueberspringen (spart Shaping/Atlas).
|
// Unlesbar kleine Glyphen ueberspringen (spart Shaping/Atlas).
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ pub mod types;
|
|||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
pub mod gpu;
|
pub mod gpu;
|
||||||
|
|
||||||
|
// Browser-Bindings (wasm32 + WebGPU), nur mit Feature "web". Setzt "render" voraus
|
||||||
|
// (dieselbe GPU-Schicht wie das native Fenster), fuegt aber KEINE winit-Abhaengigkeit
|
||||||
|
// hinzu — die Canvas dient direkt als wgpu-Surface.
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
pub use demo::{demo_scene, initial_view_box};
|
pub use demo::{demo_scene, initial_view_box};
|
||||||
pub use ortho::{compute_ortho_matrix, meet_scale, mm_to_device_px, Mat4};
|
pub use ortho::{compute_ortho_matrix, meet_scale, mm_to_device_px, Mat4};
|
||||||
pub use tessellate::{
|
pub use tessellate::{
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Browser-Bindings (wasm32 + WebGPU): fuehrt die native GPU-Schicht (`gpu::Renderer`)
|
||||||
|
// als WASM-Modul in die App-UI, mit einer HTML-Canvas als wgpu-Surface. Bewusst
|
||||||
|
// duenn — die gesamte Zeichenlogik (Tessellierung, Pipelines, Text) ist mit dem
|
||||||
|
// nativen Fenster geteilt; hier liegt nur die Canvas-/WebGPU-Anbindung + die
|
||||||
|
// wasm-bindgen-Fassade.
|
||||||
|
//
|
||||||
|
// Ablauf im Frontend (siehe src/plan/useWasmPlanRenderer.ts):
|
||||||
|
// const r = await WebPlanRenderer.new(canvas, undefined);
|
||||||
|
// r.set_scene(JSON.stringify(planToRenderScene(plan)));
|
||||||
|
// r.set_view_box(x, y, w, h); r.resize(px_w, px_h); r.render();
|
||||||
|
//
|
||||||
|
// Szenenformat = dieselbe serde-Struct wie der native Push (render2d::types::Scene),
|
||||||
|
// als JSON-String hereingereicht. Pan/Zoom aendern nur die ViewBox (kein Re-Tess).
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::HtmlCanvasElement;
|
||||||
|
|
||||||
|
use crate::gpu::Renderer;
|
||||||
|
use crate::types::{Scene, ViewBox};
|
||||||
|
|
||||||
|
/// Eingebettete Plan-Schrift (Inter). cosmic-text findet auf wasm KEINE
|
||||||
|
/// Systemfonts — ohne eingebettete Bytes bliebe der Grundriss textlos. Die
|
||||||
|
/// Familie meldet sich als "Inter Variable"; `Renderer::load_font` uebernimmt
|
||||||
|
/// den tatsaechlichen Namen automatisch.
|
||||||
|
const INTER_TTF: &[u8] = include_bytes!("../assets/Inter.ttf");
|
||||||
|
|
||||||
|
/// Der Browser-Renderer: haelt Surface + Device/Queue + den geteilten `Renderer`
|
||||||
|
/// und die aktuelle ViewBox. Wird per `WebPlanRenderer.new(canvas)` aus JS erzeugt.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct WebPlanRenderer {
|
||||||
|
surface: wgpu::Surface<'static>,
|
||||||
|
device: wgpu::Device,
|
||||||
|
queue: wgpu::Queue,
|
||||||
|
config: wgpu::SurfaceConfiguration,
|
||||||
|
renderer: Renderer,
|
||||||
|
view_box: ViewBox,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl WebPlanRenderer {
|
||||||
|
/// Erzeugt den Renderer asynchron (Adapter/Device via WebGPU). `font_bytes`
|
||||||
|
/// optional — ohne Angabe wird die eingebettete Inter-Schrift genutzt.
|
||||||
|
/// Aus JS: `await WebPlanRenderer.new(canvasEl, undefined)`.
|
||||||
|
#[allow(clippy::new_ret_no_self)]
|
||||||
|
pub async fn new(
|
||||||
|
canvas: HtmlCanvasElement,
|
||||||
|
font_bytes: Option<Vec<u8>>,
|
||||||
|
) -> Result<WebPlanRenderer, JsValue> {
|
||||||
|
// Panics als lesbare Konsolen-Fehler statt "unreachable" ausgeben.
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
let width = canvas.width().max(1);
|
||||||
|
let height = canvas.height().max(1);
|
||||||
|
|
||||||
|
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::BROWSER_WEBGPU,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(wgpu::SurfaceTarget::Canvas(canvas))
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("create_surface: {e:?}")))?;
|
||||||
|
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| JsValue::from_str("kein WebGPU-Adapter (navigator.gpu?)"))?;
|
||||||
|
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(
|
||||||
|
&wgpu::DeviceDescriptor {
|
||||||
|
label: Some("render2d.web.device"),
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
required_limits: wgpu::Limits::downlevel_webgl2_defaults()
|
||||||
|
.using_resolution(adapter.limits()),
|
||||||
|
memory_hints: wgpu::MemoryHints::Performance,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("request_device: {e:?}")))?;
|
||||||
|
|
||||||
|
// WebGPU-Canvas liefert i.d.R. bgra8unorm (kein sRGB-Format fuer die
|
||||||
|
// Surface). Wir nehmen das erste angebotene Format — MSAA-Textur nutzt
|
||||||
|
// dasselbe (siehe Renderer::ensure_msaa).
|
||||||
|
let caps = surface.get_capabilities(&adapter);
|
||||||
|
let format = caps
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|f| !f.is_srgb())
|
||||||
|
.unwrap_or(caps.formats[0]);
|
||||||
|
let config = wgpu::SurfaceConfiguration {
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
present_mode: wgpu::PresentMode::Fifo,
|
||||||
|
alpha_mode: caps.alpha_modes[0],
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
let mut renderer = Renderer::new(&device, format);
|
||||||
|
// Font einspielen: uebergebene Bytes ODER die eingebettete Inter-Schrift.
|
||||||
|
renderer.load_font(font_bytes.unwrap_or_else(|| INTER_TTF.to_vec()));
|
||||||
|
|
||||||
|
web_sys::console::log_1(&JsValue::from_str(&format!(
|
||||||
|
"render2d WebGPU-Viewport initialisiert ({}x{}, {:?})",
|
||||||
|
width, height, format
|
||||||
|
)));
|
||||||
|
|
||||||
|
Ok(WebPlanRenderer {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
renderer,
|
||||||
|
// Platzhalter bis set_view_box; verhindert Division durch 0.
|
||||||
|
view_box: ViewBox::new(0.0, 0.0, width as f32, height as f32),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt die zu zeichnende Szene aus einem JSON-String (Format =
|
||||||
|
/// render2d::types::Scene, identisch zum nativen Push). Re-tesselliert + laedt
|
||||||
|
/// die GPU-Puffer; ruft NICHT selbst `render` — das macht der Aufrufer.
|
||||||
|
pub fn set_scene(&mut self, json: &str) -> Result<(), JsValue> {
|
||||||
|
let scene: Scene = serde_json::from_str(json)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Szene parsen: {e}")))?;
|
||||||
|
self.renderer.upload_scene(&self.device, &scene);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aktuellen Ausschnitt (viewBox-Einheiten, Bildschirm-Raum) setzen. Aendert
|
||||||
|
/// nur die Ortho-Matrix im naechsten `render` (kein Re-Tessellieren).
|
||||||
|
pub fn set_view_box(&mut self, x: f32, y: f32, w: f32, h: f32) {
|
||||||
|
self.view_box = ViewBox::new(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Papier-Massstab-Nenner (1:N) fuer echte mm-Strichbreiten setzen.
|
||||||
|
pub fn set_paper_scale(&mut self, n: f32) {
|
||||||
|
if n > 0.0 {
|
||||||
|
self.renderer.paper_scale_n = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Surface an eine neue Pixelgroesse anpassen (DPR beachtet der Aufrufer).
|
||||||
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
|
let (w, h) = (width.max(1), height.max(1));
|
||||||
|
if self.config.width == w && self.config.height == h {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.config.width = w;
|
||||||
|
self.config.height = h;
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zeichnet EINEN Frame in die Surface-Textur.
|
||||||
|
pub fn render(&mut self) {
|
||||||
|
let frame = match self.surface.get_current_texture() {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
web_sys::console::warn_1(&JsValue::from_str(&format!(
|
||||||
|
"render2d Surface-Fehler: {e:?}"
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let view = frame
|
||||||
|
.texture
|
||||||
|
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
self.renderer.render(
|
||||||
|
&self.device,
|
||||||
|
&self.queue,
|
||||||
|
&view,
|
||||||
|
self.view_box,
|
||||||
|
(self.config.width, self.config.height),
|
||||||
|
);
|
||||||
|
frame.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# wasm-pack-Ausgabe des render2d-Browser-Renderers (Feature "web"). Wird per
|
||||||
|
# `npm run build:engine` erzeugt und NICHT eingecheckt (Build-Artefakt, groß, an
|
||||||
|
# Toolchain gebunden). Der Quellcode liegt in src-tauri/render2d/src/web.rs.
|
||||||
|
pkg/
|
||||||
+33
-11
@@ -18,6 +18,7 @@ import { docToLines, type SvgLine } from "../text/renderHtml";
|
|||||||
import type { EdgeGrip, Vec2 } from "../model/types";
|
import type { EdgeGrip, Vec2 } from "../model/types";
|
||||||
import type { DraftShape, SnapResult, ToolHandlers, ToolId, ToolMods } from "../tools/types";
|
import type { DraftShape, SnapResult, ToolHandlers, ToolId, ToolMods } from "../tools/types";
|
||||||
import { useGlPlanRenderer } from "./useGlPlanRenderer";
|
import { useGlPlanRenderer } from "./useGlPlanRenderer";
|
||||||
|
import { useWasmPlanRenderer } from "./useWasmPlanRenderer";
|
||||||
|
|
||||||
const PX_PER_M = 90; // viewBox-Einheiten je Meter (Modell → SVG-Benutzerraum)
|
const PX_PER_M = 90; // viewBox-Einheiten je Meter (Modell → SVG-Benutzerraum)
|
||||||
const PAD = 60; // Rand in viewBox-Einheiten
|
const PAD = 60; // Rand in viewBox-Einheiten
|
||||||
@@ -410,19 +411,40 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
// WebGL2-GPU-Renderer ist der STANDARD-Pfad. Der SVG-Renderer bleibt als
|
// Engine-Auswahl. STANDARD-Pfad: WebGL2 (useGlPlanRenderer); der SVG-Renderer
|
||||||
// automatischer Fallback (nur falls WebGL2/Shader nicht verfügbar) — es gibt
|
// bleibt automatischer Fallback (nur falls WebGL2/Shader nicht verfügbar), `?gl=0`
|
||||||
// KEINE User-Umschaltung mehr. `?gl=0` erzwingt zu Debug-Zwecken reines SVG.
|
// erzwingt reines SVG. INTEGRATIONS-SPIKE: Mit `?engine=wasm` UND vorhandenem
|
||||||
const wantGpu =
|
// WebGPU (navigator.gpu) übernimmt statt WebGL2 der native render2d-Renderer als
|
||||||
|
// WASM/WebGPU-Viewport (useWasmPlanRenderer) — dieselbe Szene, dieselbe ViewBox,
|
||||||
|
// dasselbe SVG-Overlay (Text/Griffe/Auswahl) darüber. Fehlt WebGPU trotz Flag:
|
||||||
|
// stiller Fallback auf WebGL2 (+ Warnung).
|
||||||
|
const engineParam =
|
||||||
typeof window === "undefined"
|
typeof window === "undefined"
|
||||||
|
? null
|
||||||
|
: new URLSearchParams(window.location.search).get("engine");
|
||||||
|
const hasWebGpu = typeof navigator !== "undefined" && "gpu" in navigator;
|
||||||
|
const wantWasm = engineParam === "wasm" && hasWebGpu;
|
||||||
|
useEffect(() => {
|
||||||
|
if (engineParam === "wasm" && !hasWebGpu) {
|
||||||
|
console.warn("engine=wasm angefragt, aber navigator.gpu fehlt → WebGL2-Fallback");
|
||||||
|
}
|
||||||
|
}, [engineParam, hasWebGpu]);
|
||||||
|
// WebGL2 nur, wenn NICHT der WASM-Pfad aktiv ist (und `?gl!=0`).
|
||||||
|
const wantGl =
|
||||||
|
!wantWasm &&
|
||||||
|
(typeof window === "undefined"
|
||||||
? true
|
? true
|
||||||
: new URLSearchParams(window.location.search).get("gl") !== "0";
|
: new URLSearchParams(window.location.search).get("gl") !== "0");
|
||||||
const { render: renderGl, updateGeometry, ready: glReady } = useGlPlanRenderer(
|
// Beide Hooks unbedingt aufrufen (Hook-Regeln); je nach Flag ist genau einer aktiv.
|
||||||
canvasRef,
|
const glHook = useGlPlanRenderer(canvasRef, wantGl);
|
||||||
wantGpu,
|
const wasmHook = useWasmPlanRenderer(canvasRef, wantWasm);
|
||||||
);
|
const activeEngine = wantWasm ? wasmHook : glHook;
|
||||||
// GL WIRKLICH aktiv? Steuert das Umschalten SVG↔GL: bei false rendert der volle
|
const { render: renderGl, updateGeometry, ready: glReady } = activeEngine;
|
||||||
// SVG-Pfad (Fallback), bei true übernimmt die GL-Ebene + Text-Overlay.
|
// „GPU-Canvas montieren?" — beide GPU-Pfade brauchen das Canvas (an der ABSICHT
|
||||||
|
// montiert, nicht am ready, sonst könnte die Engine mangels Canvas nie starten).
|
||||||
|
const wantGpu = wantGl || wantWasm;
|
||||||
|
// Engine WIRKLICH aktiv? Steuert das Umschalten SVG↔GPU: bei false rendert der
|
||||||
|
// volle SVG-Pfad (Fallback), bei true übernimmt die GPU-Ebene + Text-Overlay.
|
||||||
const useGpuRenderer = wantGpu && glReady;
|
const useGpuRenderer = wantGpu && glReady;
|
||||||
|
|
||||||
// Stabiler Papier-Massstab-Nenner N (1:N) als REFERENZ für die Print-Strich-
|
// Stabiler Papier-Massstab-Nenner N (1:N) als REFERENZ für die Print-Strich-
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kompatibilitäts-Shim: wgpu 22 sendet in `requestDevice` noch das Limit
|
||||||
|
* `maxInterStageShaderComponents`, das AUS DER WebGPU-Spec ENTFERNT wurde. Neuere
|
||||||
|
* Chromium-Builds lehnen ein nicht-`undefined`-Wert dafür mit `OperationError`
|
||||||
|
* ab (→ Device-Erzeugung schlägt fehl). Wir entfernen den Schlüssel EINMAL global
|
||||||
|
* aus dem Descriptor, bevor wgpu ihn stellt. Entfällt, sobald render2d auf ein
|
||||||
|
* wgpu (+ glyphon) hochgezogen wird, das dieses Limit nicht mehr mitschickt.
|
||||||
|
*/
|
||||||
|
let requestDevicePatched = false;
|
||||||
|
function patchRequestDeviceLimits(): void {
|
||||||
|
if (requestDevicePatched) return;
|
||||||
|
const AdapterCtor = (globalThis as any).GPUAdapter;
|
||||||
|
if (!AdapterCtor?.prototype?.requestDevice) return;
|
||||||
|
const orig = AdapterCtor.prototype.requestDevice;
|
||||||
|
AdapterCtor.prototype.requestDevice = function (
|
||||||
|
this: unknown,
|
||||||
|
desc?: any,
|
||||||
|
) {
|
||||||
|
if (desc?.requiredLimits && "maxInterStageShaderComponents" in desc.requiredLimits) {
|
||||||
|
const requiredLimits = { ...desc.requiredLimits };
|
||||||
|
delete requiredLimits.maxInterStageShaderComponents;
|
||||||
|
desc = { ...desc, requiredLimits };
|
||||||
|
}
|
||||||
|
return orig.call(this, desc);
|
||||||
|
};
|
||||||
|
requestDevicePatched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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;
|
||||||
|
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 () => {
|
||||||
|
patchRequestDeviceLimits(); // wgpu-22-Limit-Shim (siehe oben)
|
||||||
|
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). */
|
||||||
|
const render = useCallback(
|
||||||
|
(viewBox: ViewBox, paperScaleN = 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_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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user