diff --git a/package.json b/package.json index 5a4b1e3..d299165 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "electron": "scripts/electron-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", "build:engine3d": "wasm-pack build src-tauri/render3d --release --target web --out-dir ../../src/engine/pkg3d --out-name render3d --no-default-features --features web", - "build:geometry": "wasm-pack build src-tauri/geometry --release --target web --out-dir ../../src/engine/pkgGeometry --out-name geometry --no-default-features --features web" + "build:geometry": "wasm-pack build src-tauri/geometry --release --target web --out-dir ../../src/engine/pkgGeometry --out-name geometry --no-default-features --features web", + "build:kernel2d": "wasm-pack build src-tauri/kernel2d --release --target web --out-dir ../../src/engine/pkgKernel2d --out-name kernel2d --no-default-features --features web" }, "dependencies": { "@mlightcad/libredwg-web": "^0.7.7", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4cd2716..8b0ec6e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -6,7 +6,9 @@ members = ["."] # hier ausschliessen, damit `cad-tauri` sie dennoch per Pfad als Abhaengigkeit # nutzen kann. geometry: neu ausgeschlossen (war Member), seit es zu WASM gebaut # wird und das TS-Frontend die Joins direkt per WASM aufruft (statt TS-Duplikat). -exclude = ["render2d", "render3d", "geometry"] +# kernel2d: analog — eigenstaendiger 2D-Geometrie-Kern (Port von kernel2d.ts), +# per wasm-pack (Feature "web") zu WASM gebaut, ausserhalb des cad-tauri-Workspace. +exclude = ["render2d", "render3d", "geometry", "kernel2d"] [package] name = "cad-tauri" diff --git a/src-tauri/kernel2d/Cargo.lock b/src-tauri/kernel2d/Cargo.lock new file mode 100644 index 0000000..e379c23 --- /dev/null +++ b/src-tauri/kernel2d/Cargo.lock @@ -0,0 +1,195 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "kernel2d" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "robust", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src-tauri/kernel2d/Cargo.toml b/src-tauri/kernel2d/Cargo.toml new file mode 100644 index 0000000..8ba44b8 --- /dev/null +++ b/src-tauri/kernel2d/Cargo.toml @@ -0,0 +1,40 @@ +# Eigener leerer Workspace-Block: entkoppelt kernel2d vollstaendig vom +# cad-tauri-Workspace (src-tauri/Cargo.toml), damit `cargo test`/`wasm-pack` +# aus diesem Verzeichnis heraus nicht faelschlich dessen Workspace erben. +# Muster: render2d/render3d. (Parent-`exclude` allein greift beim Bauen aus +# dem Unterverzeichnis nicht zuverlaessig.) +[workspace] + +[package] +name = "kernel2d" +version = "0.1.0" +edition = "2021" +description = "2D-Geometrie-Kern (Port von src/geometry/kernel2d.ts) — handgeschriebene f64-Mathematik, headless per `cargo test` UND per wasm-pack (Feature \"web\") zu WASM baubar. Hinter identischer TS-Fassade; TS-Legacy bleibt Differential-Referenz." + +# cdylib: von wasm-pack (Feature "web") fuer das .wasm-Modul. rlib: als +# Pfad-Abhaengigkeit und fuer den Test-/Example-Build (parity). Muster: +# render2d/render3d/geometry. +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# Standard: reiner f64-Rechenkern, headless per `cargo test` pruefbar. +default = [] +# Browser-Bindings: dieselbe Geometrie hinter wasm-bindgen-Batch-Fassaden, +# aus TS via wasm-pack aufgerufen. Nur fuer wasm32-unknown-unknown sinnvoll. +web = ["dep:wasm-bindgen", "dep:serde_json", "dep:console_error_panic_hook"] +# ADDITIV, NICHT im web-Default: exakte Orientierungs-Praedikate (robust::orient2d) +# nur intern fuer Korrektheits-Golden-Cases (detectRooms/point-in-polygon). Der +# Zufalls-Diff-Test gegen TS-naiv laeuft OHNE dieses Feature (siehe PORT_PLAN §3). +robust-predicates = ["dep:robust"] + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = { version = "1", optional = true } +wasm-bindgen = { version = "0.2", optional = true } +console_error_panic_hook = { version = "0.1", optional = true } +robust = { version = "1", optional = true } + +# Nur fuers Paritaets-Beispiel (examples/parity.rs) — JSON von stdin lesen. +[dev-dependencies] +serde_json = "1" diff --git a/src-tauri/kernel2d/src/lib.rs b/src-tauri/kernel2d/src/lib.rs new file mode 100644 index 0000000..19e2701 --- /dev/null +++ b/src-tauri/kernel2d/src/lib.rs @@ -0,0 +1,163 @@ +// kernel2d — Rust/WASM-Port von `src/geometry/kernel2d.ts` (+ reine Geometrie aus +// room/ceiling/roomArea/stair). Handgeschriebene f64-Mathematik, KEINE externen +// Geometrie-Crates: Akzeptanzkriterium ist Differential-Paritaet gegen die naive +// TS-Routine (siehe PORT_PLAN.md). Fremd-Crates mit anderem Algorithmus braechen +// die Paritaet per Konstruktion. +// +// Aufbau (waechst ueber die Phasen des PORT_PLAN): +// - Phase 1 (hier): Vec2 + Vektor-Helfer (Port von src/model/geometry.ts) + +// leere Batch-WASM-Fassade. `cargo test` + `build:kernel2d` gruen. +// - Phase 2+: Schnitt/Offset/Trim/Fillet/Fläche/Kreis/detectRooms/… . +// +// KRITISCHE PARITAETS-REGELN (PORT_PLAN §6), gelten fuer den ganzen Port: +// - `len` = Math.hypot → `f64::hypot` (NICHT (x²+y²).sqrt()). +// - `normalize` Null-Guard: `len || 1` → `if l==0.0 {1.0} else {l}` (Ergebnis +// {0,0}, kein NaN). +// - Zwei Epsilons: EPS=1e-7 (kernel2d) UND hartkodiert 1e-9 in lineIntersect. +// - Term-Reihenfolge in cross/signedArea/Diskriminante exakt beibehalten +// (f64 nicht assoziativ; kein Kahan/Reorder). + +use serde::{Deserialize, Serialize}; + +/// EPS aus kernel2d.ts (Primitive/Schnitt/Trim). ACHTUNG: `lineIntersect` +/// benutzt bewusst ein ANDERES, hartkodiertes 1e-9 — nicht dieses EPS. +pub const EPS: f64 = 1e-7; + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] +pub struct Vec2 { + pub x: f64, + pub y: f64, +} + +impl Vec2 { + pub const fn new(x: f64, y: f64) -> Self { + Vec2 { x, y } + } +} + +// --- Vektor-Helfer: 1:1-Port aus src/model/geometry.ts ----------------------- + +#[inline] +pub fn sub(a: Vec2, b: Vec2) -> Vec2 { + Vec2 { x: a.x - b.x, y: a.y - b.y } +} + +#[inline] +pub fn add(a: Vec2, b: Vec2) -> Vec2 { + Vec2 { x: a.x + b.x, y: a.y + b.y } +} + +#[inline] +pub fn scale(a: Vec2, s: f64) -> Vec2 { + Vec2 { x: a.x * s, y: a.y * s } +} + +/// `len` = `Math.hypot` → `f64::hypot` (NICHT sqrt(x²+y²), siehe §6). +#[inline] +pub fn len(a: Vec2) -> f64 { + a.x.hypot(a.y) +} + +/// Null-Guard wie TS `len(a) || 1`: bei Laenge 0 → Divisor 1 (Ergebnis {0,0}). +#[inline] +pub fn normalize(a: Vec2) -> Vec2 { + let l = len(a); + let l = if l == 0.0 { 1.0 } else { l }; + Vec2 { x: a.x / l, y: a.y / l } +} + +/// Linke Normale (90° gegen den Uhrzeigersinn gedreht). +#[inline] +pub fn left_normal(a: Vec2) -> Vec2 { + Vec2 { x: -a.y, y: a.x } +} + +/// Kreuzprodukt (Z-Komponente). Term-Reihenfolge exakt wie TS: `p.x*q.y - p.y*q.x`. +#[inline] +pub fn cross(p: Vec2, q: Vec2) -> f64 { + p.x * q.y - p.y * q.x +} + +/// Skalarprodukt. Term-Reihenfolge exakt wie TS: `p.x*q.x + p.y*q.y`. +#[inline] +pub fn dot(p: Vec2, q: Vec2) -> f64 { + p.x * q.x + p.y * q.y +} + +/// Schnittpunkt der Geraden (a + t·da) mit (b + s·db). None bei (nahezu) +/// parallelen Richtungen. HARTKODIERTES 1e-9 (nicht EPS!) — der Offset-Miter- +/// Fallback haengt an genau dieser Schwelle (§6). +pub fn line_intersect(a: Vec2, da: Vec2, b: Vec2, db: Vec2) -> Option { + let denom = cross(da, db); + if denom.abs() < 1e-9 { + return None; // parallel → kein Schnitt + } + let t = cross(sub(b, a), db) / denom; + Some(add(a, scale(da, t))) +} + +// --- Batch-WASM-Fassade (Feature "web") -------------------------------------- +// Phase 1: nur ein Versions-/Ping-Export, um die WASM-Grenze + das Tooling +// (wasm-pack → pkgKernel2d → Vite/vitest) end-to-end gruen zu bekommen. Die +// echten Batch-Fassaden (offset_polylines_json, intersect_batch_json, …) kommen +// ab Phase 2, Muster: geometry::compute_joins_json (JSON rein/raus, O(n)-Grenze). + +/// Ping-Export: beweist die WASM-Grenze. Nimmt ein JSON-`Vec2`, spiegelt es +/// normalisiert zurueck — genug, um Init + JSON-Marshalling im vitest-Harness +/// zu verifizieren, bevor die echten Operationen landen. +#[cfg(feature = "web")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn kernel2d_normalize_json(input_json: &str) -> Result { + console_error_panic_hook::set_once(); + let v: Vec2 = serde_json::from_str(input_json) + .map_err(|e| wasm_bindgen::JsValue::from_str(&e.to_string()))?; + let out = normalize(v); + serde_json::to_string(&out).map_err(|e| wasm_bindgen::JsValue::from_str(&e.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + + const T: f64 = 1e-12; + + #[test] + fn len_uses_hypot() { + assert!((len(Vec2::new(3.0, 4.0)) - 5.0).abs() < T); + } + + #[test] + fn normalize_zero_guard_yields_origin_not_nan() { + let n = normalize(Vec2::new(0.0, 0.0)); + assert_eq!(n, Vec2::new(0.0, 0.0)); + } + + #[test] + fn cross_dot_term_order() { + let p = Vec2::new(1.0, 2.0); + let q = Vec2::new(3.0, 4.0); + assert!((cross(p, q) - (1.0 * 4.0 - 2.0 * 3.0)).abs() < T); + assert!((dot(p, q) - (1.0 * 3.0 + 2.0 * 4.0)).abs() < T); + } + + #[test] + fn line_intersect_parallel_is_none() { + let a = Vec2::new(0.0, 0.0); + let da = Vec2::new(1.0, 0.0); + let b = Vec2::new(0.0, 1.0); + let db = Vec2::new(1.0, 0.0); + assert!(line_intersect(a, da, b, db).is_none()); + } + + #[test] + fn line_intersect_crossing() { + let hit = line_intersect( + Vec2::new(0.0, 0.0), + Vec2::new(1.0, 0.0), + Vec2::new(2.0, -1.0), + Vec2::new(0.0, 1.0), + ) + .unwrap(); + assert!((hit.x - 2.0).abs() < T && hit.y.abs() < T); + } +}