
Custom Parser STEP i Terminal UI · Pure Rust
Rust / Ratatui / Crossterm / Clap / Serde
Terminalowa przeglądarka plików IFC, bo odpalanie Revita, żeby zobaczyć listę typów, to strata czasu. Wrzucasz plik, dostajesz dashboard z poziomami, kategoriami i property setami.
Revit potrafi startować minutami, a ja chciałem tylko zerknąć, ile typów ścian jest w modelu albo czy property sety się wyeksportowały. Odpalanie ciężkiego BIM-a do tak prostego sprawdzenia to absurd.
Kilkoma klawiszami przejrzeć całą zawartość pliku IFC: rodziny, property sety, instancje.
Otwierasz IFC w terminalu i od razu masz dashboard: poziomy, kategorie, typy, property sety. Bez potrzeby Revita czy przeglądarki online. Audyt modelu w 2 sekundy zamiast 2 minut.
Główna struktura danych z pre-computed HashMapami do O(1) lookupów — zero skanowania drzewa w runtime
#[derive(Debug, Serialize)]pub struct IfcProject { pub name: String, pub schema: String, pub file_path: String, pub categories: Vec<Category>, pub storeys: Vec<Storey>, pub elements: HashMap<u64, Element>, pub element_to_storey: HashMap<u64, u64>, // element -> storey O(1) pub element_properties: HashMap<u64, HashMap<String, String>>, pub instance_global_ids: HashMap<u64, String>, // instance -> GlobalId}Custom parser STEP/ISO-10303 oparty o state machine — obsługuje zagnieżdżone nawiasy, stringi w cudzysłowach i rekurencyjne listy bez regex
fn parse_values(s: &str) -> Vec<StepValue> { let mut values = Vec::new(); let mut current = String::new(); let mut in_string = false; let mut paren_depth = 0; for ch in s.chars() { match ch { '\'' if paren_depth == 0 => { in_string = !in_string; current.push(ch); } '(' if !in_string => { paren_depth += 1; current.push(ch); } ')' if !in_string => { paren_depth -= 1; current.push(ch); } ',' if !in_string && paren_depth == 0 => { values.push(Self::parse_single_value(current.trim())); current.clear(); } _ => current.push(ch), } } values}Dekoder Unicode formatu STEP — obsługuje 3 schematy kodowania (X2 2-bajtowy Unicode, X 1-bajtowy ISO, S shift) z polskich/niemieckich eksportów Revit
fn decode_step_string(s: &str) -> String { let mut result = String::with_capacity(s.len()); let mut chars = s.chars().peekable(); while let Some(ch) = chars.next() { if ch == '\\' { match chars.peek() { Some('X') => { chars.next(); // consume 'X' match chars.peek() { Some('2') => { // \\X2\\XXXX...\\X0\\ - 2-byte Unicode encoding chars.next(); // consume '2' chars.next(); // consume '\\' let mut hex = String::new(); while let Some(&c) = chars.peek() { if c == '\\' { break; } hex.push(c); chars.next(); } if chars.peek() == Some(&'\\') { chars.next(); // '\\' chars.next(); // 'X' chars.next(); // '0' chars.next(); // '\\' } for chunk in hex.as_bytes().chunks(4) { if chunk.len() == 4 { if let Ok(s) = std::str::from_utf8(chunk) { if let Ok(code) = u32::from_str_radix(s, 16) { if let Some(c) = char::from_u32(code) { result.push(c); } } } } } } Some('\\') => { // \\X\\XX - ISO 8859-1 single byte chars.next(); // consume '\\' let mut hex = String::new(); for _ in 0..2 { if let Some(&c) = chars.peek() { hex.push(c); chars.next(); } } if let Ok(code) = u8::from_str_radix(&hex, 16) { result.push(code as char); } } _ => { result.push('\\'); result.push('X'); } } } Some('\\') => { chars.next(); result.push('\\'); } Some('S') => { // \\S\\X - single char shift (ISO 8859-1 high bit) chars.next(); // 'S' chars.next(); // '\\' if let Some(c) = chars.next() { result.push(((c as u8) + 128) as char); } } _ => result.push('\\'), } } else if ch == '\'' { if chars.peek() == Some(&'\'') { chars.next(); } result.push('\''); } else { result.push(ch); } } result}Elm Architecture w Rust — state machine oparty na enumach z nawigacją vim-like dispatchowaną per widok
#[derive(Debug, Clone, Copy, PartialEq)]pub enum View { Dashboard, TypeDetail, InstanceBrowser,} #[derive(Debug, Clone, Copy, PartialEq)]pub enum FocusPanel { Levels, Categories, Types,} // Event dispatch — each view handles its own key bindingsmatch self.view { View::Dashboard => self.handle_dashboard_keys(key.code), View::TypeDetail => self.handle_detail_keys(key.code), View::InstanceBrowser => self.handle_instance_keys(key.code),} fn handle_dashboard_keys(&mut self, code: KeyCode) { match code { KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true, KeyCode::Up | KeyCode::Char('k') => self.navigate_up(), KeyCode::Down | KeyCode::Char('j') => self.navigate_down(), KeyCode::Left | KeyCode::Char('h') => self.navigate_left(), KeyCode::Right | KeyCode::Char('l') => self.navigate_right(), KeyCode::Enter => self.enter_type_detail(), _ => {} }}
Dashboard View

Type Detail View

Instance Browser