diff --git a/CHANGELOG.md b/CHANGELOG.md index a1ea967..a0a2de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Project units + +- **File -> Project units:** Choose **Inches** or **Millimeters** for sketch/display lengths (dimensions, Tab distance, add-primitive dialogs, chamfer/fillet, grid Settings fields). Stored in the `.ezy` as `projectUnit`. Changing units remaps the UI only; geometry is not rewritten. Export units remain a separate dialog. +- **Settings -> New project defaults:** **Project units** plus **Default 2D view width** / **height** (edited in that unit; stored as inches). Applied by **File -> New**. + ### Import / export units - **File -> Import** and **File -> Export** now scale geometry so sketch/display lengths stay in **inches**. STEP is read/written via millimeters (OCCT cascade/write unit); PLY import and STL/PLY export treat unitless mesh coords as inches; IGES export writes millimeters. STEP round-trips preserve size relative to project dimensions. diff --git a/docs/scripting.md b/docs/scripting.md index ea1da39..e0b2850 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -77,14 +77,14 @@ ezy.Shp Indices are **0-based** in Python. Prefer **`view.curr_sketch`** for inspection; **`ezy.sketch`** is the same object and also used for creation helpers. -| Method | Purpose | -| ------------------------------------------------------------ | ---------------------------------------------------------------------------- | -| `view.curr_sketch.name()` | Name of the current sketch (`curr_name()` alias on `ezy.sketch`) | -| `view.curr_sketch.node_count()` / `view.curr_sketch.node(i)` | Read nodes `(x, y)` on the current sketch plane | -| `view.curr_sketch.dim_count()` / `view.curr_sketch.dim(i)` | Read length dimensions | -| `view.curr_sketch.add(plane, offset, base_name)` | New sketch on `XY`, `XZ`, or `YZ` (`offset` in display units; optional name) | -| `view.curr_sketch.add_edge(x1, y1, x2, y2)` | Add a linear edge to the current sketch | -| `view.curr_sketch.finish_edges()` | Rebuild closed-face topology after bulk edge import | +| Method | Purpose | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------ | +| `view.curr_sketch.name()` | Name of the current sketch (`curr_name()` alias on `ezy.sketch`) | +| `view.curr_sketch.node_count()` / `view.curr_sketch.node(i)` | Read nodes `(x, y)` on the current sketch plane | +| `view.curr_sketch.dim_count()` / `view.curr_sketch.dim(i)` | Read length dimensions (distance in project display units) | +| `view.curr_sketch.add(plane, offset, base_name)` | New sketch on `XY`, `XZ`, or `YZ` (`offset` in project display units; optional name) | +| `view.curr_sketch.add_edge(x1, y1, x2, y2)` | Add a linear edge to the current sketch | +| `view.curr_sketch.finish_edges()` | Rebuild closed-face topology after bulk edge import | ### `ezy.Shp` diff --git a/docs/usage-settings.md b/docs/usage-settings.md index 45b50ed..8877bf0 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -39,17 +39,19 @@ Open **View -> Settings**. The window title is **Settings**. - **UI verbosity** — stepper at the top (default **6**). Values **5** and **6** show contextual **?** help buttons (hover for tooltip, click to open Read the Docs where linked). Lower values hide those buttons and some explanatory text. - At the bottom: **Defaults** — reloads bundled defaults from the app `res/` tree (including ImGui layout from that file). -Between those, the pane has **six** collapsible sections. Expand a section to see its controls; when collapsed, only the section title bar is visible. Which sections are expanded is remembered in settings (`gui.settings_headers`). +Between those, the pane has collapsible sections. Expand a section to see its controls; when collapsed, only the section title bar is visible. Which sections are expanded is remembered in settings (`gui.settings_headers`). -1. **3D view navigation** — **View rotation step** (degrees per key press for NumPad 8/2/4/6 orbit and Shift+NumPad 4/6 roll; default **45**; **`?`** opens [view roll](https://ezycad.readthedocs.io/en/latest/usage.html#view-roll) on Read the Docs). **Zoom scroll scale** (multiplier for wheel and **+**/**-** zoom; default **4**). Hold Shift while zooming for a Blender-style finer step (multiply by **0.1**). **Default 2D view width** / **height** (display units; default **3** x **3**) frame the top view used by **File -> New** and by projects with no saved camera. Numpad shortcuts are documented with Num Lock off; with Num Lock on, use main-row alternatives in [usage.md -> View navigation](usage.md#view-navigation). Stored as **`gui.view_roll_step_deg`**, **`gui.view_zoom_scroll_scale`**, **`gui.default_2d_view_width`**, and **`gui.default_2d_view_height`**. See **[usage-occt-view.md](usage-occt-view.md)**. +1. **3D view navigation** — **View rotation step** (degrees per key press for NumPad 8/2/4/6 orbit and Shift+NumPad 4/6 roll; default **45**; **`?`** opens [view roll](https://ezycad.readthedocs.io/en/latest/usage.html#view-roll) on Read the Docs). **Zoom scroll scale** (multiplier for wheel and **+**/**-** zoom; default **4**). Hold Shift while zooming for a Blender-style finer step (multiply by **0.1**). Numpad shortcuts are documented with Num Lock off; with Num Lock on, use main-row alternatives in [usage.md -> View navigation](usage.md#view-navigation). Stored as **`gui.view_roll_step_deg`** and **`gui.view_zoom_scroll_scale`**. See **[usage-occt-view.md](usage-occt-view.md)**. -2. **UI** — **Dark mode** checkbox. ImGui layout sliders apply to the **active theme** (switch **Dark mode** to edit the other theme). Grouped under **Transparency**, **Rounding**, **Borders**, **Padding**, and **Spacing**: **Window transparency** (**0.1** to **1.0**); corner rounding sliders **0** to **16** for **Windows, frames, popups**, **Scrollbars and sliders**, and **Tabs**; **Window border**, **Frame border** (**0** to **2**); **Window padding** X/Y, **Frame padding** X/Y, and **Item spacing** X/Y (**0** to **24**). +2. **New project defaults** — **Project units** (**Inches** or **Millimeters**; applied by **File -> New**; stored as **`gui.default_project_unit`**). **Default 2D view width** / **height** frame the top view for **File -> New** and projects with no saved camera; edited in the selected Project units (show `in` or `mm`); stored as inches in **`gui.default_2d_view_width`** / **`gui.default_2d_view_height`** (default **3** x **3** in). -3. **View presentation** — **Background color 1** and **Background color 2** (float RGB fields and swatches). **Gradient blend** — combo: **Horizontal**, **Vertical**, **Diagonal 1**, **Diagonal 2**, **Corner 1** … **Corner 4**. **Element hover color** — highlight for rows hovered in the **Shape List** or **Sketch List** (**Dimensions** table); stored as **`gui.elm_list_hover_color`**. **Shape selection color** — selected 3D shape highlight in the viewer (RGBA; default purple **0.75**, **0.07**, **0.85**, **1**); stored as **`gui.shape_selection_color`**. +3. **UI** — **Dark mode** checkbox. ImGui layout sliders apply to the **active theme** (switch **Dark mode** to edit the other theme). Grouped under **Transparency**, **Rounding**, **Borders**, **Padding**, and **Spacing**: **Window transparency** (**0.1** to **1.0**); corner rounding sliders **0** to **16** for **Windows, frames, popups**, **Scrollbars and sliders**, and **Tabs**; **Window border**, **Frame border** (**0** to **2**); **Window padding** X/Y, **Frame padding** X/Y, and **Item spacing** X/Y (**0** to **24**). -4. **3D view grid** — **Show grid** (checkbox; grid is drawn on the **active sketch** plane, behind coplanar geometry). **Fine grid lines** and **Major grid lines** (dense lines vs every-tenth emphasis). **Grid step**, **Grid padding** (margin around sketch content when sizing the grid), and **Grid display Z offset** in the Settings pane use the **same length scale as sketch length dimensions** (display value = model value / internal `dimension_scale`, default **100**). Saved JSON (`occt_view`) stores padding in model units (`grid_padding`). +4. **View presentation** — **Background color 1** and **Background color 2** (float RGB fields and swatches). **Gradient blend** — combo: **Horizontal**, **Vertical**, **Diagonal 1**, **Diagonal 2**, **Corner 1** … **Corner 4**. **Element hover color** — highlight for rows hovered in the **Shape List** or **Sketch List** (**Dimensions** table); stored as **`gui.elm_list_hover_color`**. **Shape selection color** — selected 3D shape highlight in the viewer (RGBA; default purple **0.75**, **0.07**, **0.85**, **1**); stored as **`gui.shape_selection_color`**. -5. **Sketch** — Nested sections (open state remembered in `gui.settings_headers`): +5. **3D view grid** — **Show grid** (checkbox; grid is drawn on the **active sketch** plane, behind coplanar geometry). **Fine grid lines** and **Major grid lines** (dense lines vs every-tenth emphasis). **Grid step**, **Grid padding** (margin around sketch content when sizing the grid), and **Grid display Z offset** in the Settings pane use the **same length scale as sketch length dimensions** (**File -> Project units**; Settings shows `in` or `mm`). Saved JSON (`occt_view`) stores values in model units (`grid_step`, `grid_padding`). + +6. **Sketch** — Nested sections (open state remembered in `gui.settings_headers`): - **Appearance** — edge and face display colors; shapes in sketch mode (faint/ghost/wire). - **Dimensions** — length-dimension appearance and behavior (most rows have **?** help). - **Nodes** — permanent node marker size and origin color; midpoint options for line/rect/slot tools. @@ -74,7 +76,7 @@ Between those, the pane has **six** collapsible sections. Expand a section to se **WebAssembly build** — Open CASCADE line-width controls (`SetWidth` / `Prs3d` line width) have no visible effect in the browser (WebGL/GLES). The **Settings** pane hides **Edge thickness**, **Dimension line width**, and **Snap guide line width** on the web build. Saved JSON may still contain those keys from a desktop session; they are not shown as editable rows in the browser UI. -6. **Startup project** — **Desktop only:** **Load last opened on startup** (checkbox, with **?**), then **Last opened path:** … or **(No path saved yet.)** Then **Save current as startup project**, **Clear saved startup** (with **?**). **WebAssembly:** no load-last row; only the two buttons and **?**. See [Startup project](#startup-project). +7. **Startup project** — **Desktop only:** **Load last opened on startup** (checkbox, with **?**), then **Last opened path:** … or **(No path saved yet.)** Then **Save current as startup project**, **Clear saved startup** (with **?**). **WebAssembly:** no load-last row; only the two buttons and **?**. See [Startup project](#startup-project). **Not in this pane** @@ -166,7 +168,7 @@ If saved layout text has no `[Docking]` section (older installs), a default dock | `grid_color2` | array of 3 numbers | Major (sparse / every-tenth) grid lines (`Aspect_Grid` tenth-line color). | | `grid_visible` | boolean | When **true**, the OCCT reference grid is drawn in the 3D view (default **true**). | | `grid_step` | number | Grid line spacing in **model** units (default **10**). Legacy `grid_x_step` / `grid_y_step` load as this value (X preferred). | -| `grid_padding` | number | Margin around active sketch content when sizing the grid, in **model** units (default **1000**; Settings shows display units). Legacy `grid_graphic_x_size` loads as padding if `grid_padding` is absent. | +| `grid_padding` | number | Margin around active sketch content when sizing the grid, in **model** units (default **1000**; Settings shows project units). Legacy `grid_graphic_x_size` loads as padding if `grid_padding` is absent. | | `grid_graphic_z_offset` | number | Grid plane offset along Z in **model** units. | ### `gui` @@ -220,8 +222,9 @@ If saved layout text has no `[Docking]` section (older installs), a default dock | `sketch_shape_faint_enabled` | boolean | Master switch for faint solids in all sketch tools (default **true**). Also **Options -> Sketch options -> Faint shapes**. When false, solids are hidden in sketch modes. | | `view_roll_step_deg` | number | Degrees per **NumPad 8**/**2**/**4**/**6** orbit and **Shift+NumPad 4**/**6** roll (allowed range **0.1** to **180** in code; default **45**). | | `view_zoom_scroll_scale` | number | Multiplier for `UpdateZoom` scroll delta from wheel and keyboard zoom (allowed range **0.25** to **64** in code; default **4**). With **Shift** held, the effective step is multiplied by **0.1** (Blender-style finer zoom). | -| `default_2d_view_width` | number | Horizontal sketch-plane span (display units) for **File -> New** / projects with no saved camera (allowed range **0.1** to **1000**; default **3**). | -| `default_2d_view_height` | number | Vertical sketch-plane span (display units) for the same framing (allowed range **0.1** to **1000**; default **3**). | +| `default_project_unit` | string | Default **File -> New** project unit: `"inch"` or `"millimeter"` (default **`inch`**). Edited under **Settings -> New project defaults**. | +| `default_2d_view_width` | number | Horizontal sketch-plane span for **File -> New** / projects with no saved camera, stored in **inches** (allowed range **0.1** to **1000**; default **3**). Settings UI shows this in **`default_project_unit`**. | +| `default_2d_view_height` | number | Vertical sketch-plane span, stored in **inches** (allowed range **0.1** to **1000**; default **3**). Settings UI shows this in **`default_project_unit`**. | | `load_last_opened_on_startup` | boolean | Desktop: open the last `.ezy` on launch. **Legacy:** `load_last_saved_on_startup` is read as a fallback if the newer key is absent. | | `last_opened_project_path` | string | Path of the last opened project for the option above. **Legacy:** `last_saved_project_path` is accepted if the newer key is missing. | @@ -244,6 +247,7 @@ Each **`settings_headers`** object may contain (default: only nested Sketch **Ap | Key | Type | Meaning | | ------------------- | ------- | ---------------------------------------- | | `view_nav` | boolean | **3D view navigation** section expanded. | +| `new_project` | boolean | **New project defaults** section expanded. | | `ui` | boolean | **UI** section expanded. | | `view_presentation` | boolean | **View presentation** section expanded. | | `grid` | boolean | **3D view grid** section expanded. | @@ -255,7 +259,7 @@ Each **`settings_headers`** object may contain (default: only nested Sketch **Ap | `sketch_underlay` | boolean | Nested **Underlay** under Sketch. | | `startup` | boolean | **Startup project** section expanded. | -Scripting API **`ezy.occt_view_settings_json()`** returns a JSON string with **`occt_view`** plus selected **`gui`** keys (including dimension and snap keys above, **`gui.permanent_node_anno_scale`**, **`gui.inspection_orthographic`**, **`gui.view_roll_step_deg`**, **`gui.view_zoom_scroll_scale`**, **`gui.default_2d_view_width`**, **`gui.default_2d_view_height`** when saved). See [scripting.md](scripting.md). +Scripting API **`ezy.occt_view_settings_json()`** returns a JSON string with **`occt_view`** plus selected **`gui`** keys (including dimension and snap keys above, **`gui.permanent_node_anno_scale`**, **`gui.inspection_orthographic`**, **`gui.view_roll_step_deg`**, **`gui.view_zoom_scroll_scale`**, **`gui.default_project_unit`**, **`gui.default_2d_view_width`**, **`gui.default_2d_view_height`** when saved). See [scripting.md](scripting.md). --- diff --git a/docs/usage-sketch.md b/docs/usage-sketch.md index 8d6a1db..83dac57 100644 --- a/docs/usage-sketch.md +++ b/docs/usage-sketch.md @@ -862,12 +862,12 @@ Edge dimension tool creates/removes **length dimensions between two sketch nodes **Technical Details:** -| | | -| -------------------: | --------------------------------------------------------- | -| **Dimension source** | Calculated from the two referenced nodes | -| **Unit system** | Displays measurements in the current unit system | -| **Auto-update** | Dimensions update automatically when geometry is modified | -| **View-only** | Does not affect the underlying geometry | +| | | +| -------------------: | ------------------------------------------------------------------------------ | +| **Dimension source** | Calculated from the two referenced nodes | +| **Unit system** | **File -> Project units** (Inches or Millimeters); dims and length entry follow it | +| **Auto-update** | Dimensions update automatically when geometry is modified | +| **View-only** | Does not affect the underlying geometry | ## Add Node Tool diff --git a/docs/usage.md b/docs/usage.md index 88191cd..dfb666c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -42,7 +42,7 @@ EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to d ### Main Components 1. **Menu Bar** - - **File** - [New](#new-project), [Open](#open-project), [Save](#save-project), Save as, [Import](#importing-3d-geometries), [Export](#exporting-3d-geometries), Examples, Exit + - **File** - [New](#new-project), [Open](#open-project), [Save](#save-project), Save as, [Project units](#project-units), [Import](#importing-3d-geometries), [Export](#exporting-3d-geometries), Examples, Exit - **Edit** - [Undo](#edit-operations), [Redo](#edit-operations) - **View** - [Settings, panes, Lua/Python consoles](usage-settings.md#view-menu) - **Help** - [About](#help-menu), [Usage Guide](#help-menu), and the separate **[Settings guide](usage-settings.md)** @@ -150,15 +150,23 @@ This is useful after **Revolve**, **Extrude**, booleans, or imports when you nee ### Basic Operations 1. #### New Project - Start with a clean workspace (default XY sketch) - - Reset the camera to a **top view** framed to **Settings -> 3D view navigation** **Default 2D view width** / **height** (defaults **3** x **3**, same length scale as sketch dimensions) + - Apply **Settings -> New project defaults** (**Project units**, **Default 2D view width** / **height**) + - Reset the camera to a **top view** framed to those width/height values 2. #### Open Project - Load existing `.ezy` files 3. #### Save Project - - Save current work to `.ezy` file + - Save current work to `.ezy` file (includes `projectUnit`) -4. **Import/Export** +4. #### Project units + - **File -> Project units -> Inches** or **Millimeters** sets how sketch lengths, dimensions, Tab distance entry, grid Settings fields, and related length inputs are shown and typed for the **open** project + - Changing units does **not** rescale stored geometry; a length that was **1** inch becomes **25.4** mm in the UI + - Stored in the `.ezy` manifest as `projectUnit` (`inch` or `millimeter`); older files without the field load as **Inches** + - Default for **File -> New** comes from **Settings -> New project defaults -> Project units** + - Independent of **File -> Export** units (export still asks separately) + +5. **Import/Export** - [Import STEP or PLY](#importing-3d-geometries) - [Export to STEP, IGES, binary STL, or binary PLY](#exporting-3d-geometries) @@ -254,11 +262,11 @@ In addition to creating 3D shapes from sketches, EzyCad supports importing exist 3. Geometry is added as 3D shape(s) in the document, scaled to project units (see below) 4. You can move, rotate, scale, and use imported bodies in [boolean operations](#boolean-operations) like native solids where the geometry allows it -**Units:** EzyCad treats sketch and display lengths as **inches**. Internal model coordinates are inches multiplied by an internal `dimension_scale` (default **100**). Import scaling: +**Units:** Sketch and display lengths follow **File -> Project units** (**Inches** or **Millimeters**). Internal model coordinates stay inch-scaled (`model = inches * dimension_scale`, default **100**). Changing project units only remaps the UI; it does not rewrite geometry. Import scaling: | Format | File units | Into EzyCad | | -------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------ | -| **STEP** (`.step`, `.stp`) | Declared in the file (often mm); OCCT reads them as mm, then converted | Scaled so lengths match inch-based project dims | +| **STEP** (`.step`, `.stp`) | Declared in the file (often mm); OCCT reads them as mm, then converted | Scaled so lengths match the inch-scaled model | | **PLY** (`.ply`) | No standard unit metadata | Vertex coords are treated as **inches** | **PLY import notes:** diff --git a/res/ezycad_settings.json b/res/ezycad_settings.json index 7363a7d..be54cb4 100644 --- a/res/ezycad_settings.json +++ b/res/ezycad_settings.json @@ -72,6 +72,7 @@ "permanent_node_anno_scale": 0.30000001192092896, "settings_headers": { "grid": false, + "new_project": false, "sketch": false, "sketch_appearance": true, "sketch_dimensions": true, @@ -83,6 +84,9 @@ "view_nav": false, "view_presentation": false }, + "default_project_unit": "inch", + "default_2d_view_width": 3.0, + "default_2d_view_height": 3.0, "show_dbg": false, "show_lua_console": false, "show_options": true, diff --git a/src/doc/gui.md b/src/doc/gui.md index 5ee2c65..8aadb26 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -14,7 +14,7 @@ Typical responsibilities: - 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). -- CAD/mesh interchange scales about the origin: project display lengths are **inches** (`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). +- 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`). ## Requirements and invariants diff --git a/src/doc/utility.md b/src/doc/utility.md index 9e733a3..360c07a 100644 --- a/src/doc/utility.md +++ b/src/doc/utility.md @@ -75,7 +75,7 @@ Includes [`utl_geom_boost.inl`](../utl_geom_boost.inl) for `ezy_geom` polygon / | Path in archive | Content | | ------------------ | ------------------------------------------------------------- | -| `manifest.json` | Document JSON (sketches, shapes, view, mode, `ui.sketchList`) | +| `manifest.json` | Document JSON (`ezyFormat`, `projectUnit`, sketches, shapes, view, mode, `ui.sketchList`) | | `assets/.rgba` | Raw RGBA pixels for underlay `"asset"` references | | Function | Role | diff --git a/src/gui.cpp b/src/gui.cpp index c4bb5ff..2456af7 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -322,6 +322,16 @@ void GUI::menu_bar_() save_file_dialog_(); } + if (ImGui::BeginMenu("Project units")) + { + const Project_unit cur = m_view->get_project_unit(); + if (ImGui::MenuItem("Inches", nullptr, cur == Project_unit::Inch)) + m_view->set_project_unit(Project_unit::Inch); + if (ImGui::MenuItem("Millimeters", nullptr, cur == Project_unit::Millimeter)) + m_view->set_project_unit(Project_unit::Millimeter); + ImGui::EndMenu(); + } + if (ui_show_feature(2)) { if (ImGui::MenuItem("Import")) @@ -841,6 +851,9 @@ void GUI::dist_edit_() const bool text_changed = ImGui::InputText("##dist_edit_text", m_dist_text_buf.data(), m_dist_text_buf.size(), ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(m_view->project_unit_suffix()); + if (text_changed && parse_dist_text_to_float_(m_dist_text_buf.data(), m_dist_val)) m_dist_callback(m_dist_val, false); @@ -2231,7 +2244,7 @@ void GUI::underlay_calib_prompt_x_distance_(const Sketch::sptr& sk) { m_underlay_calib_phase = Underlay_calib_phase::AwaitDistX; const double L_model = m_underlay_calib_x0.Distance(m_underlay_calib_x1); - const float dist_show = static_cast(L_model / m_view->get_dimension_scale()); + const float dist_show = static_cast(L_model / m_view->get_display_to_model_scale()); const ScreenCoords spos = cursor_screen_coords(); Sketch::wptr wk = sk; @@ -2251,7 +2264,7 @@ void GUI::underlay_calib_prompt_x_distance_(const Sketch::sptr& sk) if (m_underlay_calib_phase != Underlay_calib_phase::AwaitDistX) return; - const double Dx = static_cast(new_dist) * m_view->get_dimension_scale(); + const double Dx = static_cast(new_dist) * m_view->get_display_to_model_scale(); if (Dx <= 1e-12) { show_message("Distance must be positive."); @@ -2290,7 +2303,7 @@ void GUI::underlay_calib_prompt_y_distance_(const Sketch::sptr& sk) { m_underlay_calib_phase = Underlay_calib_phase::AwaitDistY; const double L_model = m_underlay_calib_y0.Distance(m_underlay_calib_y1); - const float dist_show = static_cast(L_model / m_view->get_dimension_scale()); + const float dist_show = static_cast(L_model / m_view->get_display_to_model_scale()); const ScreenCoords spos = cursor_screen_coords(); Sketch::wptr wk = sk; @@ -2310,7 +2323,7 @@ void GUI::underlay_calib_prompt_y_distance_(const Sketch::sptr& sk) if (m_underlay_calib_phase != Underlay_calib_phase::AwaitDistY) return; - const double Dy = static_cast(new_dist) * m_view->get_dimension_scale(); + const double Dy = static_cast(new_dist) * m_view->get_display_to_model_scale(); if (Dy <= 1e-12) { show_message("Distance must be positive."); diff --git a/src/gui.h b/src/gui.h index f481d63..315949f 100644 --- a/src/gui.h +++ b/src/gui.h @@ -125,7 +125,8 @@ inline constexpr double k_gui_view_roll_step_deg_default = 45.0; inline constexpr double k_gui_view_zoom_scroll_scale_min = 0.25; inline constexpr double k_gui_view_zoom_scroll_scale_max = 64.0; inline constexpr double k_gui_view_zoom_scroll_scale_default = 4.0; -/// Allowed range and default for `gui.default_2d_view_width` / `gui.default_2d_view_height` (display units; +/// Allowed range and default for `gui.default_2d_view_width` / `gui.default_2d_view_height` (inches in settings; + /// New Project / default camera framing on the sketch plane; must match Settings sliders). inline constexpr double k_gui_default_2d_view_size_min = 0.1; inline constexpr double k_gui_default_2d_view_size_max = 1000.0; @@ -167,6 +168,7 @@ struct Gui_imgui_style_settings struct Gui_settings_headers { bool view_nav{false}; + bool new_project{false}; bool ui{false}; bool view_presentation{false}; bool grid{false}; @@ -292,9 +294,11 @@ class GUI /// `gui.inspection_orthographic`. bool inspection_orthographic() const { return m_inspection_orthographic; } void set_inspection_orthographic(bool v) { m_inspection_orthographic = v; } - /// Default top-view framing width/height in display units (`gui.default_2d_view_width` / `_height`). + /// Default top-view framing width/height in inches (`gui.default_2d_view_width` / `_height`). double default_2d_view_width() const { return m_default_2d_view_width; } double default_2d_view_height() const { return m_default_2d_view_height; } + /// Unit applied by File -> New (`gui.default_project_unit`). + Project_unit default_project_unit() const { return m_default_project_unit; } bool get_dark_mode() const { return m_dark_mode; } ImVec4 get_clear_color() const; void set_mode(Mode mode); // gui_mode.cpp @@ -609,9 +613,10 @@ class GUI double m_view_roll_step_deg = k_gui_view_roll_step_deg_default; /// Multiplier for `UpdateZoom(Aspect_ScrollDelta(..., int(y * scale)))`; persisted in `gui.view_zoom_scroll_scale`. double m_view_zoom_scroll_scale = k_gui_view_zoom_scroll_scale_default; - /// Sketch-plane framing for New Project / default camera (`gui.default_2d_view_width` / `_height`, display units). + /// Sketch-plane framing for New Project / default camera (`gui.default_2d_view_width` / `_height`, inches). double m_default_2d_view_width = k_gui_default_2d_view_size_default; double m_default_2d_view_height = k_gui_default_2d_view_size_default; + Project_unit m_default_project_unit = Project_unit::Inch; bool m_inspection_orthographic = false; std::vector m_toolbar_buttons; diff --git a/src/gui_add.cpp b/src/gui_add.cpp index 91be986..2393118 100644 --- a/src/gui_add.cpp +++ b/src/gui_add.cpp @@ -13,7 +13,7 @@ void GUI::add_box_dialog_() if (!ImGui::BeginPopupModal("Add box", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; - ImGui::TextUnformatted("Values in display units."); + ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); ImGui::Spacing(); if (ImGui::BeginTable("Add box##table", 2, ImGuiTableFlags_SizingStretchProp)) @@ -68,7 +68,7 @@ void GUI::add_box_dialog_() { if (m_add_box_size.x > 0 && m_add_box_size.y > 0 && m_add_box_size.z > 0) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_box(m_add_box_origin.x * scale, m_add_box_origin.y * scale, m_add_box_origin.z * scale, m_add_box_size.x * scale, m_add_box_size.y * scale, m_add_box_size.z * scale); @@ -92,7 +92,7 @@ void GUI::add_pyramid_dialog_() if (!ImGui::BeginPopupModal("Add pyramid", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; - ImGui::TextUnformatted("Values in display units."); + ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); ImGui::Spacing(); if (ImGui::BeginTable("Add pyramid##table", 2, ImGuiTableFlags_SizingStretchProp)) { @@ -125,7 +125,7 @@ void GUI::add_pyramid_dialog_() ImGui::Spacing(); if (ImGui::Button("Add") && m_add_pyramid_side > 0) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_pyramid(m_add_pyramid_origin.x * scale, m_add_pyramid_origin.y * scale, m_add_pyramid_origin.z * scale, m_add_pyramid_side * scale); @@ -149,7 +149,7 @@ void GUI::add_sphere_dialog_() if (!ImGui::BeginPopupModal("Add sphere", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; - ImGui::TextUnformatted("Values in display units."); + ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); ImGui::Spacing(); if (ImGui::BeginTable("Add sphere##table", 2, ImGuiTableFlags_SizingStretchProp)) { @@ -183,7 +183,7 @@ void GUI::add_sphere_dialog_() ImGui::Spacing(); if (ImGui::Button("Add") && m_add_sphere_radius > 0) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_sphere(m_add_sphere_origin.x * scale, m_add_sphere_origin.y * scale, m_add_sphere_origin.z * scale, m_add_sphere_radius * scale); @@ -208,7 +208,7 @@ void GUI::add_cylinder_dialog_() if (!ImGui::BeginPopupModal("Add cylinder", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; - ImGui::TextUnformatted("Values in display units."); + ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); ImGui::Spacing(); if (ImGui::BeginTable("Add cylinder##table", 2, ImGuiTableFlags_SizingStretchProp)) { @@ -248,7 +248,7 @@ void GUI::add_cylinder_dialog_() ImGui::Spacing(); if (ImGui::Button("Add") && m_add_cylinder_radius > 0 && m_add_cylinder_height > 0) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_cylinder(m_add_cylinder_origin.x * scale, m_add_cylinder_origin.y * scale, m_add_cylinder_origin.z * scale, m_add_cylinder_radius * scale, m_add_cylinder_height * scale); @@ -272,7 +272,7 @@ void GUI::add_cone_dialog_() if (!ImGui::BeginPopupModal("Add cone", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; - ImGui::TextUnformatted("Values in display units."); + ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); ImGui::Spacing(); if (ImGui::BeginTable("Add cone##table", 2, ImGuiTableFlags_SizingStretchProp)) { @@ -318,7 +318,7 @@ void GUI::add_cone_dialog_() ImGui::Spacing(); if (ImGui::Button("Add") && m_add_cone_R1 >= 0 && m_add_cone_R2 >= 0 && m_add_cone_height > 0) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_cone(m_add_cone_origin.x * scale, m_add_cone_origin.y * scale, m_add_cone_origin.z * scale, m_add_cone_R1 * scale, m_add_cone_R2 * scale, m_add_cone_height * scale); @@ -342,7 +342,7 @@ void GUI::add_torus_dialog_() if (!ImGui::BeginPopupModal("Add torus", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; - ImGui::TextUnformatted("Values in display units."); + ImGui::Text("Values in project units (%s).", m_view->project_unit_suffix()); ImGui::Spacing(); if (ImGui::BeginTable("Add torus##table", 2, ImGuiTableFlags_SizingStretchProp)) { @@ -382,7 +382,7 @@ void GUI::add_torus_dialog_() ImGui::Spacing(); if (ImGui::Button("Add") && m_add_torus_R1 > 0 && m_add_torus_R2 > 0) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_torus(m_add_torus_origin.x * scale, m_add_torus_origin.y * scale, m_add_torus_origin.z * scale, m_add_torus_R1 * scale, m_add_torus_R2 * scale); @@ -406,7 +406,7 @@ void GUI::add_sketch_dialog_() if (!ImGui::BeginPopupModal("New sketch", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) return; - ImGui::TextUnformatted("Plane and offset along its normal (display units)."); + ImGui::Text("Plane and offset along its normal (project units, %s).", m_view->project_unit_suffix()); ImGui::Spacing(); ImGui::TextUnformatted("Reference plane"); @@ -444,7 +444,7 @@ void GUI::add_sketch_dialog_() break; } - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); const gp_Pln pln = sketch_reference_plane(plane, m_new_sketch_offset * scale); m_view->add_sketch(pln, base); ImGui::CloseCurrentPopup(); @@ -469,7 +469,7 @@ void GUI::add_menu_items_() ImGui::Separator(); if (ImGui::MenuItem("Add box")) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_box(0, 0, 0, scale, scale, scale); } @@ -482,7 +482,7 @@ void GUI::add_menu_items_() if (ImGui::MenuItem("Add pyramid")) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_pyramid(0, 0, 0, scale); } @@ -495,7 +495,7 @@ void GUI::add_menu_items_() if (ImGui::MenuItem("Add sphere")) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_sphere(0, 0, 0, scale); } @@ -508,7 +508,7 @@ void GUI::add_menu_items_() if (ImGui::MenuItem("Add cylinder")) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_cylinder(0, 0, 0, scale, scale); } @@ -521,7 +521,7 @@ void GUI::add_menu_items_() if (ImGui::MenuItem("Add cone")) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_cone(0, 0, 0, scale, 0.0, scale); } @@ -536,7 +536,7 @@ void GUI::add_menu_items_() if (ImGui::MenuItem("Add torus")) { - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); m_view->add_torus(0, 0, 0, scale, scale / 2.0); } diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index aa04c27..b3b9497 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -566,7 +566,7 @@ void GUI::options_shape_chamfer_mode_() } static char chamfer_buf[64]; - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); const double chamfer_dist = m_view->shp_chamfer().get_chamfer_dist() / scale; ImGui::PushID("chamfer_dist_micron"); @@ -594,6 +594,8 @@ void GUI::options_shape_chamfer_mode_() m_view->shp_chamfer().set_chamfer_dist(p * scale); } } + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(m_view->project_unit_suffix()); ImGui::PopID(); ImGui::EndTable(); } @@ -629,7 +631,7 @@ void GUI::options_shape_fillet_mode_() } static char fillet_buf[64]; - const double scale = m_view->get_dimension_scale(); + const double scale = m_view->get_display_to_model_scale(); const double fillet_radius = m_view->shp_fillet().get_fillet_radius() / scale; ImGui::PushID("fillet_rad_micron"); @@ -657,6 +659,8 @@ void GUI::options_shape_fillet_mode_() m_view->shp_fillet().set_fillet_radius(p * scale); } } + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(m_view->project_unit_suffix()); ImGui::PopID(); ImGui::EndTable(); diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index b449790..86e1e48 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -681,7 +681,7 @@ void Occt_view::add_sketch(const gp_Pln& pln, const std::string& base_name) void Occt_view::add_sketch_on_ref_plane(Sketch_ref_plane plane, double offset_display, const std::string& base_name) { - const gp_Pln pln = sketch_reference_plane(plane, offset_display * get_dimension_scale()); + const gp_Pln pln = sketch_reference_plane(plane, offset_display * get_display_to_model_scale()); add_sketch(pln, base_name); } @@ -1323,6 +1323,38 @@ void Occt_view::angle_input(const ScreenCoords& screen_coords) { curr_sketch().a double Occt_view::get_dimension_scale() const { return m_dimension_scale; } +double Occt_view::get_display_to_model_scale() const +{ + if (m_project_unit == Project_unit::Millimeter) + return m_dimension_scale / k_mm_per_inch; + + return m_dimension_scale; +} + +double Occt_view::to_model(double display) const { return display * get_display_to_model_scale(); } + +double Occt_view::to_display(double model) const +{ + const double scale = get_display_to_model_scale(); + return scale != 0.0 ? model / scale : 0.0; +} + +Project_unit Occt_view::get_project_unit() const { return m_project_unit; } + +void Occt_view::set_project_unit(Project_unit unit) +{ + if (m_project_unit == unit) + return; + + m_project_unit = unit; + refresh_sketch_annotations({.length_dimensions = true}); +} + +const char* Occt_view::project_unit_suffix() const +{ + return m_project_unit == Project_unit::Millimeter ? "mm" : "in"; +} + bool Occt_view::get_show_dim_input() const { return m_show_dim_input; } void Occt_view::set_show_dim_input(bool show) { m_show_dim_input = show; } @@ -2868,6 +2900,7 @@ std::string Occt_view::to_json() const using namespace nlohmann; json j; j["ezyFormat"] = k_ezy_file_format_version; + j["projectUnit"] = (m_project_unit == Project_unit::Millimeter) ? "millimeter" : "inch"; json& sketches = j["sketches"] = json::array(); json& shps = j["shapes"] = json::array(); @@ -2943,6 +2976,13 @@ void Occt_view::load(const std::string& json_str, bool restore_view) const json j = json::parse(json_str); (void)j.value("ezyFormat", 1); // Reserved for future migrations; sketch JSON migrates per-edge dim flags in Sketch_json. + m_project_unit = Project_unit::Inch; + if (j.contains("projectUnit") && j["projectUnit"].is_string()) + { + const std::string u = j["projectUnit"].get(); + if (u == "millimeter" || u == "mm") + m_project_unit = Project_unit::Millimeter; + } EZY_ASSERT(j.contains("sketches") && j["sketches"].is_array()); for (const auto& s : j["sketches"]) { @@ -3053,9 +3093,7 @@ void Occt_view::load(const std::string& json_str, bool restore_view) namespace { -// Project display lengths are inches. Model space = inches * dimension_scale (default 100). -constexpr double k_mm_per_inch = 25.4; - +// Project display lengths use Project_unit (inch or mm). Model space = inches * dimension_scale (default 100). TopoDS_Shape scale_shape_about_origin_(const TopoDS_Shape& shape, double factor) { if (shape.IsNull()) @@ -3279,6 +3317,7 @@ void Occt_view::new_file() m_assets.clear(); m_next_sketch_id = 1; m_next_shape_id = 1; + m_project_unit = m_gui.default_project_unit(); create_default_sketch_(); refresh_viewer_grid_(); diff --git a/src/gui_occt_view.h b/src/gui_occt_view.h index e75fcf7..d05f2d3 100644 --- a/src/gui_occt_view.h +++ b/src/gui_occt_view.h @@ -167,7 +167,7 @@ class Occt_view : protected AIS_ViewController void remove_sketch(const Sketch_ptr& sketch); /// Empty sketch on \a pln; \a base_name is uniquified (e.g. Sketch_xy, Sketch_xy.001). void add_sketch(const gp_Pln& pln, const std::string& base_name); - /// Like add_sketch; \a offset_display is multiplied by get_dimension_scale(). + /// Like add_sketch; \a offset_display is multiplied by get_display_to_model_scale(). void add_sketch_on_ref_plane(Sketch_ref_plane plane, double offset_display, const std::string& base_name); void curr_sketch_add_edge(double x1, double y1, double x2, double y2); void curr_sketch_rebuild_faces(); @@ -219,7 +219,17 @@ class Occt_view : protected AIS_ViewController void angle_input(const ScreenCoords& screen_coords); void refresh_sketch_annotations(const Sketch_annotation_refresh& refresh); void apply_sketch_dimensions_visibility(); + /// Inch-based model scale (`model = inches * scale`). Used for STEP/PLY import/export. double get_dimension_scale() const; + /// Display (project unit) -> model scale. Prefer for UI length I/O. + double get_display_to_model_scale() const; + double to_model(double display) const; + double to_display(double model) const; + Project_unit get_project_unit() const; + /// Sets project unit, refreshes length dimensions; does not rewrite geometry. + void set_project_unit(Project_unit unit); + /// Short label for length fields: "in" or "mm". + const char* project_unit_suffix() const; bool get_show_dim_input() const; void set_show_dim_input(bool show); std::optional get_entered_dim() const; @@ -403,6 +413,7 @@ class Occt_view : protected AIS_ViewController // Dimension related bool m_show_dim_input{false}; double m_dimension_scale{100.0}; + Project_unit m_project_unit{Project_unit::Inch}; std::optional m_entered_dim; std::list m_shps; Sketch_list m_sketches; diff --git a/src/gui_settings.cpp b/src/gui_settings.cpp index 48ba468..c963a71 100644 --- a/src/gui_settings.cpp +++ b/src/gui_settings.cpp @@ -33,6 +33,7 @@ nlohmann::json imgui_style_to_json(const Gui_imgui_style_settings& s) nlohmann::json settings_headers_to_json(const Gui_settings_headers& h) { return nlohmann::json{{"view_nav", h.view_nav}, + {"new_project", h.new_project}, {"ui", h.ui}, {"view_presentation", h.view_presentation}, {"grid", h.grid}, @@ -55,6 +56,7 @@ void parse_settings_headers_json(const nlohmann::json& obj, Gui_settings_headers return fallback; }; out.view_nav = b("view_nav", defaults.view_nav); + out.new_project = b("new_project", defaults.new_project); out.ui = b("ui", defaults.ui); out.view_presentation = b("view_presentation", defaults.view_presentation); out.grid = b("grid", defaults.grid); @@ -192,6 +194,7 @@ std::string GUI::occt_view_settings_json() const {"view_zoom_scroll_scale", m_view_zoom_scroll_scale}, {"default_2d_view_width", m_default_2d_view_width}, {"default_2d_view_height", m_default_2d_view_height}, + {"default_project_unit", (m_default_project_unit == Project_unit::Millimeter) ? "millimeter" : "inch"}, {"inspection_orthographic", m_inspection_orthographic}, {"snap_guide_color_node", [&]() @@ -287,6 +290,7 @@ void GUI::save_occt_view_settings() {"view_zoom_scroll_scale", m_view_zoom_scroll_scale}, {"default_2d_view_width", m_default_2d_view_width}, {"default_2d_view_height", m_default_2d_view_height}, + {"default_project_unit", (m_default_project_unit == Project_unit::Millimeter) ? "millimeter" : "inch"}, {"inspection_orthographic", m_inspection_orthographic}, {"snap_guide_color_node", [&]() @@ -671,6 +675,14 @@ void GUI::parse_gui_panes_settings_(const std::string& content) "], got " + std::to_string(v) + "; using default."); } + m_default_project_unit = Project_unit::Inch; + if (g.contains("default_project_unit") && g["default_project_unit"].is_string()) + { + const std::string u = g["default_project_unit"].get(); + if (u == "millimeter" || u == "mm") + m_default_project_unit = Project_unit::Millimeter; + } + Sketch_nodes::set_snap_guide_color_node(0.823295f, 0.549411f, 0.953390f); Sketch_nodes::set_snap_guide_color_axis(0.957627f, 0.064924f, 0.541537f); if (g.contains("snap_guide_color_node") && g["snap_guide_color_node"].is_array() && g["snap_guide_color_node"].size() >= 3) @@ -973,20 +985,72 @@ void GUI::settings_() "user guide.", doc_urls::k_view_navigation); + ImGui::EndTable(); + } + + if (ui_show_contextual_help()) + ImGui::TextWrapped( + "NumPad 8 / 2 / 4 / 6 orbit the view (same axes as left-drag orbit). Hold Shift and press NumPad 4 or NumPad 6, " + "main 4 / 6, or Left / Right arrow for Blender-style roll around the screen Z axis (hold to repeat). " + "Num Lock off is recommended for numpad shortcuts (see usage.md View navigation). " + "Hold Shift while scrolling or pressing +/- for finer zoom."); + } + + if (settings_collapsing_header_("New project defaults", m_settings_headers.new_project)) + { + if (ImGui::BeginTable("settings_new_project", 2, ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthFixed, k_label_col_w); + ImGui::TableSetupColumn("control", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Project units"); + ImGui::TableSetColumnIndex(1); + int unit_idx = (m_default_project_unit == Project_unit::Millimeter) ? 1 : 0; + if (ImGui::Combo("##default_project_unit", &unit_idx, "Inches\0Millimeters\0")) + { + m_default_project_unit = (unit_idx == 1) ? Project_unit::Millimeter : Project_unit::Inch; + save_occt_view_settings(); + } + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("Unit applied by File -> New. Width/height below are edited in this unit (stored as inches). " + "Does not change the open project's File -> Project units. Click ? for the guide.", + doc_urls::k_view_navigation); + + const double to_ui = + (m_default_project_unit == Project_unit::Millimeter) ? k_mm_per_inch : 1.0; + const char* unit_sfx = + (m_default_project_unit == Project_unit::Millimeter) ? "mm" : "in"; + double w_ui = m_default_2d_view_width * to_ui; + double h_ui = m_default_2d_view_height * to_ui; + const double min_ui = k_gui_default_2d_view_size_min * to_ui; + const double max_ui = k_gui_default_2d_view_size_max * to_ui; + // Drag (not a 0.1..1000 slider): default 3 in sits near the left of a linear slider and is easy to + // accidentally set to the minimum. + const float drag_spd = (m_default_project_unit == Project_unit::Millimeter) ? 1.0f : 0.05f; + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Default 2D view width"); ImGui::TableSetColumnIndex(1); - if (ImGui::SliderScalar("##default_2d_view_width", ImGuiDataType_Double, &m_default_2d_view_width, - &k_gui_default_2d_view_size_min, &k_gui_default_2d_view_size_max, "%.2f", - ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_ClampOnInput)) + ImGui::SetNextItemWidth(120.0f); + if (ImGui::DragScalar("##default_2d_view_width", ImGuiDataType_Double, &w_ui, drag_spd, &min_ui, &max_ui, "%.2f")) + { + m_default_2d_view_width = + std::clamp(w_ui / to_ui, k_gui_default_2d_view_size_min, k_gui_default_2d_view_size_max); save_occt_view_settings(); + } m_default_2d_view_width = std::clamp(m_default_2d_view_width, k_gui_default_2d_view_size_min, k_gui_default_2d_view_size_max); ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); - GUI_DOC_HELP_("Horizontal span of the sketch plane framed by File -> New and by projects with no saved camera " - "(same length scale as sketch dimensions). Default 3. Ctrl+click to type. Click ? for the guide.", + ImGui::TextUnformatted(unit_sfx); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("Horizontal span of the sketch plane framed by File -> New and by projects with no saved camera. " + "Edited in Project units above; stored as inches. Default 3 in. Ctrl+click to type. Click ? for " + "the guide.", doc_urls::k_view_navigation); ImGui::TableNextRow(); @@ -994,27 +1058,29 @@ void GUI::settings_() ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted("Default 2D view height"); ImGui::TableSetColumnIndex(1); - if (ImGui::SliderScalar("##default_2d_view_height", ImGuiDataType_Double, &m_default_2d_view_height, - &k_gui_default_2d_view_size_min, &k_gui_default_2d_view_size_max, "%.2f", - ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_ClampOnInput)) + ImGui::SetNextItemWidth(120.0f); + if (ImGui::DragScalar("##default_2d_view_height", ImGuiDataType_Double, &h_ui, drag_spd, &min_ui, &max_ui, "%.2f")) + { + m_default_2d_view_height = + std::clamp(h_ui / to_ui, k_gui_default_2d_view_size_min, k_gui_default_2d_view_size_max); save_occt_view_settings(); + } m_default_2d_view_height = std::clamp(m_default_2d_view_height, k_gui_default_2d_view_size_min, k_gui_default_2d_view_size_max); ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); - GUI_DOC_HELP_("Vertical span of the sketch plane framed by File -> New and by projects with no saved camera " - "(same length scale as sketch dimensions). Default 3. Ctrl+click to type. Click ? for the guide.", + ImGui::TextUnformatted(unit_sfx); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("Vertical span of the sketch plane framed by File -> New and by projects with no saved camera. " + "Edited in Project units above; stored as inches. Default 3 in. Ctrl+click to type. Click ? for " + "the guide.", doc_urls::k_view_navigation); ImGui::EndTable(); } if (ui_show_contextual_help()) - ImGui::TextWrapped( - "NumPad 8 / 2 / 4 / 6 orbit the view (same axes as left-drag orbit). Hold Shift and press NumPad 4 or NumPad 6, " - "main 4 / 6, or Left / Right arrow for Blender-style roll around the screen Z axis (hold to repeat). " - "Num Lock off is recommended for numpad shortcuts (see usage.md View navigation). " - "Hold Shift while scrolling or pressing +/- for finer zoom. " - "Default 2D view width/height set the top-view framing for File -> New."); + ImGui::TextWrapped("These defaults apply when you use File -> New. Open projects keep their own project units " + "(File -> Project units). Changing Project units here only remaps the width/height labels."); } if (settings_collapsing_header_("UI", m_settings_headers.ui)) @@ -1135,8 +1201,8 @@ void GUI::settings_() m_view->get_grid_colors(g1, g2); Occt_grid_rect_params gr{}; m_view->get_occt_grid_rect_params(gr); - const double dim_scale = m_view->get_dimension_scale(); - // Settings show the same length units as sketch dimensions (model / dimension_scale). + const double dim_scale = m_view->get_display_to_model_scale(); + // Settings show the same length units as sketch dimensions (project unit). double step_ui = gr.step / dim_scale; double padding_ui = gr.grid_padding / dim_scale; double graphic_z_off_ui = gr.graphic_z_offset / dim_scale; @@ -1191,6 +1257,8 @@ void GUI::settings_() const double step_min_ui = 1e-6; if (ImGui::DragScalar("##gstep", ImGuiDataType_Double, &step_ui, spd_s, &step_min_ui, nullptr, "%.8g")) geom_changed = true; + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(m_view->project_unit_suffix()); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -1200,11 +1268,13 @@ void GUI::settings_() const double padding_min_ui = 0.0; if (ImGui::DragScalar("##gpad", ImGuiDataType_Double, &padding_ui, spd_extent, &padding_min_ui, nullptr, "%.8g")) geom_changed = true; + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(m_view->project_unit_suffix()); ImGui::SameLine(0.f, ImGui::GetStyle().ItemInnerSpacing.x); GUI_DOC_HELP_("Margin added around the active sketch when sizing the grid (same length scale as sketch " - "dimensions). The grid extent follows sketch geometry plus this padding. Click ? to open the user " - "guide.", + "dimensions / project units). The grid extent follows sketch geometry plus this padding. Click ? " + "to open the user guide.", doc_urls::k_occt_view); ImGui::TableNextRow(); @@ -1214,6 +1284,8 @@ void GUI::settings_() ImGui::TableSetColumnIndex(1); if (ImGui::DragScalar("##ggz", ImGuiDataType_Double, &graphic_z_off_ui, spd_m, nullptr, nullptr, "%.8g")) geom_changed = true; + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TextUnformatted(m_view->project_unit_suffix()); ImGui::EndTable(); } diff --git a/src/scr_lua_console.cpp b/src/scr_lua_console.cpp index edc5880..59bb8fc 100644 --- a/src/scr_lua_console.cpp +++ b/src/scr_lua_console.cpp @@ -222,7 +222,7 @@ int l_view_curr_sketch_dim(lua_State* L) const std::size_t lo = sketch.dimension_node_lo(i); const std::size_t hi = sketch.dimension_node_hi(i); const Sketch_nodes& nodes = sketch.get_nodes(); - const double dist = nodes[lo].Distance(nodes[hi]) / view->get_dimension_scale(); + const double dist = nodes[lo].Distance(nodes[hi]) / view->get_display_to_model_scale(); lua_pushinteger(L, static_cast(lo + 1)); lua_pushinteger(L, static_cast(hi + 1)); lua_pushboolean(L, sketch.dimension_visible(i)); diff --git a/src/scr_python_console.cpp b/src/scr_python_console.cpp index dff4b8c..727e1dd 100644 --- a/src/scr_python_console.cpp +++ b/src/scr_python_console.cpp @@ -469,7 +469,7 @@ PYBIND11_EMBEDDED_MODULE(ezycad_native, m) const std::size_t lo = sketch.dimension_node_lo(i); const std::size_t hi = sketch.dimension_node_hi(i); const Sketch_nodes& nodes = sketch.get_nodes(); - const double dist = nodes[lo].Distance(nodes[hi]) / view->get_dimension_scale(); + const double dist = nodes[lo].Distance(nodes[hi]) / view->get_display_to_model_scale(); return py::make_tuple(lo, hi, sketch.dimension_visible(i), sketch.dimension_offset(i), sketch.dimension_name(i), dist); }, py::arg("i")); diff --git a/src/shp_extrude.cpp b/src/shp_extrude.cpp index 149b8b9..370108d 100644 --- a/src/shp_extrude.cpp +++ b/src/shp_extrude.cpp @@ -118,13 +118,13 @@ void Shp_extrude::_update_extrude(const ScreenCoords& screen_coords) if (cursor_side != Plane_side::On) m_extrude_side = cursor_side; - double scaled_dist = extrude_dist / view().get_dimension_scale(); + double scaled_dist = extrude_dist / view().get_display_to_model_scale(); if (view().get_show_dim_input()) { auto l = [this](float new_dist, bool do_finalize) { - const double entered_dist = static_cast(new_dist) * view().get_dimension_scale(); + const double entered_dist = static_cast(new_dist) * view().get_display_to_model_scale(); view().set_entered_dim(entered_dist); if (do_finalize) { @@ -161,7 +161,7 @@ void Shp_extrude::update_extrude_preview_(const double extrude_dist, const Plane gp_Pnt(m_to_extrude_pt->XYZ() + face_offset.XYZ()), m_curr_view_pln, gui().length_dimension_style()); - m_tmp_dim->SetCustomValue(extrude_dist / view().get_dimension_scale()); + m_tmp_dim->SetCustomValue(extrude_dist / view().get_display_to_model_scale()); ctx().Display(m_tmp_dim, false); diff --git a/src/shp_move.cpp b/src/shp_move.cpp index 5238501..ed75fd5 100644 --- a/src/shp_move.cpp +++ b/src/shp_move.cpp @@ -62,7 +62,7 @@ void Shp_move::show_dist_edit(const ScreenCoords& screen_coords) auto dist_edit_axis_x = [&, screen_coords](float new_dist, bool is_final) { - m_delta.override_x = new_dist * view().get_dimension_scale(); + m_delta.override_x = new_dist * view().get_display_to_model_scale(); EZY_ASSERT(move_selected(screen_coords).is_ok()); // Status should always be valid here if (is_final) check_finalize_(); @@ -70,7 +70,7 @@ void Shp_move::show_dist_edit(const ScreenCoords& screen_coords) auto dist_edit_axis_y = [&, screen_coords](float new_dist, bool is_final) { - m_delta.override_y = new_dist * view().get_dimension_scale(); + m_delta.override_y = new_dist * view().get_display_to_model_scale(); EZY_ASSERT(move_selected(screen_coords).is_ok()); if (is_final) check_finalize_(); @@ -78,22 +78,22 @@ void Shp_move::show_dist_edit(const ScreenCoords& screen_coords) auto dist_edit_axis_z = [&, screen_coords](float new_dist, bool is_final) { - m_delta.override_z = new_dist * view().get_dimension_scale(); + m_delta.override_z = new_dist * view().get_display_to_model_scale(); EZY_ASSERT(move_selected(screen_coords).is_ok()); if (is_final) check_finalize_(); }; if (!m_delta.override_x.has_value() && (no_axis_constraints || m_opts.constr_axis_x)) - gui().set_dist_edit(float(m_delta.delta.X() / view().get_dimension_scale()), + gui().set_dist_edit(float(m_delta.delta.X() / view().get_display_to_model_scale()), std::move(std::function(dist_edit_axis_x))); else if (!m_delta.override_y.has_value() && (no_axis_constraints || m_opts.constr_axis_y)) - gui().set_dist_edit(float(m_delta.delta.Y() / view().get_dimension_scale()), + gui().set_dist_edit(float(m_delta.delta.Y() / view().get_display_to_model_scale()), std::move(std::function(dist_edit_axis_y))); else if (!m_delta.override_z.has_value() && (no_axis_constraints || m_opts.constr_axis_z)) - gui().set_dist_edit(float(m_delta.delta.Z() / view().get_dimension_scale()), + gui().set_dist_edit(float(m_delta.delta.Z() / view().get_display_to_model_scale()), std::move(std::function(dist_edit_axis_z))); } diff --git a/src/skt_dims.cpp b/src/skt_dims.cpp index a3398b2..6307817 100644 --- a/src/skt_dims.cpp +++ b/src/skt_dims.cpp @@ -115,7 +115,7 @@ void Sketch_dims::rebuild_length_dimension_display_(Length_dimension& d) : &m_sketch.m_topo.dim_classifier_faces()); const double dist = m_sketch.m_nodes[d.node_idx_lo].Distance(m_sketch.m_nodes[d.node_idx_hi]); - d.dim->SetCustomValue(dist / m_sketch.m_view.get_dimension_scale()); + d.dim->SetCustomValue(dist / m_sketch.m_view.get_display_to_model_scale()); if (d.flyout_offset.has_value() && *d.flyout_offset > 0.0) { const double f = d.dim->GetFlyout(); @@ -327,7 +327,7 @@ void Sketch_dims::show_tmp_dim_preview(const gp_Pnt2d& pt_a, const gp_Pnt2d& pt_ return; } - const double dist = pt_a.Distance(pt_b) / m_sketch.m_view.get_dimension_scale(); + const double dist = pt_a.Distance(pt_b) / m_sketch.m_view.get_display_to_model_scale(); m_tmp_dim_anno = create_distance_annotation( pt_a, pt_b, m_sketch.m_pln, m_sketch.m_view.gui().length_dimension_style(), approx_sketch_interior_ref_3d_(), m_sketch.m_topo.dim_classifier_faces().empty() ? nullptr : &m_sketch.m_topo.dim_classifier_faces()); @@ -345,7 +345,7 @@ void Sketch_dims::offer_dist_edit_for_segment(const gp_Pnt2d& pt_a, const gp_Pnt auto cb = [this, edge_dir](float new_dist, bool is_finial) { - m_entered_edge_len = {edge_dir, new_dist * m_sketch.m_view.get_dimension_scale()}; + m_entered_edge_len = {edge_dir, new_dist * m_sketch.m_view.get_display_to_model_scale()}; m_show_dim_input = !is_finial; if (is_finial) m_sketch.on_enter(); diff --git a/src/skt_tools.cpp b/src/skt_tools.cpp index 856f17b..40b7931 100644 --- a/src/skt_tools.cpp +++ b/src/skt_tools.cpp @@ -342,7 +342,7 @@ void Sketch_tools::move_line_string_pt_(const ScreenCoords& screen_coords) const gp_Pnt2d& span_b = span->pt_b; m_sketch.update_edge_shp_(edge, span_a, span_b); - const double dist = span->full_len / m_sketch.m_view.get_dimension_scale(); + const double dist = span->full_len / m_sketch.m_view.get_display_to_model_scale(); m_sketch.m_dims.show_tmp_dim_preview(span_a, span_b); m_sketch.m_dims.offer_dist_edit_for_segment(span_a, span_b, dist); m_sketch.m_dims.offer_angle_edit_for_segment( @@ -408,7 +408,7 @@ void Sketch_tools::move_line_string_pt_(const ScreenCoords& screen_coords) final_pt_b = m_sketch.m_nodes[edge.node_idx_b]; } - double dist = pt_a.Distance(final_pt_b) / m_sketch.m_view.get_dimension_scale(); + double dist = pt_a.Distance(final_pt_b) / m_sketch.m_view.get_display_to_model_scale(); m_sketch.m_dims.show_tmp_dim_preview(pt_a, final_pt_b); m_sketch.m_dims.offer_dist_edit_for_segment(pt_a, final_pt_b, dist); diff --git a/src/utl_types.h b/src/utl_types.h index 6e80099..0ad440d 100644 --- a/src/utl_types.h +++ b/src/utl_types.h @@ -133,4 +133,15 @@ enum class Export_unit Millimeter }; +/// Project display/input length unit. Model space stays inch-scaled +/// (`model = inches * dimension_scale`); this only affects UI and dimensions. +enum class Project_unit +{ + Inch, + Millimeter +}; + +/// Millimeters per inch (display conversion and CAD interchange). +inline constexpr double k_mm_per_inch = 25.4; + #include "utl_types.inl" \ No newline at end of file diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index 38a4b60..bf42522 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -12,6 +12,9 @@ #include "skt_json.h" #include "skt_nodes.h" #include "utl_geom.h" +#include "utl_types.h" + +#include using namespace glm; @@ -835,3 +838,36 @@ TEST_F(Sketch_test, DimensionTool_picks_sketch_origin) ASSERT_TRUE(origin_pick.has_value()); EXPECT_TRUE(sketch.get_nodes()[*origin_pick].origin); } + +TEST_F(Sketch_test, ProjectUnit_displayConversionAndJsonRoundTrip) +{ + Headless_guard guard(view()); + + EXPECT_EQ(view().get_project_unit(), Project_unit::Inch); + EXPECT_DOUBLE_EQ(view().get_display_to_model_scale(), view().get_dimension_scale()); + EXPECT_DOUBLE_EQ(view().to_model(1.0), 100.0); + EXPECT_DOUBLE_EQ(view().to_display(100.0), 1.0); + EXPECT_STREQ(view().project_unit_suffix(), "in"); + + view().set_project_unit(Project_unit::Millimeter); + EXPECT_EQ(view().get_project_unit(), Project_unit::Millimeter); + EXPECT_NEAR(view().to_model(25.4), 100.0, 1e-9); + EXPECT_NEAR(view().to_display(100.0), 25.4, 1e-9); + EXPECT_STREQ(view().project_unit_suffix(), "mm"); + + const std::string json_mm = view().to_json(); + EXPECT_NE(json_mm.find("\"projectUnit\": \"millimeter\""), std::string::npos); + + view().set_project_unit(Project_unit::Inch); + view().load(json_mm, false); + EXPECT_EQ(view().get_project_unit(), Project_unit::Millimeter); + + // Old manifests without projectUnit default to inches. + nlohmann::json j = nlohmann::json::parse(json_mm); + j.erase("projectUnit"); + view().load(j.dump(), false); + EXPECT_EQ(view().get_project_unit(), Project_unit::Inch); + + view().new_file(); + EXPECT_EQ(view().get_project_unit(), Project_unit::Inch); +}