kernel2d-Port Phase 1: Crate-Skelett + WASM-Fassade + build:kernel2d
- src-tauri/kernel2d: eigenstaendiges Crate (cdylib+rlib, eigener leerer [workspace]), Feature web (wasm-bindgen) und additives robust-predicates. - Vektor-Helfer 1:1 aus src/model/geometry.ts portiert (hypot-len, normalize-Nullguard, hartkodiertes 1e-9 in line_intersect) + Unit-Tests. - Leere Batch-Fassade kernel2d_normalize_json als WASM-Grenzen-Ping. - package.json: build:kernel2d; src-tauri/Cargo.toml: workspace-exclude. - PORT_PLAN.md: Portierungsplan (Scope, Crate-vs-Port, Diff-Harness, Phasen).
This commit is contained in:
@@ -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"
|
||||
|
||||
Generated
+195
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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<Vec2> {
|
||||
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<String, wasm_bindgen::JsValue> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user