From 70702d063fd33f060d1962534e3747c5ab339a00 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 5 Jun 2026 00:35:53 +0300 Subject: [PATCH] perf(hash): swap std HashMap/HashSet for rustc-hash FxHash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn, hatch / image / mesh caches, draw-depth map, viewport wire caches); the default SipHash is wasted work on integer keys. Alias the std types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and set — including the cross-module draw-depth map shared between scene and the GPU pipelines — uses the faster hasher consistently. Mechanical: no behavioural change. Constructors moved from ::new() to ::default() (Fx variants carry no RandomState). Build + 13 tests green. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 1 + Cargo.toml | 3 ++ ROADMAP.md | 2 +- src/app/commands.rs | 20 ++++++------ src/app/helpers.rs | 2 +- src/app/history.rs | 2 +- src/app/mod.rs | 4 +-- src/app/update.rs | 2 +- src/entities/dimension.rs | 6 ++-- src/entities/table.rs | 14 ++++---- src/io/plot_style.rs | 6 ++-- src/io/xref.rs | 10 +++--- src/linetypes.rs | 4 +-- src/scene/block_cache.rs | 6 ++-- src/scene/hit_test.rs | 2 +- src/scene/lff.rs | 14 ++++---- src/scene/mod.rs | 56 ++++++++++++++++---------------- src/scene/pipeline/face3d_gpu.rs | 2 +- src/scene/pipeline/mod.rs | 4 +-- src/scene/pipeline/wire_gpu.rs | 4 +-- src/scene/quadtree.rs | 4 +-- src/scene/render.rs | 2 +- src/scene/solid3d_tess.rs | 4 +-- src/snap/mod.rs | 6 ++-- src/ui/properties.rs | 6 ++-- src/ui/ribbon/mod.rs | 4 +-- src/ui/ribbon/widgets.rs | 2 +- src/ui/selection_filter_popup.rs | 2 +- src/ui/shortcuts.rs | 2 +- src/ui/statusbar_config.rs | 4 +-- 30 files changed, 102 insertions(+), 98 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ae1b0780..f03b06c2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,7 @@ dependencies = [ "printpdf", "rayon", "rfd", + "rustc-hash 2.1.2", "truck-meshalgo", "truck-modeling", "truck-polymesh", diff --git a/Cargo.toml b/Cargo.toml index 4d7220151..341ad242a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ rayon = "1" ureq = { version = "3", default-features = false, features = ["rustls"] } inventory = "0.3" truck-shapeops = "0.4" +# Fast non-cryptographic hasher. Handle/u64 keys dominate the hot block, +# hatch, draw-depth and wire caches; the default SipHash is overkill there. +rustc-hash = "2" [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } diff --git a/ROADMAP.md b/ROADMAP.md index a0026315a..4e444631a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -203,7 +203,7 @@ matters. ## Phase 4 — Allocation & Memory -### 4.1 Swap `HashMap` for `rustc-hash::FxHashMap` +### 4.1 Swap `HashMap` for `rustc-hash::FxHashMap` ✅ DONE `Handle` is an integer wrapper; the default `SipHash` is overkill. `FxHashMap` gives 20-40 % in hash-heavy sites (block_cache, hatches / diff --git a/src/app/commands.rs b/src/app/commands.rs index c006bb1c5..03ea4388c 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -118,7 +118,7 @@ impl OpenCADStudio { self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { - let layers: std::collections::HashSet = self.tabs[i] + let layers: rustc_hash::FxHashSet = self.tabs[i] .scene .selected_entities() .into_iter() @@ -153,7 +153,7 @@ impl OpenCADStudio { self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { - let layers: std::collections::HashSet = self.tabs[i] + let layers: rustc_hash::FxHashSet = self.tabs[i] .scene .selected_entities() .into_iter() @@ -188,7 +188,7 @@ impl OpenCADStudio { self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { - let layers: std::collections::HashSet = self.tabs[i] + let layers: rustc_hash::FxHashSet = self.tabs[i] .scene .selected_entities() .into_iter() @@ -278,7 +278,7 @@ impl OpenCADStudio { self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { - let layers: std::collections::HashSet = self.tabs[i] + let layers: rustc_hash::FxHashSet = self.tabs[i] .scene .selected_entities() .into_iter() @@ -299,7 +299,7 @@ impl OpenCADStudio { // LAYISO — turn off all layers except those used by selected entities "LAYISO" => { - let sel_layers: std::collections::HashSet = self.tabs[i] + let sel_layers: rustc_hash::FxHashSet = self.tabs[i] .scene .selected_entities() .into_iter() @@ -3090,10 +3090,10 @@ impl OpenCADStudio { // including ones not yet in the table, which sort by // their own handle. (min_eff, max_eff) over siblings. let fb_baseline: Option<(u64, u64)> = if to_front_opt.is_some() { - let selected_set: std::collections::HashSet = + let selected_set: rustc_hash::FxHashSet = selected.iter().map(|h| h.value()).collect(); let doc_ref = &self.tabs[i].scene.document; - let overrides: std::collections::HashMap = doc_ref + let overrides: rustc_hash::FxHashMap = doc_ref .objects .values() .find_map(|obj| { @@ -4358,7 +4358,7 @@ impl OpenCADStudio { let all = sub == "ALL" || sub.is_empty(); // Collect names in use (immutable borrows — done in their own scope) - let used_layers: std::collections::HashSet = self.tabs[i] + let used_layers: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() @@ -4371,7 +4371,7 @@ impl OpenCADStudio { } }) .collect(); - let used_text_styles: std::collections::HashSet = self.tabs[i] + let used_text_styles: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() @@ -4382,7 +4382,7 @@ impl OpenCADStudio { }) .filter(|s| !s.is_empty()) .collect(); - let used_linetypes: std::collections::HashSet = self.tabs[i] + let used_linetypes: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() diff --git a/src/app/helpers.rs b/src/app/helpers.rs index 0b7a2adac..0488410cf 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -171,7 +171,7 @@ pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::Vec3 { /// Generate the next available auto group name ("*A1", "*A2", …). pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String { - let existing: std::collections::HashSet = + let existing: rustc_hash::FxHashSet = scene.groups().map(|g| g.name.clone()).collect(); for n in 1..=9999 { let name = format!("*A{n}"); diff --git a/src/app/history.rs b/src/app/history.rs index 5382b0dbb..39c476e99 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -1,5 +1,5 @@ use super::{document::HistorySnapshot, OpenCADStudio}; -use std::collections::HashSet; +use rustc_hash::FxHashSet as HashSet; impl OpenCADStudio { pub(super) fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String { diff --git a/src/app/mod.rs b/src/app/mod.rs index b8835456d..d3637cb01 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -378,7 +378,7 @@ pub(super) struct OpenCADStudio { // ── Keyboard Shortcut Editor ────────────────────────────────────────── /// User-defined function-key overrides: "F3" → command string. - shortcut_overrides: std::collections::HashMap, + shortcut_overrides: rustc_hash::FxHashMap, // ── Layout Manager Panel ────────────────────────────────────────────── layout_manager_selected: String, @@ -1324,7 +1324,7 @@ impl OpenCADStudio { // Color scheme (default: dark CAD-style) active_theme: Theme::Dark, // Keyboard shortcuts - shortcut_overrides: std::collections::HashMap::new(), + shortcut_overrides: rustc_hash::FxHashMap::default(), // Layout Manager layout_manager_selected: "Model".to_string(), layout_manager_rename_buf: String::new(), diff --git a/src/app/update.rs b/src/app/update.rs index 2f11cebd0..d60a031b4 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -214,7 +214,7 @@ impl OpenCADStudio { self.tabs[i].scene.meshes = caches.meshes; // Invalidate the wire cache so the new document is tessellated. self.tabs[i].scene.bump_geometry(); - self.tabs[i].scene.selected = std::collections::HashSet::new(); + self.tabs[i].scene.selected = rustc_hash::FxHashSet::default(); self.tabs[i].scene.preview_wires = vec![]; self.tabs[i].scene.current_layout = "Model".to_string(); crate::linetypes::populate_document(&mut self.tabs[i].scene.document); diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index f7cde4b8d..dca31cdf9 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -808,7 +808,7 @@ pub trait DimensionTess { line_weight_px: f32, world_offset: [f64; 3], anno_scale: f32, - selected_set: &std::collections::HashSet, + selected_set: &rustc_hash::FxHashSet, active_viewport: Option, bg_color: [f32; 4], view_aabb: Option<[f32; 4]>, @@ -826,7 +826,7 @@ impl DimensionTess for Dimension { line_weight_px: f32, world_offset: [f64; 3], anno_scale: f32, - selected_set: &std::collections::HashSet, + selected_set: &rustc_hash::FxHashSet, active_viewport: Option, bg_color: [f32; 4], view_aabb: Option<[f32; 4]>, @@ -862,7 +862,7 @@ fn tessellate_dimension_inner( // LOD hints — when present, synthesised dim text routes through the // top-level LOD ladder (baseline / greek / full) instead of the truck // path so far-out drawings collapse to a colored rect or baseline. - selected_set: &std::collections::HashSet, + selected_set: &rustc_hash::FxHashSet, active_viewport: Option, bg_color: [f32; 4], view_aabb: Option<[f32; 4]>, diff --git a/src/entities/table.rs b/src/entities/table.rs index 44559eba0..416360cb9 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -59,8 +59,8 @@ impl TruckConvertible for Table { // hidden borders disappear from the grid. Cells with no style still // emit the standard four borders. To avoid drawing each shared edge // twice we coalesce the segments by their (start, end) coordinates. - use std::collections::HashSet; - let mut emitted: HashSet<(i32, i32, i32, i32)> = HashSet::new(); + use rustc_hash::FxHashSet as HashSet; + let mut emitted: HashSet<(i32, i32, i32, i32)> = HashSet::default(); let try_add = |a: Vec3, b: Vec3, vis: bool, emitted: &mut HashSet<(i32, i32, i32, i32)>, pts: &mut Vec<[f32; 3]>| { if !vis { return; @@ -334,7 +334,7 @@ pub fn tessellate_table( use crate::scene::tess_util::aci_to_rgba; use crate::scene::wire_model::WireModel; use acadrust::types::Color; - use std::collections::HashMap; + use rustc_hash::FxHashMap as HashMap; if tab.rows.is_empty() || tab.columns.is_empty() { return Vec::new(); @@ -438,10 +438,10 @@ pub fn tessellate_table( }; // Accumulators keyed by quantised colour (+ weight for borders). - let mut fills: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::new(); - let mut texts: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::new(); - let mut borders: HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)> = HashMap::new(); - let mut emitted: std::collections::HashSet<(i32, i32, i32, i32)> = std::collections::HashSet::new(); + let mut fills: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::default(); + let mut texts: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::default(); + let mut borders: HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)> = HashMap::default(); + let mut emitted: rustc_hash::FxHashSet<(i32, i32, i32, i32)> = rustc_hash::FxHashSet::default(); let sel_col = WireModel::SELECTED; let mut add_edge = diff --git a/src/io/plot_style.rs b/src/io/plot_style.rs index a95667d7e..f3586d878 100644 --- a/src/io/plot_style.rs +++ b/src/io/plot_style.rs @@ -9,7 +9,7 @@ //! STB files follow the same format but use named styles instead of //! ACI indices; they are read into a `Vec`. -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use std::io::Read; use std::path::Path; @@ -72,7 +72,7 @@ impl PlotStyleTable { name: name.into(), is_stb: false, aci_entries: (0..=255).map(|_| PlotStyleEntry::default()).collect(), - named_entries: HashMap::new(), + named_entries: HashMap::default(), } } @@ -202,7 +202,7 @@ fn compress_ctb(text: &[u8]) -> Result, String> { fn parse_plot_style_text(text: &str, name: String, is_stb: bool) -> Result { let mut aci_entries: Vec = (0..=255).map(|_| PlotStyleEntry::default()).collect(); - let mut named_entries: HashMap = HashMap::new(); + let mut named_entries: HashMap = HashMap::default(); let mut style_index: usize = 1; // CTB: 1-based ACI index let mut current: Option = None; let mut current_name: String = String::new(); diff --git a/src/io/xref.rs b/src/io/xref.rs index a65eb81f8..cd260710d 100644 --- a/src/io/xref.rs +++ b/src/io/xref.rs @@ -5,7 +5,7 @@ use acadrust::entities::{Block, BlockEnd}; use acadrust::tables::TableEntry; use acadrust::types::{Handle, Vector3}; use acadrust::{CadDocument, EntityType}; -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use std::path::{Path, PathBuf}; /// Status of an external reference block. @@ -160,7 +160,7 @@ fn merge_xref_into_block( // Prefix every xref layer (including "0"). Entity layer references are // remapped below so the resolver finds the merged copy. Host layers // (incl. its own "0") are untouched — no collisions. - let mut layer_map: HashMap = HashMap::new(); + let mut layer_map: HashMap = HashMap::default(); for layer in xref_doc.layers.iter() { let old = layer.name.clone(); let new = format!("{}|{}", prefix, old); @@ -174,7 +174,7 @@ fn merge_xref_into_block( // ── Linetypes ─────────────────────────────────────────────────────── // Skip the three sentinel names — "ByLayer" / "ByBlock" / "Continuous" // are magic strings the resolver matches verbatim in both docs. - let mut linetype_map: HashMap = HashMap::new(); + let mut linetype_map: HashMap = HashMap::default(); for lt in xref_doc.line_types.iter() { let old = lt.name.clone(); if is_sentinel_linetype(&old) { @@ -195,8 +195,8 @@ fn merge_xref_into_block( // // Tracks (xref-doc BR handle → host BR handle) so entities owned by // a nested xref block can be routed to the right host block_record. - let mut br_handle_map: HashMap = HashMap::new(); - let mut block_name_map: HashMap = HashMap::new(); + let mut br_handle_map: HashMap = HashMap::default(); + let mut block_name_map: HashMap = HashMap::default(); for br in xref_doc.block_records.iter() { // Skip layout block records (*Model_Space, *Paper_Space, *Paper_Space0…) // and any further-nested xrefs (we don't recurse into xref-of-xref). diff --git a/src/linetypes.rs b/src/linetypes.rs index 5d396d5f8..ac3ef1b08 100644 --- a/src/linetypes.rs +++ b/src/linetypes.rs @@ -5,7 +5,7 @@ //! //! [`complex_lt`] returns the complex segment catalog for CPU-side rendering. -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use std::sync::OnceLock; use acadrust::tables::linetype::{LineType, LineTypeElement}; @@ -217,7 +217,7 @@ fn push_element(token: &str, out: &mut Vec) { /// A linetype is "complex" when its A-line contains at least one `[SHAPE,...]` /// element. Simple (dash-only) linetypes are excluded. fn parse_complex(src: &str) -> HashMap { - let mut catalog: HashMap = HashMap::new(); + let mut catalog: HashMap = HashMap::default(); let mut current: Option<(String, String)> = None; // (name, description) for raw in src.lines() { diff --git a/src/scene/block_cache.rs b/src/scene/block_cache.rs index d46156e1e..0ea0d1741 100644 --- a/src/scene/block_cache.rs +++ b/src/scene/block_cache.rs @@ -15,7 +15,7 @@ // a visited set so a self-referential block produces a marker rather than // recursing forever. -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use std::sync::Arc; use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3}; @@ -206,8 +206,8 @@ impl BlockCache { /// Walk all entities + all block_record contents collecting every distinct /// `block_name` that appears in an Insert (transitively). fn collect_referenced_blocks(doc: &CadDocument) -> Vec { - use std::collections::HashSet; - let mut seen: HashSet = HashSet::new(); + use rustc_hash::FxHashSet as HashSet; + let mut seen: HashSet = HashSet::default(); let mut queue: Vec = Vec::new(); for entity in doc.entities() { diff --git a/src/scene/hit_test.rs b/src/scene/hit_test.rs index 744e36080..2c6e1e05d 100644 --- a/src/scene/hit_test.rs +++ b/src/scene/hit_test.rs @@ -4,7 +4,7 @@ //! to 2-D pixel coordinates, then compared against the cursor or selection box. //! This matches the visual result the user sees. -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use acadrust::Handle; use glam::{Mat4, Vec3}; diff --git a/src/scene/lff.rs b/src/scene/lff.rs index a3b44ec6a..7e2c13b89 100644 --- a/src/scene/lff.rs +++ b/src/scene/lff.rs @@ -16,7 +16,7 @@ // Cap height is 9 glyph units, matching the `height / 9.0` text scale used // throughout the renderer. -use std::collections::{HashMap, HashSet}; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::sync::{Mutex, OnceLock}; // ── Embedded fonts ───────────────────────────────────────────────────────── @@ -154,7 +154,7 @@ fn warn_missing_glyph(font_name: &str, ch: char) { if ch.is_ascii() { return; } - let set = WARNED_GLYPHS.get_or_init(|| Mutex::new(HashSet::new())); + let set = WARNED_GLYPHS.get_or_init(|| Mutex::new(HashSet::default())); if let Ok(mut guard) = set.lock() { if guard.insert((font_name.to_string(), ch)) { eprintln!( @@ -167,7 +167,7 @@ fn warn_missing_glyph(font_name: &str, ch: char) { fn fonts_map() -> &'static HashMap { FONTS.get_or_init(|| { - let mut map = HashMap::new(); + let mut map = HashMap::default(); // Register every font under its stem and its `# Name:` header. for (stem, src) in FONTS_SRC { let f = parse_lff(src); @@ -461,12 +461,12 @@ fn parse_lff(src: &str) -> Font { letter_spacing: 3.0, word_spacing: 6.75, line_spacing: 1.0, - glyphs: HashMap::new(), - shapes: HashMap::new(), + glyphs: HashMap::default(), + shapes: HashMap::default(), }; - let mut raw: HashMap = HashMap::new(); - let mut raw_shapes: HashMap = HashMap::new(); + let mut raw: HashMap = HashMap::default(); + let mut raw_shapes: HashMap = HashMap::default(); let mut cur: Option = None; let mut cur_name: Option = None; let mut cur_glyph = RawGlyph::default(); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 89f9f2e54..b62bb47bb 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -62,7 +62,7 @@ use truck_modeling::{ use iced::time::Duration; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::rc::Rc; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -256,9 +256,9 @@ struct OffsetPrep { /// `Some` when the model BlockRecord enumerates its entities; the offset /// scan uses this set directly. `None` falls back to the legacy /// permissive owner-based interpretation. - mspace_set: Option>, + mspace_set: Option>, any_enumerated: bool, - owned_by_other_block: std::collections::HashSet, + owned_by_other_block: rustc_hash::FxHashSet, } fn offset_prep(doc: &acadrust::CadDocument, model_block: Handle) -> OffsetPrep { @@ -266,21 +266,21 @@ fn offset_prep(doc: &acadrust::CadDocument, model_block: Handle) -> OffsetPrep { .block_records .iter() .find(|br| br.handle == model_block); - let mspace_set: Option> = model_br + let mspace_set: Option> = model_br .filter(|br| !br.entity_handles.is_empty()) .map(|br| br.entity_handles.iter().copied().collect()); let any_enumerated = doc .block_records .iter() .any(|br| !br.entity_handles.is_empty()); - let owned_by_other_block: std::collections::HashSet = if mspace_set.is_none() { + let owned_by_other_block: rustc_hash::FxHashSet = if mspace_set.is_none() { doc.block_records .iter() .filter(|br| br.handle != model_block) .flat_map(|br| br.entity_handles.iter().copied()) .collect() } else { - std::collections::HashSet::new() + rustc_hash::FxHashSet::default() }; OffsetPrep { mspace_set, any_enumerated, owned_by_other_block } } @@ -718,31 +718,31 @@ impl Scene { active_model_tile: std::cell::Cell::new(0), selection: Rc::new(RefCell::new(SelectionState::default())), document: CadDocument::new(), - selected: HashSet::new(), - hidden: HashSet::new(), + selected: HashSet::default(), + hidden: HashSet::default(), hover_highlight: None, transparency_display: true, - selection_filter: HashSet::new(), + selection_filter: HashSet::default(), preview_wires: vec![], interim_wire: None, camera_generation: 0, geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed), wire_cache: RefCell::new(None), - model_tile_wire_cache: RefCell::new(HashMap::new()), + model_tile_wire_cache: RefCell::new(HashMap::default()), sort_cache: RefCell::new(None), draw_depth_cache: RefCell::new(None), hatch_cache: RefCell::new(None), wipeout_cache: RefCell::new(None), image_cache: RefCell::new(None), mesh_cache: RefCell::new(None), - viewport_wire_cache: RefCell::new(HashMap::new()), + viewport_wire_cache: RefCell::new(HashMap::default()), paper_sheet_cache: RefCell::new(None), - paper_projected_cache: RefCell::new(HashMap::new()), + paper_projected_cache: RefCell::new(HashMap::default()), current_layout: "Model".to_string(), - hatches: HashMap::new(), - meshes: HashMap::new(), - model_solids: HashMap::new(), - images: HashMap::new(), + hatches: HashMap::default(), + meshes: HashMap::default(), + model_solids: HashMap::default(), + images: HashMap::default(), active_viewport: None, bg_color: [0.11, 0.11, 0.11, 1.0], paper_bg_color: [1.0, 1.0, 1.0, 1.0], @@ -1455,7 +1455,7 @@ impl Scene { // Deduplicate by name: prefer the entry with a non-null block_record (the // real layout from the file) over the default placeholder created by // CadDocument::new(). - let mut by_name: std::collections::HashMap = Default::default(); + let mut by_name: rustc_hash::FxHashMap = Default::default(); for obj in self.document.objects.values() { if let ObjectType::Layout(l) = obj { if l.name == "Model" || l.name.is_empty() { @@ -1628,7 +1628,7 @@ impl Scene { } use acadrust::objects::ObjectType; // Per-block SortEntitiesTable overrides: block -> (entity_val -> sort_val). - let mut overrides: HashMap> = HashMap::new(); + let mut overrides: HashMap> = HashMap::default(); for obj in self.document.objects.values() { if let ObjectType::SortEntitiesTable(t) = obj { if !t.is_empty() { @@ -1643,7 +1643,7 @@ impl Scene { } let ms = self.model_space_block_handle(); // Group entities by owning block, carrying each entity's effective key. - let mut by_block: HashMap> = HashMap::new(); + let mut by_block: HashMap> = HashMap::default(); for e in self.document.entities() { let c = e.common(); // 3D meshes keep real geometric depth — exclude them from @@ -1667,7 +1667,7 @@ impl Scene { .unwrap_or(hv); by_block.entry(block).or_default().push((hv, eff)); } - let mut depth_map: HashMap = HashMap::new(); + let mut depth_map: HashMap = HashMap::default(); for (_block, mut v) in by_block { v.sort_by_key(|(_, eff)| *eff); let denom = (v.len() as f32) + 1.0; @@ -1928,7 +1928,7 @@ impl Scene { .unwrap_or(true); if needs_rebuild { - let mut idx: HashMap> = HashMap::new(); + let mut idx: HashMap> = HashMap::default(); for obj in self.document.objects.values() { if let ObjectType::SortEntitiesTable(t) = obj { if !t.is_empty() { @@ -2187,7 +2187,7 @@ impl Scene { } } } - let mut map: HashMap = HashMap::new(); + let mut map: HashMap = HashMap::default(); for br in self.document.block_records.iter() { for &eh in &br.entity_handles { map.insert(eh, br.handle); @@ -4098,11 +4098,11 @@ impl Scene { pub fn clear(&mut self) { self.document = CadDocument::new(); - self.selected = HashSet::new(); + self.selected = HashSet::default(); self.preview_wires = vec![]; self.current_layout = "Model".to_string(); - self.hatches = HashMap::new(); - self.meshes = HashMap::new(); + self.hatches = HashMap::default(); + self.meshes = HashMap::default(); *self.camera.borrow_mut() = Camera::default(); self.camera_generation += 1; self.bump_geometry(); @@ -4201,7 +4201,7 @@ impl Scene { if self.selected.is_empty() { return 0; } - let pairs: std::collections::HashSet<(&'static str, String)> = self + let pairs: rustc_hash::FxHashSet<(&'static str, String)> = self .selected .iter() .filter_map(|h| self.document.get_entity(*h)) @@ -6276,7 +6276,7 @@ impl Scene { vp_handle: Handle, screen_height_px: f32, ) -> Vec { - use std::collections::HashSet as HSet; + use rustc_hash::FxHashSet as HSet; let (frozen, vp_anno_scale, vp_aspect) = match self.document.get_entity(vp_handle) { Some(EntityType::Viewport(vp)) => { @@ -6295,7 +6295,7 @@ impl Scene { }; (f, anno, aspect) } - _ => (HSet::new(), 1.0_f32, 1.0_f32), + _ => (HSet::default(), 1.0_f32, 1.0_f32), }; // Drive the per-viewport view_aabb / wpp from the *effective* camera diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index dfb965405..7c5a163dc 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -93,7 +93,7 @@ impl Face3DGpu { face3d_wires: &[WireModel], all_wires: &[WireModel], keep_3d_mesh_fills: bool, - depth_map: &std::collections::HashMap, + depth_map: &rustc_hash::FxHashMap, ) -> Self { let depth_of = |w: &WireModel| -> f32 { w.name diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 99ea420ec..5d68adbb5 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -891,7 +891,7 @@ impl Pipeline { &mut self, device: &wgpu::Device, wires: &[WireModel], - depth_map: &std::collections::HashMap, + depth_map: &rustc_hash::FxHashMap, ) { let depth_of = |w: &WireModel| -> f32 { w.name @@ -987,7 +987,7 @@ impl Pipeline { face3d_wires: &[WireModel], all_wires: &[WireModel], wireframe_only: bool, - depth_map: &std::collections::HashMap, + depth_map: &rustc_hash::FxHashMap, ) { // Edge buffer is always built from `face3d_wires`, so 3DFACE // outlines stay on the screen regardless of mode. diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index a0356c748..314dd185f 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -267,7 +267,7 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve /// Looks up a wire's draw-order depth from the per-entity map using the /// handle encoded in its `name`. Falls back to 0.0 (transient / preview /// wires that carry no document handle). -fn wire_draw_depth(wire: &WireModel, depth_map: &std::collections::HashMap) -> f32 { +fn wire_draw_depth(wire: &WireModel, depth_map: &rustc_hash::FxHashMap) -> f32 { wire .name .parse::() @@ -290,7 +290,7 @@ impl WireGpu { pub fn from_batch( device: &wgpu::Device, wires: &[WireModel], - depth_map: &std::collections::HashMap, + depth_map: &rustc_hash::FxHashMap, ) -> Vec { let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum(); if total_segs == 0 { diff --git a/src/scene/quadtree.rs b/src/scene/quadtree.rs index f1fb49fbc..039d5a96f 100644 --- a/src/scene/quadtree.rs +++ b/src/scene/quadtree.rs @@ -57,7 +57,7 @@ pub struct QuadTree { nodes: Vec, /// `handle → (node_idx, item_idx_within_node)` so removal/update /// is O(1) without walking the tree. - locator: std::collections::HashMap, + locator: rustc_hash::FxHashMap, /// Items whose AABB falls outside the root bounds. Surfaced on /// every query — small set in practice (typically empty for /// well-bounded drawings). @@ -74,7 +74,7 @@ impl QuadTree { }; Self { nodes: vec![root], - locator: std::collections::HashMap::new(), + locator: rustc_hash::FxHashMap::default(), overflow: Vec::new(), } } diff --git a/src/scene/render.rs b/src/scene/render.rs index ce2f99003..649c36731 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -39,7 +39,7 @@ pub struct ViewportData { /// by the wire / face3d pipelines as a clip-z bias. WireModels carry no /// depth field (84 construction sites); the bias is looked up by handle /// at GPU-upload time from this map instead. - pub(super) draw_depths: Arc>, + pub(super) draw_depths: Arc>, pub(super) hatches: Arc>, /// Wipeout fills — rendered in a separate pass AFTER wires. pub(super) wipeout_hatches: Arc>, diff --git a/src/scene/solid3d_tess.rs b/src/scene/solid3d_tess.rs index 71ff8ba88..24e999894 100644 --- a/src/scene/solid3d_tess.rs +++ b/src/scene/solid3d_tess.rs @@ -10,7 +10,7 @@ // All other surface types are silently skipped; partial results are still // returned so the solid renders with at least its planar faces. -use std::collections::HashSet; +use rustc_hash::FxHashSet as HashSet; use std::f64::consts::TAU; use acadrust::entities::acis::types::Sense; @@ -237,7 +237,7 @@ fn collect_face_polygon(sat: &SatDocument, face: &SatFace) -> Vec<[f64; 3]> { let first_ptr = sat_loop.first_coedge(); let mut cur = first_ptr; let mut pts: Vec<[f64; 3]> = Vec::new(); - let mut visited: HashSet = HashSet::new(); + let mut visited: HashSet = HashSet::default(); loop { if cur.is_null() { diff --git a/src/snap/mod.rs b/src/snap/mod.rs index 215940598..548e4ded1 100644 --- a/src/snap/mod.rs +++ b/src/snap/mod.rs @@ -63,7 +63,7 @@ pub struct SnapResult { // ── Snapper ─────────────────────────────────────────────────────────────── -use std::collections::HashSet; +use rustc_hash::FxHashSet as HashSet; pub struct Snapper { /// Global snap on/off toggle. When false, all snapping is bypassed @@ -87,7 +87,7 @@ pub struct Snapper { impl Default for Snapper { fn default() -> Self { - let mut enabled = HashSet::new(); + let mut enabled = HashSet::default(); enabled.insert(SnapType::Endpoint); enabled.insert(SnapType::Midpoint); enabled.insert(SnapType::Center); @@ -257,7 +257,7 @@ impl Snapper { let tmp = Snapper { snap_enabled: true, enabled: { - let mut s = HashSet::new(); + let mut s = HashSet::default(); s.insert(SnapType::Tangent); s }, diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 6c0a011ac..86de668eb 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -7,7 +7,7 @@ //! • Linetype → read-only for now //! • Geometry → text_input per coordinate / dimension field -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use std::fmt; use crate::ui::ROW_H; @@ -142,12 +142,12 @@ impl Default for PropertiesPanel { selected_group: None, linetype_items: vec![], selection_group_combo: combo_box::State::new(vec![]), - choice_combos: HashMap::new(), + choice_combos: HashMap::default(), layer_combo: combo_box::State::new(vec![]), lineweight_combo: combo_box::State::new(lw_options()), linetype_combo: combo_box::State::new(vec![]), hatch_pattern_combo: combo_box::State::new(crate::scene::hatch_patterns::names()), - edit_buf: HashMap::new(), + edit_buf: HashMap::default(), color_picker_open: false, color_palette_open: false, } diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index c8a48f640..9c0052caa 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -6,7 +6,7 @@ // // Dropdown items within a group are collected into columns of 3 rows. -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use acadrust::types::{Color as AcadColor, LineWeight}; use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, text}; @@ -73,7 +73,7 @@ impl Ribbon { wireframe: false, ortho_mode: true, open_dropdown: None, - last_cmd: HashMap::new(), + last_cmd: HashMap::default(), layer_names: vec!["0".to_string()], active_layer: "0".to_string(), layer_infos: vec![LayerInfo { diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index dc8cdc4c6..1b4e2be7c 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -1,7 +1,7 @@ // Shared rendering helpers, button styles, colours, layout constants, and // free functions used by the Ribbon view/overlay methods. -use std::collections::HashMap; +use rustc_hash::FxHashMap as HashMap; use std::time::Duration; use acadrust::types::{Color as AcadColor, LineWeight}; diff --git a/src/ui/selection_filter_popup.rs b/src/ui/selection_filter_popup.rs index 91e449d09..c71819436 100644 --- a/src/ui/selection_filter_popup.rs +++ b/src/ui/selection_filter_popup.rs @@ -2,7 +2,7 @@ //! A checked row means that type can be picked; unchecking it excludes the //! type from interactive selection. Opened from the FILTER status pill. -use std::collections::HashSet; +use rustc_hash::FxHashSet as HashSet; use iced::widget::{button, column, container, mouse_area, row, text}; use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; diff --git a/src/ui/shortcuts.rs b/src/ui/shortcuts.rs index fb1a42660..8cd53bfe5 100644 --- a/src/ui/shortcuts.rs +++ b/src/ui/shortcuts.rs @@ -79,7 +79,7 @@ fn section<'a>(title: &'static str) -> Element<'a, Message> { } pub fn view_window<'a>( - overrides: &'a std::collections::HashMap, + overrides: &'a rustc_hash::FxHashMap, ) -> Element<'a, Message> { // ── Toolbar ─────────────────────────────────────────────────────────── let toolbar = container( diff --git a/src/ui/statusbar_config.rs b/src/ui/statusbar_config.rs index 1b1407341..5a72655d1 100644 --- a/src/ui/statusbar_config.rs +++ b/src/ui/statusbar_config.rs @@ -5,7 +5,7 @@ //! adds or removes that pill from the bar. The choice is persisted so it //! survives across sessions. -use std::collections::HashSet; +use rustc_hash::FxHashSet as HashSet; use std::path::PathBuf; /// Identifies a toggleable status-bar pill. @@ -121,7 +121,7 @@ pub struct StatusBarConfig { impl StatusBarConfig { /// Load the saved customization, or all-visible when none exists. pub fn load() -> Self { - let mut hidden = HashSet::new(); + let mut hidden = HashSet::default(); if let Some(path) = config_path() { if let Ok(body) = std::fs::read_to_string(path) { for line in body.lines() {