diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2c2790f..686aeb61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ 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.
+
+- **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.
- **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..ce005d25 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,18 +249,13 @@ 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.
-
-| 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**. 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**
2. Pick a `.step`, `.stp`, or `.ply` file
-3. Review the label/value table; for STEP, optionally enable **Union shapes**
-4. Click **Import into project**
+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 assemblies, optionally check **Union shapes**
+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
@@ -303,9 +298,11 @@ 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.
+- 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..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** shows metadata then loads STEP/PLY).
+- 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/doc/shape.md b/src/doc/shape.md
index be55f622..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 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). 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 cfcc1fef..d8b0b865 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. 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
| Component | Role |
diff --git a/src/gui.cpp b/src/gui.cpp
index a105fe7b..ad234a61 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,9 @@ void GUI::render_gui()
shape_list_();
shape_info_dialog_();
file_inspector_dialog_();
+ // 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_();
@@ -3024,11 +3029,193 @@ 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; }
+
+void GUI::cancel_cad_busy_()
+{
+ if (!m_cad_busy_progress.IsNull())
+ m_cad_busy_progress->request_cancel();
+}
+
+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";
+ 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();
+ 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
+ // OpenPopup + one painted frame before the main-thread Transfer freezes the UI.
+ m_cad_busy_defer_frames = 2;
+ m_cad_busy_progress = {};
+#endif
+}
+
+void GUI::finish_step_import_(Status st, Occt_view::Step_import_geom& geom)
+{
+ 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_();
+}
+
+void GUI::poll_cad_busy_()
+{
+ if (m_cad_busy_kind != Cad_busy_kind::Import)
+ return;
+
+#ifdef __EMSCRIPTEN__
+ if (m_cad_busy_defer_frames > 0)
+ {
+ --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, {});
+ finish_step_import_(st, geom);
+#else
+ 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();
+ finish_step_import_(st, geom);
+#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;
+
+#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))
+ {
+ if (!m_cad_busy_modal_open && cad_busy_())
+ cancel_cad_busy_();
+
+ return;
+ }
+
+ 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());
+
+ 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...");
+
+ if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f)))
+ cancel_cad_busy_();
+
+ ImGui::EndPopup();
+#endif
}
void GUI::close_file_inspector_()
@@ -3036,9 +3223,8 @@ 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_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_()
@@ -3048,74 +3234,56 @@ 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 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;
+ 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_union))
+ 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();
-
- 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)
@@ -4251,7 +4419,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 +4437,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..43b69cfd 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,7 +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, 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);
@@ -434,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_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;
// Mode + Options panel (gui_mode.cpp)
void options_();
@@ -702,40 +712,59 @@ 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};
- bool m_file_inspector_union{false};
- 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;
- 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};
+ 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,
+ 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__
+ /// 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
+ 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 129b5ba8..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 bool union_shapes)
+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,45 +3942,90 @@ 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)
+ 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();
}
- std::vector id_by_index(named.size(), 0);
+ if (geom.mode == Step_import_mode::Flat_solids)
+ {
+ std::vector added;
+ added.reserve(geom.named.size());
+
+ for (utl_cad_file_info::Named_node& node : geom.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(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)];
@@ -4000,6 +4062,16 @@ Status Occt_view::import_step(const std::string& step_data, const bool union_sha
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 cd90d71e..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,9 +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).
- /// 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,
+ 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;
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