DWG/DXF-Import-Spike: acadrust baut zu wasm32 (Crate dwgimport)
- src-tauri/dwgimport: eigenstaendiges Crate (cdylib+rlib, eigener [workspace]), dep acadrust 0.4 (MPL-2.0, pure Rust). parse_dxf_summary(bytes) via DxfReader::from_reader + Cursor<Vec<u8>> -> JSON (version/entityCount/ entityTypeCounts/layerNames). wasm-Fassade parse_dxf_summary_json. - getrandom wasm_js-Feature noetig (transitiv via ahash) -> als Dep gesetzt. - Kernbeweis: acadrust kompiliert nach wasm32 (839 KB), DXF-Parse headless getestet (5 Entities korrekt). Beweist den DWG/DXF-Import-Weg im Rust/WASM-Kern. - build:dwgimport-Script, src-tauri/Cargo.toml exclude erweitert.
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
// dwgimport — SPIKE: beweist, dass acadrust (MPL-2.0) aus einem Byte-Buffer
|
||||
// ein DXF parst und nach wasm32-unknown-unknown kompiliert.
|
||||
//
|
||||
// Aufbau:
|
||||
// - `parse_dxf_summary`: headless, feature-frei testbar.
|
||||
// - `#[cfg(feature="web")]` wasm-bindgen-Fassade darueber.
|
||||
//
|
||||
// API-Stuetzpunkt acadrust:
|
||||
// `DxfReader::from_reader<R: Read + Seek + 'static>(reader: R)`
|
||||
// → std::io::Cursor<&[u8]> implementiert Read + Seek → WASM-tauglich.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
|
||||
use acadrust::DxfReader;
|
||||
|
||||
// serde_json nur bei Feature "web" als optionale Dep, aber serde ist immer da.
|
||||
// Fuer den headless-Kern bauen wir das JSON mit einem einfachen Format-String,
|
||||
// damit keine Compile-Time-Dep auf serde_json im Default-Feature noetig ist.
|
||||
|
||||
/// Parst ein DXF aus einem Byte-Slice und gibt ein JSON-Summary zurueck.
|
||||
///
|
||||
/// Rueckgabe-JSON-Schema:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "version": "AC1015",
|
||||
/// "entityCount": 5,
|
||||
/// "entityTypeCounts": { "LINE": 3, "CIRCLE": 2 },
|
||||
/// "layerNames": ["0", "Wand"]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Fehler: String mit Fehlerbeschreibung.
|
||||
pub fn parse_dxf_summary(bytes: &[u8]) -> Result<String, String> {
|
||||
// Cursor<Vec<u8>> implementiert Read + Seek + 'static — kein std::fs noetig.
|
||||
// (from_reader verlangt R: 'static; Cursor<&[u8]> wuerde die Lifetime verletzen.)
|
||||
let cursor = Cursor::new(bytes.to_vec());
|
||||
let reader = DxfReader::from_reader(cursor)
|
||||
.map_err(|e| format!("DxfReader::from_reader fehlgeschlagen: {e}"))?;
|
||||
|
||||
let doc = reader
|
||||
.read()
|
||||
.map_err(|e| format!("DxfReader::read fehlgeschlagen: {e}"))?;
|
||||
|
||||
// Versions-String aus dem Enum (Debug-Format, z. B. "AC1015").
|
||||
let version = format!("{:?}", doc.version);
|
||||
|
||||
// Entitaeten zaehlen und nach Typ aufschluesseln.
|
||||
let mut type_counts: HashMap<&str, usize> = HashMap::new();
|
||||
for entity in doc.entities() {
|
||||
let typ = entity.as_entity().entity_type();
|
||||
*type_counts.entry(typ).or_insert(0) += 1;
|
||||
}
|
||||
let entity_count = type_counts.values().sum::<usize>();
|
||||
|
||||
// Layer-Namen aus der Layer-Tabelle.
|
||||
let mut layer_names: Vec<String> = doc
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect();
|
||||
layer_names.sort();
|
||||
|
||||
// JSON-Serialisierung ohne optionale serde_json-Dep im Default-Feature.
|
||||
// Fuer den headless-Test reicht ein sauberer Format-String.
|
||||
let type_counts_json = {
|
||||
let mut pairs: Vec<String> = type_counts
|
||||
.iter()
|
||||
.map(|(k, v)| format!(" \"{k}\": {v}"))
|
||||
.collect();
|
||||
pairs.sort(); // deterministisch fuer Tests
|
||||
format!("{{\n{}\n }}", pairs.join(",\n"))
|
||||
};
|
||||
|
||||
let layer_names_json = {
|
||||
let quoted: Vec<String> = layer_names
|
||||
.iter()
|
||||
.map(|n| format!("\"{}\"", n.replace('"', "\\\"")))
|
||||
.collect();
|
||||
format!("[{}]", quoted.join(", "))
|
||||
};
|
||||
|
||||
Ok(format!(
|
||||
"{{\n \"version\": \"{version}\",\n \"entityCount\": {entity_count},\n \"entityTypeCounts\": {type_counts_json},\n \"layerNames\": {layer_names_json}\n}}"
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WASM-Fassade (nur bei Feature "web" compiliert)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
mod wasm {
|
||||
use super::parse_dxf_summary;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Initialisiert den Panic-Hook fuer bessere Fehlermeldungen im Browser.
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn init_panic_hook() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
/// WASM-Fassade: nimmt einen Byte-Buffer (Uint8Array aus JS) und gibt ein
|
||||
/// JSON-String zurueck, das das DXF-Summary beschreibt.
|
||||
///
|
||||
/// JS-Aufruf:
|
||||
/// ```js
|
||||
/// import init, { parse_dxf_summary_json } from './pkgDwgImport/dwgimport.js';
|
||||
/// await init();
|
||||
/// const json = parse_dxf_summary_json(new Uint8Array(buffer));
|
||||
/// ```
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_dxf_summary_json(bytes: &[u8]) -> Result<String, JsValue> {
|
||||
parse_dxf_summary(bytes)
|
||||
.map_err(|e| JsValue::from_str(&e))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Minimales ASCII-DXF mit 3 LINE- und 2 CIRCLE-Entitaeten auf Layer "WAND"
|
||||
/// und Layer "0". Wird direkt als Byte-Slice eingebettet — kein Datei-IO.
|
||||
const MINI_DXF: &str = r#" 0
|
||||
SECTION
|
||||
2
|
||||
HEADER
|
||||
9
|
||||
$ACADVER
|
||||
1
|
||||
AC1015
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
TABLES
|
||||
0
|
||||
TABLE
|
||||
2
|
||||
LAYER
|
||||
70
|
||||
2
|
||||
0
|
||||
LAYER
|
||||
2
|
||||
0
|
||||
70
|
||||
0
|
||||
62
|
||||
7
|
||||
6
|
||||
Continuous
|
||||
0
|
||||
LAYER
|
||||
2
|
||||
WAND
|
||||
70
|
||||
0
|
||||
62
|
||||
3
|
||||
6
|
||||
Continuous
|
||||
0
|
||||
ENDTAB
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
SECTION
|
||||
2
|
||||
ENTITIES
|
||||
0
|
||||
LINE
|
||||
8
|
||||
WAND
|
||||
10
|
||||
0.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
100.0
|
||||
21
|
||||
0.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
LINE
|
||||
8
|
||||
WAND
|
||||
10
|
||||
100.0
|
||||
20
|
||||
0.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
100.0
|
||||
21
|
||||
50.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
LINE
|
||||
8
|
||||
0
|
||||
10
|
||||
100.0
|
||||
20
|
||||
50.0
|
||||
30
|
||||
0.0
|
||||
11
|
||||
0.0
|
||||
21
|
||||
0.0
|
||||
31
|
||||
0.0
|
||||
0
|
||||
CIRCLE
|
||||
8
|
||||
WAND
|
||||
10
|
||||
50.0
|
||||
20
|
||||
25.0
|
||||
30
|
||||
0.0
|
||||
40
|
||||
10.0
|
||||
0
|
||||
CIRCLE
|
||||
8
|
||||
0
|
||||
10
|
||||
20.0
|
||||
20
|
||||
10.0
|
||||
30
|
||||
0.0
|
||||
40
|
||||
5.0
|
||||
0
|
||||
ENDSEC
|
||||
0
|
||||
EOF
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn test_parse_mini_dxf() {
|
||||
let result = parse_dxf_summary(MINI_DXF.as_bytes());
|
||||
assert!(result.is_ok(), "parse_dxf_summary schlug fehl: {:?}", result);
|
||||
|
||||
let json = result.unwrap();
|
||||
println!("DXF-Summary:\n{json}");
|
||||
|
||||
// Version muss AC1015 sein.
|
||||
assert!(json.contains("AC1015"), "Version AC1015 nicht im JSON: {json}");
|
||||
|
||||
// Genau 5 Entitaeten: 3 LINE + 2 CIRCLE.
|
||||
assert!(
|
||||
json.contains("\"entityCount\": 5"),
|
||||
"entityCount soll 5 sein: {json}"
|
||||
);
|
||||
assert!(
|
||||
json.contains("\"LINE\": 3"),
|
||||
"LINE-Zaehler soll 3 sein: {json}"
|
||||
);
|
||||
assert!(
|
||||
json.contains("\"CIRCLE\": 2"),
|
||||
"CIRCLE-Zaehler soll 2 sein: {json}"
|
||||
);
|
||||
|
||||
// Layer "WAND" und "0" muessen erscheinen.
|
||||
assert!(json.contains("\"WAND\""), "Layer WAND fehlt: {json}");
|
||||
assert!(json.contains("\"0\""), "Layer 0 fehlt: {json}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user