From 0102934f2e792151722a12fb8f25e9c260046e67 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sat, 1 Aug 2026 10:54:10 -0600 Subject: [PATCH 1/4] Start --- CHANGELOG.md | 2 ++ docs/usage.md | 17 +++++++------ src/doc/gui.md | 2 +- src/doc/shape.md | 2 +- src/doc/utility.md | 2 ++ src/gui.cpp | 56 ++++++++++++++++++++++++++++++------------- src/gui.h | 6 +++-- src/gui_occt_view.cpp | 33 +++++++++++++++++++++++-- src/gui_occt_view.h | 5 ++-- src/utl_types.h | 8 +++++++ 10 files changed, 101 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c2790f..0b435d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **STEP Import as**: the Import dialog replaces the **Union shapes** checkbox with an **Import as** combo (**Preserve hierarchy** default, **Flat solids**, **Union shapes**). Hierarchy keeps XCAF assembly groups and product/instance names; flat adds leaf solids at the document root; union fuses into one solid. + - **Configurable hotkeys**: **Settings -> Keyboard shortcuts** remaps shape tools (Move/Rotate/Scale/Extrude/Chamfer/Fillet/Polar duplicate/Cross-section), sketch tools (inspection, from face, operation axis, node/line/multi-line/arc/square/rectangle/circle/slot, Dimension), boolean commands (Cut/Fuse/Common), Delete, New/Open/Save, and Undo/Redo. Bindings persist as **`gui.hotkeys`** (human-readable chords; missing keys use defaults). Toolbar tooltips follow remaps. **Delete** / **Backspace** stay fixed delete aliases; Ctrl+Shift+Z stays a fixed redo alias. Fixed, unsupported, and conflicting chords are rejected on assign with a status toast (and inline message); remaps accept letters, digits, and Space only (punctuation/numpad rejected); per-row **Reset** rejects when the factory chord is already bound elsewhere (same conflict message as assign); hotkey capture clears when Settings closes. A **?** at the top of the section opens the [Keyboard shortcuts](https://ezycad.readthedocs.io/en/latest/usage-settings.html#keyboard-shortcuts) settings guide. - **Extrude Twist**: Options **Twist** checkbox. Two-phase flow locks height first, then sets twist angle about the face centroid (mouse, or Shift+Tab for degrees). Height length dim is cleared on lock; a temporary angle annotation on the extruded front face shows degrees during twist. With **Both sides**, ends twist symmetrically by +/- half the angle. Geometry uses ruled thru-sections with compatibility off (keeps tooth pairing) and cuts twisted hole solids so face bores survive; straight prism when twist is zero. Dense-face **Extrude fast preview** also applies during Twist (face copies translate and rotate; finalize builds the solid). diff --git a/docs/usage.md b/docs/usage.md index 01d71630..9a19946b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -127,7 +127,7 @@ Each row (left to right): - **Name** - Expandable tree row with an editable name. Click the row to select that solid (or all descendant solids for a group) and update the current group. **Ctrl+click** toggles multi-select. Drag to reparent (empty area below the list = document root). - **Right-click the name** - Solids: **Shape info...** / **Delete**. Groups: **Ungroup** (moves **all** direct children to the group's parent, then removes the group) / **Delete** (cascade-deletes the whole subtree). -Boolean results stay under the shared parent of their inputs when all inputs share one parent; otherwise they are placed at the document root. **File -> Import** STEP assemblies preserve product/assembly groups in the tree (unless **Union shapes** is checked). +Boolean results stay under the shared parent of their inputs when all inputs share one parent; otherwise they are placed at the document root. **File -> Import** STEP assemblies use **Import as** (default **Preserve hierarchy**) to keep product/assembly groups in the tree, import **Flat solids** at the root, or **Union shapes** into one solid. The **current group** and rows that match the **current 3D selection** use a highlighted full-row style. Hovering a visible solid row also highlights that shape in the 3D view (**Settings -> View presentation -> Element hover color**). @@ -249,7 +249,7 @@ The typical modeling workflow in EzyCad follows these steps: ### Import dialog -**File -> Import** opens an **Import** window for STEP or PLY. Review metadata, optionally enable **Union shapes** (STEP), then click **Import into project**. The window closes after a successful import. +**File -> Import** opens an **Import** window for STEP or PLY. Review metadata, choose how STEP assemblies land in the Shape List (**Import as**, default **Preserve hierarchy**), then click **Import into project**. The window closes after a successful import. | Format | What the Import dialog shows | | -------------------------- | --------------------------------------------------------- | @@ -259,7 +259,7 @@ The typical modeling workflow in EzyCad follows these steps: **How to use:** 1. Choose **File -> Import** 2. Pick a `.step`, `.stp`, or `.ply` file -3. Review the label/value table; for STEP, optionally enable **Union shapes** +3. Review the label/value table; for STEP, choose **Import as** if needed 4. Click **Import into project** For in-document topology of an already-loaded solid, use [Shape info](#shape-info) from the Shape List. @@ -284,7 +284,7 @@ In addition to creating 3D shapes from sketches, EzyCad supports importing exist **How to import:** 1. Use **File -> Import** 2. Pick a `.step`, `.stp`, or `.ply` file -3. Review metadata in the [Import dialog](#import-dialog); for STEP assemblies, optionally check **Union shapes** +3. Review metadata in the [Import dialog](#import-dialog); for STEP, choose **Import as** if needed 4. Click **Import into project** - geometry is added as 3D shape(s) in the document, scaled to project units (see below) 5. You can move, rotate, scale, and use imported bodies in [boolean operations](#boolean-operations) like native solids where the geometry allows it @@ -303,9 +303,12 @@ In addition to creating 3D shapes from sketches, EzyCad supports importing exist **STEP import notes:** - If the file cannot be read or contains no transferable geometry, a **message** explains the failure (invalid data, empty transfer, etc.). -- Assemblies often arrive with XCAF product structure. Import builds Shape List **groups** for assemblies and leaf solids for bodies (unless **Union shapes** merges everything into one solid). The Import dialog **Import bodies** count shows how many leaf solids will be added. -- When the STEP file includes product or part names, those names appear in the Shape List (duplicate names get `.001`, `.002`, ...). Unnamed bodies stay as `Shape`. -- **Union shapes** (Import dialog) fuses those bodies into one solid before adding to the document. If union fails, the import is aborted and nothing is added. +- Assemblies often arrive with XCAF product structure. The Import dialog **Import as** combo controls how they land in the Shape List: + - **Preserve hierarchy** (default) - builds Shape List **groups** for assemblies and leaf solids for bodies; group and part names come from product/instance names in the file when present. + - **Flat solids** - adds leaf solids only at the document root (no assembly groups); keeps per-body names from the file. + - **Union shapes** - fuses those bodies into one solid before adding to the document. If union fails, the import is aborted and nothing is added. +- The Import dialog **Import bodies** count shows how many leaf solids will be added (before union). +- When the STEP file includes product or part names, those names appear in the Shape List (duplicate names get `.001`, `.002`, ...). Unnamed bodies stay as `Shape`; unnamed assemblies use `Assembly`. **Note:** **IGES** and **STL** are available for **export** only, not import. diff --git a/src/doc/gui.md b/src/doc/gui.md index 3b0ef7db..8ce512d0 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -13,7 +13,7 @@ Typical responsibilities: - ImGui frame: menu bar, dock space (passthrough central node for 3D input), toolbar, Sketch List, Shape List, Options, Settings, dist/angle popups. - Mode switching (`Mode` enum in [`mode.h`](../mode.h)) and parent-mode Esc behavior. - Persisted preferences (`ezycad_settings.json` via [`gui_settings.cpp`](../gui_settings.cpp)). -- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** shows metadata then loads STEP/PLY). +- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** shows metadata then loads STEP/PLY; STEP **Import as** chooses preserve hierarchy / flat solids / union). - CAD/mesh interchange scales about the origin: project display lengths follow **File -> Project units** (`Project_unit`; Inch or Millimeter). Model space stays inch-scaled (`model = inches * dimension_scale`). STEP import converts OCCT cascade **mm** into model space; PLY import treats coords as inches. **File -> Export** asks for **Inches** or **Millimeters** (STEP/IGES declare that unit; STL/PLY write unitless coords in that scale). `.ezy` persists `projectUnit`. **Settings -> New project defaults** stores `gui.default_project_unit` and inch-based default 2D framing for **File -> New**. - Contextual help links (`doc_urls` in `gui.h`). diff --git a/src/doc/shape.md b/src/doc/shape.md index be55f622..4046e7ae 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -35,7 +35,7 @@ Typical uses: - Each solid stores a `gp_Ax3` local frame. New geometry defaults to a world-aligned frame at its bounding-box center. Baked move/rotate/scale transforms update the frame; project JSON and shape undo records preserve it. - `Shp_ptr` is `opencascade::handle`. New shapes are allocated with `new Shp(ctx(), topo_shape)` then registered through `Occt_view::add_shp_()`. Groups use `Shp::create_group` (empty compound, never displayed). - Hierarchy: `parent_id` (0 = root) + `sibling_order`. Organizational groups only (no transform inheritance). Helpers: `shape_children`, `shape_descendant_solids`, `group_shapes`, `ungroup_shape`, `reparent_shape`, `would_reparent_create_cycle`. -- **Current group** (`Occt_view::current_group_id`, 0 = root): Shape List click sets it; empty groups are valid. Primitives / extrude / revolve / PLY / unioned STEP import call `add_shp_(..., use_current_group=true)` so new solids land under that group. Booleans keep `assign_result_parent_`. +- **Current group** (`Occt_view::current_group_id`, 0 = root): Shape List click sets it; empty groups are valid. Primitives / extrude / revolve / PLY / unioned STEP import (`Step_import_mode::Union_shapes`) call `add_shp_(..., use_current_group=true)` so new solids land under that group. Hierarchical and flat STEP import place roots at document root (`parent_id` 0). Booleans keep `assign_result_parent_`. - `Shp_operation_base` is a **`friend` of `Occt_view`** so operations can call `add_shp_()`, read selection, and use pick helpers without exposing those on the public view API. - **`Occt_view&` must outlive** all `Shp_*` operation objects (they are member subobjects of the view). - Boolean/polar results call `assign_result_parent_` so the new solid shares the inputs' parent when all match; otherwise root (`parent_id` 0). diff --git a/src/doc/utility.md b/src/doc/utility.md index cfcc1fef..fb1ab721 100644 --- a/src/doc/utility.md +++ b/src/doc/utility.md @@ -135,6 +135,8 @@ Used by **File -> Import**. Reads file bytes only until the user confirms import | `read_step_named_bodies` | STEPCAF/XCAF bodies + product names (flat; falls back to plain reader) | | `read_step_named_tree` | STEPCAF/XCAF assembly tree as group/leaf `Named_node`s (falls back flat) | +`Occt_view::import_step` takes `Step_import_mode` (`utl_types.h`): preserve hierarchy (default), flat root leaves, or union. + ## Logging and debug | Component | Role | diff --git a/src/gui.cpp b/src/gui.cpp index a105fe7b..e878a410 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -3037,8 +3037,8 @@ void GUI::close_file_inspector_() m_file_inspector_path.clear(); m_file_inspector_bytes.clear(); m_file_inspector_lines.clear(); - m_file_inspector_fmt = utl_cad_file_info::Format::Unknown; - m_file_inspector_union = false; + m_file_inspector_fmt = utl_cad_file_info::Format::Unknown; + m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; } void GUI::file_inspector_dialog_() @@ -3059,20 +3059,30 @@ void GUI::file_inspector_dialog_() if (utl_cad_file_info::can_import(m_file_inspector_fmt) && !m_file_inspector_bytes.empty()) { - const bool can_union = m_file_inspector_fmt == utl_cad_file_info::Format::Step; - if (!can_union) - m_file_inspector_union = false; - - ImGui::BeginDisabled(!can_union); - ImGui::Checkbox("Union shapes", &m_file_inspector_union); - ImGui::EndDisabled(); - if (ui_show_contextual_help() && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) - ImGui::SetTooltip(can_union ? "Fuse multiple STEP roots into one shape on import." - : "Union applies to multi-shape STEP files only."); + const bool is_step = m_file_inspector_fmt == utl_cad_file_info::Format::Step; + if (is_step) + { + static const char* k_step_import_labels[] = { + "Preserve hierarchy", + "Flat solids", + "Union shapes", + }; + int mode_i = static_cast(m_file_inspector_step_mode); + ImGui::SetNextItemWidth(220.0f); + if (ImGui::Combo("Import as", &mode_i, k_step_import_labels, IM_ARRAYSIZE(k_step_import_labels))) + m_file_inspector_step_mode = static_cast(mode_i); + if (ui_show_contextual_help() && ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Preserve hierarchy: Shape List groups from the STEP assembly (default).\n" + "Flat solids: leaf solids only at the document root.\n" + "Union shapes: fuse all bodies into one solid."); + } + else + m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; if (ImGui::Button("Import into project")) { - if (on_import_file(m_file_inspector_path, m_file_inspector_bytes, m_file_inspector_union)) + if (on_import_file(m_file_inspector_path, m_file_inspector_bytes, m_file_inspector_step_mode)) { close_file_inspector_(); ImGui::End(); @@ -4251,7 +4261,7 @@ void GUI::on_file(const std::string& file_path, const std::string& file_bytes, b show_message("Opened: " + std::filesystem::path(file_path).filename().string()); } -bool GUI::on_import_file(const std::string& file_path, const std::string& file_data, const bool union_shapes) +bool GUI::on_import_file(const std::string& file_path, const std::string& file_data, const Step_import_mode step_mode) { std::string ext = std::filesystem::path(file_path).extension().string(); for (char& c : ext) @@ -4269,14 +4279,26 @@ bool GUI::on_import_file(const std::string& file_path, const std::string& file_d return true; } - if (Status st = m_view->import_step(file_data, union_shapes); !st.is_ok()) + if (Status st = m_view->import_step(file_data, step_mode); !st.is_ok()) { show_message(st.message()); return false; } - show_message(union_shapes ? "Imported (union): " + std::filesystem::path(file_path).filename().string() - : "Imported: " + std::filesystem::path(file_path).filename().string()); + const std::string base = "Imported: " + std::filesystem::path(file_path).filename().string(); + switch (step_mode) + { + case Step_import_mode::Union_shapes: + show_message("Imported (union): " + std::filesystem::path(file_path).filename().string()); + break; + case Step_import_mode::Flat_solids: + show_message("Imported (flat): " + std::filesystem::path(file_path).filename().string()); + break; + case Step_import_mode::Preserve_hierarchy: + default: + show_message(base); + break; + } return true; } diff --git a/src/gui.h b/src/gui.h index 5f4eebc4..7fb6b464 100644 --- a/src/gui.h +++ b/src/gui.h @@ -381,7 +381,9 @@ class GUI #endif void on_file(const std::string& file_path, const std::string& file_bytes, bool announce_load = true); - [[nodiscard]] bool on_import_file(const std::string& file_path, const std::string& file_data, bool union_shapes = false); + [[nodiscard]] bool on_import_file(const std::string& file_path, + const std::string& file_data, + Step_import_mode step_mode = Step_import_mode::Preserve_hierarchy); void on_inspector_file(const std::string& file_path, const std::string& file_data); /// Emscripten `on_sketch_underlay_selected` routes here (must be public for C callback). void on_sketch_underlay_file(const std::string& file_path, const std::string& file_bytes); @@ -703,7 +705,7 @@ class GUI Shp_ptr m_shape_info_shp; std::vector m_shape_info_lines; bool m_file_inspector_open{false}; - bool m_file_inspector_union{false}; + Step_import_mode m_file_inspector_step_mode{Step_import_mode::Preserve_hierarchy}; std::string m_file_inspector_path; std::string m_file_inspector_bytes; utl_cad_file_info::Format m_file_inspector_fmt{utl_cad_file_info::Format::Unknown}; diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index 129b5ba8..ae51b777 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -3899,7 +3899,7 @@ Status Occt_view::export_document(Export_format fmt, Export_unit unit, const std return Status::user_error("Unknown export format."); } -Status Occt_view::import_step(const std::string& step_data, const bool union_shapes) +Status Occt_view::import_step(const std::string& step_data, const Step_import_mode mode) { std::vector named; if (Status st = utl_cad_file_info::read_step_named_tree(step_data, named); !st.is_ok()) @@ -3925,7 +3925,7 @@ Status Occt_view::import_step(const std::string& step_data, const bool union_sha if (!any_leaf) return Status::user_error("STEP: no valid shapes in file."); - if (union_shapes) + if (mode == Step_import_mode::Union_shapes) { TopoDS_Shape result; bool have = false; @@ -3957,6 +3957,35 @@ Status Occt_view::import_step(const std::string& step_data, const bool union_sha return Status::ok(); } + if (mode == Step_import_mode::Flat_solids) + { + std::vector added; + added.reserve(named.size()); + + for (utl_cad_file_info::Named_node& node : named) + { + if (node.is_group || node.shape.IsNull()) + continue; + + Shp_ptr shp = new Shp(*m_ctx, node.shape); + if (!node.name.empty()) + shp->set_name(unique_shape_name_(node.name.c_str())); + else + shp->set_name(unique_shape_name_("Shape")); + + shp->set_parent_id(0); + shp->set_sibling_order(next_sibling_order(0)); + add_shp_(shp); + added.push_back(capture_shape_rec(*shp)); + } + + if (added.empty()) + return Status::user_error("STEP: no valid shapes in file."); + + push_undo_delta(std::make_unique(std::move(added))); + return Status::ok(); + } + std::vector id_by_index(named.size(), 0); std::vector added; added.reserve(named.size()); diff --git a/src/gui_occt_view.h b/src/gui_occt_view.h index cd90d71e..3705fa39 100644 --- a/src/gui_occt_view.h +++ b/src/gui_occt_view.h @@ -96,8 +96,9 @@ class Occt_view : protected AIS_ViewController Ezy_asset_store& asset_store() { return m_assets; } const Ezy_asset_store& asset_store() const { return m_assets; } /// Import STEP (OCCT reads cascade mm) scaled into model space (inches * dimension_scale). - /// When \a union_shapes is true and the file has multiple roots, fuse them into one solid first. - [[nodiscard]] Status import_step(const std::string& step_data, bool union_shapes = false); + /// \a mode selects hierarchy groups, flat root leaves, or a single fused solid. + [[nodiscard]] Status import_step(const std::string& step_data, + Step_import_mode mode = Step_import_mode::Preserve_hierarchy); /// Import PLY (coords treated as inches) scaled into model space (* dimension_scale). bool import_ply(const std::string& ply_bytes); diff --git a/src/utl_types.h b/src/utl_types.h index 52173f6b..31b388ba 100644 --- a/src/utl_types.h +++ b/src/utl_types.h @@ -145,6 +145,14 @@ enum class Export_unit Millimeter }; +/// How STEP assemblies land in the Shape List on import. +enum class Step_import_mode +{ + Preserve_hierarchy, // XCAF assemblies -> groups; leaves keep file names + Flat_solids, // leaf solids only at document root + Union_shapes // fuse all leaves into one solid +}; + /// Project display/input length unit. Model space stays inch-scaled /// (`model = inches * dimension_scale`); this only affects UI and dimensions. enum class Project_unit From ecf7dca139a7256b0193daf2039d8e555230af3a Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sat, 1 Aug 2026 12:33:57 -0600 Subject: [PATCH 2/4] Import progress bar. --- CHANGELOG.md | 2 + docs/usage.md | 6 +- src/doc/gui.md | 2 +- src/doc/shape.md | 2 +- src/doc/utility.md | 2 +- src/gui.cpp | 293 ++++++++++++++++++++++++++- src/gui.h | 91 ++++++--- src/gui_occt_view.cpp | 111 +++++++---- src/gui_occt_view.h | 25 ++- src/utl_cad_file_info.cpp | 405 +++++++++++++++++++++++++------------- src/utl_cad_file_info.h | 14 +- src/utl_occt_progress.cpp | 35 ++++ src/utl_occt_progress.h | 45 +++++ 13 files changed, 811 insertions(+), 222 deletions(-) create mode 100644 src/utl_occt_progress.cpp create mode 100644 src/utl_occt_progress.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b435d22..7be3938c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **STEP Import as**: the Import dialog replaces the **Union shapes** checkbox with an **Import as** combo (**Preserve hierarchy** default, **Flat solids**, **Union shapes**). Hierarchy keeps XCAF assembly groups and product/instance names; flat adds leaf solids at the document root; union fuses into one solid. +- **STEP import progress**: after choosing a STEP file (and again on **Import into project**), a modal shows stage text and transfer progress. Desktop runs the OCCT transfer on a background thread so the UI stays responsive (Cancel available); metadata collect uses a single Transfer pass instead of parsing the file twice. + - **Configurable hotkeys**: **Settings -> Keyboard shortcuts** remaps shape tools (Move/Rotate/Scale/Extrude/Chamfer/Fillet/Polar duplicate/Cross-section), sketch tools (inspection, from face, operation axis, node/line/multi-line/arc/square/rectangle/circle/slot, Dimension), boolean commands (Cut/Fuse/Common), Delete, New/Open/Save, and Undo/Redo. Bindings persist as **`gui.hotkeys`** (human-readable chords; missing keys use defaults). Toolbar tooltips follow remaps. **Delete** / **Backspace** stay fixed delete aliases; Ctrl+Shift+Z stays a fixed redo alias. Fixed, unsupported, and conflicting chords are rejected on assign with a status toast (and inline message); remaps accept letters, digits, and Space only (punctuation/numpad rejected); per-row **Reset** rejects when the factory chord is already bound elsewhere (same conflict message as assign); hotkey capture clears when Settings closes. A **?** at the top of the section opens the [Keyboard shortcuts](https://ezycad.readthedocs.io/en/latest/usage-settings.html#keyboard-shortcuts) settings guide. - **Extrude Twist**: Options **Twist** checkbox. Two-phase flow locks height first, then sets twist angle about the face centroid (mouse, or Shift+Tab for degrees). Height length dim is cleared on lock; a temporary angle annotation on the extruded front face shows degrees during twist. With **Both sides**, ends twist symmetrically by +/- half the angle. Geometry uses ruled thru-sections with compatibility off (keeps tooth pairing) and cuts twisted hole solids so face bores survive; straight prism when twist is zero. Dense-face **Extrude fast preview** also applies during Twist (face copies translate and rotate; finalize builds the solid). diff --git a/docs/usage.md b/docs/usage.md index 9a19946b..8a8689e2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -249,7 +249,7 @@ The typical modeling workflow in EzyCad follows these steps: ### Import dialog -**File -> Import** opens an **Import** window for STEP or PLY. Review metadata, choose how STEP assemblies land in the Shape List (**Import as**, default **Preserve hierarchy**), then click **Import into project**. The window closes after a successful import. +**File -> Import** opens an **Import** window for STEP or PLY. Large STEP files show a progress modal while metadata is read (and again while importing). Review metadata, choose how STEP assemblies land in the Shape List (**Import as**, default **Preserve hierarchy**), then click **Import into project**. The window closes after a successful import. | Format | What the Import dialog shows | | -------------------------- | --------------------------------------------------------- | @@ -258,9 +258,9 @@ The typical modeling workflow in EzyCad follows these steps: **How to use:** 1. Choose **File -> Import** -2. Pick a `.step`, `.stp`, or `.ply` file +2. Pick a `.step`, `.stp`, or `.ply` file (STEP may show a progress modal while reading) 3. Review the label/value table; for STEP, choose **Import as** if needed -4. Click **Import into project** +4. Click **Import into project** (STEP may show a progress modal again while transferring) For in-document topology of an already-loaded solid, use [Shape info](#shape-info) from the Shape List. diff --git a/src/doc/gui.md b/src/doc/gui.md index 8ce512d0..2ccf31d9 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -13,7 +13,7 @@ Typical responsibilities: - ImGui frame: menu bar, dock space (passthrough central node for 3D input), toolbar, Sketch List, Shape List, Options, Settings, dist/angle popups. - Mode switching (`Mode` enum in [`mode.h`](../mode.h)) and parent-mode Esc behavior. - Persisted preferences (`ezycad_settings.json` via [`gui_settings.cpp`](../gui_settings.cpp)). -- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** shows metadata then loads STEP/PLY; STEP **Import as** chooses preserve hierarchy / flat solids / union). +- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** shows metadata then loads STEP/PLY; STEP **Import as** chooses preserve hierarchy / flat solids / union). STEP metadata/import uses a progress modal (`Atomic_progress_indicator`); desktop runs OCCT Transfer off the UI thread. - CAD/mesh interchange scales about the origin: project display lengths follow **File -> Project units** (`Project_unit`; Inch or Millimeter). Model space stays inch-scaled (`model = inches * dimension_scale`). STEP import converts OCCT cascade **mm** into model space; PLY import treats coords as inches. **File -> Export** asks for **Inches** or **Millimeters** (STEP/IGES declare that unit; STL/PLY write unitless coords in that scale). `.ezy` persists `projectUnit`. **Settings -> New project defaults** stores `gui.default_project_unit` and inch-based default 2D framing for **File -> New**. - Contextual help links (`doc_urls` in `gui.h`). diff --git a/src/doc/shape.md b/src/doc/shape.md index 4046e7ae..c68e0303 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -35,7 +35,7 @@ Typical uses: - Each solid stores a `gp_Ax3` local frame. New geometry defaults to a world-aligned frame at its bounding-box center. Baked move/rotate/scale transforms update the frame; project JSON and shape undo records preserve it. - `Shp_ptr` is `opencascade::handle`. New shapes are allocated with `new Shp(ctx(), topo_shape)` then registered through `Occt_view::add_shp_()`. Groups use `Shp::create_group` (empty compound, never displayed). - Hierarchy: `parent_id` (0 = root) + `sibling_order`. Organizational groups only (no transform inheritance). Helpers: `shape_children`, `shape_descendant_solids`, `group_shapes`, `ungroup_shape`, `reparent_shape`, `would_reparent_create_cycle`. -- **Current group** (`Occt_view::current_group_id`, 0 = root): Shape List click sets it; empty groups are valid. Primitives / extrude / revolve / PLY / unioned STEP import (`Step_import_mode::Union_shapes`) call `add_shp_(..., use_current_group=true)` so new solids land under that group. Hierarchical and flat STEP import place roots at document root (`parent_id` 0). Booleans keep `assign_result_parent_`. +- **Current group** (`Occt_view::current_group_id`, 0 = root): Shape List click sets it; empty groups are valid. Primitives / extrude / revolve / PLY / unioned STEP import (`Step_import_mode::Union_shapes`) call `add_shp_(..., use_current_group=true)` so new solids land under that group. Hierarchical and flat STEP import place roots at document root (`parent_id` 0). STEP import may prepare geometry off-thread (`prepare_step_import`) then `commit_step_import` on the UI thread. Booleans keep `assign_result_parent_`. - `Shp_operation_base` is a **`friend` of `Occt_view`** so operations can call `add_shp_()`, read selection, and use pick helpers without exposing those on the public view API. - **`Occt_view&` must outlive** all `Shp_*` operation objects (they are member subobjects of the view). - Boolean/polar results call `assign_result_parent_` so the new solid shares the inputs' parent when all match; otherwise root (`parent_id` 0). diff --git a/src/doc/utility.md b/src/doc/utility.md index fb1ab721..72f42d7b 100644 --- a/src/doc/utility.md +++ b/src/doc/utility.md @@ -135,7 +135,7 @@ Used by **File -> Import**. Reads file bytes only until the user confirms import | `read_step_named_bodies` | STEPCAF/XCAF bodies + product names (flat; falls back to plain reader) | | `read_step_named_tree` | STEPCAF/XCAF assembly tree as group/leaf `Named_node`s (falls back flat) | -`Occt_view::import_step` takes `Step_import_mode` (`utl_types.h`): preserve hierarchy (default), flat root leaves, or union. +`Occt_view::import_step` takes `Step_import_mode` (`utl_types.h`): preserve hierarchy (default), flat root leaves, or union. Heavy work splits into `prepare_step_import` (thread-safe geometry) + `commit_step_import` (UI thread). `collect` / STEP Transfer accept optional `Atomic_progress_indicator` / `Message_ProgressRange` (`utl_occt_progress.h`). ## Logging and debug diff --git a/src/gui.cpp b/src/gui.cpp index e878a410..301ba093 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include "utl_settings.h" @@ -148,6 +150,8 @@ void GUI::render_gui() shape_list_(); shape_info_dialog_(); file_inspector_dialog_(); + poll_cad_busy_(); + cad_busy_dialog_(); options_(); message_status_window_(); error_modal_dialog_(); @@ -3031,6 +3035,276 @@ void GUI::open_file_inspector_(const std::string& file_path, const std::string& m_file_inspector_open = true; } +bool GUI::cad_busy_() const { return m_cad_busy_kind != Cad_busy_kind::Idle; } + +void GUI::cancel_cad_busy_() +{ + if (!m_cad_busy_progress.IsNull()) + m_cad_busy_progress->request_cancel(); +} + +void GUI::begin_step_inspect_(const std::string& file_path, const std::string& file_bytes) +{ + if (cad_busy_()) + return; + + m_cad_busy_kind = Cad_busy_kind::Inspect; + m_cad_busy_path = file_path; + m_cad_busy_bytes = file_bytes; + m_cad_busy_title = "Reading STEP"; + m_cad_busy_progress = new Atomic_progress_indicator(); + m_cad_busy_progress->set_stage("Starting..."); + m_cad_busy_open_popup = true; + m_cad_busy_modal_open = true; + +#ifndef __EMSCRIPTEN__ + const std::string path = m_cad_busy_path; + const std::string bytes = m_cad_busy_bytes; + const Atomic_progress_indicator_ptr prog = m_cad_busy_progress; + m_cad_busy_inspect_fut = + std::async(std::launch::async, [path, bytes, prog]() { return utl_cad_file_info::collect(path, bytes, prog); }); +#else + m_cad_busy_run_next_frame = true; +#endif +} + +void GUI::begin_step_import_(const Step_import_mode mode) +{ + if (cad_busy_() || m_file_inspector_bytes.empty() || !m_view) + return; + + m_cad_busy_kind = Cad_busy_kind::Import; + m_cad_busy_path = m_file_inspector_path; + m_cad_busy_bytes = m_file_inspector_bytes; + m_cad_busy_import_mode = mode; + m_cad_busy_title = "Importing STEP"; + m_cad_busy_progress = new Atomic_progress_indicator(); + m_cad_busy_progress->set_stage("Starting..."); + m_cad_busy_open_popup = true; + m_cad_busy_modal_open = true; + +#ifndef __EMSCRIPTEN__ + const std::string bytes = m_cad_busy_bytes; + const Step_import_mode m = mode; + const double scale = m_view->step_import_model_scale(); + const Atomic_progress_indicator_ptr prog = m_cad_busy_progress; + m_cad_busy_import_fut = std::async(std::launch::async, + [bytes, m, scale, prog]() -> std::pair + { + Occt_view::Step_import_geom geom; + Status st = Occt_view::prepare_step_import(bytes, m, scale, geom, prog); + return {st, std::move(geom)}; + }); +#else + m_cad_busy_run_next_frame = true; +#endif +} + +void GUI::poll_cad_busy_() +{ + if (!cad_busy_()) + return; + +#ifdef __EMSCRIPTEN__ + if (m_cad_busy_run_next_frame) + { + // Paint the modal for one frame, then run the heavy work on the next poll. + m_cad_busy_run_next_frame = false; + return; + } + + if (m_cad_busy_kind == Cad_busy_kind::Inspect) + { + const auto lines = utl_cad_file_info::collect(m_cad_busy_path, m_cad_busy_bytes, m_cad_busy_progress); + const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); + m_cad_busy_kind = Cad_busy_kind::Idle; + m_cad_busy_progress = {}; + m_cad_busy_modal_open = false; + if (cancelled) + { + show_message("STEP read cancelled."); + return; + } + + m_file_inspector_path = m_cad_busy_path; + m_file_inspector_bytes = m_cad_busy_bytes; + m_file_inspector_fmt = utl_cad_file_info::Format::Step; + m_file_inspector_lines = lines; + m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; + m_file_inspector_open = true; + return; + } + + if (m_cad_busy_kind == Cad_busy_kind::Import) + { + Occt_view::Step_import_geom geom; + Status st = Occt_view::prepare_step_import(m_cad_busy_bytes, m_cad_busy_import_mode, m_view->step_import_model_scale(), + geom, m_cad_busy_progress); + const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); + m_cad_busy_kind = Cad_busy_kind::Idle; + m_cad_busy_progress = {}; + m_cad_busy_modal_open = false; + if (cancelled || (!st.is_ok() && st.message().find("cancelled") != std::string::npos)) + { + show_message("STEP import cancelled."); + return; + } + + if (!st.is_ok()) + { + show_message(st.message()); + return; + } + + if (Status commit = m_view->commit_step_import(geom); !commit.is_ok()) + { + show_message(commit.message()); + return; + } + + const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); + switch (m_cad_busy_import_mode) + { + case Step_import_mode::Union_shapes: + show_message("Imported (union): " + name); + break; + case Step_import_mode::Flat_solids: + show_message("Imported (flat): " + name); + break; + default: + show_message("Imported: " + name); + break; + } + close_file_inspector_(); + } +#else + if (m_cad_busy_kind == Cad_busy_kind::Inspect) + { + if (!m_cad_busy_inspect_fut.valid()) + return; + + if (m_cad_busy_inspect_fut.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return; + + auto lines = m_cad_busy_inspect_fut.get(); + const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); + m_cad_busy_kind = Cad_busy_kind::Idle; + m_cad_busy_progress = {}; + m_cad_busy_modal_open = false; + if (cancelled) + { + show_message("STEP read cancelled."); + return; + } + + m_file_inspector_path = m_cad_busy_path; + m_file_inspector_bytes = m_cad_busy_bytes; + m_file_inspector_fmt = utl_cad_file_info::Format::Step; + m_file_inspector_lines = std::move(lines); + m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; + m_file_inspector_open = true; + return; + } + + if (m_cad_busy_kind == Cad_busy_kind::Import) + { + if (!m_cad_busy_import_fut.valid()) + return; + + if (m_cad_busy_import_fut.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return; + + auto [st, geom] = m_cad_busy_import_fut.get(); + const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); + m_cad_busy_kind = Cad_busy_kind::Idle; + m_cad_busy_progress = {}; + m_cad_busy_modal_open = false; + if (cancelled || (!st.is_ok() && st.message().find("cancelled") != std::string::npos)) + { + show_message("STEP import cancelled."); + return; + } + + if (!st.is_ok()) + { + show_message(st.message()); + return; + } + + if (Status commit = m_view->commit_step_import(geom); !commit.is_ok()) + { + show_message(commit.message()); + return; + } + + const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); + switch (m_cad_busy_import_mode) + { + case Step_import_mode::Union_shapes: + show_message("Imported (union): " + name); + break; + case Step_import_mode::Flat_solids: + show_message("Imported (flat): " + name); + break; + default: + show_message("Imported: " + name); + break; + } + close_file_inspector_(); + } +#endif +} + +void GUI::cad_busy_dialog_() +{ + if (m_cad_busy_open_popup) + { + ImGui::OpenPopup("##EzyCadCadBusy"); + m_cad_busy_open_popup = false; + } + + if (!cad_busy_() && !m_cad_busy_modal_open) + return; + + ImGui::SetNextWindowSize(ImVec2(420.0f, 0.0f), ImGuiCond_Appearing); + if (!ImGui::BeginPopupModal("##EzyCadCadBusy", &m_cad_busy_modal_open, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (!m_cad_busy_modal_open && cad_busy_()) + cancel_cad_busy_(); + + return; + } + + ImGui::TextUnformatted(m_cad_busy_title.empty() ? "Working..." : m_cad_busy_title.c_str()); + const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); + if (!name.empty()) + ImGui::TextWrapped("%s", name.c_str()); + + std::string stage = m_cad_busy_progress.IsNull() ? std::string("Working...") : m_cad_busy_progress->stage(); + if (stage.empty()) + stage = "Working..."; + ImGui::TextWrapped("%s", stage.c_str()); + + const float pos = m_cad_busy_progress.IsNull() ? 0.f : m_cad_busy_progress->position(); + if (pos > 0.001f) + { + char overlay[32]; + std::snprintf(overlay, sizeof(overlay), "%.0f%%", static_cast(pos) * 100.0); + ImGui::ProgressBar(pos, ImVec2(-1.0f, 0.0f), overlay); + } + else + ImGui::ProgressBar(-1.0f * static_cast(ImGui::GetTime()), ImVec2(-1.0f, 0.0f), "Working..."); + +#ifndef __EMSCRIPTEN__ + if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) + cancel_cad_busy_(); +#else + ImGui::TextDisabled("Please wait..."); +#endif + + ImGui::EndPopup(); +} + void GUI::close_file_inspector_() { m_file_inspector_open = false; @@ -3072,23 +3346,26 @@ void GUI::file_inspector_dialog_() if (ImGui::Combo("Import as", &mode_i, k_step_import_labels, IM_ARRAYSIZE(k_step_import_labels))) m_file_inspector_step_mode = static_cast(mode_i); if (ui_show_contextual_help() && ImGui::IsItemHovered()) - ImGui::SetTooltip( - "Preserve hierarchy: Shape List groups from the STEP assembly (default).\n" - "Flat solids: leaf solids only at the document root.\n" - "Union shapes: fuse all bodies into one solid."); + ImGui::SetTooltip("Preserve hierarchy: Shape List groups from the STEP assembly (default).\n" + "Flat solids: leaf solids only at the document root.\n" + "Union shapes: fuse all bodies into one solid."); } else m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; + ImGui::BeginDisabled(cad_busy_()); if (ImGui::Button("Import into project")) { - if (on_import_file(m_file_inspector_path, m_file_inspector_bytes, m_file_inspector_step_mode)) + if (m_file_inspector_fmt == utl_cad_file_info::Format::Step) + begin_step_import_(m_file_inspector_step_mode); + else if (on_import_file(m_file_inspector_path, m_file_inspector_bytes, m_file_inspector_step_mode)) { close_file_inspector_(); ImGui::End(); return; } } + ImGui::EndDisabled(); } ImGui::Separator(); @@ -4304,7 +4581,11 @@ bool GUI::on_import_file(const std::string& file_path, const std::string& file_d void GUI::on_inspector_file(const std::string& file_path, const std::string& file_data) { - open_file_inspector_(file_path, file_data); + const utl_cad_file_info::Format fmt = utl_cad_file_info::detect(file_path, file_data); + if (fmt == utl_cad_file_info::Format::Step) + begin_step_inspect_(file_path, file_data); + else + open_file_inspector_(file_path, file_data); } #ifdef __EMSCRIPTEN__ diff --git a/src/gui.h b/src/gui.h index 7fb6b464..931930ca 100644 --- a/src/gui.h +++ b/src/gui.h @@ -4,6 +4,9 @@ #include // For message status window (from previous request) #include #include +#ifndef __EMSCRIPTEN__ +#include +#endif #include #include #include @@ -381,9 +384,8 @@ class GUI #endif void on_file(const std::string& file_path, const std::string& file_bytes, bool announce_load = true); - [[nodiscard]] bool on_import_file(const std::string& file_path, - const std::string& file_data, - Step_import_mode step_mode = Step_import_mode::Preserve_hierarchy); + [[nodiscard]] bool on_import_file(const std::string& file_path, const std::string& file_data, + Step_import_mode step_mode = Step_import_mode::Preserve_hierarchy); void on_inspector_file(const std::string& file_path, const std::string& file_data); /// Emscripten `on_sketch_underlay_selected` routes here (must be public for C callback). void on_sketch_underlay_file(const std::string& file_path, const std::string& file_bytes); @@ -436,6 +438,12 @@ class GUI void file_inspector_dialog_(); void open_file_inspector_(const std::string& file_path, const std::string& file_bytes); void close_file_inspector_(); + void cad_busy_dialog_(); + void poll_cad_busy_(); + void begin_step_inspect_(const std::string& file_path, const std::string& file_bytes); + void begin_step_import_(Step_import_mode mode); + void cancel_cad_busy_(); + [[nodiscard]] bool cad_busy_() const; // Mode + Options panel (gui_mode.cpp) void options_(); @@ -710,34 +718,55 @@ class GUI std::string m_file_inspector_bytes; utl_cad_file_info::Format m_file_inspector_fmt{utl_cad_file_info::Format::Unknown}; std::vector m_file_inspector_lines; - std::string m_about_markdown; - uint32_t m_about_splash_gl{0}; - glm::ivec2 m_about_splash_size{512, 512}; - bool m_about_assets_loaded{false}; - bool m_open_add_box_popup{false}; - glm::dvec3 m_add_box_origin{0.0, 0.0, 0.0}; - glm::dvec3 m_add_box_size{1.0, 1.0, 1.0}; - bool m_open_add_pyramid_popup{false}; - glm::dvec3 m_add_pyramid_origin{0.0, 0.0, 0.0}; - double m_add_pyramid_side{1}; - bool m_open_add_sphere_popup{false}; - glm::dvec3 m_add_sphere_origin{0.0, 0.0, 0.0}; - double m_add_sphere_radius{1}; - bool m_open_add_cylinder_popup{false}; - glm::dvec3 m_add_cylinder_origin{0.0, 0.0, 0.0}; - double m_add_cylinder_radius{1}, m_add_cylinder_height{1}; - bool m_open_add_cone_popup{false}; - glm::dvec3 m_add_cone_origin{0.0, 0.0, 0.0}; - double m_add_cone_R1{1}, m_add_cone_R2{0}, m_add_cone_height{1}; - bool m_open_add_torus_popup{false}; - glm::dvec3 m_add_torus_origin{0.0, 0.0, 0.0}; - double m_add_torus_R1{1}, m_add_torus_R2{0.5}; - bool m_open_add_sketch_popup{false}; - int m_new_sketch_plane{0}; // 0=XY, 1=XZ, 2=YZ - double m_new_sketch_offset{}; - bool m_hide_all_shapes{false}; - int m_ui_verbosity{k_gui_ui_verbosity_default}; - bool m_dark_mode{false}; + + enum class Cad_busy_kind : uint8_t + { + Idle, + Inspect, + Import + }; + Cad_busy_kind m_cad_busy_kind{Cad_busy_kind::Idle}; + bool m_cad_busy_open_popup{false}; + bool m_cad_busy_modal_open{false}; + Atomic_progress_indicator_ptr m_cad_busy_progress; + std::string m_cad_busy_path; + std::string m_cad_busy_bytes; + std::string m_cad_busy_title; + Step_import_mode m_cad_busy_import_mode{Step_import_mode::Preserve_hierarchy}; +#ifdef __EMSCRIPTEN__ + bool m_cad_busy_run_next_frame{false}; +#else + std::future> m_cad_busy_inspect_fut; + std::future> m_cad_busy_import_fut; +#endif + std::string m_about_markdown; + uint32_t m_about_splash_gl{0}; + glm::ivec2 m_about_splash_size{512, 512}; + bool m_about_assets_loaded{false}; + bool m_open_add_box_popup{false}; + glm::dvec3 m_add_box_origin{0.0, 0.0, 0.0}; + glm::dvec3 m_add_box_size{1.0, 1.0, 1.0}; + bool m_open_add_pyramid_popup{false}; + glm::dvec3 m_add_pyramid_origin{0.0, 0.0, 0.0}; + double m_add_pyramid_side{1}; + bool m_open_add_sphere_popup{false}; + glm::dvec3 m_add_sphere_origin{0.0, 0.0, 0.0}; + double m_add_sphere_radius{1}; + bool m_open_add_cylinder_popup{false}; + glm::dvec3 m_add_cylinder_origin{0.0, 0.0, 0.0}; + double m_add_cylinder_radius{1}, m_add_cylinder_height{1}; + bool m_open_add_cone_popup{false}; + glm::dvec3 m_add_cone_origin{0.0, 0.0, 0.0}; + double m_add_cone_R1{1}, m_add_cone_R2{0}, m_add_cone_height{1}; + bool m_open_add_torus_popup{false}; + glm::dvec3 m_add_torus_origin{0.0, 0.0, 0.0}; + double m_add_torus_R1{1}, m_add_torus_R2{0.5}; + bool m_open_add_sketch_popup{false}; + int m_new_sketch_plane{0}; // 0=XY, 1=XZ, 2=YZ + double m_new_sketch_offset{}; + bool m_hide_all_shapes{false}; + int m_ui_verbosity{k_gui_ui_verbosity_default}; + bool m_dark_mode{false}; #ifndef NDEBUG bool m_show_dbg{false}; #endif diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index ae51b777..5fdc6c6c 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -3899,23 +3899,40 @@ Status Occt_view::export_document(Export_format fmt, Export_unit unit, const std return Status::user_error("Unknown export format."); } -Status Occt_view::import_step(const std::string& step_data, const Step_import_mode mode) +double Occt_view::step_import_model_scale() const { return step_import_to_model_scale_(get_dimension_scale()); } + +Status Occt_view::prepare_step_import(const std::string& step_data, const Step_import_mode mode, const double to_model_scale, + Step_import_geom& out, const Atomic_progress_indicator_ptr& progress) { - std::vector named; - if (Status st = utl_cad_file_info::read_step_named_tree(step_data, named); !st.is_ok()) + out = {}; + out.mode = mode; + + Message_ProgressRange range; + if (!progress.IsNull()) + { + progress->set_stage("Transferring STEP..."); + range = progress->Start(); + } + + if (Status st = utl_cad_file_info::read_step_named_tree(step_data, out.named, range); !st.is_ok()) return st; - const double to_model = step_import_to_model_scale_(get_dimension_scale()); - for (utl_cad_file_info::Named_node& node : named) + if (!progress.IsNull() && progress->cancelled()) + return Status::user_error("STEP: import cancelled."); + + if (!progress.IsNull()) + progress->set_stage("Scaling shapes..."); + + for (utl_cad_file_info::Named_node& node : out.named) { if (node.is_group || node.shape.IsNull()) continue; - node.shape = scale_shape_about_origin_(node.shape, to_model); + node.shape = scale_shape_about_origin_(node.shape, to_model_scale); } bool any_leaf = false; - for (const utl_cad_file_info::Named_node& n : named) + for (const utl_cad_file_info::Named_node& n : out.named) if (!n.is_group && !n.shape.IsNull()) { any_leaf = true; @@ -3925,44 +3942,60 @@ Status Occt_view::import_step(const std::string& step_data, const Step_import_mo if (!any_leaf) return Status::user_error("STEP: no valid shapes in file."); - if (mode == Step_import_mode::Union_shapes) + if (mode != Step_import_mode::Union_shapes) + return Status::ok(); + + if (!progress.IsNull()) + progress->set_stage("Fusing shapes..."); + + TopoDS_Shape result; + bool have = false; + for (utl_cad_file_info::Named_node& node : out.named) { - TopoDS_Shape result; - bool have = false; - for (utl_cad_file_info::Named_node& node : named) + if (node.is_group || node.shape.IsNull()) + continue; + + if (!have) { - if (node.is_group || node.shape.IsNull()) - continue; + result = node.shape; + have = true; + continue; + } - if (!have) - { - result = node.shape; - have = true; - continue; - } + BRepAlgoAPI_Fuse fuse_op(result, node.shape); + if (!fuse_op.IsDone()) + return Status::user_error("STEP: union of imported shapes failed."); - BRepAlgoAPI_Fuse fuse_op(result, node.shape); - if (!fuse_op.IsDone()) - return Status::user_error("STEP: union of imported shapes failed."); + result = fuse_op.Shape(); + if (result.IsNull()) + return Status::user_error("STEP: union produced an empty shape."); + } - result = fuse_op.Shape(); - if (result.IsNull()) - return Status::user_error("STEP: union produced an empty shape."); - } + out.fused = result; + out.named.clear(); + return Status::ok(); +} - Shp_ptr shp = new Shp(*m_ctx, result); +Status Occt_view::commit_step_import(Step_import_geom& geom) +{ + if (geom.mode == Step_import_mode::Union_shapes) + { + if (geom.fused.IsNull()) + return Status::user_error("STEP: union produced an empty shape."); + + Shp_ptr shp = new Shp(*m_ctx, geom.fused); shp->set_name(unique_shape_name_("Fused")); add_shp_(shp, true); push_undo_delta(std::make_unique(std::vector{capture_shape_rec(*shp)})); return Status::ok(); } - if (mode == Step_import_mode::Flat_solids) + if (geom.mode == Step_import_mode::Flat_solids) { std::vector added; - added.reserve(named.size()); + added.reserve(geom.named.size()); - for (utl_cad_file_info::Named_node& node : named) + for (utl_cad_file_info::Named_node& node : geom.named) { if (node.is_group || node.shape.IsNull()) continue; @@ -3986,13 +4019,13 @@ Status Occt_view::import_step(const std::string& step_data, const Step_import_mo return Status::ok(); } - std::vector id_by_index(named.size(), 0); + std::vector id_by_index(geom.named.size(), 0); std::vector added; - added.reserve(named.size()); + added.reserve(geom.named.size()); - for (size_t i = 0; i < named.size(); ++i) + for (size_t i = 0; i < geom.named.size(); ++i) { - utl_cad_file_info::Named_node& node = named[i]; + utl_cad_file_info::Named_node& node = geom.named[i]; Shape_id parent_id = 0; if (node.parent_index >= 0 && static_cast(node.parent_index) < id_by_index.size()) parent_id = id_by_index[static_cast(node.parent_index)]; @@ -4029,6 +4062,16 @@ Status Occt_view::import_step(const std::string& step_data, const Step_import_mo return Status::ok(); } +Status Occt_view::import_step(const std::string& step_data, const Step_import_mode mode, + const Atomic_progress_indicator_ptr& progress) +{ + Step_import_geom geom; + if (Status st = prepare_step_import(step_data, mode, step_import_model_scale(), geom, progress); !st.is_ok()) + return st; + + return commit_step_import(geom); +} + bool Occt_view::import_ply(const std::string& ply_bytes) { TopoDS_Shape shape; diff --git a/src/gui_occt_view.h b/src/gui_occt_view.h index 3705fa39..216ed496 100644 --- a/src/gui_occt_view.h +++ b/src/gui_occt_view.h @@ -29,7 +29,9 @@ #include "shp_cross_section.h" #include "utl_types.h" #include "utl_asset_store.h" +#include "utl_cad_file_info.h" #include "utl_geom.h" +#include "utl_occt_progress.h" #include @@ -95,10 +97,29 @@ class Occt_view : protected AIS_ViewController void load(const std::string& json_str, bool restore_view = true); Ezy_asset_store& asset_store() { return m_assets; } const Ezy_asset_store& asset_store() const { return m_assets; } + /// Geometry prepared off the UI thread for STEP import (no AIS / document mutation). + struct Step_import_geom + { + Step_import_mode mode{Step_import_mode::Preserve_hierarchy}; + std::vector named; + TopoDS_Shape fused; + }; + /// Import STEP (OCCT reads cascade mm) scaled into model space (inches * dimension_scale). /// \a mode selects hierarchy groups, flat root leaves, or a single fused solid. - [[nodiscard]] Status import_step(const std::string& step_data, - Step_import_mode mode = Step_import_mode::Preserve_hierarchy); + [[nodiscard]] Status import_step(const std::string& step_data, Step_import_mode mode = Step_import_mode::Preserve_hierarchy, + const Atomic_progress_indicator_ptr& progress = {}); + + /// Read/transfer/scale/fuse STEP on a worker thread. Call \ref commit_step_import on the UI thread. + [[nodiscard]] static Status prepare_step_import(const std::string& step_data, Step_import_mode mode, double to_model_scale, + Step_import_geom& out, const Atomic_progress_indicator_ptr& progress = {}); + + /// Add prepared STEP geometry to the document (UI thread). + [[nodiscard]] Status commit_step_import(Step_import_geom& geom); + + /// Model-space scale factor for STEP import (mm cascade -> inches * dimension_scale). + [[nodiscard]] double step_import_model_scale() const; + /// Import PLY (coords treated as inches) scaled into model space (* dimension_scale). bool import_ply(const std::string& ply_bytes); diff --git a/src/utl_cad_file_info.cpp b/src/utl_cad_file_info.cpp index d17f6a25..58d7b231 100644 --- a/src/utl_cad_file_info.cpp +++ b/src/utl_cad_file_info.cpp @@ -8,9 +8,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -21,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -167,56 +170,23 @@ void append_common(std::vector& lines, const std::string& file_path, const add_line(lines, "Exportable", "yes (File -> Export)"); } -std::vector collect_step(const std::string& file_path, const std::string& file_bytes) +void set_progress_stage(const Atomic_progress_indicator_ptr& progress, const char* stage) { - std::vector lines; - append_common(lines, file_path, file_bytes, Format::Step); - add_blank(lines); - - Interface_Static::SetCVal("xstep.cascade.unit", "MM"); - - STEPControl_Reader reader; - std::istringstream stream(file_bytes); - const IFSelect_ReturnStatus read_st = reader.ReadStream("", stream); - if (read_st != IFSelect_RetDone) - { - add_line(lines, "Status", "could not read STEP data"); - return lines; - } - - const int nb_roots = reader.NbRootsForTransfer(); - add_line(lines, "Roots", std::to_string(nb_roots)); - - const int transferred = reader.TransferRoots(); - add_line(lines, "Transferred", std::to_string(transferred)); - add_line(lines, "Shapes", std::to_string(reader.NbShapes())); - - std::vector bodies; - for (int i = 1; i <= reader.NbShapes(); ++i) - append_cad_import_bodies(reader.Shape(i), bodies); - add_line(lines, "Import bodies", std::to_string(bodies.size())); - - int named_count = 0; - { - std::vector named; - if (read_step_named_bodies(file_bytes, named).is_ok()) - { - for (const Named_body& b : named) - if (!b.name.empty()) - ++named_count; - } - } - add_line(lines, "Named bodies", std::to_string(named_count)); + if (!progress.IsNull()) + progress->set_stage(stage); +} - const char* cascade = Interface_Static::CVal("xstep.cascade.unit"); - if (cascade && cascade[0] != '\0') - add_line(lines, "Cascade unit", cascade); +Message_ProgressRange start_progress(const Atomic_progress_indicator_ptr& progress) +{ + if (progress.IsNull()) + return Message_ProgressRange(); - add_blank(lines); - append_shape_summary(lines, reader.OneShape()); - return lines; + return progress->Start(); } +std::vector collect_step(const std::string& file_path, const std::string& file_bytes, + const Atomic_progress_indicator_ptr& progress); + std::vector collect_iges(const std::string& file_path, const std::string& file_bytes) { std::vector lines; @@ -356,85 +326,7 @@ std::vector collect_ply(const std::string& file_path, const std::string& f return lines; } -} // namespace - -Format detect(const std::string& file_path, const std::string& file_bytes) -{ - const std::string ext = to_lower_ext(file_path); - if (ext == ".step" || ext == ".stp") - return Format::Step; - - if (ext == ".igs" || ext == ".iges") - return Format::Iges; - - if (ext == ".stl") - return Format::Stl; - - if (ext == ".ply") - return Format::Ply; - - // Content sniff when extension is missing or wrong (e.g. browser basename only). - if (looks_like_ply(file_bytes)) - return Format::Ply; - - if (looks_like_step(file_bytes)) - return Format::Step; - - if (looks_like_stl(file_bytes)) - return Format::Stl; - - if (looks_like_iges(file_bytes)) - return Format::Iges; - - return Format::Unknown; -} - -bool can_import(Format fmt) { return fmt == Format::Step || fmt == Format::Ply; } - -const char* format_label(Format fmt) -{ - switch (fmt) - { - case Format::Step: - return "STEP"; - case Format::Iges: - return "IGES"; - case Format::Stl: - return "STL"; - case Format::Ply: - return "PLY"; - default: - return "Unknown"; - } -} -std::vector collect(const std::string& file_path, const std::string& file_bytes) -{ - const Format fmt = detect(file_path, file_bytes); - switch (fmt) - { - case Format::Step: - return collect_step(file_path, file_bytes); - case Format::Iges: - return collect_iges(file_path, file_bytes); - case Format::Stl: - return collect_stl(file_path, file_bytes); - case Format::Ply: - return collect_ply(file_path, file_bytes); - default: - { - std::vector lines; - append_common(lines, file_path, file_bytes, Format::Unknown); - add_blank(lines); - add_line(lines, "Status", "unsupported or unrecognized format"); - add_line(lines, "Hint", "Open STEP, IGES, STL, or PLY"); - return lines; - } - } -} - -namespace -{ std::string trim_name(std::string s) { while (!s.empty() && std::isspace(static_cast(s.back()))) @@ -548,7 +440,8 @@ void append_tree_from_label(const XCAFDoc_ShapeTool_ptr& shapes, const TDF_Label } } -Status read_step_named_bodies_xcaf(const std::string& file_bytes, std::vector& out) +Status read_step_named_bodies_xcaf(const std::string& file_bytes, std::vector& out, + const Message_ProgressRange& progress) { Interface_Static::SetCVal("xstep.cascade.unit", "MM"); @@ -571,7 +464,7 @@ Status read_step_named_bodies_xcaf(const std::string& file_bytes, std::vectorMain()); @@ -592,7 +485,8 @@ Status read_step_named_bodies_xcaf(const std::string& file_bytes, std::vector& out) +Status read_step_named_bodies_plain(const std::string& file_bytes, std::vector& out, + const Message_ProgressRange& progress) { Interface_Static::SetCVal("xstep.cascade.unit", "MM"); @@ -601,7 +495,7 @@ Status read_step_named_bodies_plain(const std::string& file_bytes, std::vector& out) +bool collect_step_has_status_error(const std::vector& lines) { - out.clear(); - const Status xcaf = read_step_named_bodies_xcaf(file_bytes, out); - if (xcaf.is_ok()) + for (const Line& line : lines) + if (line.label == "Status") + return true; + + return false; +} + +std::vector collect_step_xcaf(const std::string& file_path, const std::string& file_bytes, + const Atomic_progress_indicator_ptr& progress) +{ + std::vector lines; + append_common(lines, file_path, file_bytes, Format::Step); + add_blank(lines); + + Interface_Static::SetCVal("xstep.cascade.unit", "MM"); + + set_progress_stage(progress, "Reading STEP..."); + STEPCAFControl_Reader reader; + reader.SetNameMode(true); + reader.SetColorMode(false); + reader.SetLayerMode(false); + + std::istringstream stream(file_bytes); + const IFSelect_ReturnStatus read_st = reader.ReadStream("", stream); + if (read_st != IFSelect_RetDone) + { + add_line(lines, "Status", "could not read STEP data"); + return lines; + } + + add_line(lines, "Roots", std::to_string(reader.NbRootsForTransfer())); + + XCAFApp_Application_ptr app = XCAFApp_Application::GetApplication(); + if (app.IsNull()) + { + add_line(lines, "Status", "XCAF application unavailable"); + return lines; + } + + TDocStd_Document_ptr doc; + app->NewDocument("MDTV-XCAF", doc); + if (doc.IsNull()) + { + add_line(lines, "Status", "could not create XCAF document"); + return lines; + } + + set_progress_stage(progress, "Transferring STEP..."); + if (!reader.Transfer(doc, start_progress(progress))) + { + add_line(lines, "Status", "no geometry was transferred"); + return lines; + } + + if (!progress.IsNull() && progress->cancelled()) + { + add_line(lines, "Status", "cancelled"); + return lines; + } + + XCAFDoc_ShapeTool_ptr shapes = XCAFDoc_DocumentTool::ShapeTool(doc->Main()); + if (shapes.IsNull()) + { + add_line(lines, "Status", "missing XCAF shape tool"); + return lines; + } + + NCollection_Sequence free_shapes; + shapes->GetFreeShapes(free_shapes); + add_line(lines, "Transferred", std::to_string(free_shapes.Length())); + add_line(lines, "Shapes", std::to_string(free_shapes.Length())); + + std::vector named; + for (int i = 1; i <= free_shapes.Length(); ++i) + append_named_from_label(shapes, free_shapes.Value(i), named); + + add_line(lines, "Import bodies", std::to_string(named.size())); + int named_count = 0; + for (const Named_body& b : named) + if (!b.name.empty()) + ++named_count; + add_line(lines, "Named bodies", std::to_string(named_count)); + + const char* cascade = Interface_Static::CVal("xstep.cascade.unit"); + if (cascade && cascade[0] != '\0') + add_line(lines, "Cascade unit", cascade); + + TopoDS_Compound compound; + BRep_Builder builder; + builder.MakeCompound(compound); + for (const Named_body& b : named) + if (!b.shape.IsNull()) + builder.Add(compound, b.shape); + + add_blank(lines); + append_shape_summary(lines, compound); + return lines; +} + +std::vector collect_step_plain(const std::string& file_path, const std::string& file_bytes, + const Atomic_progress_indicator_ptr& progress) +{ + std::vector lines; + append_common(lines, file_path, file_bytes, Format::Step); + add_blank(lines); + + Interface_Static::SetCVal("xstep.cascade.unit", "MM"); + + set_progress_stage(progress, "Reading STEP..."); + STEPControl_Reader reader; + std::istringstream stream(file_bytes); + if (reader.ReadStream("", stream) != IFSelect_RetDone) + { + add_line(lines, "Status", "could not read STEP data"); + return lines; + } + + add_line(lines, "Roots", std::to_string(reader.NbRootsForTransfer())); + + set_progress_stage(progress, "Transferring STEP..."); + const int transferred = reader.TransferRoots(start_progress(progress)); + add_line(lines, "Transferred", std::to_string(transferred)); + add_line(lines, "Shapes", std::to_string(reader.NbShapes())); + + std::vector bodies; + for (int i = 1; i <= reader.NbShapes(); ++i) + append_cad_import_bodies(reader.Shape(i), bodies); + add_line(lines, "Import bodies", std::to_string(bodies.size())); + add_line(lines, "Named bodies", "0"); + + const char* cascade = Interface_Static::CVal("xstep.cascade.unit"); + if (cascade && cascade[0] != '\0') + add_line(lines, "Cascade unit", cascade); + + add_blank(lines); + append_shape_summary(lines, reader.OneShape()); + return lines; +} + +std::vector collect_step(const std::string& file_path, const std::string& file_bytes, + const Atomic_progress_indicator_ptr& progress) +{ + // Single Transfer pass (XCAF preferred). Avoids the old double-parse for Named bodies. + std::vector xcaf = collect_step_xcaf(file_path, file_bytes, progress); + if (!collect_step_has_status_error(xcaf)) return xcaf; - out.clear(); - return read_step_named_bodies_plain(file_bytes, out); + if (!progress.IsNull() && progress->cancelled()) + return xcaf; + + return collect_step_plain(file_path, file_bytes, progress); } -Status read_step_named_tree_xcaf(const std::string& file_bytes, std::vector& out) +Status read_step_named_tree_xcaf(const std::string& file_bytes, std::vector& out, + const Message_ProgressRange& progress) { Interface_Static::SetCVal("xstep.cascade.unit", "MM"); @@ -657,7 +695,7 @@ Status read_step_named_tree_xcaf(const std::string& file_bytes, std::vectorMain()); @@ -685,17 +723,106 @@ Status read_step_named_tree_xcaf(const std::string& file_bytes, std::vector collect(const std::string& file_path, const std::string& file_bytes, + const Atomic_progress_indicator_ptr& progress) +{ + const Format fmt = detect(file_path, file_bytes); + switch (fmt) + { + case Format::Step: + return collect_step(file_path, file_bytes, progress); + case Format::Iges: + return collect_iges(file_path, file_bytes); + case Format::Stl: + return collect_stl(file_path, file_bytes); + case Format::Ply: + return collect_ply(file_path, file_bytes); + default: + { + std::vector lines; + append_common(lines, file_path, file_bytes, Format::Unknown); + add_blank(lines); + add_line(lines, "Status", "unsupported or unrecognized format"); + add_line(lines, "Hint", "Open STEP, IGES, STL, or PLY"); + return lines; + } + } +} + +Status read_step_named_bodies(const std::string& file_bytes, std::vector& out, + const Message_ProgressRange& progress) +{ + out.clear(); + const Status xcaf = read_step_named_bodies_xcaf(file_bytes, out, progress); + if (xcaf.is_ok()) + return xcaf; + + out.clear(); + return read_step_named_bodies_plain(file_bytes, out, Message_ProgressRange()); +} -Status read_step_named_tree(const std::string& file_bytes, std::vector& out) +Status read_step_named_tree(const std::string& file_bytes, std::vector& out, const Message_ProgressRange& progress) { out.clear(); - const Status xcaf = read_step_named_tree_xcaf(file_bytes, out); + const Status xcaf = read_step_named_tree_xcaf(file_bytes, out, progress); if (xcaf.is_ok()) return xcaf; out.clear(); std::vector flat; - const Status plain = read_step_named_bodies_plain(file_bytes, flat); + const Status plain = read_step_named_bodies_plain(file_bytes, flat, Message_ProgressRange()); if (!plain.is_ok()) return plain; diff --git a/src/utl_cad_file_info.h b/src/utl_cad_file_info.h index 33ea032a..f0b581fc 100644 --- a/src/utl_cad_file_info.h +++ b/src/utl_cad_file_info.h @@ -4,9 +4,11 @@ #include #include +#include #include #include "utl.h" +#include "utl_occt_progress.h" /// Read-only metadata for CAD/mesh files EzyCad can import and/or export. /// Does not modify the document. @@ -50,12 +52,16 @@ struct Named_node [[nodiscard]] const char* format_label(Format fmt); -/// Collect label/value rows for the Tools -> Inspector window. -[[nodiscard]] std::vector collect(const std::string& file_path, const std::string& file_bytes); +/// Collect label/value rows for the Import dialog. +/// Optional \a progress receives stage text and Transfer percent (STEP/IGES). +[[nodiscard]] std::vector collect(const std::string& file_path, const std::string& file_bytes, + const Atomic_progress_indicator_ptr& progress = {}); /// Read STEP bodies with XCAF product/part names when present (flat; assemblies expanded to leaves). -[[nodiscard]] Status read_step_named_bodies(const std::string& file_bytes, std::vector& out); +[[nodiscard]] Status read_step_named_bodies(const std::string& file_bytes, std::vector& out, + const Message_ProgressRange& progress = Message_ProgressRange()); /// Read STEP as a group/leaf tree (XCAF assemblies preserved). Falls back to flat bodies as root leaves. -[[nodiscard]] Status read_step_named_tree(const std::string& file_bytes, std::vector& out); +[[nodiscard]] Status read_step_named_tree(const std::string& file_bytes, std::vector& out, + const Message_ProgressRange& progress = Message_ProgressRange()); } // namespace utl_cad_file_info diff --git a/src/utl_occt_progress.cpp b/src/utl_occt_progress.cpp new file mode 100644 index 00000000..a50156ed --- /dev/null +++ b/src/utl_occt_progress.cpp @@ -0,0 +1,35 @@ +#include "utl_occt_progress.h" + +void Atomic_progress_indicator::set_stage(std::string stage) +{ + std::lock_guard lock(m_mu); + m_stage = std::move(stage); +} + +std::string Atomic_progress_indicator::stage() const +{ + std::lock_guard lock(m_mu); + return m_stage; +} + +std::string Atomic_progress_indicator::scope_name() const +{ + std::lock_guard lock(m_mu); + return m_scope; +} + +void Atomic_progress_indicator::Show(const Message_ProgressScope& theScope, const bool /*isForce*/) +{ + m_pos.store(static_cast(GetPosition()), std::memory_order_relaxed); + const char* name = theScope.Name(); + std::lock_guard lock(m_mu); + if (name && name[0] != '\0') + m_scope = name; +} + +void Atomic_progress_indicator::Reset() +{ + m_pos.store(0.f, std::memory_order_relaxed); + std::lock_guard lock(m_mu); + m_scope.clear(); +} diff --git a/src/utl_occt_progress.h b/src/utl_occt_progress.h new file mode 100644 index 00000000..802231e3 --- /dev/null +++ b/src/utl_occt_progress.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "utl_types.h" + +using Message_ProgressIndicator_ptr = opencascade::handle; + +/// Thread-safe OCCT progress sink for background STEP transfer (poll from the UI thread). +class Atomic_progress_indicator : public Message_ProgressIndicator +{ + DEFINE_STANDARD_RTTI_INLINE(Atomic_progress_indicator, Message_ProgressIndicator) + +public: + Atomic_progress_indicator() = default; + + void request_cancel() { m_cancel.store(true, std::memory_order_relaxed); } + [[nodiscard]] bool cancelled() const { return m_cancel.load(std::memory_order_relaxed); } + + /// Overall position in [0, 1] from the last OCCT Show() (0 if transfer has not started). + [[nodiscard]] float position() const { return m_pos.load(std::memory_order_relaxed); } + + void set_stage(std::string stage); + [[nodiscard]] std::string stage() const; + [[nodiscard]] std::string scope_name() const; + +protected: + void Show(const Message_ProgressScope& theScope, const bool /*isForce*/) override; + bool UserBreak() override { return m_cancel.load(std::memory_order_relaxed); } + void Reset() override; + +private: + std::atomic m_pos{0.f}; + std::atomic m_cancel{false}; + mutable std::mutex m_mu; + std::string m_stage; + std::string m_scope; +}; + +using Atomic_progress_indicator_ptr = opencascade::handle; From 892715e034980cb608fc9b7e366f7f1288fedf35 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sat, 1 Aug 2026 13:09:20 -0600 Subject: [PATCH 3/4] Simplify import dialog. --- CHANGELOG.md | 2 +- docs/usage.md | 16 +-- src/doc/gui.md | 2 +- src/doc/utility.md | 2 +- src/gui.cpp | 266 ++++++++++----------------------------------- src/gui.h | 15 +-- 6 files changed, 74 insertions(+), 229 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be3938c..7069d0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **STEP Import as**: the Import dialog replaces the **Union shapes** checkbox with an **Import as** combo (**Preserve hierarchy** default, **Flat solids**, **Union shapes**). Hierarchy keeps XCAF assembly groups and product/instance names; flat adds leaf solids at the document root; union fuses into one solid. -- **STEP import progress**: after choosing a STEP file (and again on **Import into project**), a modal shows stage text and transfer progress. Desktop runs the OCCT transfer on a background thread so the UI stays responsive (Cancel available); metadata collect uses a single Transfer pass instead of parsing the file twice. +- **STEP import progress**: on **Import into project**, a modal shows stage text and transfer progress. Desktop runs the OCCT transfer on a background thread so the UI stays responsive (Cancel available). The Import dialog no longer shows a file metadata summary (opens immediately after file pick). - **Configurable hotkeys**: **Settings -> Keyboard shortcuts** remaps shape tools (Move/Rotate/Scale/Extrude/Chamfer/Fillet/Polar duplicate/Cross-section), sketch tools (inspection, from face, operation axis, node/line/multi-line/arc/square/rectangle/circle/slot, Dimension), boolean commands (Cut/Fuse/Common), Delete, New/Open/Save, and Undo/Redo. Bindings persist as **`gui.hotkeys`** (human-readable chords; missing keys use defaults). Toolbar tooltips follow remaps. **Delete** / **Backspace** stay fixed delete aliases; Ctrl+Shift+Z stays a fixed redo alias. Fixed, unsupported, and conflicting chords are rejected on assign with a status toast (and inline message); remaps accept letters, digits, and Space only (punctuation/numpad rejected); per-row **Reset** rejects when the factory chord is already bound elsewhere (same conflict message as assign); hotkey capture clears when Settings closes. A **?** at the top of the section opens the [Keyboard shortcuts](https://ezycad.readthedocs.io/en/latest/usage-settings.html#keyboard-shortcuts) settings guide. diff --git a/docs/usage.md b/docs/usage.md index 8a8689e2..14865c4e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -249,18 +249,13 @@ The typical modeling workflow in EzyCad follows these steps: ### Import dialog -**File -> Import** opens an **Import** window for STEP or PLY. Large STEP files show a progress modal while metadata is read (and again while importing). Review metadata, choose how STEP assemblies land in the Shape List (**Import as**, default **Preserve hierarchy**), then click **Import into project**. The window closes after a successful import. - -| Format | What the Import dialog shows | -| -------------------------- | --------------------------------------------------------- | -| **STEP** (`.step`, `.stp`) | Roots/shapes, import bodies, named bodies, topology, bbox | -| **PLY** (`.ply`) | Encoding, vertex and face counts from the header | +**File -> Import** opens an **Import** window for STEP or PLY. Choose how STEP assemblies land in the Shape List (**Import as**, default **Preserve hierarchy**), then click **Import into project**. Large STEP files show a progress modal while transferring. The window closes after a successful import. **How to use:** 1. Choose **File -> Import** -2. Pick a `.step`, `.stp`, or `.ply` file (STEP may show a progress modal while reading) -3. Review the label/value table; for STEP, choose **Import as** if needed -4. Click **Import into project** (STEP may show a progress modal again while transferring) +2. Pick a `.step`, `.stp`, or `.ply` file +3. For STEP, choose **Import as** if needed +4. Click **Import into project** (STEP may show a progress modal while transferring) For in-document topology of an already-loaded solid, use [Shape info](#shape-info) from the Shape List. @@ -284,7 +279,7 @@ In addition to creating 3D shapes from sketches, EzyCad supports importing exist **How to import:** 1. Use **File -> Import** 2. Pick a `.step`, `.stp`, or `.ply` file -3. Review metadata in the [Import dialog](#import-dialog); for STEP, choose **Import as** if needed +3. In the [Import dialog](#import-dialog), for STEP choose **Import as** if needed 4. Click **Import into project** - geometry is added as 3D shape(s) in the document, scaled to project units (see below) 5. You can move, rotate, scale, and use imported bodies in [boolean operations](#boolean-operations) like native solids where the geometry allows it @@ -307,7 +302,6 @@ In addition to creating 3D shapes from sketches, EzyCad supports importing exist - **Preserve hierarchy** (default) - builds Shape List **groups** for assemblies and leaf solids for bodies; group and part names come from product/instance names in the file when present. - **Flat solids** - adds leaf solids only at the document root (no assembly groups); keeps per-body names from the file. - **Union shapes** - fuses those bodies into one solid before adding to the document. If union fails, the import is aborted and nothing is added. -- The Import dialog **Import bodies** count shows how many leaf solids will be added (before union). - When the STEP file includes product or part names, those names appear in the Shape List (duplicate names get `.001`, `.002`, ...). Unnamed bodies stay as `Shape`; unnamed assemblies use `Assembly`. **Note:** **IGES** and **STL** are available for **export** only, not import. diff --git a/src/doc/gui.md b/src/doc/gui.md index 2ccf31d9..44214f2d 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -13,7 +13,7 @@ Typical responsibilities: - ImGui frame: menu bar, dock space (passthrough central node for 3D input), toolbar, Sketch List, Shape List, Options, Settings, dist/angle popups. - Mode switching (`Mode` enum in [`mode.h`](../mode.h)) and parent-mode Esc behavior. - Persisted preferences (`ezycad_settings.json` via [`gui_settings.cpp`](../gui_settings.cpp)). -- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** shows metadata then loads STEP/PLY; STEP **Import as** chooses preserve hierarchy / flat solids / union). STEP metadata/import uses a progress modal (`Atomic_progress_indicator`); desktop runs OCCT Transfer off the UI thread. +- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** confirms STEP/PLY with **Import as** for hierarchy / flat / union). STEP **Import into project** uses a progress modal (`Atomic_progress_indicator`); desktop runs OCCT Transfer off the UI thread. - CAD/mesh interchange scales about the origin: project display lengths follow **File -> Project units** (`Project_unit`; Inch or Millimeter). Model space stays inch-scaled (`model = inches * dimension_scale`). STEP import converts OCCT cascade **mm** into model space; PLY import treats coords as inches. **File -> Export** asks for **Inches** or **Millimeters** (STEP/IGES declare that unit; STL/PLY write unitless coords in that scale). `.ezy` persists `projectUnit`. **Settings -> New project defaults** stores `gui.default_project_unit` and inch-based default 2D framing for **File -> New**. - Contextual help links (`doc_urls` in `gui.h`). diff --git a/src/doc/utility.md b/src/doc/utility.md index 72f42d7b..d8b0b865 100644 --- a/src/doc/utility.md +++ b/src/doc/utility.md @@ -135,7 +135,7 @@ Used by **File -> Import**. Reads file bytes only until the user confirms import | `read_step_named_bodies` | STEPCAF/XCAF bodies + product names (flat; falls back to plain reader) | | `read_step_named_tree` | STEPCAF/XCAF assembly tree as group/leaf `Named_node`s (falls back flat) | -`Occt_view::import_step` takes `Step_import_mode` (`utl_types.h`): preserve hierarchy (default), flat root leaves, or union. Heavy work splits into `prepare_step_import` (thread-safe geometry) + `commit_step_import` (UI thread). `collect` / STEP Transfer accept optional `Atomic_progress_indicator` / `Message_ProgressRange` (`utl_occt_progress.h`). +`Occt_view::import_step` takes `Step_import_mode` (`utl_types.h`): preserve hierarchy (default), flat root leaves, or union. Heavy work splits into `prepare_step_import` (thread-safe geometry) + `commit_step_import` (UI thread). STEP Transfer accepts optional `Atomic_progress_indicator` / `Message_ProgressRange` (`utl_occt_progress.h`). `collect` remains available for tooling but is not shown in the Import dialog. ## Logging and debug diff --git a/src/gui.cpp b/src/gui.cpp index 301ba093..cbab7e69 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -3028,11 +3028,11 @@ void GUI::shape_info_dialog_() void GUI::open_file_inspector_(const std::string& file_path, const std::string& file_bytes) { - m_file_inspector_path = file_path; - m_file_inspector_bytes = file_bytes; - m_file_inspector_fmt = utl_cad_file_info::detect(file_path, file_bytes); - m_file_inspector_lines = utl_cad_file_info::collect(file_path, file_bytes); - m_file_inspector_open = true; + m_file_inspector_path = file_path; + m_file_inspector_bytes = file_bytes; + m_file_inspector_fmt = utl_cad_file_info::detect(file_path, file_bytes); + m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; + m_file_inspector_open = true; } bool GUI::cad_busy_() const { return m_cad_busy_kind != Cad_busy_kind::Idle; } @@ -3043,31 +3043,6 @@ void GUI::cancel_cad_busy_() m_cad_busy_progress->request_cancel(); } -void GUI::begin_step_inspect_(const std::string& file_path, const std::string& file_bytes) -{ - if (cad_busy_()) - return; - - m_cad_busy_kind = Cad_busy_kind::Inspect; - m_cad_busy_path = file_path; - m_cad_busy_bytes = file_bytes; - m_cad_busy_title = "Reading STEP"; - m_cad_busy_progress = new Atomic_progress_indicator(); - m_cad_busy_progress->set_stage("Starting..."); - m_cad_busy_open_popup = true; - m_cad_busy_modal_open = true; - -#ifndef __EMSCRIPTEN__ - const std::string path = m_cad_busy_path; - const std::string bytes = m_cad_busy_bytes; - const Atomic_progress_indicator_ptr prog = m_cad_busy_progress; - m_cad_busy_inspect_fut = - std::async(std::launch::async, [path, bytes, prog]() { return utl_cad_file_info::collect(path, bytes, prog); }); -#else - m_cad_busy_run_next_frame = true; -#endif -} - void GUI::begin_step_import_(const Step_import_mode mode) { if (cad_busy_() || m_file_inspector_bytes.empty() || !m_view) @@ -3100,158 +3075,73 @@ void GUI::begin_step_import_(const Step_import_mode mode) #endif } -void GUI::poll_cad_busy_() +void GUI::finish_step_import_(Status st, Occt_view::Step_import_geom& geom) { - if (!cad_busy_()) - return; + const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); + m_cad_busy_kind = Cad_busy_kind::Idle; + m_cad_busy_progress = {}; + m_cad_busy_modal_open = false; -#ifdef __EMSCRIPTEN__ - if (m_cad_busy_run_next_frame) + if (cancelled || (!st.is_ok() && st.message().find("cancelled") != std::string::npos)) { - // Paint the modal for one frame, then run the heavy work on the next poll. - m_cad_busy_run_next_frame = false; + show_message("STEP import cancelled."); return; } - if (m_cad_busy_kind == Cad_busy_kind::Inspect) + if (!st.is_ok()) { - const auto lines = utl_cad_file_info::collect(m_cad_busy_path, m_cad_busy_bytes, m_cad_busy_progress); - const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); - m_cad_busy_kind = Cad_busy_kind::Idle; - m_cad_busy_progress = {}; - m_cad_busy_modal_open = false; - if (cancelled) - { - show_message("STEP read cancelled."); - return; - } - - m_file_inspector_path = m_cad_busy_path; - m_file_inspector_bytes = m_cad_busy_bytes; - m_file_inspector_fmt = utl_cad_file_info::Format::Step; - m_file_inspector_lines = lines; - m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; - m_file_inspector_open = true; + show_message(st.message()); return; } - if (m_cad_busy_kind == Cad_busy_kind::Import) + if (Status commit = m_view->commit_step_import(geom); !commit.is_ok()) { - Occt_view::Step_import_geom geom; - Status st = Occt_view::prepare_step_import(m_cad_busy_bytes, m_cad_busy_import_mode, m_view->step_import_model_scale(), - geom, m_cad_busy_progress); - const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); - m_cad_busy_kind = Cad_busy_kind::Idle; - m_cad_busy_progress = {}; - m_cad_busy_modal_open = false; - if (cancelled || (!st.is_ok() && st.message().find("cancelled") != std::string::npos)) - { - show_message("STEP import cancelled."); - return; - } - - if (!st.is_ok()) - { - show_message(st.message()); - return; - } - - if (Status commit = m_view->commit_step_import(geom); !commit.is_ok()) - { - show_message(commit.message()); - return; - } - - const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); - switch (m_cad_busy_import_mode) - { - case Step_import_mode::Union_shapes: - show_message("Imported (union): " + name); - break; - case Step_import_mode::Flat_solids: - show_message("Imported (flat): " + name); - break; - default: - show_message("Imported: " + name); - break; - } - close_file_inspector_(); - } -#else - if (m_cad_busy_kind == Cad_busy_kind::Inspect) - { - if (!m_cad_busy_inspect_fut.valid()) - return; - - if (m_cad_busy_inspect_fut.wait_for(std::chrono::seconds(0)) != std::future_status::ready) - return; - - auto lines = m_cad_busy_inspect_fut.get(); - const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); - m_cad_busy_kind = Cad_busy_kind::Idle; - m_cad_busy_progress = {}; - m_cad_busy_modal_open = false; - if (cancelled) - { - show_message("STEP read cancelled."); - return; - } - - m_file_inspector_path = m_cad_busy_path; - m_file_inspector_bytes = m_cad_busy_bytes; - m_file_inspector_fmt = utl_cad_file_info::Format::Step; - m_file_inspector_lines = std::move(lines); - m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; - m_file_inspector_open = true; + show_message(commit.message()); return; } - if (m_cad_busy_kind == Cad_busy_kind::Import) + const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); + switch (m_cad_busy_import_mode) { - if (!m_cad_busy_import_fut.valid()) - return; + case Step_import_mode::Union_shapes: + show_message("Imported (union): " + name); + break; + case Step_import_mode::Flat_solids: + show_message("Imported (flat): " + name); + break; + default: + show_message("Imported: " + name); + break; + } + close_file_inspector_(); +} - if (m_cad_busy_import_fut.wait_for(std::chrono::seconds(0)) != std::future_status::ready) - return; +void GUI::poll_cad_busy_() +{ + if (m_cad_busy_kind != Cad_busy_kind::Import) + return; - auto [st, geom] = m_cad_busy_import_fut.get(); - const bool cancelled = !m_cad_busy_progress.IsNull() && m_cad_busy_progress->cancelled(); - m_cad_busy_kind = Cad_busy_kind::Idle; - m_cad_busy_progress = {}; - m_cad_busy_modal_open = false; - if (cancelled || (!st.is_ok() && st.message().find("cancelled") != std::string::npos)) - { - show_message("STEP import cancelled."); - return; - } +#ifdef __EMSCRIPTEN__ + if (m_cad_busy_run_next_frame) + { + // Paint the modal for one frame, then run the heavy work on the next poll. + m_cad_busy_run_next_frame = false; + return; + } - if (!st.is_ok()) - { - show_message(st.message()); - return; - } + Occt_view::Step_import_geom geom; + Status st = Occt_view::prepare_step_import(m_cad_busy_bytes, m_cad_busy_import_mode, m_view->step_import_model_scale(), + geom, m_cad_busy_progress); + finish_step_import_(st, geom); +#else + if (!m_cad_busy_import_fut.valid()) + return; - if (Status commit = m_view->commit_step_import(geom); !commit.is_ok()) - { - show_message(commit.message()); - return; - } + if (m_cad_busy_import_fut.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return; - const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); - switch (m_cad_busy_import_mode) - { - case Step_import_mode::Union_shapes: - show_message("Imported (union): " + name); - break; - case Step_import_mode::Flat_solids: - show_message("Imported (flat): " + name); - break; - default: - show_message("Imported: " + name); - break; - } - close_file_inspector_(); - } + auto [st, geom] = m_cad_busy_import_fut.get(); + finish_step_import_(st, geom); #endif } @@ -3310,7 +3200,6 @@ void GUI::close_file_inspector_() m_file_inspector_open = false; m_file_inspector_path.clear(); m_file_inspector_bytes.clear(); - m_file_inspector_lines.clear(); m_file_inspector_fmt = utl_cad_file_info::Format::Unknown; m_file_inspector_step_mode = Step_import_mode::Preserve_hierarchy; } @@ -3322,15 +3211,18 @@ void GUI::file_inspector_dialog_() const std::string name = std::filesystem::path(m_file_inspector_path).filename().string(); const std::string title = "Import: " + (name.empty() ? std::string("file") : name); - ImGui::SetNextWindowSize(ImVec2(480.0f, 0.0f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(360.0f, 0.0f), ImGuiCond_FirstUseEver); bool open = m_file_inspector_open; - if (!ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_None)) + if (!ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_AlwaysAutoResize)) { m_file_inspector_open = open; ImGui::End(); return; } + if (!name.empty()) + ImGui::TextUnformatted(name.c_str()); + if (utl_cad_file_info::can_import(m_file_inspector_fmt) && !m_file_inspector_bytes.empty()) { const bool is_step = m_file_inspector_fmt == utl_cad_file_info::Format::Step; @@ -3367,42 +3259,8 @@ void GUI::file_inspector_dialog_() } ImGui::EndDisabled(); } - - ImGui::Separator(); - - const float max_h = ImGui::GetTextLineHeightWithSpacing() * 20.0f; - if (ImGui::BeginChild("file_inspector_scroll", ImVec2(0.0f, max_h), ImGuiChildFlags_Borders, - ImGuiWindowFlags_AlwaysVerticalScrollbar)) - { - if (ImGui::BeginTable("file_inspector_tbl", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_RowBg)) - { - ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, 140.0f); - ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthStretch); - - for (const utl_cad_file_info::Line& line : m_file_inspector_lines) - { - if (line.label.empty() && line.value.empty()) - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); - continue; - } - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(line.label.c_str()); - ImGui::TableSetColumnIndex(1); - ImGui::TextWrapped("%s", line.value.c_str()); - } - - ImGui::EndTable(); - } - - ImGui::EndChild(); - } + else + ImGui::TextWrapped("This file type cannot be imported."); m_file_inspector_open = open; if (!open) @@ -4581,11 +4439,7 @@ bool GUI::on_import_file(const std::string& file_path, const std::string& file_d void GUI::on_inspector_file(const std::string& file_path, const std::string& file_data) { - const utl_cad_file_info::Format fmt = utl_cad_file_info::detect(file_path, file_data); - if (fmt == utl_cad_file_info::Format::Step) - begin_step_inspect_(file_path, file_data); - else - open_file_inspector_(file_path, file_data); + open_file_inspector_(file_path, file_data); } #ifdef __EMSCRIPTEN__ diff --git a/src/gui.h b/src/gui.h index 931930ca..4d1b44e2 100644 --- a/src/gui.h +++ b/src/gui.h @@ -440,8 +440,8 @@ class GUI void close_file_inspector_(); void cad_busy_dialog_(); void poll_cad_busy_(); - void begin_step_inspect_(const std::string& file_path, const std::string& file_bytes); void begin_step_import_(Step_import_mode mode); + void finish_step_import_(Status st, Occt_view::Step_import_geom& geom); void cancel_cad_busy_(); [[nodiscard]] bool cad_busy_() const; @@ -712,17 +712,15 @@ class GUI bool m_shape_info_open{false}; Shp_ptr m_shape_info_shp; std::vector m_shape_info_lines; - bool m_file_inspector_open{false}; - Step_import_mode m_file_inspector_step_mode{Step_import_mode::Preserve_hierarchy}; - std::string m_file_inspector_path; - std::string m_file_inspector_bytes; - utl_cad_file_info::Format m_file_inspector_fmt{utl_cad_file_info::Format::Unknown}; - std::vector m_file_inspector_lines; + bool m_file_inspector_open{false}; + Step_import_mode m_file_inspector_step_mode{Step_import_mode::Preserve_hierarchy}; + std::string m_file_inspector_path; + std::string m_file_inspector_bytes; + utl_cad_file_info::Format m_file_inspector_fmt{utl_cad_file_info::Format::Unknown}; enum class Cad_busy_kind : uint8_t { Idle, - Inspect, Import }; Cad_busy_kind m_cad_busy_kind{Cad_busy_kind::Idle}; @@ -736,7 +734,6 @@ class GUI #ifdef __EMSCRIPTEN__ bool m_cad_busy_run_next_frame{false}; #else - std::future> m_cad_busy_inspect_fut; std::future> m_cad_busy_import_fut; #endif std::string m_about_markdown; From 2eef05aa4cb78009a76b73223af3b2dd132e47bf Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sat, 1 Aug 2026 14:47:16 -0600 Subject: [PATCH 4/4] Improve --- CHANGELOG.md | 2 +- docs/usage.md | 2 +- src/doc/gui.md | 2 +- src/gui.cpp | 55 +++++++++++++++++++++++++++++++++++--------------- src/gui.h | 3 ++- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7069d0fb..686aeb61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **STEP Import as**: the Import dialog replaces the **Union shapes** checkbox with an **Import as** combo (**Preserve hierarchy** default, **Flat solids**, **Union shapes**). Hierarchy keeps XCAF assembly groups and product/instance names; flat adds leaf solids at the document root; union fuses into one solid. -- **STEP import progress**: on **Import into project**, a modal shows stage text and transfer progress. Desktop runs the OCCT transfer on a background thread so the UI stays responsive (Cancel available). The Import dialog no longer shows a file metadata summary (opens immediately after file pick). +- **STEP import progress**: on **Import into project**, an **Importing...** modal appears while transferring. Desktop shows stage/progress and Cancel (OCCT transfer on a background thread). Web shows a simple wait dialog (no Cancel; transfer runs on the main thread after the dialog paints). The Import dialog no longer shows a file metadata summary (opens immediately after file pick). - **Configurable hotkeys**: **Settings -> Keyboard shortcuts** remaps shape tools (Move/Rotate/Scale/Extrude/Chamfer/Fillet/Polar duplicate/Cross-section), sketch tools (inspection, from face, operation axis, node/line/multi-line/arc/square/rectangle/circle/slot, Dimension), boolean commands (Cut/Fuse/Common), Delete, New/Open/Save, and Undo/Redo. Bindings persist as **`gui.hotkeys`** (human-readable chords; missing keys use defaults). Toolbar tooltips follow remaps. **Delete** / **Backspace** stay fixed delete aliases; Ctrl+Shift+Z stays a fixed redo alias. Fixed, unsupported, and conflicting chords are rejected on assign with a status toast (and inline message); remaps accept letters, digits, and Space only (punctuation/numpad rejected); per-row **Reset** rejects when the factory chord is already bound elsewhere (same conflict message as assign); hotkey capture clears when Settings closes. A **?** at the top of the section opens the [Keyboard shortcuts](https://ezycad.readthedocs.io/en/latest/usage-settings.html#keyboard-shortcuts) settings guide. diff --git a/docs/usage.md b/docs/usage.md index 14865c4e..ce005d25 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -249,7 +249,7 @@ The typical modeling workflow in EzyCad follows these steps: ### Import dialog -**File -> Import** opens an **Import** window for STEP or PLY. Choose how STEP assemblies land in the Shape List (**Import as**, default **Preserve hierarchy**), then click **Import into project**. Large STEP files show a progress modal while transferring. The window closes after a successful import. +**File -> Import** opens an **Import** window for STEP or PLY. Choose how STEP assemblies land in the Shape List (**Import as**, default **Preserve hierarchy**), then click **Import into project**. STEP import shows an **Importing...** dialog while transferring (desktop also shows stage/progress and Cancel). The window closes after a successful import. **How to use:** 1. Choose **File -> Import** diff --git a/src/doc/gui.md b/src/doc/gui.md index 44214f2d..78b0ee22 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -13,7 +13,7 @@ Typical responsibilities: - ImGui frame: menu bar, dock space (passthrough central node for 3D input), toolbar, Sketch List, Shape List, Options, Settings, dist/angle popups. - Mode switching (`Mode` enum in [`mode.h`](../mode.h)) and parent-mode Esc behavior. - Persisted preferences (`ezycad_settings.json` via [`gui_settings.cpp`](../gui_settings.cpp)). -- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** confirms STEP/PLY with **Import as** for hierarchy / flat / union). STEP **Import into project** uses a progress modal (`Atomic_progress_indicator`); desktop runs OCCT Transfer off the UI thread. +- Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** confirms STEP/PLY with **Import as** for hierarchy / flat / union). STEP **Import into project** shows an Importing modal; desktop uses `Atomic_progress_indicator` + background Transfer + Cancel; WASM paints the modal for two frames then runs Transfer on the main thread (no Cancel). - CAD/mesh interchange scales about the origin: project display lengths follow **File -> Project units** (`Project_unit`; Inch or Millimeter). Model space stays inch-scaled (`model = inches * dimension_scale`). STEP import converts OCCT cascade **mm** into model space; PLY import treats coords as inches. **File -> Export** asks for **Inches** or **Millimeters** (STEP/IGES declare that unit; STL/PLY write unitless coords in that scale). `.ezy` persists `projectUnit`. **Settings -> New project defaults** stores `gui.default_project_unit` and inch-based default 2D framing for **File -> New**. - Contextual help links (`doc_urls` in `gui.h`). diff --git a/src/gui.cpp b/src/gui.cpp index cbab7e69..ad234a61 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -150,8 +150,9 @@ void GUI::render_gui() shape_list_(); shape_info_dialog_(); file_inspector_dialog_(); - poll_cad_busy_(); + // Paint the busy modal before poll so WASM can show "Importing..." for a frame first. cad_busy_dialog_(); + poll_cad_busy_(); options_(); message_status_window_(); error_modal_dialog_(); @@ -3052,13 +3053,13 @@ void GUI::begin_step_import_(const Step_import_mode mode) m_cad_busy_path = m_file_inspector_path; m_cad_busy_bytes = m_file_inspector_bytes; m_cad_busy_import_mode = mode; - m_cad_busy_title = "Importing STEP"; - m_cad_busy_progress = new Atomic_progress_indicator(); - m_cad_busy_progress->set_stage("Starting..."); - m_cad_busy_open_popup = true; - m_cad_busy_modal_open = true; + m_cad_busy_title = "Importing"; + m_cad_busy_open_popup = true; + m_cad_busy_modal_open = true; #ifndef __EMSCRIPTEN__ + m_cad_busy_progress = new Atomic_progress_indicator(); + m_cad_busy_progress->set_stage("Starting..."); const std::string bytes = m_cad_busy_bytes; const Step_import_mode m = mode; const double scale = m_view->step_import_model_scale(); @@ -3071,7 +3072,9 @@ void GUI::begin_step_import_(const Step_import_mode mode) return {st, std::move(geom)}; }); #else - m_cad_busy_run_next_frame = true; + // OpenPopup + one painted frame before the main-thread Transfer freezes the UI. + m_cad_busy_defer_frames = 2; + m_cad_busy_progress = {}; #endif } @@ -3122,16 +3125,15 @@ void GUI::poll_cad_busy_() return; #ifdef __EMSCRIPTEN__ - if (m_cad_busy_run_next_frame) + if (m_cad_busy_defer_frames > 0) { - // Paint the modal for one frame, then run the heavy work on the next poll. - m_cad_busy_run_next_frame = false; + --m_cad_busy_defer_frames; return; } Occt_view::Step_import_geom geom; Status st = Occt_view::prepare_step_import(m_cad_busy_bytes, m_cad_busy_import_mode, m_view->step_import_model_scale(), - geom, m_cad_busy_progress); + geom, {}); finish_step_import_(st, geom); #else if (!m_cad_busy_import_fut.valid()) @@ -3156,6 +3158,30 @@ void GUI::cad_busy_dialog_() if (!cad_busy_() && !m_cad_busy_modal_open) return; +#ifdef __EMSCRIPTEN__ + // Simple wait dialog (no Cancel / no close). Transfer runs on the main thread after paint. + if (!cad_busy_()) + { + if (ImGui::BeginPopupModal("##EzyCadCadBusy", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + return; + } + + ImGui::SetNextWindowSize(ImVec2(280.0f, 0.0f), ImGuiCond_Appearing); + if (!ImGui::BeginPopupModal("##EzyCadCadBusy", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) + return; + + ImGui::TextUnformatted("Importing..."); + const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); + if (!name.empty()) + ImGui::TextWrapped("%s", name.c_str()); + + ImGui::EndPopup(); +#else ImGui::SetNextWindowSize(ImVec2(420.0f, 0.0f), ImGuiCond_Appearing); if (!ImGui::BeginPopupModal("##EzyCadCadBusy", &m_cad_busy_modal_open, ImGuiWindowFlags_AlwaysAutoResize)) { @@ -3165,7 +3191,7 @@ void GUI::cad_busy_dialog_() return; } - ImGui::TextUnformatted(m_cad_busy_title.empty() ? "Working..." : m_cad_busy_title.c_str()); + ImGui::TextUnformatted(m_cad_busy_title.empty() ? "Importing" : m_cad_busy_title.c_str()); const std::string name = std::filesystem::path(m_cad_busy_path).filename().string(); if (!name.empty()) ImGui::TextWrapped("%s", name.c_str()); @@ -3185,14 +3211,11 @@ void GUI::cad_busy_dialog_() else ImGui::ProgressBar(-1.0f * static_cast(ImGui::GetTime()), ImVec2(-1.0f, 0.0f), "Working..."); -#ifndef __EMSCRIPTEN__ if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) cancel_cad_busy_(); -#else - ImGui::TextDisabled("Please wait..."); -#endif ImGui::EndPopup(); +#endif } void GUI::close_file_inspector_() diff --git a/src/gui.h b/src/gui.h index 4d1b44e2..43b69cfd 100644 --- a/src/gui.h +++ b/src/gui.h @@ -732,7 +732,8 @@ class GUI std::string m_cad_busy_title; Step_import_mode m_cad_busy_import_mode{Step_import_mode::Preserve_hierarchy}; #ifdef __EMSCRIPTEN__ - bool m_cad_busy_run_next_frame{false}; + /// Frames to paint the Importing modal before starting the blocking STEP transfer. + int m_cad_busy_defer_frames{0}; #else std::future> m_cad_busy_import_fut; #endif