From 9a6535eb67fc9785115ac4b4de6233039809b1cf Mon Sep 17 00:00:00 2001 From: Trailcode Date: Fri, 18 Sep 2026 17:47:41 -0600 Subject: [PATCH 01/12] Start --- CHANGELOG.md | 2 + agents/plans/assembly-inspection-mode.md | 14 +- docs/scripting.md | 18 +-- docs/usage-settings.md | 2 +- docs/usage.md | 95 ++++++----- src/doc/gui.md | 88 +++++----- src/doc/script.md | 18 +-- src/doc/shape.md | 30 ++-- src/doc/undo-redo.md | 2 +- src/gui.cpp | 195 ++++++++++++++--------- src/gui.h | 14 +- src/gui_mode.cpp | 111 ++++++------- src/gui_occt_view.cpp | 50 +++--- src/mode.cpp | 91 ++++++++++- src/mode.h | 36 ++++- src/scr_lua_console.cpp | 2 +- src/scr_python_console.cpp | 2 +- src/shp_cyl_align.cpp | 2 +- src/shp_move.cpp | 2 +- src/shp_polar_dup.cpp | 2 +- src/shp_rotate.cpp | 2 +- src/shp_scale.cpp | 2 +- src/shp_set_frame.cpp | 4 +- tests/shp_tests.cpp | 40 ++--- tests/skt_tests.cpp | 2 +- 25 files changed, 503 insertions(+), 323 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd692733..2f9fe936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Task toolbar**: Sketch / Design / Workbench switcher; the tools row shows only the current task. Move / Rotate / Align shafts live under Workbench; Scale stays on Design. G / R / J (and S for Scale) still enter those tools from any task. +- **Mode names**: Design idle is `Design_inspection` (was `Normal`); Sketch idle is `Sketch_inspection` (was `Sketch_inspection_mode`). `ezy.get_mode()` / `ezy.set_mode` use the new names; `Normal` and `Sketch_inspection_mode` still parse. - **Move / rotate / scale space**: Options **Local** (default) or **World**. Local uses the first selected solid's frame (origin and X/Y/Z from **Show axes**). World keeps global XYZ at that solid's bounding-box center. Persisted as **`gui.transform_space`**. - **Desktop deps**: MSVC GLFW/GLEW come from **vcpkg** (`vcpkg.json` + toolchain) instead of NuGet; local configure matches CI. - **Extrude Both sides**: Options **Both sides** defaults to on (session sticky; not a Settings key). diff --git a/agents/plans/assembly-inspection-mode.md b/agents/plans/assembly-inspection-mode.md index 80909326..40fabe17 100644 --- a/agents/plans/assembly-inspection-mode.md +++ b/agents/plans/assembly-inspection-mode.md @@ -25,7 +25,7 @@ Post-v1 product design. Do **not** ship Assembly inspection until [phase 3 place Two phases of work, two kinds of idle mode: -1. **Make geometry** — Sketch inspection + Inspection (`Mode::Normal`): draw profiles, see solids alongside (or faint), extrude, fillet, boolean. +1. **Make geometry** — Sketch inspection + Inspection (`Mode::Design_inspection`): draw profiles, see solids alongside (or faint), extrude, fillet, boolean. 2. **Arrange components** — Assembly inspection (future): duplicate / place / move Parts relative to each other; later mates and instances. That split matches SolidWorks / Inventor / Fusion (sketch nested in part, part nested in assembly). EzyCad names the idle states “inspection” (= browse/select + launch tools + Esc parent), not metrology “Inspect.” @@ -46,12 +46,12 @@ flowchart TB ## What exists today -| UI label | Mode | Role | -| ---------------------- | ------------------------------ | ----------------------------------------------------------------------------- | -| Inspection mode | `Mode::Normal` | Idle 3D solids: selection filter, materials, booleans, transforms | -| Sketch inspection mode | `Mode::Sketch_inspection_mode` | Idle 2D sketch: show sketch, ortho, faint solids; Esc parent for sketch tools | +| UI label | Mode | Role | +| ---------------------- | ------------------------- | ----------------------------------------------------------------------------- | +| Inspection mode | `Mode::Design_inspection` | Idle 3D solids: selection filter, materials, booleans, transforms | +| Sketch inspection mode | `Mode::Sketch_inspection` | Idle 2D sketch: show sketch, ortho, faint solids; Esc parent for sketch tools | -Esc ladder (see [docs/usage.md](../../docs/usage.md)): sketch tool → sketch inspection → Normal. +Esc ladder (see [docs/usage.md](../../docs/usage.md)): sketch tool → sketch inspection → `Design_inspection`. **Not first-class yet:** Parts, instances, mates, inherited parent transforms. Shape List **groups** are organizational (STEP XCAF hierarchy on import). Move / Rotate / Scale / Align shafts **bake** into leaf BREP — one-shot geometry edits, not assembly placement. Toolbar icons named `Assembly_*.png` are FreeCAD-style assets for those shape tools, not an assembly workbench. @@ -125,7 +125,7 @@ Shared idea: layered contexts with Esc / finish stepping outward. EzyCad’s “ ## Product stance (fixed for this plan) -1. Keep `Mode::Normal` as the 3D idle mode through hierarchy phase 1–2; keep bake Move/Duplicate there until placement lands. +1. Keep `Mode::Design_inspection` as the 3D idle mode through hierarchy phase 1–2; keep bake Move/Duplicate there until placement lands. 2. Introduce `Assembly_inspection` (name TBD in code) only with phase 3+ Part + relative placement; mates/instances can follow. 3. Then: Inspection = make/edit geometry; Assembly inspection = arrange Parts; Sketch inspection stays under Part. 4. No v1 Assembly inspection mode. diff --git a/docs/scripting.md b/docs/scripting.md index bbaeb1cb..246f541b 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -44,15 +44,15 @@ ezy.Shp ### `ezy` -| Method | Purpose | -| ------------------------------- | ----------------------------------------------- | -| `ezy.log(msg)` | Print to the console and the main **Log** pane | -| `ezy.msg(text [, kind])` | Status toast and Log; optional kind (see below) | -| `ezy.get_mode()` | Current application mode name (string) | -| `ezy.set_mode(name)` | Switch mode by name | -| `ezy.save_occt_view_settings()` | Write settings JSON (including view colors) | -| `ezy.occt_view_settings_json()` | Settings JSON string | -| `ezy.help()` | Print binding summary | +| Method | Purpose | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ezy.log(msg)` | Print to the console and the main **Log** pane | +| `ezy.msg(text [, kind])` | Status toast and Log; optional kind (see below) | +| `ezy.get_mode()` | Current application mode name (string) | +| `ezy.set_mode(name)` | Switch mode by name (`Design_inspection`, `Sketch_inspection`, `Workbench_move`, `Scale`, ...). Retired names `Normal` / `Sketch_inspection_mode` / `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align` still work. | +| `ezy.save_occt_view_settings()` | Write settings JSON (including view colors) | +| `ezy.occt_view_settings_json()` | Settings JSON string | +| `ezy.help()` | Print binding summary | Optional `kind` for `ezy.msg` is `success`, `info` (default), `constraint`, `warning`, or `error`. Unknown names use info. Toast color follows the kind. diff --git a/docs/usage-settings.md b/docs/usage-settings.md index f6fb8580..3d5fe7b0 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -101,7 +101,7 @@ If you resize the pane narrower than its controls, a **horizontal scrollbar** ap Under **Selection**: -- **Selection Mode** — combo for the 3D pick filter (vertices, edges, faces, solids, and combinations). The **?** button links to [shape selection filter hotkeys](usage.md#shape-selection-filter-normal-mode-only) in the usage guide. +- **Selection Mode** — combo for the 3D pick filter (vertices, edges, faces, solids, and combinations). The **?** button links to [shape selection filter hotkeys](usage.md#shape-selection-filter-inspection-and-workbench) in the usage guide. The same digits apply in **Workbench** idle. - **Orthographic projection** — checkbox (appears in the Options panel for all non-sketch modes) toggling an orthographic camera (sketch modes still force orthographic). Persisted as **`gui.inspection_orthographic`**. Under **Material**: diff --git a/docs/usage.md b/docs/usage.md index 5765ba5d..fd53e9b6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -48,9 +48,9 @@ EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to d - **Help** - [About](#help-menu), [Usage Guide](#help-menu), and the separate **[Settings guide](usage-settings.md)** 2. **Toolbar** - - Quick access to commonly used tools - - Mode selection buttons - - Operation tools + - **Task** buttons (Sketch, Design, Workbench) pick the work context and enter that task's idle / inspection mode. + - The **tools** row shows only the modes and commands for the current task (unrelated tools are hidden). + - Hotkeys still work from any task: G / R / J switch to Workbench and enter Move / Rotate / Align shafts; S switches to Design and enters Scale; sketch letters switch to Sketch. 3. **Sketch List** - [View and manage 2D sketches](#sketch-list) @@ -221,9 +221,10 @@ Press Esc to cancel the current action or step back to a broader mode - **If something is in progress:** Esc cancels it and discards the change. Examples: cancel a line you are drawing, revert an unconfirmed [move](#shape-move-tool-g)/[rotate](#shape-rotate-tool-r)/[scale](#shape-scale-tool-s), cancel [extrude](#extrude-sketch-face-tool-e) preview, clear the distance or angle input dialog. - **If nothing is in progress:** Esc steps the application to the **parent mode** (one level up): - From a **sketch tool** (e.g. Add line, Add circle, Operation axis) -> **Sketch inspection mode**. - - From **Sketch inspection**, **Normal**, or any **shape tool** ([Move](#shape-move-tool-g), [Rotate](#shape-rotate-tool-r), [Scale](#shape-scale-tool-s), [Extrude](#extrude-sketch-face-tool-e), [Chamfer](#other-feature-operations) (C), [Fillet](#other-feature-operations) (F), [Polar duplicate](#shape-polar-duplicate-tool), [Create sketch from face](usage-sketch.md#create-sketch-from-planar-face-tool)) -> **Normal** (inspection) mode. + - From **Sketch inspection**, **Design** tools ([Extrude](#extrude-sketch-face-tool-e), [Scale](#shape-scale-tool-s), [Chamfer](#other-feature-operations) (C), [Fillet](#other-feature-operations) (F), [Polar duplicate](#shape-polar-duplicate-tool), [Create sketch from face](usage-sketch.md#create-sketch-from-planar-face-tool)), or Design **Inspection** -> **Design inspection**. + - From a **Workbench** tool ([Move](#shape-move-tool-g), [Rotate](#shape-rotate-tool-r), [Align shafts](#align-shafts-tool-j)) -> **Workbench** idle (not Design inspection). Use the task buttons to jump between Sketch, Design, and Workbench. -So repeated Esc from a sketch drawing tool first cancels the current element, then returns to Sketch inspection, then to Normal. +So repeated Esc from a sketch drawing tool first cancels the current element, then returns to Sketch inspection, then to Design inspection. Workbench tools step back to Workbench idle. ## Modeling Tools @@ -474,7 +475,7 @@ The shape scale tool allows you to uniformly scale selected shapes around a pivo **How to Use:** 1. **Select shapes:** Select one or more shapes in the 3D view or Shape List. -2. ![Shape Scale Tool](res/icons/Part_Scale.png) **Activate Scale Tool:** Click the *Shape scale* icon in the toolbar (or choose Scale from the Edit/Transform area if present). +2. ![Shape Scale Tool](res/icons/Part_Scale.png) **Activate Scale Tool:** Click the *Shape scale* icon on the **Design** tools toolbar (or press S). 3. **Choose space (Optional):** Options **Local** (default) scales about the first selected solid's frame origin. **World** uses that solid's bounding-box center. 4. **Move the mouse:** - The tool uses that pivot and a view-aligned plane. @@ -674,7 +675,7 @@ Boolean operations (also called CSG or boolean tools) combine or modify 3D solid ![Shape Common](res/icons/Part_Common.png) **Common** — keep only the overlapping volume (intersection) of the selected bodies. -These tools are in the main toolbar (after the polar duplicate button). They are **immediate commands**, not persistent modes like Move (G) or Extrude (E). +These tools are on the **Design** tools toolbar (after polar duplicate). They are **immediate commands**, not persistent modes like Move (G) or Extrude (E). **Features:** @@ -727,7 +728,7 @@ If fewer than two shapes are selected you will see an error message and nothing **Keyboard notes:** -There are default chords for the boolean tools (Ctrl+Shift+C cut, Ctrl+Shift+F fuse, Ctrl+Shift+M common; remappable in **Settings -> Keyboard shortcuts**). You can also activate them from the toolbar after multi-selecting in Normal mode. General selection and view hotkeys still apply. +There are default chords for the boolean tools (Ctrl+Shift+C cut, Ctrl+Shift+F fuse, Ctrl+Shift+M common; remappable in **Settings -> Keyboard shortcuts**). You can also activate them from the Design tools toolbar after multi-selecting in Inspection mode. General selection and view hotkeys still apply. For more on 3D solids and the viewer, see [3D viewer (Open CASCADE)](usage-occt-view.md). @@ -754,35 +755,35 @@ Mode, file, and edit chords in the **General Operations** and **Modeling Shortcu ### Modeling Shortcuts -| | | -| --------------------------------------------: | -------------------------- | -| G | Move mode | -| R | Rotate mode | -| S | Scale mode | -| J | Align shafts mode | -| E | Extrude mode | -| C | Chamfer mode | -| F | Fillet mode | -| D | Dimension tool (sketch) | -| I | Sketch inspection | -| P | Sketch from planar face | -| Shift+A | Operation axis | -| N | Add node | -| L | Add line edge | -| Shift+L | Add multi-line edge | -| A | Add arc | -| Q | Add square | -| B | Add rectangle (two points) | -| Shift+B | Add rectangle (center) | -| O | Add circle | -| Shift+O | Add circle (three points) | -| U | Add slot | -| Shift+U | Add bone | -| Shift+P | Polar duplicate | -| Shift+X | Cross-section | -| Ctrl+Shift+C | Shape cut | -| Ctrl+Shift+F | Shape fuse | -| Ctrl+Shift+M | Shape common | +| | | +| --------------------------------------------: | ----------------------------------------- | +| G | Move mode (switches to Workbench) | +| R | Rotate mode (switches to Workbench) | +| S | Scale mode (switches to Design) | +| J | Align shafts mode (switches to Workbench) | +| E | Extrude mode | +| C | Chamfer mode | +| F | Fillet mode | +| D | Dimension tool (sketch) | +| I | Sketch inspection | +| P | Sketch from planar face | +| Shift+A | Operation axis | +| N | Add node | +| L | Add line edge | +| Shift+L | Add multi-line edge | +| A | Add arc | +| Q | Add square | +| B | Add rectangle (two points) | +| Shift+B | Add rectangle (center) | +| O | Add circle | +| Shift+O | Add circle (three points) | +| U | Add slot | +| Shift+U | Add bone | +| Shift+P | Polar duplicate | +| Shift+X | Cross-section | +| Ctrl+Shift+C | Shape cut | +| Ctrl+Shift+F | Shape fuse | +| Ctrl+Shift+M | Shape common | ### View navigation @@ -802,11 +803,11 @@ Mode, file, and edit chords in the **General Operations** and **Modeling Shortcu **Num Lock (numeric keypad):** **Num Lock off** is what we test against and recommend. The shortcuts below assume the keypad produces **NumPad** key codes (orbit, axis snap, zoom, roll, and keypad selection digits). With **Num Lock on**, Windows and other systems often remap the keypad (digits vs arrow/Home/End behavior), so numpad shortcuts may not match this document. Use the alternatives in the table (main-row 4 / 6, Shift+Left / Right, main + / -, main 1-9 for selection) or turn **Num Lock off**. -Same idea as Blender **View Roll** for Shift+NumPad 4 / NumPad 6, Shift+4 / 6, or Shift+Left / Right. Plain NumPad 8 / NumPad 2 / NumPad 4 / NumPad 6 (no modifiers) **orbit** instead of setting the [selection filter](#shape-selection-filter-normal-mode-only); use the main keyboard **4** / **6** / **2** / **8** for Shell, Wire, CompSolid, or Vertex in **Normal** mode. **NumPad 5** is reserved for axis snap (not the Face filter); use main keyboard **5** for Face in **Normal** mode. +Same idea as Blender **View Roll** for Shift+NumPad 4 / NumPad 6, Shift+4 / 6, or Shift+Left / Right. Plain NumPad 8 / NumPad 2 / NumPad 4 / NumPad 6 (no modifiers) **orbit** instead of setting the [selection filter](#shape-selection-filter-inspection-and-workbench); use the main keyboard **4** / **6** / **2** / **8** for Shell, Wire, CompSolid, or Vertex in **Inspection** or **Workbench**. **NumPad 5** is reserved for axis snap (not the Face filter); use main keyboard **5** for Face in those modes. -### Shape selection filter (Normal mode only) +### Shape selection filter (Inspection and Workbench) -In **Normal** mode, number keys set the **Selection Mode** filter for picking 3D shapes (same control as **Options -> Selection Mode**). Main keyboard **1-9** and keypad **1-9** are supported, except **keypad 5** and **keypad 2**, **4**, **6**, **8** (see [View navigation](#view-navigation)). The key order matches the list in the **Selection Mode** control (from compound down to whole shape): +In **Design Inspection** and **Workbench** idle, number keys set the **Selection Mode** filter for picking 3D shapes (same control as **Options -> Selection Mode**). Main keyboard **1-9** and keypad **1-9** are supported, except **keypad 5** and **keypad 2**, **4**, **6**, **8** (see [View navigation](#view-navigation)). The key order matches the list in the **Selection Mode** control (from compound down to whole shape): | Key | Filter | | -----------: | --------- | @@ -913,16 +914,24 @@ Contributors should follow **[ezycad_code_style.md](ezycad_code_style.md)** for ## Tool Icons -### Basic Operations +### Tasks +- ![Workbench_Sketcher](res/icons/Workbench_Sketcher.png) - Sketch task +- ![User](res/icons/User.png) - Design task +- ![Workbench_Assembly](res/icons/Workbench_Assembly.png) - Workbench task + +### Design - ![User](res/icons/User.png) - Inspection mode +- ![Macro_FaceToSketch_48](res/icons/Macro_FaceToSketch_48.png) - Create sketch from planar face +- ![Part_Scale](res/icons/Part_Scale.png) - Shape scale (S) + +### Workbench +- ![Workbench_Assembly](res/icons/Workbench_Assembly.png) - Workbench idle - ![Assembly_AxialMove](res/icons/Assembly_AxialMove.png) - Shape move (G) - ![Draft_Rotate](res/icons/Draft_Rotate.png) - Shape rotate (R) -- ![Part_Scale](res/icons/Part_Scale.png) - Shape scale (S) - ![Assembly_Move](res/icons/Assembly_Move.png) - Align shafts (J) ### Sketch Tools - ![Workbench_Sketcher_none](res/icons/Workbench_Sketcher_none.png) - Sketch inspection mode -- ![Macro_FaceToSketch_48](res/icons/Macro_FaceToSketch_48.png) - Create sketch from planar face - ![Sketcher_MirrorSketch](res/icons/Sketcher_MirrorSketch.png) - Operational axis (then use Mirror/Revolve buttons + angle field in the Options panel; Clear axis) - ![Sketcher_CreatePoint](res/icons/Sketcher_CreatePoint.png) - Add node - ![Sketcher_Element_Line_Edge](res/icons/Sketcher_Element_Line_Edge.png) - Add line edge diff --git a/src/doc/gui.md b/src/doc/gui.md index a2024c12..43ad87e5 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -10,8 +10,8 @@ Maintainers: update this file when GUI input routing, mode/options behavior, set 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. +- ImGui frame: menu bar, dock space (passthrough central node for 3D input), task+tools toolbar, Sketch List, Shape List, Options, Settings, dist/angle popups. +- Mode switching (`Mode` enum in [`mode.h`](../mode.h)), **task** (`Task`: Sketch / Design / Workbench), and parent-mode Esc behavior. - Persisted preferences (`ezycad_settings.json` via [`gui_settings.cpp`](../gui_settings.cpp)). - Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** confirms STEP/PLY with **Import as** for hierarchy / flat / union). STEP **Import into project** 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**. @@ -40,11 +40,17 @@ Typical responsibilities: | 3 | `sync_sketch_add_mid_pt_edges_if_applicable_()` | | 4 | Update toolbar active state | -`set_parent_mode()` maps each tool mode back to `Normal` or `Sketch_inspection_mode` via `GUI::parent_mode_of` (see parent map in `gui_mode.cpp`). Undo/redo uses the same map so stored `Move` / `Rotate` / `Scale` / `Shape_shaft_align` / `Shape_set_frame` restore their parent instead of re-entering a free-drag or face-pick tool (see [undo-redo.md](undo-redo.md#mode-restoration)). +`set_parent_mode()` maps each tool mode via `GUI::parent_mode_of` (see parent map in `gui_mode.cpp`): sketch drawing tools -> `Sketch_inspection`; Design tools (including `Scale`) -> `Design_inspection`; Workbench Move / Rotate / Align shafts -> `Workbench_inspection`. Undo/redo uses the same map so stored `Workbench_move` / `Workbench_rotate` / `Scale` / `Workbench_shaft_align` / `Shape_set_frame` restore their parent instead of re-entering a free-drag or face-pick tool (see [undo-redo.md](undo-redo.md#mode-restoration)). `Workbench_inspection` parents to itself (Esc does not jump to `Design_inspection`). + +### Tasks / toolbar (filter) + +The chrome toolbar is one ImGui window with two regions: **task** switcher (Sketch / Design / Workbench) then a `|` then **tools** for `task_of(get_mode())` only (option 1: hide unrelated modes). Clicking a task button calls `set_mode(idle_mode_of(task))`. Hotkeys still call `set_mode` on the tool; that updates the active task so the tools row follows (G/R/J from Sketch/Design enter Workbench; S enters Design Scale). Helpers: `task_of`, `idle_mode_of`, `is_workbench_mode`, `is_workbench_transform_mode`, `is_shape_browse_mode` in [`mode.h`](../mode.h). + +`Mode::Design_inspection`, `Mode::Workbench_inspection`, and the three task buttons are toolbar-only (no remappable `Gui_action`). Transform tools keep `mode.move` / `mode.rotate` / `mode.scale` / `mode.cyl_align`. Retired script names `Normal` / `Sketch_inspection_mode` / `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align` still parse via `mode_from_string`. ### New mode or toolbar command (hotkeys) -When adding a `Mode` to [`mode.h`](../mode.h) (`EZY_MODE_LIST`), a toolbar button, or a one-shot `Command`, update remappable hotkeys in the **same change**. Skip only for modes that must stay toolbar-only (document that choice). `Mode::Shape_set_frame` is Shape List-only (no toolbar button / remappable hotkey). +When adding a `Mode` to [`mode.h`](../mode.h) (`EZY_MODE_LIST`), a toolbar button, or a one-shot `Command`, update remappable hotkeys in the **same change**. Skip only for modes that must stay toolbar-only (document that choice). `Mode::Shape_set_frame` is Shape List-only (no toolbar button / remappable hotkey). `Mode::Design_inspection`, `Mode::Workbench_inspection`, and the Sketch / Design / Workbench task buttons are toolbar-only (no remappable hotkey). | Step | Touch | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -56,9 +62,9 @@ When adding a `Mode` to [`mode.h`](../mode.h) (`EZY_MODE_LIST`), a toolbar butto Pick a default that does not collide with existing `c_actions` chords or fixed keys (Esc, Enter, Tab, digits, unmodified X/Y/Z axis toggles — reserved via `is_reserved_chord`). Also wire parent-mode / Options / doc URL maps as usual for new modes. -`Occt_view::on_mode` also sets `AIS_ViewController::SetAllowHighlight(false)` for `Move` / `Rotate` / `Scale` (and `ClearDetected`) so idle mouse moves do not run dynamic `MoveTo` while transform preview leaves selection BVHs at the pre-transform pose; other modes restore highlight. Orbit/pan still receive `UpdateMousePosition` when buttons are held. When LMB finalizes an active transform (operands loaded), `on_mouse_button` skips `PressMouseButton` / `ReleaseMouseButton` for that click so AIS `SelectDetected` on release cannot replace the restored multi-selection with the single shape under the cursor. +`Occt_view::on_mode` also sets `AIS_ViewController::SetAllowHighlight(false)` for `Workbench_move` / `Workbench_rotate` / `Scale` (and `ClearDetected`) so idle mouse moves do not run dynamic `MoveTo` while transform preview leaves selection BVHs at the pre-transform pose; other modes restore highlight. Orbit/pan still receive `UpdateMousePosition` when buttons are held. When LMB finalizes an active transform (operands loaded), `on_mouse_button` skips `PressMouseButton` / `ReleaseMouseButton` for that click so AIS `SelectDetected` on release cannot replace the restored multi-selection with the single shape under the cursor. -Because the faint/selection-mode redisplay `Erase`s shapes (dropping the AIS selection), `on_mode` snapshots the entered selection for `Move` / `Rotate` / `Scale` / `Shape_cross_section` and restores it via `Occt_view::set_selected_shps` after `sync_sketch_shape_faint_style()`. The same helper restores the operands when a transform tool finishes (see `restore_operation_selection_` in [shape.md](shape.md)); `Occt_view::cancel` therefore treats `Move` / `Rotate` / `Scale` as already handled instead of switching mode again. Transform tools are also seeded via `Shp_move` / `Shp_rotate` / `Shp_scale::begin(enter_selection)` so multi-select operands do not depend on AIS selection surviving the mode switch (`ensure_operation_shps_` still falls back to the AIS selection when the seed was empty). Move/Rotate/Scale use `TopAbs_SHAPE` (whole-object) selection mode. +Because the faint/selection-mode redisplay `Erase`s shapes (dropping the AIS selection), `on_mode` snapshots the entered selection for `Workbench_move` / `Workbench_rotate` / `Scale` / `Shape_cross_section` and restores it via `Occt_view::set_selected_shps` after `sync_sketch_shape_faint_style()`. The same helper restores the operands when a transform tool finishes (see `restore_operation_selection_` in [shape.md](shape.md)); `Occt_view::cancel` therefore treats those transform modes as already handled instead of switching mode again. Transform tools are also seeded via `Shp_move` / `Shp_rotate` / `Shp_scale::begin(enter_selection)` so multi-select operands do not depend on AIS selection surviving the mode switch (`ensure_operation_shps_` still falls back to the AIS selection when the seed was empty). Move/Rotate/Scale use `TopAbs_SHAPE` (whole-object) selection mode. `Occt_view::on_mode` ends with `sync_sketch_shape_faint_style()`: while `is_sketch_mode`, document shapes follow **Options/Settings** `gui.sketch_shape_faint_enabled` (master) plus `gui.sketch_shape_faint_style` (**0** hide / **1** ghost / **2** wire) and `gui.sketch_shape_faint_opacity` (ghost). Outside sketch mode, faint overrides clear and shapes show at full strength (unless Shape List **Hide all**). New shapes call the same sync from `add_shp_` / `insert_shape_rec`. The **Faint shapes** checkbox lives in `options_sketch_common_` (every sketch tool). @@ -78,7 +84,7 @@ Constants and ranges live in `gui.h` (`k_gui_ui_verbosity_*`, dimension defaults ### Dist / angle edit popups -Tab and Shift+Tab in the 3D view open numeric entry via `GUI::set_dist_edit` / `set_angle_edit` (sketch / extrude). Move, Rotate, and Align shafts (`Shape_shaft_align`) skip that path and handle Tab in their mode key handlers (`show_dist_edit` / `show_angle_edit` / depth and twist edits). The angle popup shows a `deg` suffix. While active (`is_dist_or_angle_edit_active()`), keys route to `on_key()` instead of ImGui text fields (`main` checks this). +Tab and Shift+Tab in the 3D view open numeric entry via `GUI::set_dist_edit` / `set_angle_edit` (sketch / extrude). Move, Rotate, and Align shafts (`Workbench_shaft_align`) skip that path and handle Tab in their mode key handlers (`show_dist_edit` / `show_angle_edit` / depth and twist edits). The angle popup shows a `deg` suffix. While active (`is_dist_or_angle_edit_active()`), keys route to `on_key()` instead of ImGui text fields (`main` checks this). ## Architecture @@ -152,24 +158,24 @@ Overlay popups (`FloatEdit`, `AngleEdit`, `MessageStatus`, modals) keep `NoSaved Remappable chords live in `Gui_hotkeys` (`gui_hotkeys.h` / `.cpp`), owned by `GUI::m_hotkeys`. Stable action ids (e.g. `mode.move`, `mode.add_edge`, `cmd.shape_cut`, `edit.undo`) map to `Key_chord { key, mods }`. Persistence: `gui.hotkeys` in `ezycad_settings.json` as human-readable strings (`"G"`, `"Shift+L"`, `"Ctrl+Shift+C"`); missing keys merge to built-in defaults. On load, `merge_from_json` drops reserved/invalid chords, then resolves duplicate chords: later actions reset to factory; if that factory chord is still held by an earlier remap, that earlier row is also restored to factory (defaults are unique, so `action_for` never keeps a silent collision). Settings **Keyboard shortcuts** captures the next `GLFW_PRESS` (Esc cancels; `set_chord` rejects conflicts and **reserved** fixed chords via `is_reserved_chord`). Per-row **Reset** calls `reset_action` (factory chord via `set_chord`, so duplicates are rejected with the same inline conflict message). Capture is cleared when Settings closes. Toolbar tooltips for remappable modes and boolean commands are rebuilt via `sync_toolbar_hotkey_tooltips_()`. -| Input | Condition | Handler | -| --------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `+` / `-` / numpad +/- | No Ctrl/Alt | `Occt_view::zoom_view_wheel_notches` | -| Shift + 4/6 / arrows / numpad 4/6 | No Ctrl/Alt | `Occt_view::roll_view_z_deg` | -| Numpad 5 | No modifiers | `Occt_view::snap_view_to_nearest_standard_axis` | -| Numpad 2/4/6/8 | No modifiers | `Occt_view::orbit_view_screen_step_deg` | -| Hotkey capture active | Settings | `try_capture_hotkey_press_` (assign / Esc cancel / conflict message) | -| `1`-`9` / numpad `1`-`9` | `Mode::Normal` only | `set_shp_selection_mode` (TopAbs enum index); fixed | -| Esc | | cancel capture if listening; else `cancel_underlay_calib_`, `Occt_view::cancel`, hide dist/angle edit | -| Tab | not Move/Rotate/Align shafts | `Occt_view::dimension_input`; those modes: `break` into mode handlers | -| Shift+Tab | not Move/Rotate/Align shafts | `Occt_view::angle_input`; those modes: `break` into mode handlers (cyl-align: clock/angle) | -| Enter | not Rotate/Align shafts | hide edits, `Occt_view::on_enter`; those modes: `break` into mode handlers (finalize) | -| Delete / Backspace | | `Occt_view::delete_selected` (fixed aliases; remapping `edit.delete` does not remove these) | -| Ctrl+Shift+Z | | `Occt_view::redo` (fixed second redo; remappable `edit.redo` defaults to Ctrl+Y) | -| Remappable chord | `m_hotkeys` hit | `dispatch_hotkey_action_` (`Gui_action`: sketch/shape modes, booleans, delete, copy/paste, file, undo/redo) | -| Move-mode keys | `Mode::Move` | `on_key_move_mode_` (axis constraints X/Y/Z); hardcoded | -| Rotate-mode keys | `Mode::Rotate` | `on_key_rotate_mode_` (axis pick, Tab angle); hardcoded | -| Align-shafts keys | `Mode::Shape_shaft_align` | `on_key_cyl_align_mode_` (Tab depth, Shift+Tab clock/angle, Enter finalize); hardcoded | +| Input | Condition | Handler | +| --------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `+` / `-` / numpad +/- | No Ctrl/Alt | `Occt_view::zoom_view_wheel_notches` | +| Shift + 4/6 / arrows / numpad 4/6 | No Ctrl/Alt | `Occt_view::roll_view_z_deg` | +| Numpad 5 | No modifiers | `Occt_view::snap_view_to_nearest_standard_axis` | +| Numpad 2/4/6/8 | No modifiers | `Occt_view::orbit_view_screen_step_deg` | +| Hotkey capture active | Settings | `try_capture_hotkey_press_` (assign / Esc cancel / conflict message) | +| `1`-`9` / numpad `1`-`9` | `is_shape_browse_mode` | `set_shp_selection_mode` (TopAbs enum index); fixed; Design Inspection + Workbench idle | +| Esc | | cancel capture if listening; else `cancel_underlay_calib_`, `Occt_view::cancel`, hide dist/angle edit | +| Tab | not Workbench move/rotate/align | `Occt_view::dimension_input`; those modes: `break` into mode handlers | +| Shift+Tab | not Workbench move/rotate/align | `Occt_view::angle_input`; those modes: `break` into mode handlers (cyl-align: clock/angle) | +| Enter | not Workbench rotate/align | hide edits, `Occt_view::on_enter`; those modes: `break` into mode handlers (finalize) | +| Delete / Backspace | | `Occt_view::delete_selected` (fixed aliases; remapping `edit.delete` does not remove these) | +| Ctrl+Shift+Z | | `Occt_view::redo` (fixed second redo; remappable `edit.redo` defaults to Ctrl+Y) | +| Remappable chord | `m_hotkeys` hit | `dispatch_hotkey_action_` (`Gui_action`: sketch/shape modes, booleans, delete, copy/paste, file, undo/redo) | +| Move-mode keys | `Mode::Workbench_move` | `on_key_move_mode_` (axis constraints X/Y/Z); hardcoded | +| Rotate-mode keys | `Mode::Workbench_rotate` | `on_key_rotate_mode_` (axis pick, Tab angle); hardcoded | +| Align-shafts keys | `Mode::Workbench_shaft_align` | `on_key_cyl_align_mode_` (Tab depth, Shift+Tab clock/angle, Enter finalize); hardcoded | Default remappable chords include G/R/S/J/E/C/F/D shape tools; sketch tools N/L/A/Q/B/O/U/I/P and Shift variants; Shift+P polar, Shift+X cross-section; Ctrl+Shift+C/F/M booleans; Shift+D delete; Ctrl+C / Ctrl+V copy/paste (in-app shape clipboard); Ctrl+N/O/S; Ctrl+Z / Ctrl+Y. Unmodified X/Y/Z are reserved for Move/Rotate axis toggles (`is_reserved_chord`); Shift+X remains free for cross-section. Remappable keys must pass `is_bindable_key` (letters, digits, Space, and named keys that round-trip in settings JSON); punctuation such as `,` / `.` and numpad keys are rejected. Settings **Keyboard shortcuts** has a `?` to `doc_urls::k_hotkeys` ([usage-settings.md#keyboard-shortcuts](../../docs/usage-settings.md#keyboard-shortcuts)). @@ -181,10 +187,10 @@ See also [`src/doc/sketch.md`](sketch.md) and [`src/doc/shape.md`](shape.md) for | `Mode` | Delegate | | --------------------------------------------------------- | -------------------------------------------------------------------- | -| `Move` | `shp_move().move_selected` | -| `Rotate` | `shp_rotate().rotate_selected` | +| `Workbench_move` | `shp_move().move_selected` | +| `Workbench_rotate` | `shp_rotate().rotate_selected` | | `Scale` | `shp_scale().scale_selected` | -| `Shape_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | +| `Workbench_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | | `Shape_set_frame` | `options_shape_set_frame_mode_` (Shape List only; no toolbar/hotkey) | | `Shape_polar_duplicate` | `shp_polar_dup().move_point` | | Sketch tool modes (line, arc, rect, dim, axis, bone, ...) | `curr_sketch().sketch_pt_move` | @@ -214,19 +220,19 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio `GUI::options_()` switches on `get_mode()`: -| `Mode` | Options function | -| -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `Normal` | `options_normal_mode_` (selection filter, orthographic) | -| `Move` / `Rotate` / `Scale` | `options_*_mode_` (Local/World space, constraints, axis) | -| `Shape_shaft_align` | `options_shape_shaft_align_mode_` (Flip direction, Clock rotation; pick / depth / clock help) | -| `Shape_chamfer` / `Shape_fillet` | mode + radius/distance | -| `Shape_polar_duplicate` | angle, count, rotate/combine, **Dup** button | -| `Shape_cross_section` | local XY/XZ/YZ, invert normal, hide back side, show section outline, bbox-ranged offset, Clip, Cross section sketch | -| `Sketch_inspection_mode` | `options_sketch_common_` | -| Each sketch tool mode | Matching `options_sketch_*_mode_` | -| `Sketch_operation_axis` | Mirror / Revolve / Clear axis | -| `Sketch_face_extrude` | **Extrude** (Both sides, Twist, material) above Sketch options; help mentions Settings fast preview | -| `Sketch_add_bone` | Center / radius / total-length nodes, holes; None after waist commits; `DEV_MODE` **Debug vis** | +| `Mode` | Options function | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `Design_inspection` / `Workbench_inspection` | `options_normal_mode_` (selection filter, orthographic) | +| `Workbench_move` / `Workbench_rotate` / `Scale` | `options_*_mode_` (Local/World space, constraints, axis) | +| `Workbench_shaft_align` | `options_shape_shaft_align_mode_` (Flip direction, Clock rotation; pick / depth / clock help) | +| `Shape_chamfer` / `Shape_fillet` | mode + radius/distance | +| `Shape_polar_duplicate` | angle, count, rotate/combine, **Dup** button | +| `Shape_cross_section` | local XY/XZ/YZ, invert normal, hide back side, show section outline, bbox-ranged offset, Clip, Cross section sketch | +| `Sketch_inspection` | `options_sketch_common_` | +| Each sketch tool mode | Matching `options_sketch_*_mode_` | +| `Sketch_operation_axis` | Mirror / Revolve / Clear axis | +| `Sketch_face_extrude` | **Extrude** (Both sides, Twist, material) above Sketch options; help mentions Settings fast preview | +| `Sketch_add_bone` | Center / radius / total-length nodes, holes; None after waist commits; `DEV_MODE` **Debug vis** | ### Options panel layout (sketch tools) diff --git a/src/doc/script.md b/src/doc/script.md index 0e1b872f..2bd9424b 100644 --- a/src/doc/script.md +++ b/src/doc/script.md @@ -106,15 +106,15 @@ Aliases: global `view` == `ezy.view`; `Shp` == `ezy.Shp`; `view.add_sketch` / `f ### `ezy` (root) -| Method | C++ delegate | -| ------------------------------- | -------------------------------------- | -| `ezy.log(msg)` | `GUI::log_message` + console history | -| `ezy.msg(text [, kind])` | `GUI::show_message` (toast + Log) | -| `ezy.get_mode()` | `GUI::get_mode()` -> mode name string | -| `ezy.set_mode(name)` | `GUI::set_mode(mode_from_string(...))` | -| `ezy.save_occt_view_settings()` | `GUI::save_occt_view_settings()` | -| `ezy.occt_view_settings_json()` | `GUI::occt_view_settings_json()` | -| `ezy.help()` / global `help()` | Prints binding summary to console | +| Method | C++ delegate | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `ezy.log(msg)` | `GUI::log_message` + console history | +| `ezy.msg(text [, kind])` | `GUI::show_message` (toast + Log) | +| `ezy.get_mode()` | `GUI::get_mode()` -> mode name string (`c_mode_strs`; Workbench tools are `Workbench_move` etc.; Scale is `Scale`) | +| `ezy.set_mode(name)` | `GUI::set_mode(mode_from_string(...))` (aliases: `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align`) | +| `ezy.save_occt_view_settings()` | `GUI::save_occt_view_settings()` | +| `ezy.occt_view_settings_json()` | `GUI::occt_view_settings_json()` | +| `ezy.help()` / global `help()` | Prints binding summary to console | Optional `kind` for `ezy.msg` is `success`, `info` (default), `constraint`, `warning`, or `error` (`parse_status_msg`). diff --git a/src/doc/shape.md b/src/doc/shape.md index 5cb16ca6..43d81237 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -167,21 +167,21 @@ Protected helpers used by all operation classes: `GUI` and `Occt_view` dispatch by `Mode` and toolbar actions: -| Mode / action | Mouse move (`GUI::on_mouse_pos`) | Left click | Tab / Enter | Esc (`Occt_view::cancel`) | -| ----------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------- | -| `Mode::Move` | `shp_move().move_selected` | `shp_move().finalize` | `shp_move().show_dist_edit` (`gui_mode`) | `shp_move().cancel` -> `Normal` | -| `Mode::Rotate` | `shp_rotate().rotate_selected` | `shp_rotate().finalize` | `shp_rotate().show_angle_edit` | `shp_rotate().cancel` -> `Normal` | -| `Mode::Scale` | `shp_scale().scale_selected` | `shp_scale().finalize` | -- | `shp_scale().cancel` -> `Normal` | -| `Mode::Shape_shaft_align` | `drag_depth` / `drag_twist` | Pick faces; LMB depth->clock if Clock rotation on; LMB/Enter finalize | Tab depth; Shift+Tab clock/angle; Enter finalize | `shp_cyl_align().cancel` -> `Normal` | -| `Mode::Sketch_face_extrude` | `sketch_face_extrude(..., true)` | Pick face; with preview: `on_left_click()` locks height->twist if Twist on, else `finalize` (`GUI` skips re-pick) | Tab: height (`dimension_input`); Shift+Tab: twist angle in twist phase | `m_shp_extrude.cancel` | -| `Mode::Shape_fillet` | -- | `shp_fillet().add_fillet(..., Fillet_mode)` | -- | -- | -| `Mode::Shape_chamfer` | -- | `shp_chamfer().add_chamfer(..., Chamfer_mode)` | -- | -- | -| `Mode::Shape_polar_duplicate` | `shp_polar_dup().move_point` | `shp_polar_dup().add_point` | -- | `shp_polar_dup().reset` on mode change | -| `Mode::Shape_cross_section` | -- | -- | Auto-preview on enter / selection / Options; Clip replaces solids | Preview cleared; Clip commits | -| Fuse / cut / common (toolbar) | -- | `selected_fuse` / `selected_cut` / `selected_common` (one-shot) | -- | -- | -| Primitives (menu / script) | -- | `Occt_view::add_box`, `add_sphere`, ... | -- | -- | -| Revolve (sketch Options) | -- | `Occt_view::revolve_selected` -> `add_shp_` | -- | -- | -| Polar duplicate commit | -- | Options **Dup** button -> `shp_polar_dup().dup()` | -- | -- | +| Mode / action | Mouse move (`GUI::on_mouse_pos`) | Left click | Tab / Enter | Esc (`Occt_view::cancel`) | +| ----------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------- | +| `Mode::Workbench_move` | `shp_move().move_selected` | `shp_move().finalize` | `shp_move().show_dist_edit` (`gui_mode`) | `shp_move().cancel` -> `Workbench_inspection` | +| `Mode::Workbench_rotate` | `shp_rotate().rotate_selected` | `shp_rotate().finalize` | `shp_rotate().show_angle_edit` | `shp_rotate().cancel` -> `Workbench_inspection` | +| `Mode::Scale` | `shp_scale().scale_selected` | `shp_scale().finalize` | -- | `shp_scale().cancel` -> `Design_inspection` | +| `Mode::Workbench_shaft_align` | `drag_depth` / `drag_twist` | Pick faces; LMB depth->clock if Clock rotation on; LMB/Enter finalize | Tab depth; Shift+Tab clock/angle; Enter finalize | `shp_cyl_align().cancel` -> `Workbench_inspection` | +| `Mode::Sketch_face_extrude` | `sketch_face_extrude(..., true)` | Pick face; with preview: `on_left_click()` locks height->twist if Twist on, else `finalize` (`GUI` skips re-pick) | Tab: height (`dimension_input`); Shift+Tab: twist angle in twist phase | `m_shp_extrude.cancel` | +| `Mode::Shape_fillet` | -- | `shp_fillet().add_fillet(..., Fillet_mode)` | -- | -- | +| `Mode::Shape_chamfer` | -- | `shp_chamfer().add_chamfer(..., Chamfer_mode)` | -- | -- | +| `Mode::Shape_polar_duplicate` | `shp_polar_dup().move_point` | `shp_polar_dup().add_point` | -- | `shp_polar_dup().reset` on mode change | +| `Mode::Shape_cross_section` | -- | -- | Auto-preview on enter / selection / Options; Clip replaces solids | Preview cleared; Clip commits | +| Fuse / cut / common (toolbar) | -- | `selected_fuse` / `selected_cut` / `selected_common` (one-shot) | -- | -- | +| Primitives (menu / script) | -- | `Occt_view::add_box`, `add_sphere`, ... | -- | -- | +| Revolve (sketch Options) | -- | `Occt_view::revolve_selected` -> `add_shp_` | -- | -- | +| Polar duplicate commit | -- | Options **Dup** button -> `shp_polar_dup().dup()` | -- | -- | Pre-selection: entering Move / Rotate / Scale / cross-section snapshots selected solids in `Occt_view::on_mode` and restores AIS selection via `set_selected_shps` after selection-mode / faint redisplay. Transform tools are also seeded with `begin(enter_selection)` so multi-select operands do not depend on AIS alone (`ensure_operation_shps_()` still falls back to the AIS selection when the seed was empty). diff --git a/src/doc/undo-redo.md b/src/doc/undo-redo.md index 897f04fe..50c635e9 100644 --- a/src/doc/undo-redo.md +++ b/src/doc/undo-redo.md @@ -50,7 +50,7 @@ This prevents re-entrant history pushes while applying a step. ### Mode restoration -After applying a delta or reloading a snapshot, `Occt_view` restores a UI mode via `GUI::set_mode`. Stored `Move` / `Rotate` / `Scale` / `Shape_shaft_align` / `Shape_set_frame` are mapped to their parent mode (`GUI::parent_mode_of`, normally `Normal`) so undo/redo does not re-enter a free-drag tool or face-pick with no target. Other stored modes are restored as-is. If the restored mode is `Sketch_inspection_mode`, the sketch list pane is shown again. +After applying a delta or reloading a snapshot, `Occt_view` restores a UI mode via `GUI::set_mode`. Stored `Workbench_move` / `Workbench_rotate` / `Scale` / `Workbench_shaft_align` / `Shape_set_frame` are mapped to their parent mode (`GUI::parent_mode_of`: Workbench idle or `Design_inspection`) so undo/redo does not re-enter a free-drag tool or face-pick with no target. Other stored modes are restored as-is. If the restored mode is `Sketch_inspection`, the sketch list pane is shown again. ### View camera diff --git a/src/gui.cpp b/src/gui.cpp index d9a631f9..5c0db921 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -175,36 +175,48 @@ void GUI::render_occt() { m_view->do_frame(); } // Initialize toolbar buttons void GUI::initialize_toolbar_() { + m_task_buttons = { + // clang-format off + {load_texture("res/icons/Workbench_Sketcher.png"), Task::Sketch, "Sketch"}, + {load_texture("res/icons/User.png"), Task::Design, "Design"}, + {load_texture("res/icons/Workbench_Assembly.png"), Task::Workbench, "Workbench"}, + // clang-format on + }; + m_toolbar_buttons = { // clang-format off - {load_texture("res/icons/User.png"), true, "Inspection mode", Mode::Normal}, - {load_texture("res/icons/Workbench_Sketcher_none.png"), false, "Sketch inspection mode", Mode::Sketch_inspection_mode}, - {load_texture("res/icons/Assembly_AxialMove.png"), false, "Shape move", Mode::Move}, - {load_texture("res/icons/Draft_Rotate.png"), false, "Shape rotate", Mode::Rotate}, - {load_texture("res/icons/Part_Scale.png"), false, "Shape Scale", Mode::Scale}, - {load_texture("res/icons/Assembly_Move.png"), false, "Align shafts", Mode::Shape_shaft_align}, - {load_texture("res/icons/Macro_FaceToSketch_48.png"), false, "Create a sketch from planar face", Mode::Sketch_from_planar_face}, - {load_texture("res/icons/Sketcher_MirrorSketch.png"), false, "Operational axis", Mode::Sketch_operation_axis}, - {load_texture("res/icons/Sketcher_CreatePoint.png"), false, "Add node", Mode::Sketch_add_node}, - {load_texture("res/icons/Sketcher_Element_Line_Edge.png"), false, "Add line edge", Mode::Sketch_add_edge}, - {load_texture("res/icons/ls.png"), false, "Add multi-line edge", Mode::Sketch_add_multi_edges}, - {load_texture("res/icons/Sketcher_Element_Arc_Edge.png"), false, "Add arc circle", Mode::Sketch_add_seg_circle_arc}, - {load_texture("res/icons/Sketcher_CreateSquare.png"), false, "Add square", Mode::Sketch_add_square}, - {load_texture("res/icons/Sketcher_CreateRectangle.png"), false, "Add rectangle from two points", Mode::Sketch_add_rectangle}, - {load_texture("res/icons/Sketcher_CreateRectangle_Center.png"), false, "Add rectangle with center point", Mode::Sketch_add_rectangle_center_pt}, - {load_texture("res/icons/Sketcher_CreateCircle.png"), false, "Add circle", Mode::Sketch_add_circle}, - {load_texture("res/icons/Sketcher_Create3PointCircle.png"), false, "Add circle from three points", Mode::Sketch_add_circle_3_pts}, - {load_texture("res/icons/Sketcher_CreateSlot.png"), false, "Add slot", Mode::Sketch_add_slot}, - {load_texture("res/icons/Sketcher_CreateBone.png"), false, "Add bone", Mode::Sketch_add_bone}, - {load_texture("res/icons/TechDraw_LengthDimension.png"), false, "Length dimension", Mode::Sketch_dim_anno}, - {load_texture("res/icons/Design456_Extrude.png"), false, "Extrude sketch face", Mode::Sketch_face_extrude}, - {load_texture("res/icons/PartDesign_Chamfer.png"), false, "Chamfer", Mode::Shape_chamfer}, - {load_texture("res/icons/PartDesign_Fillet.png"), false, "Fillet", Mode::Shape_fillet}, - {load_texture("res/icons/Draft_PolarArray.png"), false, "Shape polar duplicate", Mode::Shape_polar_duplicate}, - {load_texture("res/icons/Curves_ExtractSubshape.png"), false, "Shape cross-section", Mode::Shape_cross_section}, - {load_texture("res/icons/Part_Cut.png"), false, "Shape cut", Command::Shape_cut}, - {load_texture("res/icons/Part_Fuse.png"), false, "Shape fuse", Command::Shape_fuse}, - {load_texture("res/icons/Part_Common.png"), false, "Shape common", Command::Shape_common}, + // Sketch + {load_texture("res/icons/Workbench_Sketcher_none.png"), false, "Sketch inspection mode", Mode::Sketch_inspection, Task::Sketch}, + {load_texture("res/icons/Sketcher_MirrorSketch.png"), false, "Operational axis", Mode::Sketch_operation_axis, Task::Sketch}, + {load_texture("res/icons/Sketcher_CreatePoint.png"), false, "Add node", Mode::Sketch_add_node, Task::Sketch}, + {load_texture("res/icons/Sketcher_Element_Line_Edge.png"), false, "Add line edge", Mode::Sketch_add_edge, Task::Sketch}, + {load_texture("res/icons/ls.png"), false, "Add multi-line edge", Mode::Sketch_add_multi_edges, Task::Sketch}, + {load_texture("res/icons/Sketcher_Element_Arc_Edge.png"), false, "Add arc circle", Mode::Sketch_add_seg_circle_arc, Task::Sketch}, + {load_texture("res/icons/Sketcher_CreateSquare.png"), false, "Add square", Mode::Sketch_add_square, Task::Sketch}, + {load_texture("res/icons/Sketcher_CreateRectangle.png"), false, "Add rectangle from two points", Mode::Sketch_add_rectangle, Task::Sketch}, + {load_texture("res/icons/Sketcher_CreateRectangle_Center.png"), false, "Add rectangle with center point", Mode::Sketch_add_rectangle_center_pt, Task::Sketch}, + {load_texture("res/icons/Sketcher_CreateCircle.png"), false, "Add circle", Mode::Sketch_add_circle, Task::Sketch}, + {load_texture("res/icons/Sketcher_Create3PointCircle.png"), false, "Add circle from three points", Mode::Sketch_add_circle_3_pts, Task::Sketch}, + {load_texture("res/icons/Sketcher_CreateSlot.png"), false, "Add slot", Mode::Sketch_add_slot, Task::Sketch}, + {load_texture("res/icons/Sketcher_CreateBone.png"), false, "Add bone", Mode::Sketch_add_bone, Task::Sketch}, + {load_texture("res/icons/TechDraw_LengthDimension.png"), false, "Length dimension", Mode::Sketch_dim_anno, Task::Sketch}, + // Design + {load_texture("res/icons/User.png"), true, "Inspection mode", Mode::Design_inspection, Task::Design}, + {load_texture("res/icons/Macro_FaceToSketch_48.png"), false, "Create a sketch from planar face", Mode::Sketch_from_planar_face, Task::Design}, + {load_texture("res/icons/Design456_Extrude.png"), false, "Extrude sketch face", Mode::Sketch_face_extrude, Task::Design}, + {load_texture("res/icons/Part_Scale.png"), false, "Shape Scale", Mode::Scale, Task::Design}, + {load_texture("res/icons/PartDesign_Chamfer.png"), false, "Chamfer", Mode::Shape_chamfer, Task::Design}, + {load_texture("res/icons/PartDesign_Fillet.png"), false, "Fillet", Mode::Shape_fillet, Task::Design}, + {load_texture("res/icons/Draft_PolarArray.png"), false, "Shape polar duplicate", Mode::Shape_polar_duplicate, Task::Design}, + {load_texture("res/icons/Curves_ExtractSubshape.png"), false, "Shape cross-section", Mode::Shape_cross_section, Task::Design}, + {load_texture("res/icons/Part_Cut.png"), false, "Shape cut", Command::Shape_cut, Task::Design}, + {load_texture("res/icons/Part_Fuse.png"), false, "Shape fuse", Command::Shape_fuse, Task::Design}, + {load_texture("res/icons/Part_Common.png"), false, "Shape common", Command::Shape_common, Task::Design}, + // Workbench + {load_texture("res/icons/Workbench_Assembly.png"), false, "Workbench", Mode::Workbench_inspection, Task::Workbench}, + {load_texture("res/icons/Assembly_AxialMove.png"), false, "Shape move", Mode::Workbench_move, Task::Workbench}, + {load_texture("res/icons/Draft_Rotate.png"), false, "Shape rotate", Mode::Workbench_rotate, Task::Workbench}, + {load_texture("res/icons/Assembly_Move.png"), false, "Align shafts", Mode::Workbench_shaft_align, Task::Workbench}, // clang-format on }; @@ -240,15 +252,15 @@ void GUI::sync_toolbar_hotkey_tooltips_() }; // clang-format off - tip_mode(Mode::Move, "Shape move", Gui_action::Mode_move); - tip_mode(Mode::Rotate, "Shape rotate", Gui_action::Mode_rotate); + tip_mode(Mode::Workbench_move, "Shape move", Gui_action::Mode_move); + tip_mode(Mode::Workbench_rotate, "Shape rotate", Gui_action::Mode_rotate); tip_mode(Mode::Scale, "Shape Scale", Gui_action::Mode_scale); - tip_mode(Mode::Shape_shaft_align, "Align shafts", Gui_action::Mode_cyl_align); + tip_mode(Mode::Workbench_shaft_align, "Align shafts", Gui_action::Mode_cyl_align); tip_mode(Mode::Sketch_dim_anno, "Length dimension", Gui_action::Mode_dimension); tip_mode(Mode::Sketch_face_extrude, "Extrude sketch face", Gui_action::Mode_extrude); tip_mode(Mode::Shape_chamfer, "Chamfer", Gui_action::Mode_chamfer); tip_mode(Mode::Shape_fillet, "Fillet", Gui_action::Mode_fillet); - tip_mode(Mode::Sketch_inspection_mode, "Sketch inspection mode", Gui_action::Mode_sketch_inspection); + tip_mode(Mode::Sketch_inspection, "Sketch inspection mode", Gui_action::Mode_sketch_inspection); tip_mode(Mode::Sketch_from_planar_face, "Create a sketch from planar face", Gui_action::Mode_sketch_from_face); tip_mode(Mode::Sketch_operation_axis, "Operational axis", Gui_action::Mode_operation_axis); tip_mode(Mode::Sketch_add_node, "Add node", Gui_action::Mode_add_node); @@ -792,56 +804,89 @@ void GUI::toolbar_() ImGui::Begin("Toolbar", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking); - ImVec2 button_size(32, 32); + const ImVec2 button_size(32, 32); + const Task active_task = task_of(m_mode); - for (int i = 0; i < m_toolbar_buttons.size(); i++) + auto push_active = [](bool active) { - ImGui::PushID(i); + if (!active) + return false; - bool was_active = false; - if (m_toolbar_buttons[i].is_active) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.26f, 0.59f, 0.98f, 1.00f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.59f, 0.98f, 1.00f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.06f, 0.53f, 0.98f, 1.00f)); + return true; + }; + + auto run_command = [this](Command cmd) + { + switch (cmd) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.26f, 0.59f, 0.98f, 1.00f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.26f, 0.59f, 0.98f, 1.00f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.06f, 0.53f, 0.98f, 1.00f)); - was_active = true; + case Command::Shape_cut: + if (Status s = m_view->shp_cut().selected_cut(); !s.is_ok()) + show_status(s); + break; + + case Command::Shape_fuse: + if (Status s = m_view->shp_fuse().selected_fuse(); !s.is_ok()) + show_status(s); + break; + + case Command::Shape_common: + if (Status s = m_view->shp_common().selected_common(); !s.is_ok()) + show_status(s); + break; + + default: + EZY_ASSERT(false); + break; } + }; - // Add a unique string ID (e.g., "button0", "button1", etc.) - char button_id[16]; - snprintf(button_id, sizeof(button_id), "button%d", i); - if (ImGui::ImageButton(button_id, (ImTextureID)(intptr_t)m_toolbar_buttons[i].texture_id, button_size)) - { - if (m_toolbar_buttons[i].data.index() == 1) - switch (std::get(m_toolbar_buttons[i].data)) - { - case Command::Shape_cut: - if (Status s = m_view->shp_cut().selected_cut(); !s.is_ok()) - show_status(s); + int widget_id = 0; + for (const Task_button& tb : m_task_buttons) + { + ImGui::PushID(widget_id++); + const bool was_active = push_active(tb.task == active_task); + char button_id[16]; + snprintf(button_id, sizeof(button_id), "task%d", static_cast(tb.task)); + if (ImGui::ImageButton(button_id, (ImTextureID)(intptr_t)tb.texture_id, button_size)) + set_mode(idle_mode_of(tb.task)); - break; + if (ui_show_help(1) && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", tb.tooltip); - case Command::Shape_fuse: - if (Status s = m_view->shp_fuse().selected_fuse(); !s.is_ok()) - show_status(s); + if (was_active) + ImGui::PopStyleColor(3); - break; + ImGui::SameLine(); + ImGui::PopID(); + } - case Command::Shape_common: - if (Status s = m_view->shp_common().selected_common(); !s.is_ok()) - show_status(s); + ImGui::SameLine(); + ImGui::TextDisabled("|"); + ImGui::SameLine(); - break; + for (int i = 0; i < static_cast(m_toolbar_buttons.size()); ++i) + { + Toolbar_button& b = m_toolbar_buttons[static_cast(i)]; + if (b.task != active_task) + continue; - default: - EZY_ASSERT(false); - } + ImGui::PushID(widget_id++); + const bool was_active = push_active(b.is_active); + char button_id[16]; + snprintf(button_id, sizeof(button_id), "button%d", i); + if (ImGui::ImageButton(button_id, (ImTextureID)(intptr_t)b.texture_id, button_size)) + { + if (b.data.index() == 1) + run_command(std::get(b.data)); else - set_mode(std::get(m_toolbar_buttons[i].data)); + set_mode(std::get(b.data)); } if (ui_show_help(1) && ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", m_toolbar_buttons[i].tooltip.c_str()); + ImGui::SetTooltip("%s", b.tooltip.c_str()); if (was_active) ImGui::PopStyleColor(3); @@ -1462,7 +1507,7 @@ void GUI::sketch_list_() { m_view->set_curr_sketch(sketch); if (!is_sketch_mode(get_mode())) - set_mode(Mode::Sketch_inspection_mode); + set_mode(Mode::Sketch_inspection); } if (ui_show_contextual_help() && ImGui::IsItemHovered()) @@ -1545,7 +1590,7 @@ void GUI::sketch_list_() { m_view->set_curr_sketch(sketch); if (!is_sketch_mode(get_mode())) - set_mode(Mode::Sketch_inspection_mode); + set_mode(Mode::Sketch_inspection); m_sketch_properties_sketch = sketch; m_sketch_properties_open = true; } @@ -3361,13 +3406,13 @@ void GUI::on_mouse_pos(const ScreenCoords& screen_coords) switch (get_mode()) { - case Mode::Move: + case Mode::Workbench_move: if (Status s = m_view->shp_move().move_selected(screen_coords); !s.is_ok()) show_status(s); break; - case Mode::Rotate: + case Mode::Workbench_rotate: if (Status s = m_view->shp_rotate().rotate_selected(screen_coords); !s.is_ok()) show_status(s); @@ -3379,7 +3424,7 @@ void GUI::on_mouse_pos(const ScreenCoords& screen_coords) break; - case Mode::Shape_shaft_align: + case Mode::Workbench_shaft_align: if (m_view->shp_cyl_align().is_twist_phase()) { if (Status s = m_view->shp_cyl_align().drag_twist(screen_coords); !s.is_ok()) @@ -3423,10 +3468,10 @@ void GUI::on_left_click_(const ScreenCoords& screen_coords) switch (m_mode) { // clang-format off - case Mode::Move: m_view->shp_move().finalize(); break; - case Mode::Rotate: m_view->shp_rotate().finalize(); break; + case Mode::Workbench_move: m_view->shp_move().finalize(); break; + case Mode::Workbench_rotate: m_view->shp_rotate().finalize(); break; case Mode::Scale: m_view->shp_scale().finalize(); break; - case Mode::Shape_shaft_align: + case Mode::Workbench_shaft_align: if (m_view->shp_cyl_align().is_dragging()) m_view->shp_cyl_align().on_left_click(); else if (Status s = m_view->shp_cyl_align().pick(screen_coords); !s.is_ok()) @@ -3944,7 +3989,7 @@ void GUI::on_file(const std::string& file_path, const std::string& file_bytes, b apply_sketch_list_ui_from_json_(j); apply_shape_list_ui_from_json_(j); m_last_saved_path = file_path; - Mode opened_mode = Mode::Normal; + Mode opened_mode = Mode::Design_inspection; if (j.contains("mode") && j["mode"].is_number_integer()) { const int idx = j["mode"].get(); diff --git a/src/gui.h b/src/gui.h index abf61c30..1167a529 100644 --- a/src/gui.h +++ b/src/gui.h @@ -247,7 +247,7 @@ inline constexpr const char* k_sketching_2d = "https://ezycad.re inline constexpr const char* k_line_edge_midpoint_nodes = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#line-edge-option-add-midpoint-nodes"; inline constexpr const char* k_line_edge_place_from_center = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#line-edge-option-place-from-center"; inline constexpr const char* k_revolve_solid_conversion = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#revolve-solid-conversion"; -inline constexpr const char* k_shape_selection_filter = "https://ezycad.readthedocs.io/en/latest/usage.html#shape-selection-filter-normal-mode-only"; +inline constexpr const char* k_shape_selection_filter = "https://ezycad.readthedocs.io/en/latest/usage.html#shape-selection-filter-inspection-and-workbench"; inline constexpr const char* k_add_node_tool = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#add-node-tool"; inline constexpr const char* k_bone_creation_tool = "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#bone-creation-tool"; inline constexpr const char* k_shape_rotate_tool = "https://ezycad.readthedocs.io/en/latest/usage.html#shape-rotate-tool-r"; @@ -372,7 +372,7 @@ class GUI ImVec4 get_clear_color() const; void set_mode(Mode mode); // gui_mode.cpp void set_parent_mode(); // gui_mode.cpp - /// Parent mode for Escape / tool exit (e.g. Move -> Normal, sketch tools -> Sketch_inspection_mode). + /// Parent mode for Escape / tool exit (e.g. Workbench_move -> Workbench_inspection, sketch tools -> Sketch_inspection). static Mode parent_mode_of(Mode mode); // gui_mode.cpp void set_dist_edit(float dist, std::function&& callback, const std::optional screen_coords = std::nullopt); @@ -467,6 +467,13 @@ class GUI bool is_active; std::string tooltip; std::variant data; + Task task; + }; + struct Task_button + { + uint32_t texture_id; + Task task; + const char* tooltip; }; void dist_edit_(); void angle_edit_(); @@ -672,7 +679,7 @@ class GUI bool m_angle_edit_focus_pending{false}; // Mode related - Mode m_mode = Mode::Normal; + Mode m_mode = Mode::Design_inspection; Chamfer_mode m_chamfer_mode = Chamfer_mode::Shape; Fillet_mode m_fillet_mode = Fillet_mode::Shape; int m_edge_dim_label_h = 3; @@ -732,6 +739,7 @@ class GUI Project_unit m_default_project_unit = Project_unit::Inch; bool m_inspection_orthographic = false; std::vector m_toolbar_buttons; + std::vector m_task_buttons; // Message status window std::string m_message; diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 5aaf5917..d16aa588 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -43,12 +43,13 @@ std::string GUI::get_doc_url_for_mode(Mode mode) { static const std::unordered_map doc_urls = { // clang-format off - {Mode::Normal, "https://ezycad.readthedocs.io/en/latest/usage.html#user-interface"}, - {Mode::Move, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-move-tool-g"}, - {Mode::Rotate, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-rotate-tool-r"}, - {Mode::Scale, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-scale-tool-s"}, - {Mode::Shape_shaft_align, "https://ezycad.readthedocs.io/en/latest/usage.html#align-shafts-tool-j"}, - {Mode::Sketch_inspection_mode, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#sketch-origin"}, + {Mode::Design_inspection, "https://ezycad.readthedocs.io/en/latest/usage.html#user-interface"}, + {Mode::Workbench_inspection, "https://ezycad.readthedocs.io/en/latest/usage.html#user-interface"}, + {Mode::Workbench_move, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-move-tool-g"}, + {Mode::Workbench_rotate, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-rotate-tool-r"}, + {Mode::Scale, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-scale-tool-s"}, + {Mode::Workbench_shaft_align, "https://ezycad.readthedocs.io/en/latest/usage.html#align-shafts-tool-j"}, + {Mode::Sketch_inspection, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#sketch-origin"}, {Mode::Sketch_from_planar_face, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#create-sketch-from-planar-face-tool"}, {Mode::Sketch_face_extrude, "https://ezycad.readthedocs.io/en/latest/usage.html#extrude-sketch-face-tool-e"}, {Mode::Shape_chamfer, "https://ezycad.readthedocs.io/en/latest/usage.html#other-feature-operations"}, @@ -128,32 +129,33 @@ Mode GUI::parent_mode_of(Mode mode) { static const std::map parent_modes = { // clang-format off - {Mode::Normal, Mode::Normal}, - {Mode::Move, Mode::Normal}, - {Mode::Scale, Mode::Normal}, - {Mode::Rotate, Mode::Normal}, - {Mode::Shape_shaft_align, Mode::Normal}, - {Mode::Sketch_inspection_mode, Mode::Normal}, - {Mode::Sketch_from_planar_face, Mode::Normal}, - {Mode::Sketch_face_extrude, Mode::Normal}, - {Mode::Shape_chamfer, Mode::Normal}, - {Mode::Shape_fillet, Mode::Normal}, - {Mode::Shape_polar_duplicate, Mode::Normal}, - {Mode::Sketch_add_node, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_edge, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_multi_edges, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_seg_circle_arc, Mode::Sketch_inspection_mode}, - {Mode::Sketch_operation_axis, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_square, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_rectangle, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_rectangle_center_pt, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_circle, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_circle_3_pts, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_slot, Mode::Sketch_inspection_mode}, - {Mode::Sketch_add_bone, Mode::Sketch_inspection_mode}, - {Mode::Sketch_dim_anno, Mode::Sketch_inspection_mode}, - {Mode::Shape_cross_section, Mode::Normal}, - {Mode::Shape_set_frame, Mode::Normal}, + {Mode::Design_inspection, Mode::Design_inspection}, + {Mode::Workbench_inspection, Mode::Workbench_inspection}, + {Mode::Workbench_move, Mode::Workbench_inspection}, + {Mode::Scale, Mode::Design_inspection}, + {Mode::Workbench_rotate, Mode::Workbench_inspection}, + {Mode::Workbench_shaft_align, Mode::Workbench_inspection}, + {Mode::Sketch_inspection, Mode::Design_inspection}, + {Mode::Sketch_from_planar_face, Mode::Design_inspection}, + {Mode::Sketch_face_extrude, Mode::Design_inspection}, + {Mode::Shape_chamfer, Mode::Design_inspection}, + {Mode::Shape_fillet, Mode::Design_inspection}, + {Mode::Shape_polar_duplicate, Mode::Design_inspection}, + {Mode::Sketch_add_node, Mode::Sketch_inspection}, + {Mode::Sketch_add_edge, Mode::Sketch_inspection}, + {Mode::Sketch_add_multi_edges, Mode::Sketch_inspection}, + {Mode::Sketch_add_seg_circle_arc, Mode::Sketch_inspection}, + {Mode::Sketch_operation_axis, Mode::Sketch_inspection}, + {Mode::Sketch_add_square, Mode::Sketch_inspection}, + {Mode::Sketch_add_rectangle, Mode::Sketch_inspection}, + {Mode::Sketch_add_rectangle_center_pt, Mode::Sketch_inspection}, + {Mode::Sketch_add_circle, Mode::Sketch_inspection}, + {Mode::Sketch_add_circle_3_pts, Mode::Sketch_inspection}, + {Mode::Sketch_add_slot, Mode::Sketch_inspection}, + {Mode::Sketch_add_bone, Mode::Sketch_inspection}, + {Mode::Sketch_dim_anno, Mode::Sketch_inspection}, + {Mode::Shape_cross_section, Mode::Design_inspection}, + {Mode::Shape_set_frame, Mode::Design_inspection}, // clang-format on }; @@ -279,7 +281,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) // // Other modes: chamfer/fillet/sketch may override selection mode via Occt_view::on_mode(). // ------------------------------------------------------------------------- - if (get_mode() == Mode::Normal) + if (is_shape_browse_mode(get_mode())) { int idx = -1; if (key >= GLFW_KEY_1 && key <= GLFW_KEY_9) @@ -316,7 +318,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) { // Move / Rotate / Align shafts handle Tab in their mode key handlers (distance / angle / depth / twist). const Mode mode = get_mode(); - if (mode == Mode::Move || mode == Mode::Rotate || mode == Mode::Shape_shaft_align) + if (mode == Mode::Workbench_move || mode == Mode::Workbench_rotate || mode == Mode::Workbench_shaft_align) break; bool shift_pressed = (mods & GLFW_MOD_SHIFT) != 0; @@ -329,7 +331,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) case GLFW_KEY_ENTER: // Rotate / Align shafts finalize on Enter in their mode key handlers. - if (get_mode() == Mode::Rotate || get_mode() == Mode::Shape_shaft_align) + if (get_mode() == Mode::Workbench_rotate || get_mode() == Mode::Workbench_shaft_align) break; hide_sketch_origin_set_edit(true); @@ -364,15 +366,15 @@ void GUI::on_key(int key, int scancode, int action, int mods) switch (get_mode()) { - case Mode::Move: + case Mode::Workbench_move: on_key_move_mode_(key); break; - case Mode::Rotate: + case Mode::Workbench_rotate: on_key_rotate_mode_(key); break; - case Mode::Shape_shaft_align: + case Mode::Workbench_shaft_align: on_key_cyl_align_mode_(key, mods); break; @@ -386,15 +388,15 @@ void GUI::dispatch_hotkey_action_(Gui_action action) // clang-format off switch (action) { - case Gui_action::Mode_move: set_mode(Mode::Move); break; - case Gui_action::Mode_rotate: set_mode(Mode::Rotate); break; + case Gui_action::Mode_move: set_mode(Mode::Workbench_move); break; + case Gui_action::Mode_rotate: set_mode(Mode::Workbench_rotate); break; case Gui_action::Mode_scale: set_mode(Mode::Scale); break; - case Gui_action::Mode_cyl_align: set_mode(Mode::Shape_shaft_align); break; + case Gui_action::Mode_cyl_align: set_mode(Mode::Workbench_shaft_align); break; case Gui_action::Mode_extrude: set_mode(Mode::Sketch_face_extrude); break; case Gui_action::Mode_chamfer: set_mode(Mode::Shape_chamfer); break; case Gui_action::Mode_fillet: set_mode(Mode::Shape_fillet); break; case Gui_action::Mode_dimension: set_mode(Mode::Sketch_dim_anno); break; - case Gui_action::Mode_sketch_inspection: set_mode(Mode::Sketch_inspection_mode); break; + case Gui_action::Mode_sketch_inspection: set_mode(Mode::Sketch_inspection); break; case Gui_action::Mode_sketch_from_face: set_mode(Mode::Sketch_from_planar_face); break; case Gui_action::Mode_operation_axis: set_mode(Mode::Sketch_operation_axis); break; case Gui_action::Mode_add_node: set_mode(Mode::Sketch_add_node); break; @@ -515,11 +517,12 @@ void GUI::options_() // clang-format off switch (get_mode()) { - case Mode::Normal: options_normal_mode_(); break; - case Mode::Move: options_move_mode_(); break; - case Mode::Rotate: options_rotate_mode_(); break; - case Mode::Scale: options_scale_mode_(); break; - case Mode::Shape_shaft_align: options_shape_shaft_align_mode_(); break; + case Mode::Design_inspection: options_normal_mode_(); break; + case Mode::Workbench_inspection: options_normal_mode_(); break; + case Mode::Workbench_move: options_move_mode_(); break; + case Mode::Workbench_rotate: options_rotate_mode_(); break; + case Mode::Scale: options_scale_mode_(); break; + case Mode::Workbench_shaft_align: options_shape_shaft_align_mode_(); break; case Mode::Shape_chamfer: options_shape_chamfer_mode_(); break; case Mode::Shape_fillet: options_shape_fillet_mode_(); break; case Mode::Shape_polar_duplicate: options_shape_polar_duplicate_mode_(); break; @@ -527,7 +530,7 @@ void GUI::options_() case Mode::Shape_set_frame: options_shape_set_frame_mode_(); break; // Sketch related modes: - case Mode::Sketch_inspection_mode: options_sketch_inspection_mode_(); break; + case Mode::Sketch_inspection: options_sketch_inspection_mode_(); break; case Mode::Sketch_from_planar_face: options_sketch_from_planer_face_mode_(); break; case Mode::Sketch_operation_axis: options_sketch_operation_axis_mode_(); break; case Mode::Sketch_face_extrude: options_sketch_face_extrude_mode_(); break; @@ -555,14 +558,14 @@ void GUI::options_() void GUI::options_sketch_inspection_mode_() { - EZY_ASSERT(get_mode() == Mode::Sketch_inspection_mode); + EZY_ASSERT(get_mode() == Mode::Sketch_inspection); options_sketch_common_(); } void GUI::options_normal_mode_() { - EZY_ASSERT(get_mode() == Mode::Normal); + EZY_ASSERT(is_shape_browse_mode(get_mode())); ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); @@ -592,7 +595,7 @@ void GUI::options_normal_mode_() } ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); - GUI_DOC_HELP_("Hotkeys: 1-9 (Normal mode) set filter when the 3D view has focus, not while typing in UI. Click ? to " + GUI_DOC_HELP_("Hotkeys: 1-9 (Inspection / Workbench) set filter when the 3D view has focus, not while typing in UI. Click ? to " "open the user guide.", doc_urls::k_shape_selection_filter); @@ -647,7 +650,7 @@ void GUI::options_transform_space_() void GUI::options_move_mode_() { - EZY_ASSERT(get_mode() == Mode::Move); + EZY_ASSERT(get_mode() == Mode::Workbench_move); ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); @@ -680,7 +683,7 @@ void GUI::options_scale_mode_() void GUI::options_shape_shaft_align_mode_() { - EZY_ASSERT(get_mode() == Mode::Shape_shaft_align); + EZY_ASSERT(get_mode() == Mode::Workbench_shaft_align); ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); @@ -724,7 +727,7 @@ void GUI::options_shape_set_frame_mode_() void GUI::options_rotate_mode_() { - EZY_ASSERT(get_mode() == Mode::Rotate); + EZY_ASSERT(get_mode() == Mode::Workbench_rotate); ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index c7e5f54c..77b9cdb6 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -585,14 +585,14 @@ void Occt_view::cancel(Set_parent_mode set_parent_mode) switch (get_mode()) { - // Transform tools already return to Normal in their reset() and re-select the operands; + // Transform tools already return to Workbench_inspection in their reset() and re-select the operands; // a further set_mode() here would redisplay shapes and drop that selection again. - case Mode::Move: + case Mode::Workbench_move: shp_move().cancel(); operation_canceled = true; break; - case Mode::Rotate: + case Mode::Workbench_rotate: shp_rotate().cancel(); operation_canceled = true; break; @@ -602,7 +602,7 @@ void Occt_view::cancel(Set_parent_mode set_parent_mode) operation_canceled = true; break; - case Mode::Shape_shaft_align: + case Mode::Workbench_shaft_align: shp_cyl_align().cancel(); operation_canceled = true; break; @@ -614,7 +614,7 @@ void Occt_view::cancel(Set_parent_mode set_parent_mode) case Mode::Shape_cross_section: shp_cross_section().clear(); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); break; default: @@ -637,7 +637,7 @@ void Occt_view::revolve_selected(const double angle) add_shp_(*revolved, true); push_undo_delta(std::make_unique(std::vector{capture_shape_rec(**revolved)})); // Leave sketch mode so the new solid is shown at full strength (sketch tools use faint/hide). - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); } else { @@ -665,7 +665,7 @@ void Occt_view::create_sketch_from_planar_face_(const ScreenCoords& screen_coord push_undo_delta(std::make_unique(Sketch_struct_delta::Kind::Add, Sketch_json::to_json(*m_cur_sketch, m_assets), true)); // fit_face_in_view(*face); - m_gui.set_mode(Mode::Sketch_inspection_mode); + m_gui.set_mode(Mode::Sketch_inspection); return; } @@ -717,7 +717,7 @@ void Occt_view::add_sketch(const gp_Pln& pln, const std::string& base_name) refresh_viewer_grid_(); push_undo_delta(std::make_unique(Sketch_struct_delta::Kind::Add, Sketch_json::to_json(*m_cur_sketch, m_assets), true)); - m_gui.set_mode(Mode::Sketch_inspection_mode); + m_gui.set_mode(Mode::Sketch_inspection); } void Occt_view::add_sketch_on_ref_plane(Sketch_ref_plane plane, double offset_display, const std::string& base_name) @@ -773,7 +773,7 @@ Status Occt_view::create_sketch_from_cross_section(const std::string& base_name) refresh_viewer_grid_(); push_undo_delta(std::make_unique(Sketch_struct_delta::Kind::Add, Sketch_json::to_json(*m_cur_sketch, m_assets), true)); - m_gui.set_mode(Mode::Sketch_inspection_mode); + m_gui.set_mode(Mode::Sketch_inspection); std::ostringstream msg; msg << "Created sketch '" << name << "' with " << counts.imported @@ -2883,10 +2883,10 @@ void Occt_view::on_mouse_button(int theButton, int theAction, int theMods) if (theButton == GLFW_MOUSE_BUTTON_LEFT) { // clang-format off - const bool finalize_transform = (get_mode() == Mode::Move && shp_move().has_operation_shps()) || - (get_mode() == Mode::Rotate && shp_rotate().has_operation_shps()) || + const bool finalize_transform = (get_mode() == Mode::Workbench_move && shp_move().has_operation_shps()) || + (get_mode() == Mode::Workbench_rotate && shp_rotate().has_operation_shps()) || (get_mode() == Mode::Scale && shp_scale().has_operation_shps()) || - (get_mode() == Mode::Shape_shaft_align && shp_cyl_align().is_dragging()); + (get_mode() == Mode::Workbench_shaft_align && shp_cyl_align().is_dragging()); // clang-format on if (finalize_transform) { @@ -3611,7 +3611,7 @@ void Occt_view::on_mode() // stay at the pre-transform pose. Disable AIS_ViewController dynamic highlight (skips MoveTo // while idle) so hover cannot paint a wireframe ghost there; orbit/pan still get mouse updates. const Mode mode = get_mode(); - const bool transform_preview = mode == Mode::Move || mode == Mode::Rotate || mode == Mode::Scale; + const bool transform_preview = mode == Mode::Workbench_move || mode == Mode::Workbench_rotate || mode == Mode::Scale; SetAllowHighlight(!transform_preview); if (transform_preview && !m_ctx.IsNull()) m_ctx->ClearDetected(false); @@ -3682,10 +3682,10 @@ void Occt_view::on_mode() case Mode::Sketch_from_planar_face: set_shp_selection_mode(TopAbs_FACE); break; case Mode::Shape_chamfer: on_chamfer_mode(); break; // Will update selection mode case Mode::Shape_fillet: on_fillet_mode(); break; // Will update selection mode - case Mode::Move: set_shp_selection_mode(TopAbs_SHAPE); break; - case Mode::Rotate: set_shp_selection_mode(TopAbs_SHAPE); break; + case Mode::Workbench_move: set_shp_selection_mode(TopAbs_SHAPE); break; + case Mode::Workbench_rotate: set_shp_selection_mode(TopAbs_SHAPE); break; case Mode::Scale: set_shp_selection_mode(TopAbs_SHAPE); break; - case Mode::Shape_shaft_align: set_shp_selection_mode(TopAbs_FACE); break; + case Mode::Workbench_shaft_align: set_shp_selection_mode(TopAbs_FACE); break; case Mode::Shape_set_frame: set_shp_selection_mode(TopAbs_FACE); break; case Mode::Shape_cross_section: set_shp_selection_mode(TopAbs_COMPOUND); break; default: @@ -3711,10 +3711,10 @@ void Occt_view::on_mode() { switch (mode) { - case Mode::Move: + case Mode::Workbench_move: shp_move().begin(enter_selection); break; - case Mode::Rotate: + case Mode::Workbench_rotate: shp_rotate().begin(enter_selection); break; case Mode::Scale: @@ -3725,7 +3725,7 @@ void Occt_view::on_mode() } } - if (mode == Mode::Shape_shaft_align) + if (mode == Mode::Workbench_shaft_align) shp_cyl_align().begin(); if (mode == Mode::Shape_cross_section && !enter_selection.empty()) @@ -4071,7 +4071,7 @@ bool Occt_view::undo() m_redo_stack.push_back(std::move(redo_entry)); const Mode restore_mode = mode_for_history_restore_(state.mode); m_gui.set_mode(restore_mode); - if (restore_mode == Mode::Sketch_inspection_mode) + if (restore_mode == Mode::Sketch_inspection) m_gui.set_show_sketch_list(true); m_restoring = false; @@ -4104,7 +4104,7 @@ bool Occt_view::redo() m_undo_stack.push_back(std::move(undo_entry)); const Mode restore_mode = mode_for_history_restore_(state.mode); m_gui.set_mode(restore_mode); - if (restore_mode == Mode::Sketch_inspection_mode) + if (restore_mode == Mode::Sketch_inspection) m_gui.set_show_sketch_list(true); m_restoring = false; @@ -4738,7 +4738,7 @@ void Occt_view::new_file() create_default_sketch_(); refresh_viewer_grid_(); reset_default_view(); - m_gui.set_mode(Mode::Normal); + m_gui.set_mode(Mode::Design_inspection); } namespace @@ -4822,10 +4822,10 @@ Mode mode_for_history_restore_(Mode mode) { switch (mode) { - case Mode::Move: - case Mode::Rotate: + case Mode::Workbench_move: + case Mode::Workbench_rotate: case Mode::Scale: - case Mode::Shape_shaft_align: + case Mode::Workbench_shaft_align: case Mode::Shape_set_frame: return GUI::parent_mode_of(mode); default: diff --git a/src/mode.cpp b/src/mode.cpp index dd489652..669f2a13 100644 --- a/src/mode.cpp +++ b/src/mode.cpp @@ -6,14 +6,33 @@ Mode mode_from_string(std::string_view name) if (c_mode_strs[i] == name) return static_cast(i); - return Mode::Normal; + // Retired names kept so scripts / older docs still resolve. + if (name == "Normal") + return Mode::Design_inspection; + + if (name == "Sketch_inspection_mode") + return Mode::Sketch_inspection; + + if (name == "Move") + return Mode::Workbench_move; + + if (name == "Rotate") + return Mode::Workbench_rotate; + + if (name == "Workbench_scale") + return Mode::Scale; + + if (name == "Shape_shaft_align" || name == "Shape_cyl_align") + return Mode::Workbench_shaft_align; + + return Mode::Design_inspection; } bool is_sketch_mode(const Mode mode) { switch (mode) { - case Mode::Sketch_inspection_mode: + case Mode::Sketch_inspection: case Mode::Sketch_add_node: case Mode::Sketch_add_edge: case Mode::Sketch_add_seg_circle_arc: @@ -34,3 +53,71 @@ bool is_sketch_mode(const Mode mode) return false; } } + +bool is_workbench_transform_mode(const Mode mode) +{ + switch (mode) + { + case Mode::Workbench_move: + case Mode::Workbench_rotate: + case Mode::Workbench_shaft_align: + return true; + + default: + return false; + } +} + +bool is_workbench_mode(const Mode mode) +{ + return mode == Mode::Workbench_inspection || is_workbench_transform_mode(mode); +} + +bool is_shape_browse_mode(const Mode mode) +{ + return mode == Mode::Design_inspection || mode == Mode::Workbench_inspection; +} + +Task task_of(Mode mode) +{ + if (is_workbench_mode(mode)) + return Task::Workbench; + + switch (mode) + { + case Mode::Sketch_inspection: + case Mode::Sketch_add_node: + case Mode::Sketch_add_edge: + case Mode::Sketch_add_seg_circle_arc: + case Mode::Sketch_add_multi_edges: + case Mode::Sketch_add_square: + case Mode::Sketch_add_rectangle: + case Mode::Sketch_add_rectangle_center_pt: + case Mode::Sketch_add_circle: + case Mode::Sketch_add_circle_3_pts: + case Mode::Sketch_add_slot: + case Mode::Sketch_add_bone: + case Mode::Sketch_operation_axis: + case Mode::Sketch_dim_anno: + return Task::Sketch; + + default: + return Task::Design; + } +} + +Mode idle_mode_of(Task task) +{ + switch (task) + { + case Task::Sketch: + return Mode::Sketch_inspection; + + case Task::Workbench: + return Mode::Workbench_inspection; + + case Task::Design: + default: + return Mode::Design_inspection; + } +} diff --git a/src/mode.h b/src/mode.h index 0c4fba63..b1eea74d 100644 --- a/src/mode.h +++ b/src/mode.h @@ -7,11 +7,11 @@ // Single source of truth: order here defines enum numeric values and c_mode_strs indices // (mode_from_string, persistence, etc.). Do not reorder without migrating saved data. #define EZY_MODE_LIST(X) \ - X(Normal) \ - X(Move) \ + X(Design_inspection) \ + X(Workbench_move) \ X(Scale) \ - X(Rotate) \ - X(Sketch_inspection_mode) /* inspecting sketch elements */ \ + X(Workbench_rotate) \ + X(Sketch_inspection) /* inspecting sketch elements */ \ X(Sketch_from_planar_face) /* sketch from a planar face */ \ X(Sketch_face_extrude) /* extrude a sketch face */ \ X(Shape_chamfer) \ @@ -31,8 +31,9 @@ X(Sketch_add_bone) /* two centers, then r1, r2, waist clicks */ \ X(Sketch_dim_anno) \ X(Shape_cross_section) \ - X(Shape_shaft_align) \ - X(Shape_set_frame) /* Shape List only: pick face for local frame */ + X(Workbench_shaft_align) \ + X(Shape_set_frame) /* Shape List only: pick face for local frame */ \ + X(Workbench_inspection) /* Workbench browse/select home; not Mode::Design_inspection */ enum class Mode { @@ -110,7 +111,26 @@ constexpr std::array(Bone_holes::_count)> static_assert(c_bone_holes_strs.size() == static_cast(Bone_holes::_count)); -bool is_sketch_mode(const Mode mode); +/// Toolbar task switcher (Sketch / Design / Workbench). Distinct from Mode. +enum class Task +{ + Sketch, + Design, + Workbench, + _count +}; -/// Return Mode for a name (e.g. "Normal", "Sketch_add_edge"). Returns Normal if not found. +bool is_sketch_mode(const Mode mode); +/// Workbench idle + Move / Rotate / Align shafts. +bool is_workbench_mode(const Mode mode); +/// Workbench Move / Rotate / Align shafts (not idle; Scale is Design). +bool is_workbench_transform_mode(const Mode mode); +/// Design Inspection or Workbench idle (solid browse / selection-filter digits). +bool is_shape_browse_mode(const Mode mode); + +Task task_of(Mode mode); +Mode idle_mode_of(Task task); + +/// Return Mode for a name (e.g. "Design_inspection", "Sketch_add_edge"). Returns Design_inspection if not found. +/// Also accepts retired names: Normal, Sketch_inspection_mode, Move, Rotate, Workbench_scale, Shape_shaft_align. Mode mode_from_string(std::string_view name); diff --git a/src/scr_lua_console.cpp b/src/scr_lua_console.cpp index 93ab755f..7fbf4d68 100644 --- a/src/scr_lua_console.cpp +++ b/src/scr_lua_console.cpp @@ -103,7 +103,7 @@ int l_ezy_get_mode(lua_State* L) GUI* gui = get_gui(L); if (!gui) { - lua_pushstring(L, "Normal"); + lua_pushstring(L, "Design_inspection"); return 1; } Mode m = gui->get_mode(); diff --git a/src/scr_python_console.cpp b/src/scr_python_console.cpp index 03b3ce07..de0af390 100644 --- a/src/scr_python_console.cpp +++ b/src/scr_python_console.cpp @@ -333,7 +333,7 @@ PYBIND11_EMBEDDED_MODULE(ezycad_native, m) [] { if (!g_py_gui) - return std::string("Normal"); + return std::string("Design_inspection"); Mode mmode = g_py_gui->get_mode(); return std::string(c_mode_strs[static_cast(mmode)]); diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index f627594f..cc5c4a5c 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -339,5 +339,5 @@ void Shp_cyl_align::cancel() void Shp_cyl_align::reset() { begin(); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Workbench_inspection); } diff --git a/src/shp_move.cpp b/src/shp_move.cpp index 45463d37..0766d4c2 100644 --- a/src/shp_move.cpp +++ b/src/shp_move.cpp @@ -145,7 +145,7 @@ void Shp_move::cancel() void Shp_move::reset() { clear_all(m_opts, m_delta, m_move_pln, m_center, m_shps); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Workbench_inspection); } Move_options& Shp_move::get_opts() { return m_opts; } diff --git a/src/shp_polar_dup.cpp b/src/shp_polar_dup.cpp index 173e25f2..ee8d32df 100644 --- a/src/shp_polar_dup.cpp +++ b/src/shp_polar_dup.cpp @@ -174,7 +174,7 @@ Status Shp_polar_dup::dup() delete_operation_shps_(); view().push_undo_delta(std::make_unique(std::move(removed), std::move(added))); - gui().set_mode(Mode::Normal); // Will call reset() + gui().set_mode(Mode::Design_inspection); // Will call reset() return Status::ok(); } diff --git a/src/shp_rotate.cpp b/src/shp_rotate.cpp index 682aaea8..bcfc9f49 100644 --- a/src/shp_rotate.cpp +++ b/src/shp_rotate.cpp @@ -289,7 +289,7 @@ void Shp_rotate::reset() { clear_all(m_angle, m_shps, m_initial_mouse_pos, m_rotate_pln, m_captured_axis_dir, m_center); clear_rotation_vis_(); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Workbench_inspection); } void Shp_rotate::clear_rotation_vis_() diff --git a/src/shp_scale.cpp b/src/shp_scale.cpp index 96445379..078635c4 100644 --- a/src/shp_scale.cpp +++ b/src/shp_scale.cpp @@ -117,7 +117,7 @@ void Shp_scale::reset() m_scale_factor = 1.0; - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); } void Shp_scale::cancel() diff --git a/src/shp_set_frame.cpp b/src/shp_set_frame.cpp index 1199ecad..a326a847 100644 --- a/src/shp_set_frame.cpp +++ b/src/shp_set_frame.cpp @@ -108,7 +108,7 @@ Status Shp_set_frame::pick(const ScreenCoords& screen_coords) // Leave pick mode before the undo push so history stores Normal, not Shape_set_frame. const Shp_ptr target = m_target; clear_all(m_target); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); view().set_shape_frame(target, *frame); target->set_show_frame_axes(true); gui().show_message("Local frame updated.", Status_msg::Success); @@ -118,5 +118,5 @@ Status Shp_set_frame::pick(const ScreenCoords& screen_coords) void Shp_set_frame::cancel() { clear_all(m_target); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); } diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index fe220a8b..1a7c8544 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -519,7 +519,7 @@ TEST_F(Shp_test, Cross_section_sketch_imports_box_midplane_lines) const Status status = view().create_sketch_from_cross_section(); ASSERT_TRUE(status.is_ok()) << status.message(); EXPECT_EQ(view().get_sketches().size(), sketches_before + 1u); - EXPECT_EQ(gui().get_mode(), Mode::Sketch_inspection_mode); + EXPECT_EQ(gui().get_mode(), Mode::Sketch_inspection); EXPECT_GE(Sketch_access::get_linear_edge_count(view().curr_sketch()), 4u); } @@ -728,7 +728,7 @@ TEST_F(Shp_test, Undo_delete_shape_restores_brep) TEST_F(Shp_test, Delete_shape_clears_frame_ais) { // Static GUI can be left in a sketch mode by an earlier test; that suppresses frame AIS. - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); view().add_box(0, 0, 0, 10, 10, 10); ASSERT_EQ(view().get_shapes().size(), 1u); @@ -804,9 +804,9 @@ TEST_F(Shp_test, Undo_interleaves_sketch_delta_and_shape_add) EXPECT_EQ(view().get_shapes().size(), 1u); } -TEST_F(Shp_test, Set_frame_undo_stays_in_normal) +TEST_F(Shp_test, Set_frame_undo_stays_in_design_inspection) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); view().add_box(0, 0, 0, 10, 10, 10); @@ -824,16 +824,16 @@ TEST_F(Shp_test, Set_frame_undo_stays_in_normal) view().set_shape_frame(shp, after); shp->set_show_frame_axes(true); view().shp_set_frame().cancel(); - EXPECT_EQ(gui().get_mode(), Mode::Normal); + EXPECT_EQ(gui().get_mode(), Mode::Design_inspection); EXPECT_FALSE(view().shp_set_frame().has_target()); EXPECT_TRUE(view().undo()); - EXPECT_EQ(gui().get_mode(), Mode::Normal); + EXPECT_EQ(gui().get_mode(), Mode::Design_inspection); EXPECT_FALSE(view().shp_set_frame().has_target()); EXPECT_TRUE(shp->get_frame().Direction().IsEqual(before.Direction(), 1e-9)); EXPECT_TRUE(view().redo()); - EXPECT_EQ(gui().get_mode(), Mode::Normal); + EXPECT_EQ(gui().get_mode(), Mode::Design_inspection); EXPECT_TRUE(shp->get_frame().Direction().IsEqual(after.Direction(), 1e-9)); } @@ -1042,7 +1042,7 @@ TEST_F(Shp_test, Hide_all_preserves_per_shape_visibility) TEST_F(Shp_test, Hide_all_clears_frame_ais) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); view().add_box(0, 0, 0, 10, 10, 10); @@ -1073,7 +1073,7 @@ TEST_F(Shp_test, Hide_all_clears_frame_ais) TEST_F(Shp_test, Hidden_group_clears_child_frame_ais) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); view().add_box(0, 0, 0, 10, 10, 10); @@ -1476,14 +1476,14 @@ TEST_F(Shp_test, Transform_translation_local_x_constraint) TEST_F(Shp_test, Rotate_axis_can_be_set_before_first_drag) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); view().add_box(0, 0, 0, 10, 10, 10); Shp_ptr shp = view().get_shapes().back(); ASSERT_FALSE(shp.IsNull()); select_shapes(view(), {shp}); - gui().set_mode(Mode::Rotate); + gui().set_mode(Mode::Workbench_rotate); EXPECT_TRUE(view().shp_rotate().has_operation_shps()); view().shp_rotate().set_rotation_axis(Rotation_axis::Z_axis); EXPECT_EQ(view().shp_rotate().get_rotation_axis(), Rotation_axis::Z_axis); @@ -1491,14 +1491,14 @@ TEST_F(Shp_test, Rotate_axis_can_be_set_before_first_drag) TEST_F(Shp_test, Rotate_view_to_object_keeps_drag_frame_after_orbit) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); view().add_box(0, 0, 0, 10, 10, 10); Shp_ptr shp = view().get_shapes().back(); ASSERT_FALSE(shp.IsNull()); select_shapes(view(), {shp}); - gui().set_mode(Mode::Rotate); + gui().set_mode(Mode::Workbench_rotate); ASSERT_TRUE(view().shp_rotate().has_operation_shps()); EXPECT_EQ(view().shp_rotate().get_rotation_axis(), Rotation_axis::View_to_object); @@ -1521,14 +1521,14 @@ TEST_F(Shp_test, Rotate_view_to_object_keeps_drag_frame_after_orbit) TEST_F(Shp_test, Rotate_constrained_keeps_axis_plane_when_facing_test_would_flip) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); view().add_box(0, 0, 0, 10, 10, 10); Shp_ptr shp = view().get_shapes().back(); ASSERT_FALSE(shp.IsNull()); select_shapes(view(), {shp}); - gui().set_mode(Mode::Rotate); + gui().set_mode(Mode::Workbench_rotate); ASSERT_TRUE(view().shp_rotate().has_operation_shps()); view().shp_rotate().set_rotation_axis(Rotation_axis::X_axis); @@ -1549,7 +1549,7 @@ TEST_F(Shp_test, Rotate_constrained_keeps_axis_plane_when_facing_test_would_flip TEST_F(Shp_test, Scale_space_change_mid_drag_keeps_factor) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); const Transform_space saved_space = gui().get_transform_space(); struct Restore_space @@ -1588,12 +1588,12 @@ TEST_F(Shp_test, Scale_space_change_mid_drag_keeps_factor) ASSERT_TRUE(Shp_scale_access::apply_distance(view().shp_scale(), dist_after).is_ok()); EXPECT_NEAR(Shp_scale_access::scale_factor(view().shp_scale()), factor_before, 1e-6); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); } TEST_F(Shp_test, Rotate_space_change_mid_drag_keeps_angle) { - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); gui().set_hide_all_shapes(false); const Transform_space saved_space = gui().get_transform_space(); struct Restore_space @@ -1610,7 +1610,7 @@ TEST_F(Shp_test, Rotate_space_change_mid_drag_keeps_angle) view().set_shape_frame(shp, gp_Ax3(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0), gp_Dir(1.0, 0.0, 0.0))); select_shapes(view(), {shp}); - gui().set_mode(Mode::Rotate); + gui().set_mode(Mode::Workbench_rotate); ASSERT_TRUE(view().shp_rotate().has_operation_shps()); view().shp_rotate().set_rotation_axis(Rotation_axis::Z_axis); ASSERT_TRUE(Shp_rotate_access::ensure_start(view().shp_rotate()).is_ok()); @@ -1634,5 +1634,5 @@ TEST_F(Shp_test, Rotate_space_change_mid_drag_keeps_angle) ASSERT_TRUE(Shp_rotate_access::apply_world(view().shp_rotate(), gp_Pnt(0.0, 20.0, 0.0), axis_z, world_pln).is_ok()); EXPECT_NEAR(Shp_rotate_access::angle(view().shp_rotate()), angle_before, 1e-6); - gui().set_mode(Mode::Normal); + gui().set_mode(Mode::Design_inspection); } diff --git a/tests/skt_tests.cpp b/tests/skt_tests.cpp index 47b621a2..9f77a0cb 100644 --- a/tests/skt_tests.cpp +++ b/tests/skt_tests.cpp @@ -554,7 +554,7 @@ TEST_F(Sketch_test, SketchOriginNodeSurvivesOnMode) TopoDS_Wire outer_wire = create_wire_box(default_plane, center, 20.0, 10.0); Sketch sketch("FaceOrigin", view(), default_plane, outer_wire); - // Same path as create_sketch_from_planar_face_ -> set_mode(Sketch_inspection_mode). + // Same path as create_sketch_from_planar_face_ -> set_mode(Sketch_inspection). sketch.on_mode(); std::optional origin_idx; From de5d3dd52bc8c500e4ac6fb0fab2d161d789a5bd Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 20 Sep 2026 16:36:21 -0600 Subject: [PATCH 02/12] Fix --- CHANGELOG.md | 1 + .../gh-214-shape-list-hierarchy-phase3.md | 2 +- agents/plans/README.md | 21 +++---- agents/plans/assembly-inspection-mode.md | 58 ++++++++++++------- agents/plans/shape-list-hierarchy-phase3.md | 10 ++-- agents/plans/tasks-workbenches.md | 30 ++++++++++ docs/usage.md | 1 - src/doc/gui.md | 2 +- src/gui.cpp | 3 +- src/gui_mode.cpp | 4 +- 10 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 agents/plans/tasks-workbenches.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9fe936..44bbf0d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Workbench toolbar**: the tools row no longer repeats the Workbench task icon; idle is the task button only. - **Shape List right-click**: the row context menu opens from the whole entry (checkboxes, **M**, tree arrow, name field, and padding), not only from some widgets. Right-click on the name field opens the same menu. - **Shape rotate axis**: Choosing X/Y/Z (or view-to-object) before the first drag is kept. Constrained rotation uses the plane perpendicular to that axis when the view faces it. Local space rotates about the assigned frame, not only world XYZ through the bbox. - **Shape rotate drag**: View-to-object axis and the constrained axis-vs-view plane are captured on the first mouse sample. Orbit or pan during rotate no longer changes the axis/plane against the original mouse anchor. diff --git a/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md b/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md index f8cda1d9..66dd218a 100644 --- a/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md +++ b/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md @@ -18,7 +18,7 @@ Shape List hierarchy phase 3: parent transforms and advanced assembly model ### Summary -Longer-term Shape List / document hierarchy work that should **not** ship with the organizational outliner. Phase 1 delivered folder-style groups without changing leaf BREP or transform semantics. Phase 2 covers outliner polish (reorder, duplicate, selection). Phase 3 is the product decision layer: inherited placement, optional boolean history, and part-local frames. +Longer-term Shape List / document hierarchy. Phase 1 delivered folder-style groups. Phase 3 (in progress): inherited placement, Part/Origin/planes, Workbench vs Design list views. Editable Boolean history remains deferred (fuse/cut/common still replace inputs; they now evaluate world BREP). ### Context diff --git a/agents/plans/README.md b/agents/plans/README.md index fe27ad0a..7d7e3e97 100644 --- a/agents/plans/README.md +++ b/agents/plans/README.md @@ -26,14 +26,15 @@ shp-origin-orientation --> cross-section-tool --> sketch-from-shape-section sketch-mode-shape-faint (done; parallel UX) ``` -| Plan | Status | Load only when prompt is about | -| ---------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------- | -| [shp-origin-orientation.md](shp-origin-orientation.md) | partial | shape origin, shape frame, shape axes / orientation annotation | -| [cross-section-tool.md](cross-section-tool.md) | done | cross-section tool, section cut preview, cutting-plane experiment | -| [sketch-from-shape-section.md](sketch-from-shape-section.md) | partial | "sketch from shape", section → editable sketch | -| [sketch-mode-shape-faint.md](sketch-mode-shape-faint.md) | done | faint/ghost/wireframe shapes in sketch mode, alpha blending solids | -| [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) | deferred | parent transform inheritance, Parts/planes, Boolean history (#214) | -| [assembly-inspection-mode.md](assembly-inspection-mode.md) | deferred | assembly idle/inspection mode, Part vs arrange context, Move semantics | -| [wasm-multithreading.md](wasm-multithreading.md) | planning | WASM/Emscripten pthreads, SharedArrayBuffer, parallel OCCT on web | +| Plan | Status | Load only when prompt is about | +| ---------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------ | +| [shp-origin-orientation.md](shp-origin-orientation.md) | partial | shape origin, shape frame, shape axes / orientation annotation | +| [cross-section-tool.md](cross-section-tool.md) | done | cross-section tool, section cut preview, cutting-plane experiment | +| [sketch-from-shape-section.md](sketch-from-shape-section.md) | partial | "sketch from shape", section → editable sketch | +| [sketch-mode-shape-faint.md](sketch-mode-shape-faint.md) | done | faint/ghost/wireframe shapes in sketch mode, alpha blending solids | +| [tasks-workbenches.md](tasks-workbenches.md) | partial | Sketch/Design/Workbench task toolbar, tools-row filter, list views | +| [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) | partial | parent transform inheritance, Parts/planes, Boolean history (#214) | +| [assembly-inspection-mode.md](assembly-inspection-mode.md) | deferred | assembly idle/inspection mode, Part vs arrange context, Workbench list | +| [wasm-multithreading.md](wasm-multithreading.md) | planning | WASM/Emscripten pthreads, SharedArrayBuffer, parallel OCCT on web | | [wasm-alt-drag-multiselect.md](wasm-alt-drag-multiselect.md) | implemented | WASM Alt+LMB multi-select (#220); AABB pick supplement; close issue when ready | -| [configurable-hotkeys.md](configurable-hotkeys.md) | done | remappable shortcuts, keybindings, `gui.hotkeys`, free Dimension off D | +| [configurable-hotkeys.md](configurable-hotkeys.md) | done | remappable shortcuts, keybindings, `gui.hotkeys`, free Dimension off D | diff --git a/agents/plans/assembly-inspection-mode.md b/agents/plans/assembly-inspection-mode.md index 40fabe17..a415e923 100644 --- a/agents/plans/assembly-inspection-mode.md +++ b/agents/plans/assembly-inspection-mode.md @@ -25,37 +25,39 @@ Post-v1 product design. Do **not** ship Assembly inspection until [phase 3 place Two phases of work, two kinds of idle mode: -1. **Make geometry** — Sketch inspection + Inspection (`Mode::Design_inspection`): draw profiles, see solids alongside (or faint), extrude, fillet, boolean. -2. **Arrange components** — Assembly inspection (future): duplicate / place / move Parts relative to each other; later mates and instances. +1. **Make geometry** — Sketch inspection + Design inspection (`Mode::Design_inspection`): draw profiles, see solids alongside (or faint), extrude, fillet, boolean. +2. **Arrange components** — Workbench inspection (`Mode::Workbench_inspection`): duplicate / place / move Parts relative to each other; later mates and instances. -That split matches SolidWorks / Inventor / Fusion (sketch nested in part, part nested in assembly). EzyCad names the idle states “inspection” (= browse/select + launch tools + Esc parent), not metrology “Inspect.” +That split matches SolidWorks / Inventor / Fusion (sketch nested in part, part nested in assembly). EzyCad names the idle states "inspection" (= browse/select + launch tools + Esc parent), not metrology "Inspect." Workbench is the arrange task today; a later `Assembly_inspection` name can replace it if mates/instances need a fourth idle. ```mermaid flowchart TB - assemblyIdle[Assembly_inspection] - partIdle[Inspection_Normal] + workbenchIdle[Workbench_inspection] + designIdle[Design_inspection] sketchIdle[Sketch_inspection] sketchTools[Sketch_tools] - assemblyIdle -->|"edit part"| partIdle - partIdle -->|"edit sketch"| sketchIdle + workbenchIdle -->|"edit part"| designIdle + designIdle -->|"edit sketch"| sketchIdle sketchIdle --> sketchTools sketchTools -->|"Esc"| sketchIdle - sketchIdle -->|"Esc"| partIdle - partIdle -->|"Esc"| assemblyIdle + sketchIdle -->|"Esc"| designIdle ``` +Esc from Workbench tools returns to Workbench idle (not Design). Task buttons jump between Sketch, Design, and Workbench. + ## What exists today -| UI label | Mode | Role | -| ---------------------- | ------------------------- | ----------------------------------------------------------------------------- | -| Inspection mode | `Mode::Design_inspection` | Idle 3D solids: selection filter, materials, booleans, transforms | -| Sketch inspection mode | `Mode::Sketch_inspection` | Idle 2D sketch: show sketch, ortho, faint solids; Esc parent for sketch tools | +| UI label | Mode | Role | +| ---------------------- | ---------------------------- | ----------------------------------------------------------------------------- | +| Design inspection | `Mode::Design_inspection` | Idle 3D modeling: selection filter, materials, booleans, Scale | +| Workbench inspection | `Mode::Workbench_inspection` | Idle 3D arrange: Move / Rotate / Align shafts; Workbench list of Parts | +| Sketch inspection mode | `Mode::Sketch_inspection` | Idle 2D sketch: show sketch, ortho, faint solids; Esc parent for sketch tools | -Esc ladder (see [docs/usage.md](../../docs/usage.md)): sketch tool → sketch inspection → `Design_inspection`. +Esc ladder (see [docs/usage.md](../../docs/usage.md)): sketch tool -> sketch inspection -> `Design_inspection`. Workbench tools -> `Workbench_inspection`. -**Not first-class yet:** Parts, instances, mates, inherited parent transforms. Shape List **groups** are organizational (STEP XCAF hierarchy on import). Move / Rotate / Scale / Align shafts **bake** into leaf BREP — one-shot geometry edits, not assembly placement. Toolbar icons named `Assembly_*.png` are FreeCAD-style assets for those shape tools, not an assembly workbench. +**Phase 3 engine:** relative placement, typed nodes (Body, Group, Part, Origin, Plane), Move/Rotate/Scale update placement. Shape List **groups** remain folders. Toolbar icons named `Assembly_*.png` are FreeCAD-style assets for Workbench tools. True mates / shared instances remain later. -Planned model: [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) (#214) — relative placement, typed nodes (Body, Group, Part, …), non-baking transforms. +See [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) (#214). ## The awkward nuance (and why it feels wrong) @@ -123,16 +125,30 @@ CAD products rarely brand the idle state “inspection”; they use document or Shared idea: layered contexts with Esc / finish stepping outward. EzyCad’s “inspection” vocabulary can stay for consistency, or later rename all three to Sketch / Part / Assembly **context** (larger UX rename). +## Lists (Design vs Workbench) + +Same document (`Occt_view::m_shps`), **different views**. Not a second shape store. + +| Task | List shows | Duplicate | +| ------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| **Design** | Bodies / features / groups you are editing (Part Origin/planes locked) | Clone or feature-array geometry | +| **Workbench** | Parts (and groups / root bodies until wrapped) as placeable units; hide inner body trees | New placement of the same Part (instances after Phase 3) | + +Polar array of **parts** belongs on Workbench after placement. Polar array of a body can stay Design. Sketch List stays Sketch-only. + +Do **not** ship a second pane that lists the same baked solids, or two independent `m_shps` lists. + ## Product stance (fixed for this plan) -1. Keep `Mode::Design_inspection` as the 3D idle mode through hierarchy phase 1–2; keep bake Move/Duplicate there until placement lands. -2. Introduce `Assembly_inspection` (name TBD in code) only with phase 3+ Part + relative placement; mates/instances can follow. -3. Then: Inspection = make/edit geometry; Assembly inspection = arrange Parts; Sketch inspection stays under Part. -4. No v1 Assembly inspection mode. +1. Design idle is `Mode::Design_inspection`; Workbench idle is `Mode::Workbench_inspection`. +2. Phase 3 placement / Parts: Move/Rotate on a Part updates relative placement (`world = parent * local`); children follow. Bake stays only for true geometry edits (fillet, scale-as-feature if needed). +3. Workbench list is the arrange tree (Parts). Design list is the modeling tree (bodies). Duplicate-many-parts is instance/array, not 50 unique BREPs. +4. True mates stay later. Sketch inspection stays under Part. ## Related - Implementation of placement / Parts: [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) +- Task toolbars: Workbench vs Design tools row (see `src/doc/gui.md`) - Modes / Esc parent map: [src/doc/gui.md](../../src/doc/gui.md), [src/mode.h](../../src/mode.h) - User Esc ladder / tool docs: [docs/usage.md](../../docs/usage.md) -- Shape List groups today: [src/doc/shape.md](../../src/doc/shape.md) +- Shape List: [src/doc/shape.md](../../src/doc/shape.md) diff --git a/agents/plans/shape-list-hierarchy-phase3.md b/agents/plans/shape-list-hierarchy-phase3.md index 4c8da764..d49cad64 100644 --- a/agents/plans/shape-list-hierarchy-phase3.md +++ b/agents/plans/shape-list-hierarchy-phase3.md @@ -1,8 +1,8 @@ --- -status: deferred +status: partial topic: shape-list-hierarchy-phase3 depends_on: null -blocks: null +blocks: [assembly-inspection-mode] github_issue: 214 --- @@ -10,7 +10,7 @@ github_issue: 214 **Load only when** the prompt is about parent transform inheritance, Part/Origin/plane objects, editable Boolean history, or Shape List hierarchy phase 3 (#214). Skip otherwise ([token-lean](../conventions/token-lean.md)). Index: [plans/README.md](README.md). -Post-v1 work. Do **not** ship any of this in v1. +In progress: typed nodes, relative placement, Part/Origin/planes, Workbench vs Design list filter. Editable Boolean history (section 4) can follow; fuse/cut/common still replace inputs but evaluate **world** BREP so placed parts boolean correctly. ## Scope and fixed decisions @@ -53,6 +53,7 @@ Post-v1 work. Do **not** ship any of this in v1. ## 5. Complete lifecycle, UI, and interchange semantics +- Shape List pane: Design shows the modeling tree; Workbench filters to Parts/groups/root bodies (hide inner Body trees under a Part). One document, two views. See [assembly-inspection-mode.md](assembly-inspection-mode.md#lists-design-vs-workbench). - Expand Boolean rows as Base/Tool or numbered Operand children; add operation change, reorder, recompute, show operands, remove operand, and dissolve actions in [`src/gui.cpp`](../../src/gui.cpp). - Cascade-delete Group, Part, and Boolean subtrees. Dissolve promotes ordinary children preserve-world; Part drops system references; Boolean removes its result and promotes operands. - Traverse logical outputs for export: Body exports world geometry, Boolean exports its cached result once, Group/Part recurse, and Origin/Plane export nothing. Keep STEP/IGES/STL/PLY flattened; only `.ezy` preserves history. @@ -71,4 +72,5 @@ Post-v1 work. Do **not** ship any of this in v1. 1. Placement/migration/reparent/export tests must pass before enabling inherited transforms. 2. Inherited transforms and Part references must be stable before Boolean history depends on them. 3. Boolean history ships only after nested recompute, failure recovery, persistence, undo, and export tests pass. -4. No phase 3 behavior is included in v1; sketches remain structurally unchanged throughout. +4. Sketches remain in `m_sketches` / Sketch List; no sketch Shape List parent. +5. Design vs Workbench lists are **views** of one `m_shps` document (not two stores). Design: bodies/features (Origin/Plane locked). Workbench: Parts (and groups / unwrapped root bodies) as placeable units. Polar array of parts belongs on Workbench after instances exist. diff --git a/agents/plans/tasks-workbenches.md b/agents/plans/tasks-workbenches.md new file mode 100644 index 00000000..0bbbd96c --- /dev/null +++ b/agents/plans/tasks-workbenches.md @@ -0,0 +1,30 @@ +--- +status: partial +topic: tasks-workbenches +depends_on: null +blocks: [assembly-inspection-mode] +github_issue: null +--- + +# Tasks / workbenches (toolbar filter) + +**Load only when** the prompt is about Sketch / Design / Workbench task buttons, the tools-row filter, or Workbench vs Design Shape List. Skip otherwise ([token-lean](../conventions/token-lean.md)). Index: [plans/README.md](README.md). + +Shipped: three tasks; tools row shows only the current task (option 1: hide unrelated modes). Workbench idle is `Mode::Workbench_inspection`. Move / Rotate / Align shafts are Workbench; Scale stays Design. Hotkeys auto-switch task (G / R / J -> Workbench, S -> Design). + +## Workbench vs Design lists + +Same `Occt_view::m_shps`, **different views**. Do not add a second shape store. + +| Task | List shows | Duplicate / array | +| --------- | -------------------------------------------------------- | -------------------------------------------------------- | +| Design | Bodies / features / groups (Part Origin / planes locked) | Clone or feature-array of geometry | +| Workbench | Parts (groups / root bodies until wrapped); hide inners | New placement of the same Part (instances after phase 3) | + +Polar array of **parts** belongs on Workbench after placement. Polar array of a body can stay Design. + +## Related + +- Placement / Parts: [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) +- Arrange idle: [assembly-inspection-mode.md](assembly-inspection-mode.md) +- Modes / toolbar: [src/doc/gui.md](../../src/doc/gui.md) diff --git a/docs/usage.md b/docs/usage.md index fd53e9b6..fa8b463b 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -925,7 +925,6 @@ Contributors should follow **[ezycad_code_style.md](ezycad_code_style.md)** for - ![Part_Scale](res/icons/Part_Scale.png) - Shape scale (S) ### Workbench -- ![Workbench_Assembly](res/icons/Workbench_Assembly.png) - Workbench idle - ![Assembly_AxialMove](res/icons/Assembly_AxialMove.png) - Shape move (G) - ![Draft_Rotate](res/icons/Draft_Rotate.png) - Shape rotate (R) - ![Assembly_Move](res/icons/Assembly_Move.png) - Align shafts (J) diff --git a/src/doc/gui.md b/src/doc/gui.md index 43ad87e5..eeba7682 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -44,7 +44,7 @@ Typical responsibilities: ### Tasks / toolbar (filter) -The chrome toolbar is one ImGui window with two regions: **task** switcher (Sketch / Design / Workbench) then a `|` then **tools** for `task_of(get_mode())` only (option 1: hide unrelated modes). Clicking a task button calls `set_mode(idle_mode_of(task))`. Hotkeys still call `set_mode` on the tool; that updates the active task so the tools row follows (G/R/J from Sketch/Design enter Workbench; S enters Design Scale). Helpers: `task_of`, `idle_mode_of`, `is_workbench_mode`, `is_workbench_transform_mode`, `is_shape_browse_mode` in [`mode.h`](../mode.h). +The chrome toolbar is one ImGui window with two regions: **task** switcher (Sketch / Design / Workbench) then a `|` then **tools** for `task_of(get_mode())` only (option 1: hide unrelated modes). Clicking a task button calls `set_mode(idle_mode_of(task))`. Workbench idle (`Mode::Workbench_inspection`) is the task button only — not repeated on the tools row. Hotkeys still call `set_mode` on the tool; that updates the active task so the tools row follows (G/R/J from Sketch/Design enter Workbench; S enters Design Scale). Helpers: `task_of`, `idle_mode_of`, `is_workbench_mode`, `is_workbench_transform_mode`, `is_shape_browse_mode` in [`mode.h`](../mode.h). `Mode::Design_inspection`, `Mode::Workbench_inspection`, and the three task buttons are toolbar-only (no remappable `Gui_action`). Transform tools keep `mode.move` / `mode.rotate` / `mode.scale` / `mode.cyl_align`. Retired script names `Normal` / `Sketch_inspection_mode` / `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align` still parse via `mode_from_string`. diff --git a/src/gui.cpp b/src/gui.cpp index 5c0db921..fce3a5e6 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -212,8 +212,7 @@ void GUI::initialize_toolbar_() {load_texture("res/icons/Part_Cut.png"), false, "Shape cut", Command::Shape_cut, Task::Design}, {load_texture("res/icons/Part_Fuse.png"), false, "Shape fuse", Command::Shape_fuse, Task::Design}, {load_texture("res/icons/Part_Common.png"), false, "Shape common", Command::Shape_common, Task::Design}, - // Workbench - {load_texture("res/icons/Workbench_Assembly.png"), false, "Workbench", Mode::Workbench_inspection, Task::Workbench}, + // Workbench (idle is the task button; do not repeat Workbench_inspection here) {load_texture("res/icons/Assembly_AxialMove.png"), false, "Shape move", Mode::Workbench_move, Task::Workbench}, {load_texture("res/icons/Draft_Rotate.png"), false, "Shape rotate", Mode::Workbench_rotate, Task::Workbench}, {load_texture("res/icons/Assembly_Move.png"), false, "Align shafts", Mode::Workbench_shaft_align, Task::Workbench}, diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index d16aa588..bb060480 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -91,7 +91,9 @@ const char* GUI::current_mode_description_() const if (std::get(b.data) == m_mode) return b.tooltip.c_str(); - // Modes entered only from Shape List / menus (no toolbar button). + // Modes entered only from Shape List / menus / task buttons (no tools-row button). + if (m_mode == Mode::Workbench_inspection) + return "Workbench"; if (m_mode == Mode::Shape_set_frame) return "Set local frame"; From 99bc30236c50398ff94d553a49dd4dfa29c24705 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Sun, 20 Sep 2026 17:25:55 -0600 Subject: [PATCH 03/12] Improve --- CHANGELOG.md | 4 +- .../gh-214-shape-list-hierarchy-phase3.md | 2 +- agents/plans/README.md | 2 +- agents/plans/assembly-inspection-mode.md | 18 +- agents/plans/shape-list-hierarchy-phase3.md | 4 +- agents/plans/tasks-workbenches.md | 16 +- docs/scripting.md | 18 +- docs/usage-settings.md | 2 + docs/usage.md | 94 ++- res/ezycad_settings.json | 1 + src/doc/gui.md | 122 +-- src/doc/script.md | 18 +- src/doc/shape.md | 45 +- src/doc/undo-redo.md | 2 +- src/gui.cpp | 27 +- src/gui.h | 10 + src/gui_mode.cpp | 34 +- src/gui_occt_view.cpp | 784 +++++++++++++++++- src/gui_occt_view.h | 24 + src/gui_settings.cpp | 2 + src/gui_shp_pane.cpp | 377 ++++++++- src/mode.cpp | 19 +- src/mode.h | 13 +- src/shp.cpp | 30 +- src/shp.h | 16 + src/shp_cyl_align.cpp | 5 +- src/shp_delta.cpp | 2 + src/shp_delta.h | 2 + src/shp_move.cpp | 5 +- src/shp_operation.cpp | 5 +- src/shp_rotate.cpp | 5 +- src/shp_scale.cpp | 2 +- src/shp_transform.cpp | 3 +- tests/shp_tests.cpp | 175 ++++ 34 files changed, 1635 insertions(+), 253 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44bbf0d4..26535611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Task toolbar**: Sketch / Design / Workbench switcher; the tools row shows only the current task. Move / Rotate / Align shafts live under Workbench; Scale stays on Design. G / R / J (and S for Scale) still enter those tools from any task. +- **Task toolbar**: Sketch / Design / Workbench switcher; the tools row shows only the current task. Design has its own Move / Rotate / Scale / Align shafts (bake bodies for CSG). Workbench has Move / Rotate / Align shafts (instance pose). G / R / J stay on the current task; S enters Design Scale. - **Mode names**: Design idle is `Design_inspection` (was `Normal`); Sketch idle is `Sketch_inspection` (was `Sketch_inspection_mode`). `ezy.get_mode()` / `ezy.set_mode` use the new names; `Normal` and `Sketch_inspection_mode` still parse. - **Move / rotate / scale space**: Options **Local** (default) or **World**. Local uses the first selected solid's frame (origin and X/Y/Z from **Show axes**). World keeps global XYZ at that solid's bounding-box center. Persisted as **`gui.transform_space`**. - **Desktop deps**: MSVC GLFW/GLEW come from **vcpkg** (`vcpkg.json` + toolchain) instead of NuGet; local configure matches CI. @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Workbench List**: separate pane of geometry **links** to Shape List solids (own location/rotation). **Add to Workbench** from the Shape List (button, context menu, or drag). Design geometry edits update links; Design move/rotate/scale do not move instances. Workbench Move / Rotate / Align shafts edit the instance frame only. `.ezy` stores `workbench[]` (no BREP). Settings: **`gui.show_workbench_list`**. + - **Status toast kinds**: the bottom-right toast is colored by kind (**success**, **info**, **constraint**, **warning**, **error**) instead of always red. Failed `Status` toasts follow `Result_status` (`User_error` is amber constraint). Scripts: `ezy.msg(text [, kind])`. **Add bone** uses info for the next click, amber for rejected input, green on commit, and red if create fails. - **Curve segment angle**: **Settings -> View presentation** slider (**2** to **20** degrees, default **6**) sets Open CASCADE AIS curve tessellation. Smaller draws smoother arcs and fillets (display only). Persisted as **`gui.curve_deviation_angle_deg`**. diff --git a/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md b/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md index 66dd218a..ec7c2acb 100644 --- a/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md +++ b/agents/drafts/issues/active/gh-214-shape-list-hierarchy-phase3.md @@ -18,7 +18,7 @@ Shape List hierarchy phase 3: parent transforms and advanced assembly model ### Summary -Longer-term Shape List / document hierarchy. Phase 1 delivered folder-style groups. Phase 3 (in progress): inherited placement, Part/Origin/planes, Workbench vs Design list views. Editable Boolean history remains deferred (fuse/cut/common still replace inputs; they now evaluate world BREP). +Longer-term Shape List / document hierarchy. Phase 1 delivered folder-style groups. Phase 3 (in progress): inherited placement, Part/Origin/planes. Workbench List is a second store of geometry links (`m_wbk_shps`), not a filter of `m_shps`. Editable Boolean history remains deferred (fuse/cut/common still replace inputs; they now evaluate world BREP). ### Context diff --git a/agents/plans/README.md b/agents/plans/README.md index 7d7e3e97..7989c131 100644 --- a/agents/plans/README.md +++ b/agents/plans/README.md @@ -32,7 +32,7 @@ sketch-mode-shape-faint (done; parallel UX) | [cross-section-tool.md](cross-section-tool.md) | done | cross-section tool, section cut preview, cutting-plane experiment | | [sketch-from-shape-section.md](sketch-from-shape-section.md) | partial | "sketch from shape", section → editable sketch | | [sketch-mode-shape-faint.md](sketch-mode-shape-faint.md) | done | faint/ghost/wireframe shapes in sketch mode, alpha blending solids | -| [tasks-workbenches.md](tasks-workbenches.md) | partial | Sketch/Design/Workbench task toolbar, tools-row filter, list views | +| [tasks-workbenches.md](tasks-workbenches.md) | partial | Sketch/Design/Workbench task toolbar, Workbench List links | | [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) | partial | parent transform inheritance, Parts/planes, Boolean history (#214) | | [assembly-inspection-mode.md](assembly-inspection-mode.md) | deferred | assembly idle/inspection mode, Part vs arrange context, Workbench list | | [wasm-multithreading.md](wasm-multithreading.md) | planning | WASM/Emscripten pthreads, SharedArrayBuffer, parallel OCCT on web | diff --git a/agents/plans/assembly-inspection-mode.md b/agents/plans/assembly-inspection-mode.md index a415e923..bbde6ff1 100644 --- a/agents/plans/assembly-inspection-mode.md +++ b/agents/plans/assembly-inspection-mode.md @@ -49,7 +49,7 @@ Esc from Workbench tools returns to Workbench idle (not Design). Task buttons ju | UI label | Mode | Role | | ---------------------- | ---------------------------- | ----------------------------------------------------------------------------- | -| Design inspection | `Mode::Design_inspection` | Idle 3D modeling: selection filter, materials, booleans, Scale | +| Design inspection | `Mode::Design_inspection` | Idle 3D modeling: selection filter, materials, booleans, Scale, Align shafts | | Workbench inspection | `Mode::Workbench_inspection` | Idle 3D arrange: Move / Rotate / Align shafts; Workbench list of Parts | | Sketch inspection mode | `Mode::Sketch_inspection` | Idle 2D sketch: show sketch, ortho, faint solids; Esc parent for sketch tools | @@ -127,22 +127,20 @@ Shared idea: layered contexts with Esc / finish stepping outward. EzyCad’s “ ## Lists (Design vs Workbench) -Same document (`Occt_view::m_shps`), **different views**. Not a second shape store. +Two stores: Design `m_shps` (Shape List) and Workbench `m_wbk_shps` (Workbench List). Leaves in the workbench are **links** (`source_id` + own frame), not a second BREP and not a filter of `m_shps`. -| Task | List shows | Duplicate | -| ------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| **Design** | Bodies / features / groups you are editing (Part Origin/planes locked) | Clone or feature-array geometry | -| **Workbench** | Parts (and groups / root bodies until wrapped) as placeable units; hide inner body trees | New placement of the same Part (instances after Phase 3) | +| Task | List shows | Copy / instance | +| ------------- | ------------------------------------------- | --------------------------------------------- | +| **Design** | Bodies / features / groups you are editing | Clone or feature-array geometry | +| **Workbench** | Linked placements of Design solids / groups | `add_to_workbench`; later true Part instances | -Polar array of **parts** belongs on Workbench after placement. Polar array of a body can stay Design. Sketch List stays Sketch-only. - -Do **not** ship a second pane that lists the same baked solids, or two independent `m_shps` lists. +Design geometry edits (same `Shape_id`) refresh links. Design move/rotate/scale do not move instances. Workbench transforms edit the instance frame. Polar array of parts can follow on Workbench. Sketch List stays Sketch-only. See [tasks-workbenches.md](tasks-workbenches.md). ## Product stance (fixed for this plan) 1. Design idle is `Mode::Design_inspection`; Workbench idle is `Mode::Workbench_inspection`. 2. Phase 3 placement / Parts: Move/Rotate on a Part updates relative placement (`world = parent * local`); children follow. Bake stays only for true geometry edits (fillet, scale-as-feature if needed). -3. Workbench list is the arrange tree (Parts). Design list is the modeling tree (bodies). Duplicate-many-parts is instance/array, not 50 unique BREPs. +3. Workbench List is the arrange tree (links with own pose). Design list is the modeling tree. Do not duplicate BREP for each placement. 4. True mates stay later. Sketch inspection stays under Part. ## Related diff --git a/agents/plans/shape-list-hierarchy-phase3.md b/agents/plans/shape-list-hierarchy-phase3.md index d49cad64..5c5331e5 100644 --- a/agents/plans/shape-list-hierarchy-phase3.md +++ b/agents/plans/shape-list-hierarchy-phase3.md @@ -53,7 +53,7 @@ In progress: typed nodes, relative placement, Part/Origin/planes, Workbench vs D ## 5. Complete lifecycle, UI, and interchange semantics -- Shape List pane: Design shows the modeling tree; Workbench filters to Parts/groups/root bodies (hide inner Body trees under a Part). One document, two views. See [assembly-inspection-mode.md](assembly-inspection-mode.md#lists-design-vs-workbench). +- Shape List is the Design tree (`m_shps`). Workbench List is a second store of geometry links (`m_wbk_shps`). See [tasks-workbenches.md](tasks-workbenches.md) and [assembly-inspection-mode.md](assembly-inspection-mode.md#lists-design-vs-workbench). - Expand Boolean rows as Base/Tool or numbered Operand children; add operation change, reorder, recompute, show operands, remove operand, and dissolve actions in [`src/gui.cpp`](../../src/gui.cpp). - Cascade-delete Group, Part, and Boolean subtrees. Dissolve promotes ordinary children preserve-world; Part drops system references; Boolean removes its result and promotes operands. - Traverse logical outputs for export: Body exports world geometry, Boolean exports its cached result once, Group/Part recurse, and Origin/Plane export nothing. Keep STEP/IGES/STL/PLY flattened; only `.ezy` preserves history. @@ -73,4 +73,4 @@ In progress: typed nodes, relative placement, Part/Origin/planes, Workbench vs D 2. Inherited transforms and Part references must be stable before Boolean history depends on them. 3. Boolean history ships only after nested recompute, failure recovery, persistence, undo, and export tests pass. 4. Sketches remain in `m_sketches` / Sketch List; no sketch Shape List parent. -5. Design vs Workbench lists are **views** of one `m_shps` document (not two stores). Design: bodies/features (Origin/Plane locked). Workbench: Parts (and groups / unwrapped root bodies) as placeable units. Polar array of parts belongs on Workbench after instances exist. +5. Design vs Workbench are two lists: `m_shps` (bodies) and `m_wbk_shps` (links with own placement). Polar array of parts belongs on Workbench after instances exist. diff --git a/agents/plans/tasks-workbenches.md b/agents/plans/tasks-workbenches.md index 0bbbd96c..bd1e17af 100644 --- a/agents/plans/tasks-workbenches.md +++ b/agents/plans/tasks-workbenches.md @@ -10,18 +10,20 @@ github_issue: null **Load only when** the prompt is about Sketch / Design / Workbench task buttons, the tools-row filter, or Workbench vs Design Shape List. Skip otherwise ([token-lean](../conventions/token-lean.md)). Index: [plans/README.md](README.md). -Shipped: three tasks; tools row shows only the current task (option 1: hide unrelated modes). Workbench idle is `Mode::Workbench_inspection`. Move / Rotate / Align shafts are Workbench; Scale stays Design. Hotkeys auto-switch task (G / R / J -> Workbench, S -> Design). +Shipped: three tasks; tools row shows only the current task. Design has `Design_move` / `Design_rotate` / `Scale` / `Design_shaft_align` (bake for CSG). Workbench has `Workbench_move` / `Workbench_rotate` / `Workbench_shaft_align` (instance pose). G / R / J follow the current task; S enters Design Scale. ## Workbench vs Design lists -Same `Occt_view::m_shps`, **different views**. Do not add a second shape store. +Two stores, one document: Design `Occt_view::m_shps` and Workbench `Occt_view::m_wbk_shps`. Workbench leaves are **geometry links** (`source_id`), not BREP copies. -| Task | List shows | Duplicate / array | -| --------- | -------------------------------------------------------- | -------------------------------------------------------- | -| Design | Bodies / features / groups (Part Origin / planes locked) | Clone or feature-array of geometry | -| Workbench | Parts (groups / root bodies until wrapped); hide inners | New placement of the same Part (instances after phase 3) | +| Task | Store / pane | What a row is | +| --------- | ----------------------------- | ------------------------------------------------- | +| Design | `m_shps` / Shape List | Bodies / groups you model | +| Workbench | `m_wbk_shps` / Workbench List | Linked instances with their own frame (placement) | -Polar array of **parts** belongs on Workbench after placement. Polar array of a body can stay Design. +Copy Shape List solids or groups onto the Workbench (**Add to Workbench**, context menu, or drag). Design fillet/chamfer (same `Shape_id`) refreshes linked local geom. Design move/rotate bake does not move instances. Workbench Move/Rotate/Align update the instance frame only. Deleting a Design source removes its Workbench links. `.ezy` writes `workbench[]` (ids, `sourceId`, frame; no geom). + +Polar array of **parts** can follow on Workbench after instances exist. Polar array of a body stays Design. ## Related diff --git a/docs/scripting.md b/docs/scripting.md index 246f541b..6e3b6715 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -44,15 +44,15 @@ ezy.Shp ### `ezy` -| Method | Purpose | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ezy.log(msg)` | Print to the console and the main **Log** pane | -| `ezy.msg(text [, kind])` | Status toast and Log; optional kind (see below) | -| `ezy.get_mode()` | Current application mode name (string) | -| `ezy.set_mode(name)` | Switch mode by name (`Design_inspection`, `Sketch_inspection`, `Workbench_move`, `Scale`, ...). Retired names `Normal` / `Sketch_inspection_mode` / `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align` still work. | -| `ezy.save_occt_view_settings()` | Write settings JSON (including view colors) | -| `ezy.occt_view_settings_json()` | Settings JSON string | -| `ezy.help()` | Print binding summary | +| Method | Purpose | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ezy.log(msg)` | Print to the console and the main **Log** pane | +| `ezy.msg(text [, kind])` | Status toast and Log; optional kind (see below) | +| `ezy.get_mode()` | Current application mode name (string) | +| `ezy.set_mode(name)` | Switch mode by name (`Design_inspection`, `Design_move`, `Design_shaft_align`, `Workbench_move`, `Scale`, ...). Retired names `Normal` / `Sketch_inspection_mode` / `Move` (Design move) / `Rotate` (Design rotate) / `Workbench_scale` / `Shape_shaft_align` still work. | +| `ezy.save_occt_view_settings()` | Write settings JSON (including view colors) | +| `ezy.occt_view_settings_json()` | Settings JSON string | +| `ezy.help()` | Print binding summary | Optional `kind` for `ezy.msg` is `success`, `info` (default), `constraint`, `warning`, or `error`. Unknown names use info. Toast color follows the kind. diff --git a/docs/usage-settings.md b/docs/usage-settings.md index 3d5fe7b0..becbb71b 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -20,6 +20,7 @@ This guide covers the **Settings** pane (what is on screen), the **View** menu ( - **Options** - Show or hide the Options panel. - **Sketch List** - Show or hide the Sketch List pane. - **Shape List** - Show or hide the Shape List pane. +- **Workbench List** - Show or hide the Workbench List pane. - **Log** - Show or hide the Log window. - **Lua Console** - Show or hide the interactive Lua prompt and `res/scripts/lua` editors. See [Scripting](scripting.md). - **Python Console** - Same for Python when the app is built with embedded Python (native only; not in the WebAssembly build). @@ -220,6 +221,7 @@ If saved layout text has no `[Docking]` section (older installs), a default dock | `show_options` | boolean | Options panel visible. | | `show_sketch_list` | boolean | Sketch List pane visible. | | `show_shape_list` | boolean | Shape List pane visible. | +| `show_workbench_list` | boolean | Workbench List pane visible (default **true**). | | `log_window_visible` | boolean | Log window visible. | | `show_settings_dialog` | boolean | Whether the Settings pane was open when last saved (usually false). | | `show_lua_console` | boolean | Lua console pane visible (default **false**). | diff --git a/docs/usage.md b/docs/usage.md index fa8b463b..c17e3154 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -50,7 +50,7 @@ EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to d 2. **Toolbar** - **Task** buttons (Sketch, Design, Workbench) pick the work context and enter that task's idle / inspection mode. - The **tools** row shows only the modes and commands for the current task (unrelated tools are hidden). - - Hotkeys still work from any task: G / R / J switch to Workbench and enter Move / Rotate / Align shafts; S switches to Design and enters Scale; sketch letters switch to Sketch. + - Hotkeys follow the current task: G / R / J enter that task's Move / Rotate / Align shafts (Design bakes the body for CSG; Workbench places a linked instance). S enters Design Scale. Sketch letters switch to Sketch. 3. **Sketch List** - [View and manage 2D sketches](#sketch-list) @@ -62,7 +62,10 @@ EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to d - [List 3D solids, materials, and display options](#shape-list) - [Inspect shape topology and properties](#shape-info) -5. **Options Panel** +5. **Workbench List** + - [Place linked copies of Shape List solids](#workbench-list) + +6. **Options Panel** - The top of the panel always shows the name of the current tool/mode (matching the toolbar tooltip), followed immediately by a small **"?"** button. Clicking the "?" opens the online user guide directly to the section describing that specific tool (contextual help; see the per-mode links in the source `get_doc_url_for_mode` map). - Related controls are grouped by headings (for example **Sketch options**, **Extrude**, **Selection**, **Material**, **Polar duplicate**), depending on the active tool. - In non-sketch modes the Options panel shows **Selection** (in Normal/Inspection), tool-specific controls, **Orthographic projection** (toggles the camera mode, and **Material**. Sketch modes force orthographic projection and show sketch-specific options instead. **Face extrude** reads the same preset in its Options **Material** row. @@ -70,7 +73,7 @@ EzyCad (Easy CAD) is an open-source CAD application for hobbyist machinists to d - **Move**, **Rotate**, and **Scale**: transform options only (no material row there). - Sketch-related options (snap, length dimension placement, face extrude, shortcuts) are summarized in **[usage-settings.md](usage-settings.md#options-panel)**. -6. **Log Window** +7. **Log Window** - View operation history - Check for errors and warnings - Monitor system status (including status toast messages: green success, light info, amber constraint, yellow warning, red error) @@ -114,6 +117,7 @@ At the top: - **Hide all** - When checked, hides every solid in the 3D view without changing each row's visibility checkbox; when cleared, solids return to their previous per-row visibility (and group visibility). - **New group** - Creates an empty organizational group under the **current group** (document root when none is set) and makes it current. - **Group** - Places the currently selected solids under a new group (enabled when one or more solids are selected in the 3D view) and makes that group current. +- **Add to Workbench** - Copies the current selection (or the current group) onto the [Workbench List](#workbench-list) as geometry links. You can also right-click a row and choose **Add to Workbench**, or drag a Shape List row onto the Workbench List. Shapes form a parent/child outliner. Groups are folders only (they do not move children when transformed). Drag a row onto a group to reparent it, onto a solid to place it under that solid's parent, or into the empty space below the list to move it to the document root. @@ -155,6 +159,18 @@ The dialog reports document fields (name, material, shaded vs wireframe display, This is useful after **Revolve**, **Extrude**, booleans, or imports when you need to confirm whether a result is a closed **Solid** or an open **Shell** / surface, or to check face and edge counts and overall size. The dialog closes automatically if the shape is deleted from the document. +### Workbench List + +The **Workbench List** is a second pane for arranging solids in the Workbench task. Open it from **View -> Workbench List**. It is not a second copy of the Shape List: each solid row is a **link** to a Shape List body. + +- Adding a shape or group from the Shape List creates Workbench rows that share that body's geometry. Fillet, chamfer, and other Design geometry edits update the Workbench solids. Moving, rotating, or scaling a body in Design does not move its Workbench instances. +- Each Workbench solid has its own local frame. **Move** / **Rotate** / **Align shafts** in the Workbench task change that instance pose only. +- The pane matches the Shape List for **Hide all**, visibility, shaded/wireframe, material, rename, groups, drag-to-reparent, and right-click **Zoom to** / **Delete**. +- Deleting a Shape List solid removes Workbench rows that link to it. Deleting a Workbench row does not delete the Design body. +- The 3D view shows Shape List solids in Sketch and Design, and Workbench solids in the Workbench task (so the two trees do not overlap). + +The window can be closed with its close button; use **View -> Workbench List** again to show it. + ## File Operations ### Supported Formats @@ -221,7 +237,7 @@ Press Esc to cancel the current action or step back to a broader mode - **If something is in progress:** Esc cancels it and discards the change. Examples: cancel a line you are drawing, revert an unconfirmed [move](#shape-move-tool-g)/[rotate](#shape-rotate-tool-r)/[scale](#shape-scale-tool-s), cancel [extrude](#extrude-sketch-face-tool-e) preview, clear the distance or angle input dialog. - **If nothing is in progress:** Esc steps the application to the **parent mode** (one level up): - From a **sketch tool** (e.g. Add line, Add circle, Operation axis) -> **Sketch inspection mode**. - - From **Sketch inspection**, **Design** tools ([Extrude](#extrude-sketch-face-tool-e), [Scale](#shape-scale-tool-s), [Chamfer](#other-feature-operations) (C), [Fillet](#other-feature-operations) (F), [Polar duplicate](#shape-polar-duplicate-tool), [Create sketch from face](usage-sketch.md#create-sketch-from-planar-face-tool)), or Design **Inspection** -> **Design inspection**. + - From **Sketch inspection**, **Design** tools ([Move](#shape-move-tool-g), [Rotate](#shape-rotate-tool-r), [Align shafts](#align-shafts-tool-j), [Extrude](#extrude-sketch-face-tool-e), [Scale](#shape-scale-tool-s), [Chamfer](#other-feature-operations) (C), [Fillet](#other-feature-operations) (F), [Polar duplicate](#shape-polar-duplicate-tool), [Create sketch from face](usage-sketch.md#create-sketch-from-planar-face-tool)), or Design **Inspection** -> **Design inspection**. - From a **Workbench** tool ([Move](#shape-move-tool-g), [Rotate](#shape-rotate-tool-r), [Align shafts](#align-shafts-tool-j)) -> **Workbench** idle (not Design inspection). Use the task buttons to jump between Sketch, Design, and Workbench. So repeated Esc from a sketch drawing tool first cancels the current element, then returns to Sketch inspection, then to Design inspection. Workbench tools step back to Workbench idle. @@ -382,7 +398,7 @@ More detail: [Sketch snapping](usage-sketch.md#sketch-snapping) in the sketch gu ![Shape Move Tool](res/icons/Assembly_AxialMove.png) -The shape move tool allows you to reposition selected shapes in the 3D viewer with precision and flexibility. +The shape move tool allows you to reposition selected shapes in the 3D viewer with precision and flexibility. In **Design**, the move is baked into the body (use this to line solids up for cut / fuse / common). In **Workbench**, only that instance's placement changes; the Design body stays put. G uses whichever task you are already in. **Features:** @@ -415,7 +431,7 @@ While moving a shape, you can press Tab to activate a floating distan ![Shape Rotate Tool](res/icons/Draft_Rotate.png) -The shape rotate tool enables precise rotation of selected shapes around a specified axis in the 3D viewer. +The shape rotate tool enables precise rotation of selected shapes around a specified axis in the 3D viewer. In **Design**, rotation is baked into the body (CSG setup). In **Workbench**, only the instance pose changes. R uses the current task. **Features:** @@ -492,7 +508,7 @@ The shape scale tool allows you to uniformly scale selected shapes around a pivo ![Align Shafts Tool](res/icons/Assembly_Move.png) -Place a shaft into a hole (or the reverse) by aligning two cylindrical faces, sliding along the shared axis for insert depth, and optionally clocking rotation about that axis (splines, keyways). This is a one-shot transform that bakes into the solid geometry (same as Move / Rotate), not a persistent assembly mate. +Place a shaft into a hole (or the reverse) by aligning two cylindrical faces, sliding along the shared axis for insert depth, and optionally clocking rotation about that axis (splines, keyways). In **Design**, the align is baked into the body (same as Move / Rotate, for CSG setup). In **Workbench**, only that instance's placement changes. J uses whichever task you are already in. This is not a persistent assembly mate. **Features:** @@ -755,35 +771,35 @@ Mode, file, and edit chords in the **General Operations** and **Modeling Shortcu ### Modeling Shortcuts -| | | -| --------------------------------------------: | ----------------------------------------- | -| G | Move mode (switches to Workbench) | -| R | Rotate mode (switches to Workbench) | -| S | Scale mode (switches to Design) | -| J | Align shafts mode (switches to Workbench) | -| E | Extrude mode | -| C | Chamfer mode | -| F | Fillet mode | -| D | Dimension tool (sketch) | -| I | Sketch inspection | -| P | Sketch from planar face | -| Shift+A | Operation axis | -| N | Add node | -| L | Add line edge | -| Shift+L | Add multi-line edge | -| A | Add arc | -| Q | Add square | -| B | Add rectangle (two points) | -| Shift+B | Add rectangle (center) | -| O | Add circle | -| Shift+O | Add circle (three points) | -| U | Add slot | -| Shift+U | Add bone | -| Shift+P | Polar duplicate | -| Shift+X | Cross-section | -| Ctrl+Shift+C | Shape cut | -| Ctrl+Shift+F | Shape fuse | -| Ctrl+Shift+M | Shape common | +| | | +| --------------------------------------------: | ------------------------------------------------ | +| G | Move (Design or Workbench, current task) | +| R | Rotate (Design or Workbench, current task) | +| S | Scale mode (Design) | +| J | Align shafts (Design or Workbench, current task) | +| E | Extrude mode | +| C | Chamfer mode | +| F | Fillet mode | +| D | Dimension tool (sketch) | +| I | Sketch inspection | +| P | Sketch from planar face | +| Shift+A | Operation axis | +| N | Add node | +| L | Add line edge | +| Shift+L | Add multi-line edge | +| A | Add arc | +| Q | Add square | +| B | Add rectangle (two points) | +| Shift+B | Add rectangle (center) | +| O | Add circle | +| Shift+O | Add circle (three points) | +| U | Add slot | +| Shift+U | Add bone | +| Shift+P | Polar duplicate | +| Shift+X | Cross-section | +| Ctrl+Shift+C | Shape cut | +| Ctrl+Shift+F | Shape fuse | +| Ctrl+Shift+M | Shape common | ### View navigation @@ -922,11 +938,13 @@ Contributors should follow **[ezycad_code_style.md](ezycad_code_style.md)** for ### Design - ![User](res/icons/User.png) - Inspection mode - ![Macro_FaceToSketch_48](res/icons/Macro_FaceToSketch_48.png) - Create sketch from planar face +- ![Assembly_AxialMove](res/icons/Assembly_AxialMove.png) - Shape move (G, bake body) +- ![Draft_Rotate](res/icons/Draft_Rotate.png) - Shape rotate (R, bake body) - ![Part_Scale](res/icons/Part_Scale.png) - Shape scale (S) ### Workbench -- ![Assembly_AxialMove](res/icons/Assembly_AxialMove.png) - Shape move (G) -- ![Draft_Rotate](res/icons/Draft_Rotate.png) - Shape rotate (R) +- ![Assembly_AxialMove](res/icons/Assembly_AxialMove.png) - Shape move (G, instance placement) +- ![Draft_Rotate](res/icons/Draft_Rotate.png) - Shape rotate (R, instance placement) - ![Assembly_Move](res/icons/Assembly_Move.png) - Align shafts (J) ### Sketch Tools diff --git a/res/ezycad_settings.json b/res/ezycad_settings.json index 2ece838d..282e5f2e 100644 --- a/res/ezycad_settings.json +++ b/res/ezycad_settings.json @@ -137,6 +137,7 @@ "show_python_console": false, "show_settings_dialog": false, "show_shape_list": true, + "show_workbench_list": true, "show_sketch_dimensions": true, "show_sketch_list": true, "sketch_edge_color": [ diff --git a/src/doc/gui.md b/src/doc/gui.md index eeba7682..edcf0429 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -10,7 +10,7 @@ Maintainers: update this file when GUI input routing, mode/options behavior, set Typical responsibilities: -- ImGui frame: menu bar, dock space (passthrough central node for 3D input), task+tools toolbar, Sketch List, Shape List, Options, Settings, dist/angle popups. +- ImGui frame: menu bar, dock space (passthrough central node for 3D input), task+tools toolbar, Sketch List, Shape List, Workbench List, Options, Settings, dist/angle popups. - Mode switching (`Mode` enum in [`mode.h`](../mode.h)), **task** (`Task`: Sketch / Design / Workbench), and parent-mode Esc behavior. - Persisted preferences (`ezycad_settings.json` via [`gui_settings.cpp`](../gui_settings.cpp)). - Project I/O (`.ezy` load/save, import/export dialogs; **File -> Import** confirms STEP/PLY with **Import as** for hierarchy / flat / union). STEP **Import into project** 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). @@ -40,13 +40,13 @@ Typical responsibilities: | 3 | `sync_sketch_add_mid_pt_edges_if_applicable_()` | | 4 | Update toolbar active state | -`set_parent_mode()` maps each tool mode via `GUI::parent_mode_of` (see parent map in `gui_mode.cpp`): sketch drawing tools -> `Sketch_inspection`; Design tools (including `Scale`) -> `Design_inspection`; Workbench Move / Rotate / Align shafts -> `Workbench_inspection`. Undo/redo uses the same map so stored `Workbench_move` / `Workbench_rotate` / `Scale` / `Workbench_shaft_align` / `Shape_set_frame` restore their parent instead of re-entering a free-drag or face-pick tool (see [undo-redo.md](undo-redo.md#mode-restoration)). `Workbench_inspection` parents to itself (Esc does not jump to `Design_inspection`). +`set_parent_mode()` maps each tool mode via `GUI::parent_mode_of` (see parent map in `gui_mode.cpp`): sketch drawing tools -> `Sketch_inspection`; Design tools (`Design_move` / `Design_rotate` / `Design_shaft_align` / `Scale` / ...) -> `Design_inspection`; Workbench Move / Rotate / Align shafts -> `Workbench_inspection`. Undo/redo uses the same map so stored transform tools restore their parent instead of re-entering a free-drag tool (see [undo-redo.md](undo-redo.md#mode-restoration)). `Workbench_inspection` parents to itself (Esc does not jump to `Design_inspection`). ### Tasks / toolbar (filter) -The chrome toolbar is one ImGui window with two regions: **task** switcher (Sketch / Design / Workbench) then a `|` then **tools** for `task_of(get_mode())` only (option 1: hide unrelated modes). Clicking a task button calls `set_mode(idle_mode_of(task))`. Workbench idle (`Mode::Workbench_inspection`) is the task button only — not repeated on the tools row. Hotkeys still call `set_mode` on the tool; that updates the active task so the tools row follows (G/R/J from Sketch/Design enter Workbench; S enters Design Scale). Helpers: `task_of`, `idle_mode_of`, `is_workbench_mode`, `is_workbench_transform_mode`, `is_shape_browse_mode` in [`mode.h`](../mode.h). +The chrome toolbar is one ImGui window with two regions: **task** switcher (Sketch / Design / Workbench) then a `|` then **tools** for `task_of(get_mode())` only (option 1: hide unrelated modes). Clicking a task button calls `set_mode(idle_mode_of(task))`. Workbench idle (`Mode::Workbench_inspection`) is the task button only — not repeated on the tools row. `mode.move` / `mode.rotate` / `mode.cyl_align` pick the Design or Workbench pair from the current task. `mode.scale` is always Design `Scale`. Helpers: `task_of`, `idle_mode_of`, `is_move_mode`, `is_rotate_mode`, `is_shaft_align_mode`, `is_workbench_mode` in [`mode.h`](../mode.h). -`Mode::Design_inspection`, `Mode::Workbench_inspection`, and the three task buttons are toolbar-only (no remappable `Gui_action`). Transform tools keep `mode.move` / `mode.rotate` / `mode.scale` / `mode.cyl_align`. Retired script names `Normal` / `Sketch_inspection_mode` / `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align` still parse via `mode_from_string`. +`Mode::Design_inspection`, `Mode::Workbench_inspection`, and the three task buttons are toolbar-only (no remappable `Gui_action`). `Design_move` / `Design_rotate` / `Design_shaft_align` share `mode.move` / `mode.rotate` / `mode.cyl_align` with the Workbench pair (no extra `Gui_action`). Retired script names `Normal` / `Sketch_inspection_mode` / `Move` (`Design_move`) / `Rotate` (`Design_rotate`) / `Workbench_scale` / `Shape_shaft_align` still parse via `mode_from_string`. ### New mode or toolbar command (hotkeys) @@ -62,11 +62,11 @@ When adding a `Mode` to [`mode.h`](../mode.h) (`EZY_MODE_LIST`), a toolbar butto Pick a default that does not collide with existing `c_actions` chords or fixed keys (Esc, Enter, Tab, digits, unmodified X/Y/Z axis toggles — reserved via `is_reserved_chord`). Also wire parent-mode / Options / doc URL maps as usual for new modes. -`Occt_view::on_mode` also sets `AIS_ViewController::SetAllowHighlight(false)` for `Workbench_move` / `Workbench_rotate` / `Scale` (and `ClearDetected`) so idle mouse moves do not run dynamic `MoveTo` while transform preview leaves selection BVHs at the pre-transform pose; other modes restore highlight. Orbit/pan still receive `UpdateMousePosition` when buttons are held. When LMB finalizes an active transform (operands loaded), `on_mouse_button` skips `PressMouseButton` / `ReleaseMouseButton` for that click so AIS `SelectDetected` on release cannot replace the restored multi-selection with the single shape under the cursor. +`Occt_view::on_mode` also sets `AIS_ViewController::SetAllowHighlight(false)` for `is_move_mode` / `is_rotate_mode` / `Scale` (and `ClearDetected`) so idle mouse moves do not run dynamic `MoveTo` while transform preview leaves selection BVHs at the pre-transform pose; other modes restore highlight. Orbit/pan still receive `UpdateMousePosition` when buttons are held. When LMB finalizes an active transform (operands loaded), `on_mouse_button` skips `PressMouseButton` / `ReleaseMouseButton` for that click so AIS `SelectDetected` on release cannot replace the restored multi-selection with the single shape under the cursor. -Because the faint/selection-mode redisplay `Erase`s shapes (dropping the AIS selection), `on_mode` snapshots the entered selection for `Workbench_move` / `Workbench_rotate` / `Scale` / `Shape_cross_section` and restores it via `Occt_view::set_selected_shps` after `sync_sketch_shape_faint_style()`. The same helper restores the operands when a transform tool finishes (see `restore_operation_selection_` in [shape.md](shape.md)); `Occt_view::cancel` therefore treats those transform modes as already handled instead of switching mode again. Transform tools are also seeded via `Shp_move` / `Shp_rotate` / `Shp_scale::begin(enter_selection)` so multi-select operands do not depend on AIS selection surviving the mode switch (`ensure_operation_shps_` still falls back to the AIS selection when the seed was empty). Move/Rotate/Scale use `TopAbs_SHAPE` (whole-object) selection mode. +Because the faint/selection-mode redisplay `Erase`s shapes (dropping the AIS selection), `on_mode` snapshots the entered selection for Move / Rotate / Scale / `Shape_cross_section` and restores it via `Occt_view::set_selected_shps` after `sync_sketch_shape_faint_style()`. The same helper restores the operands when a transform tool finishes (see `restore_operation_selection_` in [shape.md](shape.md)); `Occt_view::cancel` therefore treats those transform modes as already handled instead of switching mode again. Transform tools are also seeded via `Shp_move` / `Shp_rotate` / `Shp_scale::begin(enter_selection)` so multi-select operands do not depend on AIS selection surviving the mode switch (`ensure_operation_shps_` still falls back to the AIS selection when the seed was empty). Move/Rotate/Scale use `TopAbs_SHAPE` (whole-object) selection mode. -`Occt_view::on_mode` ends with `sync_sketch_shape_faint_style()`: while `is_sketch_mode`, document shapes follow **Options/Settings** `gui.sketch_shape_faint_enabled` (master) plus `gui.sketch_shape_faint_style` (**0** hide / **1** ghost / **2** wire) and `gui.sketch_shape_faint_opacity` (ghost). Outside sketch mode, faint overrides clear and shapes show at full strength (unless Shape List **Hide all**). New shapes call the same sync from `add_shp_` / `insert_shape_rec`. The **Faint shapes** checkbox lives in `options_sketch_common_` (every sketch tool). +`Occt_view::on_mode` ends with `sync_sketch_shape_faint_style()`: while `is_sketch_mode`, Design shapes follow **Options/Settings** `gui.sketch_shape_faint_enabled` (master) plus `gui.sketch_shape_faint_style` (**0** hide / **1** ghost / **2** wire) and `gui.sketch_shape_faint_opacity` (ghost). Workbench modes hide `m_shps` and show `m_wbk_shps`; Sketch/Design hide workbench instances. **Hide all** is per list. New shapes call the same sync from `add_shp_` / `add_wbk_shp_` / `insert_shape_rec`. The **Faint shapes** checkbox lives in `options_sketch_common_` (every sketch tool). ### UI verbosity @@ -94,7 +94,7 @@ GUI (gui.h / gui.cpp) +-- gui_mode.cpp set_mode, on_key, Options panel per Mode +-- gui_hotkeys.* remappable Gui_action <-> Key_chord map +-- gui_add.cpp Add menu dialogs (primitives, new sketch) - +-- gui_shp_pane.cpp Shape List pane, Shape info dialog, ui.shapeList + +-- gui_shp_pane.cpp Shape List + Workbench List panes, Shape info, ui.shapeList / ui.workbenchList +-- gui_settings.cpp Settings dialog, load/save ezycad_settings.json | +-- Occt_view (gui_occt_view.h / gui_occt_view.cpp / .inl) @@ -158,24 +158,24 @@ Overlay popups (`FloatEdit`, `AngleEdit`, `MessageStatus`, modals) keep `NoSaved Remappable chords live in `Gui_hotkeys` (`gui_hotkeys.h` / `.cpp`), owned by `GUI::m_hotkeys`. Stable action ids (e.g. `mode.move`, `mode.add_edge`, `cmd.shape_cut`, `edit.undo`) map to `Key_chord { key, mods }`. Persistence: `gui.hotkeys` in `ezycad_settings.json` as human-readable strings (`"G"`, `"Shift+L"`, `"Ctrl+Shift+C"`); missing keys merge to built-in defaults. On load, `merge_from_json` drops reserved/invalid chords, then resolves duplicate chords: later actions reset to factory; if that factory chord is still held by an earlier remap, that earlier row is also restored to factory (defaults are unique, so `action_for` never keeps a silent collision). Settings **Keyboard shortcuts** captures the next `GLFW_PRESS` (Esc cancels; `set_chord` rejects conflicts and **reserved** fixed chords via `is_reserved_chord`). Per-row **Reset** calls `reset_action` (factory chord via `set_chord`, so duplicates are rejected with the same inline conflict message). Capture is cleared when Settings closes. Toolbar tooltips for remappable modes and boolean commands are rebuilt via `sync_toolbar_hotkey_tooltips_()`. -| Input | Condition | Handler | -| --------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | -| `+` / `-` / numpad +/- | No Ctrl/Alt | `Occt_view::zoom_view_wheel_notches` | -| Shift + 4/6 / arrows / numpad 4/6 | No Ctrl/Alt | `Occt_view::roll_view_z_deg` | -| Numpad 5 | No modifiers | `Occt_view::snap_view_to_nearest_standard_axis` | -| Numpad 2/4/6/8 | No modifiers | `Occt_view::orbit_view_screen_step_deg` | -| Hotkey capture active | Settings | `try_capture_hotkey_press_` (assign / Esc cancel / conflict message) | -| `1`-`9` / numpad `1`-`9` | `is_shape_browse_mode` | `set_shp_selection_mode` (TopAbs enum index); fixed; Design Inspection + Workbench idle | -| Esc | | cancel capture if listening; else `cancel_underlay_calib_`, `Occt_view::cancel`, hide dist/angle edit | -| Tab | not Workbench move/rotate/align | `Occt_view::dimension_input`; those modes: `break` into mode handlers | -| Shift+Tab | not Workbench move/rotate/align | `Occt_view::angle_input`; those modes: `break` into mode handlers (cyl-align: clock/angle) | -| Enter | not Workbench rotate/align | hide edits, `Occt_view::on_enter`; those modes: `break` into mode handlers (finalize) | -| Delete / Backspace | | `Occt_view::delete_selected` (fixed aliases; remapping `edit.delete` does not remove these) | -| Ctrl+Shift+Z | | `Occt_view::redo` (fixed second redo; remappable `edit.redo` defaults to Ctrl+Y) | -| Remappable chord | `m_hotkeys` hit | `dispatch_hotkey_action_` (`Gui_action`: sketch/shape modes, booleans, delete, copy/paste, file, undo/redo) | -| Move-mode keys | `Mode::Workbench_move` | `on_key_move_mode_` (axis constraints X/Y/Z); hardcoded | -| Rotate-mode keys | `Mode::Workbench_rotate` | `on_key_rotate_mode_` (axis pick, Tab angle); hardcoded | -| Align-shafts keys | `Mode::Workbench_shaft_align` | `on_key_cyl_align_mode_` (Tab depth, Shift+Tab clock/angle, Enter finalize); hardcoded | +| Input | Condition | Handler | +| --------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `+` / `-` / numpad +/- | No Ctrl/Alt | `Occt_view::zoom_view_wheel_notches` | +| Shift + 4/6 / arrows / numpad 4/6 | No Ctrl/Alt | `Occt_view::roll_view_z_deg` | +| Numpad 5 | No modifiers | `Occt_view::snap_view_to_nearest_standard_axis` | +| Numpad 2/4/6/8 | No modifiers | `Occt_view::orbit_view_screen_step_deg` | +| Hotkey capture active | Settings | `try_capture_hotkey_press_` (assign / Esc cancel / conflict message) | +| `1`-`9` / numpad `1`-`9` | `is_shape_browse_mode` | `set_shp_selection_mode` (TopAbs enum index); fixed; Design Inspection + Workbench idle | +| Esc | | cancel capture if listening; else `cancel_underlay_calib_`, `Occt_view::cancel`, hide dist/angle edit | +| Tab | not Design/Workbench move/rotate/align | `Occt_view::dimension_input`; those modes: `break` into mode handlers | +| Shift+Tab | not Design/Workbench move/rotate/align | `Occt_view::angle_input`; those modes: `break` into mode handlers (cyl-align: clock/angle) | +| Enter | not Design/Workbench rotate/align | hide edits, `Occt_view::on_enter`; those modes: `break` into mode handlers (finalize) | +| Delete / Backspace | | `Occt_view::delete_selected` (fixed aliases; remapping `edit.delete` does not remove these) | +| Ctrl+Shift+Z | | `Occt_view::redo` (fixed second redo; remappable `edit.redo` defaults to Ctrl+Y) | +| Remappable chord | `m_hotkeys` hit | `dispatch_hotkey_action_` (`Gui_action`: sketch/shape modes, booleans, delete, copy/paste, file, undo/redo) | +| Move-mode keys | `Design_move` / `Workbench_move` | `on_key_move_mode_` (axis constraints X/Y/Z); hardcoded | +| Rotate-mode keys | `Design_rotate` / `Workbench_rotate` | `on_key_rotate_mode_` (axis pick, Tab angle); hardcoded | +| Align-shafts keys | `Design_shaft_align` / `Workbench_shaft_align` | `on_key_cyl_align_mode_` (Tab depth, Shift+Tab clock/angle, Enter finalize); hardcoded | Default remappable chords include G/R/S/J/E/C/F/D shape tools; sketch tools N/L/A/Q/B/O/U/I/P and Shift variants; Shift+P polar, Shift+X cross-section; Ctrl+Shift+C/F/M booleans; Shift+D delete; Ctrl+C / Ctrl+V copy/paste (in-app shape clipboard); Ctrl+N/O/S; Ctrl+Z / Ctrl+Y. Unmodified X/Y/Z are reserved for Move/Rotate axis toggles (`is_reserved_chord`); Shift+X remains free for cross-section. Remappable keys must pass `is_bindable_key` (letters, digits, Space, and named keys that round-trip in settings JSON); punctuation such as `,` / `.` and numpad keys are rejected. Settings **Keyboard shortcuts** has a `?` to `doc_urls::k_hotkeys` ([usage-settings.md#keyboard-shortcuts](../../docs/usage-settings.md#keyboard-shortcuts)). @@ -187,10 +187,10 @@ See also [`src/doc/sketch.md`](sketch.md) and [`src/doc/shape.md`](shape.md) for | `Mode` | Delegate | | --------------------------------------------------------- | -------------------------------------------------------------------- | -| `Workbench_move` | `shp_move().move_selected` | -| `Workbench_rotate` | `shp_rotate().rotate_selected` | +| `Design_move` / `Workbench_move` | `shp_move().move_selected` | +| `Design_rotate` / `Workbench_rotate` | `shp_rotate().rotate_selected` | | `Scale` | `shp_scale().scale_selected` | -| `Workbench_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | +| `Design_shaft_align` / `Workbench_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | | `Shape_set_frame` | `options_shape_set_frame_mode_` (Shape List only; no toolbar/hotkey) | | `Shape_polar_duplicate` | `shp_polar_dup().move_point` | | Sketch tool modes (line, arc, rect, dim, axis, bone, ...) | `curr_sketch().sketch_pt_move` | @@ -220,19 +220,19 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio `GUI::options_()` switches on `get_mode()`: -| `Mode` | Options function | -| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `Design_inspection` / `Workbench_inspection` | `options_normal_mode_` (selection filter, orthographic) | -| `Workbench_move` / `Workbench_rotate` / `Scale` | `options_*_mode_` (Local/World space, constraints, axis) | -| `Workbench_shaft_align` | `options_shape_shaft_align_mode_` (Flip direction, Clock rotation; pick / depth / clock help) | -| `Shape_chamfer` / `Shape_fillet` | mode + radius/distance | -| `Shape_polar_duplicate` | angle, count, rotate/combine, **Dup** button | -| `Shape_cross_section` | local XY/XZ/YZ, invert normal, hide back side, show section outline, bbox-ranged offset, Clip, Cross section sketch | -| `Sketch_inspection` | `options_sketch_common_` | -| Each sketch tool mode | Matching `options_sketch_*_mode_` | -| `Sketch_operation_axis` | Mirror / Revolve / Clear axis | -| `Sketch_face_extrude` | **Extrude** (Both sides, Twist, material) above Sketch options; help mentions Settings fast preview | -| `Sketch_add_bone` | Center / radius / total-length nodes, holes; None after waist commits; `DEV_MODE` **Debug vis** | +| `Mode` | Options function | +| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `Design_inspection` / `Workbench_inspection` | `options_normal_mode_` (selection filter, orthographic) | +| `Design_move` / `Workbench_move` / `Design_rotate` / `Workbench_rotate` / `Scale` | `options_*_mode_` (Local/World space, constraints, axis) | +| `Design_shaft_align` / `Workbench_shaft_align` | `options_shape_shaft_align_mode_` (Flip direction, Clock rotation; pick / depth / clock help) | +| `Shape_chamfer` / `Shape_fillet` | mode + radius/distance | +| `Shape_polar_duplicate` | angle, count, rotate/combine, **Dup** button | +| `Shape_cross_section` | local XY/XZ/YZ, invert normal, hide back side, show section outline, bbox-ranged offset, Clip, Cross section sketch | +| `Sketch_inspection` | `options_sketch_common_` | +| Each sketch tool mode | Matching `options_sketch_*_mode_` | +| `Sketch_operation_axis` | Mirror / Revolve / Clear axis | +| `Sketch_face_extrude` | **Extrude** (Both sides, Twist, material) above Sketch options; help mentions Settings fast preview | +| `Sketch_add_bone` | Center / radius / total-length nodes, holes; None after waist commits; `DEV_MODE` **Debug vis** | ### Options panel layout (sketch tools) @@ -249,22 +249,24 @@ Shared sketch controls (snap, faint shapes) live in `options_sketch_shared_contr ## ImGui frame order (`render_gui`) -| Order | Function | Purpose | -| ----- | ------------------------------------------------------------- | ----------------------------------------------- | -| 1 | `flush_view_events` | Sync camera before UI uses projection | -| 2 | `menu_bar_`, `toolbar_` | File / View / mode tools | -| 3 | `dist_edit_`, `angle_edit_` | Floating numeric entry | -| 4 | `sketch_list_`, `sketch_properties_dialog_` | Sketch List + underlay/properties | -| 5 | `shape_list_`, `shape_info_dialog_`, `file_inspector_dialog_` | Shape List + info (`gui_shp_pane.cpp`) + Import | -| 6 | `options_` | Mode-specific Options pane | -| 7 | `message_status_window_`, `about_dialog_` | Status + About | -| 8 | `add_*_dialog_` | Primitive / sketch creation popups | -| 9 | `log_window_`, consoles, `settings_`, `dbg_` | Log, Lua/Python, Settings | +| Order | Function | Purpose | +| ----- | -------------------------------------------------------------------------------- | ------------------------------------------- | +| 1 | `flush_view_events` | Sync camera before UI uses projection | +| 2 | `menu_bar_`, `toolbar_` | File / View / mode tools | +| 3 | `dist_edit_`, `angle_edit_` | Floating numeric entry | +| 4 | `sketch_list_`, `sketch_properties_dialog_` | Sketch List + underlay/properties | +| 5 | `shape_list_`, `workbench_list_`, `shape_info_dialog_`, `file_inspector_dialog_` | Shape List + Workbench List + info + Import | +| 6 | `options_` | Mode-specific Options pane | +| 7 | `message_status_window_`, `about_dialog_` | Status + About | +| 8 | `add_*_dialog_` | Primitive / sketch creation popups | +| 9 | `log_window_`, consoles, `settings_`, `dbg_` | Log, Lua/Python, Settings | `GUI::show_message` drives the transient status toast (`message_status_window_`) and also appends via `log_message`. The toast color comes from `Status_msg` (`Success`, `Info`, `Constraint`, `Warning`, `Error`; default `Info`). `show_status` maps `Result_status` (`User_error` -> Constraint, `Error` / `Topo_error` -> Error, ok -> Info). `show_error_dialog` logs `title: message` once and toasts the title as Error. Sketch List expand **Faces**: each face row supports **`E`** and right-click **Extrude** via `GUI::sketch_list_extrude_face_` (`set_mode(Sketch_face_extrude)` + `Occt_view::begin_sketch_face_extrude` / `Shp_extrude::begin_face_extrude`). Hovering a **Faces**, **Edges**, or **Nodes** row calls `Occt_view::set_sketch_list_hover_{face,edge,node}` (temporarily displays the AIS when hidden outside sketch modes; uses `Graphic3d_ZLayerId_Topmost` so solids do not occlude the highlight). +**Workbench List:** `workbench_list_()` in `gui_shp_pane.cpp` uses the same row drawer on `m_wbk_shps` (`EZY_WBK_ID` payload; accepts `EZY_SHAPE_ID` drops as `add_to_workbench`). `.ezy` `ui.workbenchList` mirrors `ui.shapeList`. Design **Add to Workbench** (toolbar button, context menu, drag). + **Shape List outliner:** `shape_list_` (`gui_shp_pane.cpp`) draws a tree of document shapes/groups via `shape_children(0)` and recursive `gui_shp_detail::Shape_list_row_drawer` rows (`TreeNodeEx`). Fixed-width vis/disp/mat columns are on the left; the name column stretches on the right with tree indent (`IndentEnable` on name only). An empty pad row after the last item is a drag-drop target for document root (`reparent_shape(..., 0)`); it shows a "Move to root" hint while dragging. Groups support expand/collapse (`ui.shapeList.expanded`), drag-drop reparent (`EZY_SHAPE_ID` payload), Group / New group / Ungroup, and cascade delete. Clicking a group sets `Occt_view::current_group_id` (including empty groups) and selects descendant solids; clicking a solid selects it and sets current group to its parent. New primitives/extrudes/revolves parent under the current group. Ctrl+click multi-selects. Row highlight for **selection** follows AIS only; the **current group** uses a weaker tint so it is not mistaken for a selected subtree after Alt-drag rectangle select clears AIS. Copy/paste (Ctrl+C/V) deep-copies the current group subtree when the selection matches that group's descendant solids. Row context menu opens on right-click anywhere on the table row (not only `InputText` / **M**). **Zoom to** calls `Occt_view::fit_shapes_in_view` (solid or group descendant solids; keeps camera orientation). Hover uses `set_shape_list_hover` on leaf solids only. `ui.shapeList.currentGroupId` is persisted in `.ezy`. **Sketch List UI in the project file:** `GUI::serialized_project_json_` writes `ui.sketchList` (scroll Y plus per-sketch `rows` keyed by sketch `id`: `expanded`, `dimensions`, `nodes`, `edges`, `faces`). `GUI::on_file` restores via `apply_sketch_list_ui_from_json_`. Subsection open state is app-owned (`Sketch_list_row_ui` + `SetNextItemOpen`), not ImGui ini storage. @@ -273,13 +275,13 @@ Sketch List expand **Faces**: each face row supports **`E`** and right-click **E ## Settings and persistence -| File | Role | -| --------------------------- | ------------------------------------------------------------------- | -| `gui_shp_pane.cpp` | Shape List, Shape info dialog, `.ezy` `ui.shapeList` | -| `gui_settings.cpp` | Settings dialog UI; read/write `ezycad_settings.json` | -| `save_occt_view_settings` | Persists `gui.*`, `occt_view.*`, pane visibility, last project path | -| `load_occt_view_settings_` | Called from `GUI::init` | -| `occt_view_settings_json()` | Scripting API for settings blob | +| File | Role | +| --------------------------- | ----------------------------------------------------------------------------------- | +| `gui_shp_pane.cpp` | Shape List + Workbench List, Shape info, `.ezy` `ui.shapeList` / `ui.workbenchList` | +| `gui_settings.cpp` | Settings dialog UI; read/write `ezycad_settings.json` | +| `save_occt_view_settings` | Persists `gui.*`, `occt_view.*`, pane visibility, last project path | +| `load_occt_view_settings_` | Called from `GUI::init` | +| `occt_view_settings_json()` | Scripting API for settings blob | Sketch edge/face display colors live under `gui.sketch_edge_*` / `gui.sketch_face_*` and are applied live via `Sketch_annotation_refresh::edge_face_style`. Sketch-mode shape ghost/wire uses `gui.sketch_shape_faint_style` / `gui.sketch_shape_faint_opacity` via `Occt_view::sync_sketch_shape_faint_style`. 3D shape selection highlight uses `gui.shape_selection_color` applied through `Occt_view::apply_shape_selection_style` (`AIS_InteractiveContext::SelectionStyle`). AIS curve tessellation uses `gui.curve_deviation_angle_deg` via `Occt_view::apply_curve_deviation` (`Prs3d_Drawer::SetDeviationAngle`). Settings collapsing-header open state is stored in `gui.settings_headers` (Sketch nests **Appearance**, **Dimensions**, **Nodes**, **Snap**, **Underlay**; also **Keyboard shortcuts** / `hotkeys`). Remappable chords: `gui.hotkeys` object via `Gui_hotkeys::to_json` / `merge_from_json`. diff --git a/src/doc/script.md b/src/doc/script.md index 2bd9424b..7ac3595f 100644 --- a/src/doc/script.md +++ b/src/doc/script.md @@ -106,15 +106,15 @@ Aliases: global `view` == `ezy.view`; `Shp` == `ezy.Shp`; `view.add_sketch` / `f ### `ezy` (root) -| Method | C++ delegate | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `ezy.log(msg)` | `GUI::log_message` + console history | -| `ezy.msg(text [, kind])` | `GUI::show_message` (toast + Log) | -| `ezy.get_mode()` | `GUI::get_mode()` -> mode name string (`c_mode_strs`; Workbench tools are `Workbench_move` etc.; Scale is `Scale`) | -| `ezy.set_mode(name)` | `GUI::set_mode(mode_from_string(...))` (aliases: `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align`) | -| `ezy.save_occt_view_settings()` | `GUI::save_occt_view_settings()` | -| `ezy.occt_view_settings_json()` | `GUI::occt_view_settings_json()` | -| `ezy.help()` / global `help()` | Prints binding summary to console | +| Method | C++ delegate | +| ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `ezy.log(msg)` | `GUI::log_message` + console history | +| `ezy.msg(text [, kind])` | `GUI::show_message` (toast + Log) | +| `ezy.get_mode()` | `GUI::get_mode()` -> mode name string (`c_mode_strs`; Design/Workbench Move are `Design_move` / `Workbench_move`) | +| `ezy.set_mode(name)` | `GUI::set_mode(mode_from_string(...))` (aliases: `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align`) | +| `ezy.save_occt_view_settings()` | `GUI::save_occt_view_settings()` | +| `ezy.occt_view_settings_json()` | `GUI::occt_view_settings_json()` | +| `ezy.help()` / global `help()` | Prints binding summary to console | Optional `kind` for `ezy.msg` is `success`, `info` (default), `constraint`, `warning`, or `error` (`parse_status_msg`). diff --git a/src/doc/shape.md b/src/doc/shape.md index 43d81237..a2c06a71 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -14,7 +14,7 @@ Unlike the sketch subsystem (2D coordinator + sub-modules), shape code is organi - Shared operation helpers in `Shp_operation_base` - One class or namespace per operation file (`Shp_fuse`, `shp_create`, `shp_info`, ...) -`Occt_view` owns the shape list (`m_shps`), constructs all operation objects, routes UI input, and handles JSON save/load for shapes. +`Occt_view` owns the Design shape list (`m_shps`) and the Workbench instance list (`m_wbk_shps`), constructs all operation objects, routes UI input, and handles JSON save/load for both. Typical uses: @@ -32,7 +32,8 @@ Typical uses: ### Lifetime and ownership -- Shapes are stored in `Occt_view::m_shps` (`std::list`). Access via `get_shapes()` or internal `add_shp_()`. +- Design shapes are stored in `Occt_view::m_shps`. Workbench instances live in `m_wbk_shps` (separate list). Access via `get_shapes()` / `get_workbench_shapes()`. Shared `allocate_shape_id()`. +- Workbench leaves are **links**: `source_id` + local geom `Inverse(trsf_from_frame(source.frame)) * source.Shape()`, instance `gp_Ax3` as placement (`trsf_from_frame` is local-to-world; do not use single-arg `gp_Trsf::SetTransformation`). `add_to_workbench` copies Design nodes; `sync_workbench_links` refreshes after Design geom replace (fillet/chamfer keep `Shape_id`). Design bake of move/rotate does not move instances. Workbench bake updates frame only. `.ezy` `workbench[]` stores ids / `sourceId` / frame (no BREP). Deleting a Design source cascade-deletes its links. - Each solid stores a `gp_Ax3` local frame (**Z** = primary axis, **Y** = up). 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. Shape List right-click toggles **Show axes** / **Show plane** / **Show up** AIS annotations (`frameDisplay` in `.ezy`), and can **Reset**, **Set from planar/cylindrical face** (`Mode::Shape_set_frame`, no toolbar/hotkey; planar origin = face area centroid), or **Flip up** / **Flip axis** via `Occt_view::set_shape_frame` (frame-only `Shape_geom_delta`). Call `clear_frame_display()` before `Remove` from the interactive context (user delete via `Occt_view::remove`, fillet/chamfer `replace_picked_shape_`, load, view dtor). `~Shp` only Nullifies those handles -- the context may already be dying. - `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`. @@ -68,11 +69,11 @@ Do not confuse this vector with `Occt_view::m_shps` (the document list). ### Transform preview vs bake -| Phase | Move / rotate / scale | Booleans / fillet / chamfer | -| ------------ | ------------------------------------------------------------------------- | --------------------------------------------------- | -| **Preview** | `SetLocalTransformation()`; `redisplay_operation_shps_after_transform_()` | N/A (immediate replace) | -| **Finalize** | `operation_shps_finalize_()` -> `bake_transform_into_geometry()` | New `Shp`; `delete_operation_shps_()`; `add_shp_()` | -| **Cancel** | `operation_shps_cancel_()` -> `ResetTransformation()` | N/A | +| Phase | Move / rotate / scale | Booleans / fillet / chamfer | +| ------------ | ---------------------------------------------------------------------------------------- | --------------------------------------------------- | +| **Preview** | `SetLocalTransformation()`; `redisplay_operation_shps_after_transform_()` | N/A (immediate replace) | +| **Finalize** | `operation_shps_finalize_()` -> `bake_transform_into_geometry()` (Workbench: frame only) | New `Shp`; `delete_operation_shps_()`; `add_shp_()` | +| **Cancel** | `operation_shps_cancel_()` -> `SetLocalTransformation(placement_trsf())` | N/A | `operation_shps_finalize_()` / `operation_shps_cancel_()` snapshot the operands; each tool calls `restore_operation_selection_()` as the last step of `finalize()` / `cancel()` (after `reset()`), which re-selects them via `Occt_view::set_selected_shps`. This is required because `reset()` switches mode and the faint/selection redisplay clears the AIS selection - without it a multi-shape move ended with only one shape selected. Finalize bakes with `bake_transform_into_geometry(shape, false)` and issues a single `UpdateCurrentViewer()`. @@ -167,21 +168,21 @@ Protected helpers used by all operation classes: `GUI` and `Occt_view` dispatch by `Mode` and toolbar actions: -| Mode / action | Mouse move (`GUI::on_mouse_pos`) | Left click | Tab / Enter | Esc (`Occt_view::cancel`) | -| ----------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------- | -| `Mode::Workbench_move` | `shp_move().move_selected` | `shp_move().finalize` | `shp_move().show_dist_edit` (`gui_mode`) | `shp_move().cancel` -> `Workbench_inspection` | -| `Mode::Workbench_rotate` | `shp_rotate().rotate_selected` | `shp_rotate().finalize` | `shp_rotate().show_angle_edit` | `shp_rotate().cancel` -> `Workbench_inspection` | -| `Mode::Scale` | `shp_scale().scale_selected` | `shp_scale().finalize` | -- | `shp_scale().cancel` -> `Design_inspection` | -| `Mode::Workbench_shaft_align` | `drag_depth` / `drag_twist` | Pick faces; LMB depth->clock if Clock rotation on; LMB/Enter finalize | Tab depth; Shift+Tab clock/angle; Enter finalize | `shp_cyl_align().cancel` -> `Workbench_inspection` | -| `Mode::Sketch_face_extrude` | `sketch_face_extrude(..., true)` | Pick face; with preview: `on_left_click()` locks height->twist if Twist on, else `finalize` (`GUI` skips re-pick) | Tab: height (`dimension_input`); Shift+Tab: twist angle in twist phase | `m_shp_extrude.cancel` | -| `Mode::Shape_fillet` | -- | `shp_fillet().add_fillet(..., Fillet_mode)` | -- | -- | -| `Mode::Shape_chamfer` | -- | `shp_chamfer().add_chamfer(..., Chamfer_mode)` | -- | -- | -| `Mode::Shape_polar_duplicate` | `shp_polar_dup().move_point` | `shp_polar_dup().add_point` | -- | `shp_polar_dup().reset` on mode change | -| `Mode::Shape_cross_section` | -- | -- | Auto-preview on enter / selection / Options; Clip replaces solids | Preview cleared; Clip commits | -| Fuse / cut / common (toolbar) | -- | `selected_fuse` / `selected_cut` / `selected_common` (one-shot) | -- | -- | -| Primitives (menu / script) | -- | `Occt_view::add_box`, `add_sphere`, ... | -- | -- | -| Revolve (sketch Options) | -- | `Occt_view::revolve_selected` -> `add_shp_` | -- | -- | -| Polar duplicate commit | -- | Options **Dup** button -> `shp_polar_dup().dup()` | -- | -- | +| Mode / action | Mouse move (`GUI::on_mouse_pos`) | Left click | Tab / Enter | Esc (`Occt_view::cancel`) | +| ---------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------- | +| `Design_move` / `Workbench_move` | `shp_move().move_selected` | `shp_move().finalize` | `shp_move().show_dist_edit` (`gui_mode`) | `shp_move().cancel` -> parent idle | +| `Design_rotate` / `Workbench_rotate` | `shp_rotate().rotate_selected` | `shp_rotate().finalize` | `shp_rotate().show_angle_edit` | `shp_rotate().cancel` -> parent idle | +| `Mode::Scale` | `shp_scale().scale_selected` | `shp_scale().finalize` | -- | `shp_scale().cancel` -> `Design_inspection` | +| `Design_shaft_align` / `Workbench_shaft_align` | `drag_depth` / `drag_twist` | Pick faces; LMB depth->clock if Clock rotation on; LMB/Enter finalize | Tab depth; Shift+Tab clock/angle; Enter finalize | `shp_cyl_align().cancel` -> parent idle | +| `Mode::Sketch_face_extrude` | `sketch_face_extrude(..., true)` | Pick face; with preview: `on_left_click()` locks height->twist if Twist on, else `finalize` (`GUI` skips re-pick) | Tab: height (`dimension_input`); Shift+Tab: twist angle in twist phase | `m_shp_extrude.cancel` | +| `Mode::Shape_fillet` | -- | `shp_fillet().add_fillet(..., Fillet_mode)` | -- | -- | +| `Mode::Shape_chamfer` | -- | `shp_chamfer().add_chamfer(..., Chamfer_mode)` | -- | -- | +| `Mode::Shape_polar_duplicate` | `shp_polar_dup().move_point` | `shp_polar_dup().add_point` | -- | `shp_polar_dup().reset` on mode change | +| `Mode::Shape_cross_section` | -- | -- | Auto-preview on enter / selection / Options; Clip replaces solids | Preview cleared; Clip commits | +| Fuse / cut / common (toolbar) | -- | `selected_fuse` / `selected_cut` / `selected_common` (one-shot) | -- | -- | +| Primitives (menu / script) | -- | `Occt_view::add_box`, `add_sphere`, ... | -- | -- | +| Revolve (sketch Options) | -- | `Occt_view::revolve_selected` -> `add_shp_` | -- | -- | +| Polar duplicate commit | -- | Options **Dup** button -> `shp_polar_dup().dup()` | -- | -- | Pre-selection: entering Move / Rotate / Scale / cross-section snapshots selected solids in `Occt_view::on_mode` and restores AIS selection via `set_selected_shps` after selection-mode / faint redisplay. Transform tools are also seeded with `begin(enter_selection)` so multi-select operands do not depend on AIS alone (`ensure_operation_shps_()` still falls back to the AIS selection when the seed was empty). diff --git a/src/doc/undo-redo.md b/src/doc/undo-redo.md index 50c635e9..38b7f40b 100644 --- a/src/doc/undo-redo.md +++ b/src/doc/undo-redo.md @@ -50,7 +50,7 @@ This prevents re-entrant history pushes while applying a step. ### Mode restoration -After applying a delta or reloading a snapshot, `Occt_view` restores a UI mode via `GUI::set_mode`. Stored `Workbench_move` / `Workbench_rotate` / `Scale` / `Workbench_shaft_align` / `Shape_set_frame` are mapped to their parent mode (`GUI::parent_mode_of`: Workbench idle or `Design_inspection`) so undo/redo does not re-enter a free-drag tool or face-pick with no target. Other stored modes are restored as-is. If the restored mode is `Sketch_inspection`, the sketch list pane is shown again. +After applying a delta or reloading a snapshot, `Occt_view` restores a UI mode via `GUI::set_mode`. Stored `Design_move` / `Design_rotate` / `Design_shaft_align` / `Workbench_move` / `Workbench_rotate` / `Workbench_shaft_align` / `Scale` / `Shape_set_frame` are mapped to their parent mode (`GUI::parent_mode_of`) so undo/redo does not re-enter a free-drag tool or face-pick with no target. Other stored modes are restored as-is. If the restored mode is `Sketch_inspection`, the sketch list pane is shown again. ### View camera diff --git a/src/gui.cpp b/src/gui.cpp index fce3a5e6..e6e74959 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -146,6 +146,7 @@ void GUI::render_gui() sketch_list_(); sketch_properties_dialog_(); shape_list_(); + workbench_list_(); shape_info_dialog_(); file_inspector_dialog_(); // Paint the busy modal before poll so WASM can show "Importing..." for a frame first. @@ -204,7 +205,10 @@ void GUI::initialize_toolbar_() {load_texture("res/icons/User.png"), true, "Inspection mode", Mode::Design_inspection, Task::Design}, {load_texture("res/icons/Macro_FaceToSketch_48.png"), false, "Create a sketch from planar face", Mode::Sketch_from_planar_face, Task::Design}, {load_texture("res/icons/Design456_Extrude.png"), false, "Extrude sketch face", Mode::Sketch_face_extrude, Task::Design}, + {load_texture("res/icons/Assembly_AxialMove.png"), false, "Shape move", Mode::Design_move, Task::Design}, + {load_texture("res/icons/Draft_Rotate.png"), false, "Shape rotate", Mode::Design_rotate, Task::Design}, {load_texture("res/icons/Part_Scale.png"), false, "Shape Scale", Mode::Scale, Task::Design}, + {load_texture("res/icons/Assembly_Move.png"), false, "Align shafts", Mode::Design_shaft_align, Task::Design}, {load_texture("res/icons/PartDesign_Chamfer.png"), false, "Chamfer", Mode::Shape_chamfer, Task::Design}, {load_texture("res/icons/PartDesign_Fillet.png"), false, "Fillet", Mode::Shape_fillet, Task::Design}, {load_texture("res/icons/Draft_PolarArray.png"), false, "Shape polar duplicate", Mode::Shape_polar_duplicate, Task::Design}, @@ -252,9 +256,12 @@ void GUI::sync_toolbar_hotkey_tooltips_() // clang-format off tip_mode(Mode::Workbench_move, "Shape move", Gui_action::Mode_move); + tip_mode(Mode::Design_move, "Shape move", Gui_action::Mode_move); tip_mode(Mode::Workbench_rotate, "Shape rotate", Gui_action::Mode_rotate); + tip_mode(Mode::Design_rotate, "Shape rotate", Gui_action::Mode_rotate); tip_mode(Mode::Scale, "Shape Scale", Gui_action::Mode_scale); tip_mode(Mode::Workbench_shaft_align, "Align shafts", Gui_action::Mode_cyl_align); + tip_mode(Mode::Design_shaft_align, "Align shafts", Gui_action::Mode_cyl_align); tip_mode(Mode::Sketch_dim_anno, "Length dimension", Gui_action::Mode_dimension); tip_mode(Mode::Sketch_face_extrude, "Extrude sketch face", Gui_action::Mode_extrude); tip_mode(Mode::Shape_chamfer, "Chamfer", Gui_action::Mode_chamfer); @@ -327,6 +334,7 @@ void GUI::seed_default_dock_layout_(ImGuiID dockspace_id) ImGuiID dock_left_top = ImGui::DockBuilderSplitNode(dock_left, ImGuiDir_Down, 0.52f, &dock_left_bottom, &dock_left); ImGui::DockBuilderDockWindow("Shape List", dock_left_top); + ImGui::DockBuilderDockWindow("Workbench List", dock_left_top); ImGui::DockBuilderDockWindow("Sketch List", dock_left_bottom); ImGui::DockBuilderDockWindow("Options", dock_right); ImGui::DockBuilderDockWindow("Log", dock_bottom); @@ -507,6 +515,12 @@ void GUI::menu_bar_() save_panes = true; } + if (ImGui::MenuItem("Workbench List", nullptr, m_show_workbench_list)) + { + m_show_workbench_list = !m_show_workbench_list; + save_panes = true; + } + if (ImGui::MenuItem("Log", nullptr, m_log_window_visible)) { m_log_window_visible = !m_log_window_visible; @@ -3265,6 +3279,7 @@ void GUI::clear_sketch_list_ui_() m_sketch_list_scroll_y = 0.f; m_sketch_list_scroll_restore = false; m_shape_list_expanded.clear(); + m_workbench_list_expanded.clear(); } nlohmann::json GUI::sketch_list_ui_to_json_() const @@ -3367,6 +3382,7 @@ std::string GUI::serialized_project_json_() const j["mode"] = static_cast(get_mode()); j["ui"]["sketchList"] = sketch_list_ui_to_json_(); j["ui"]["shapeList"] = shape_list_ui_to_json_(); + j["ui"]["workbenchList"] = workbench_list_ui_to_json_(); return j.dump(2); } @@ -3406,12 +3422,14 @@ void GUI::on_mouse_pos(const ScreenCoords& screen_coords) switch (get_mode()) { case Mode::Workbench_move: + case Mode::Design_move: if (Status s = m_view->shp_move().move_selected(screen_coords); !s.is_ok()) show_status(s); break; case Mode::Workbench_rotate: + case Mode::Design_rotate: if (Status s = m_view->shp_rotate().rotate_selected(screen_coords); !s.is_ok()) show_status(s); @@ -3424,6 +3442,7 @@ void GUI::on_mouse_pos(const ScreenCoords& screen_coords) break; case Mode::Workbench_shaft_align: + case Mode::Design_shaft_align: if (m_view->shp_cyl_align().is_twist_phase()) { if (Status s = m_view->shp_cyl_align().drag_twist(screen_coords); !s.is_ok()) @@ -3467,10 +3486,13 @@ void GUI::on_left_click_(const ScreenCoords& screen_coords) switch (m_mode) { // clang-format off - case Mode::Workbench_move: m_view->shp_move().finalize(); break; - case Mode::Workbench_rotate: m_view->shp_rotate().finalize(); break; + case Mode::Workbench_move: + case Mode::Design_move: m_view->shp_move().finalize(); break; + case Mode::Workbench_rotate: + case Mode::Design_rotate: m_view->shp_rotate().finalize(); break; case Mode::Scale: m_view->shp_scale().finalize(); break; case Mode::Workbench_shaft_align: + case Mode::Design_shaft_align: if (m_view->shp_cyl_align().is_dragging()) m_view->shp_cyl_align().on_left_click(); else if (Status s = m_view->shp_cyl_align().pick(screen_coords); !s.is_ok()) @@ -3987,6 +4009,7 @@ void GUI::on_file(const std::string& file_path, const std::string& file_bytes, b log_message("on_file: load complete"); apply_sketch_list_ui_from_json_(j); apply_shape_list_ui_from_json_(j); + apply_workbench_list_ui_from_json_(j); m_last_saved_path = file_path; Mode opened_mode = Mode::Design_inspection; if (j.contains("mode") && j["mode"].is_number_integer()) diff --git a/src/gui.h b/src/gui.h index 1167a529..d97ab6de 100644 --- a/src/gui.h +++ b/src/gui.h @@ -359,6 +359,8 @@ class GUI bool get_edge_from_center() const { return m_edge_from_center; } bool get_hide_all_shapes() const { return m_hide_all_shapes; } void set_hide_all_shapes(bool hide) { m_hide_all_shapes = hide; } + bool get_hide_all_workbench() const { return m_hide_all_workbench; } + void set_hide_all_workbench(bool hide) { m_hide_all_workbench = hide; } /// Orthographic camera toggle for non-sketch modes (forces ortho in sketch modes); persisted as /// `gui.inspection_orthographic`. bool inspection_orthographic() const { return m_inspection_orthographic; } @@ -394,6 +396,7 @@ class GUI void set_show_options(bool v) { m_show_options = v; } void set_show_sketch_list(bool v) { m_show_sketch_list = v; } void set_show_shape_list(bool v) { m_show_shape_list = v; } + void set_show_workbench_list(bool v) { m_show_workbench_list = v; } void set_log_window_visible(bool v) { m_log_window_visible = v; } void set_show_settings_dialog(bool v) { m_show_settings_dialog = v; } int ui_verbosity() const { return m_ui_verbosity; } @@ -418,6 +421,7 @@ class GUI bool show_options_effective() const { return m_show_options && ui_show_feature(1); } bool show_sketch_list_effective() const { return m_show_sketch_list && ui_show_feature(1); } bool show_shape_list_effective() const { return m_show_shape_list && ui_show_feature(1); } + bool show_workbench_list_effective() const { return m_show_workbench_list && ui_show_feature(1); } bool log_window_visible_effective() const { return m_log_window_visible && ui_show_feature(1); } bool show_lua_console_effective() const { return m_show_lua_console && ui_show_feature(2); } bool show_python_console_effective() const { return m_show_python_console && ui_show_feature(2); } @@ -496,7 +500,10 @@ class GUI friend struct gui_shp_detail::Shape_list_row_drawer; [[nodiscard]] nlohmann::json shape_list_ui_to_json_() const; void apply_shape_list_ui_from_json_(const nlohmann::json& j); + [[nodiscard]] nlohmann::json workbench_list_ui_to_json_() const; + void apply_workbench_list_ui_from_json_(const nlohmann::json& j); void shape_list_(); + void workbench_list_(); void shape_info_dialog_(); void open_shape_info_(const Shp_ptr& shape); void file_inspector_dialog_(); @@ -783,11 +790,13 @@ class GUI std::unordered_map m_sketch_list_ui; /// Shape List group expand state (project `ui.shapeList.expanded`); missing id defaults to open. std::unordered_map m_shape_list_expanded; + std::unordered_map m_workbench_list_expanded; float m_sketch_list_scroll_y{0.f}; bool m_sketch_list_scroll_restore{false}; bool m_show_sketch_list{true}; bool m_show_shape_list{true}; + bool m_show_workbench_list{true}; bool m_show_options{true}; bool m_show_settings_dialog{false}; bool m_open_about_popup{false}; @@ -846,6 +855,7 @@ class GUI int m_new_sketch_plane{0}; // 0=XY, 1=XZ, 2=YZ double m_new_sketch_offset{}; bool m_hide_all_shapes{false}; + bool m_hide_all_workbench{false}; int m_ui_verbosity{k_gui_ui_verbosity_default}; bool m_dark_mode{false}; #ifndef NDEBUG diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index bb060480..e0e1b797 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -47,8 +47,11 @@ std::string GUI::get_doc_url_for_mode(Mode mode) {Mode::Workbench_inspection, "https://ezycad.readthedocs.io/en/latest/usage.html#user-interface"}, {Mode::Workbench_move, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-move-tool-g"}, {Mode::Workbench_rotate, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-rotate-tool-r"}, + {Mode::Design_move, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-move-tool-g"}, + {Mode::Design_rotate, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-rotate-tool-r"}, {Mode::Scale, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-scale-tool-s"}, {Mode::Workbench_shaft_align, "https://ezycad.readthedocs.io/en/latest/usage.html#align-shafts-tool-j"}, + {Mode::Design_shaft_align, "https://ezycad.readthedocs.io/en/latest/usage.html#align-shafts-tool-j"}, {Mode::Sketch_inspection, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#sketch-origin"}, {Mode::Sketch_from_planar_face, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#create-sketch-from-planar-face-tool"}, {Mode::Sketch_face_extrude, "https://ezycad.readthedocs.io/en/latest/usage.html#extrude-sketch-face-tool-e"}, @@ -136,7 +139,10 @@ Mode GUI::parent_mode_of(Mode mode) {Mode::Workbench_move, Mode::Workbench_inspection}, {Mode::Scale, Mode::Design_inspection}, {Mode::Workbench_rotate, Mode::Workbench_inspection}, + {Mode::Design_move, Mode::Design_inspection}, + {Mode::Design_rotate, Mode::Design_inspection}, {Mode::Workbench_shaft_align, Mode::Workbench_inspection}, + {Mode::Design_shaft_align, Mode::Design_inspection}, {Mode::Sketch_inspection, Mode::Design_inspection}, {Mode::Sketch_from_planar_face, Mode::Design_inspection}, {Mode::Sketch_face_extrude, Mode::Design_inspection}, @@ -320,7 +326,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) { // Move / Rotate / Align shafts handle Tab in their mode key handlers (distance / angle / depth / twist). const Mode mode = get_mode(); - if (mode == Mode::Workbench_move || mode == Mode::Workbench_rotate || mode == Mode::Workbench_shaft_align) + if (is_move_mode(mode) || is_rotate_mode(mode) || is_shaft_align_mode(mode)) break; bool shift_pressed = (mods & GLFW_MOD_SHIFT) != 0; @@ -333,7 +339,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) case GLFW_KEY_ENTER: // Rotate / Align shafts finalize on Enter in their mode key handlers. - if (get_mode() == Mode::Workbench_rotate || get_mode() == Mode::Workbench_shaft_align) + if (is_rotate_mode(get_mode()) || is_shaft_align_mode(get_mode())) break; hide_sketch_origin_set_edit(true); @@ -369,14 +375,17 @@ void GUI::on_key(int key, int scancode, int action, int mods) switch (get_mode()) { case Mode::Workbench_move: + case Mode::Design_move: on_key_move_mode_(key); break; case Mode::Workbench_rotate: + case Mode::Design_rotate: on_key_rotate_mode_(key); break; case Mode::Workbench_shaft_align: + case Mode::Design_shaft_align: on_key_cyl_align_mode_(key, mods); break; @@ -390,10 +399,16 @@ void GUI::dispatch_hotkey_action_(Gui_action action) // clang-format off switch (action) { - case Gui_action::Mode_move: set_mode(Mode::Workbench_move); break; - case Gui_action::Mode_rotate: set_mode(Mode::Workbench_rotate); break; + case Gui_action::Mode_move: + set_mode(task_of(get_mode()) == Task::Workbench ? Mode::Workbench_move : Mode::Design_move); + break; + case Gui_action::Mode_rotate: + set_mode(task_of(get_mode()) == Task::Workbench ? Mode::Workbench_rotate : Mode::Design_rotate); + break; case Gui_action::Mode_scale: set_mode(Mode::Scale); break; - case Gui_action::Mode_cyl_align: set_mode(Mode::Workbench_shaft_align); break; + case Gui_action::Mode_cyl_align: + set_mode(task_of(get_mode()) == Task::Workbench ? Mode::Workbench_shaft_align : Mode::Design_shaft_align); + break; case Gui_action::Mode_extrude: set_mode(Mode::Sketch_face_extrude); break; case Gui_action::Mode_chamfer: set_mode(Mode::Shape_chamfer); break; case Gui_action::Mode_fillet: set_mode(Mode::Shape_fillet); break; @@ -522,9 +537,12 @@ void GUI::options_() case Mode::Design_inspection: options_normal_mode_(); break; case Mode::Workbench_inspection: options_normal_mode_(); break; case Mode::Workbench_move: options_move_mode_(); break; + case Mode::Design_move: options_move_mode_(); break; case Mode::Workbench_rotate: options_rotate_mode_(); break; + case Mode::Design_rotate: options_rotate_mode_(); break; case Mode::Scale: options_scale_mode_(); break; case Mode::Workbench_shaft_align: options_shape_shaft_align_mode_(); break; + case Mode::Design_shaft_align: options_shape_shaft_align_mode_(); break; case Mode::Shape_chamfer: options_shape_chamfer_mode_(); break; case Mode::Shape_fillet: options_shape_fillet_mode_(); break; case Mode::Shape_polar_duplicate: options_shape_polar_duplicate_mode_(); break; @@ -652,7 +670,7 @@ void GUI::options_transform_space_() void GUI::options_move_mode_() { - EZY_ASSERT(get_mode() == Mode::Workbench_move); + EZY_ASSERT(is_move_mode(get_mode())); ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); @@ -685,7 +703,7 @@ void GUI::options_scale_mode_() void GUI::options_shape_shaft_align_mode_() { - EZY_ASSERT(get_mode() == Mode::Workbench_shaft_align); + EZY_ASSERT(is_shaft_align_mode(get_mode())); ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); @@ -729,7 +747,7 @@ void GUI::options_shape_set_frame_mode_() void GUI::options_rotate_mode_() { - EZY_ASSERT(get_mode() == Mode::Workbench_rotate); + EZY_ASSERT(is_rotate_mode(get_mode())); ImGui::TextUnformatted(current_mode_description_()); options_doc_help_button_(); diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index 77b9cdb6..93d907c4 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -530,30 +530,34 @@ std::optional Occt_view::pt3d_on_plane(const ScreenCoords& screen_coords void Occt_view::bake_transform_into_geometry(AIS_Shape_ptr& shape, bool update_viewer) { - // Function to bake the local transformation into the geometry - // Get the current local transformation gp_Trsf current_transform = shape->LocalTransformation(); - // Get the current TopoDS_Shape + if (Shp_ptr document_shape = Shp_ptr::DownCast(shape); !document_shape.IsNull() && document_shape->is_workbench()) + { + const gp_Trsf old_pl = document_shape->placement_trsf(); + const gp_Trsf delta = current_transform * old_pl.Inverted(); + document_shape->transform_frame(delta); + document_shape->SetLocalTransformation(document_shape->placement_trsf()); + document_shape->update_frame_display(); + m_ctx->Redisplay(shape, update_viewer); + return; + } + TopoDS_Shape original_shape = shape->Shape(); - // Apply the transformation to the geometry BRepBuilderAPI_Transform transformer(original_shape, current_transform, true); TopoDS_Shape transformed_shape = transformer.Shape(); - // Update the AIS_Shape with the new geometry shape->Set(transformed_shape); if (Shp_ptr document_shape = Shp_ptr::DownCast(shape); !document_shape.IsNull()) document_shape->transform_frame(current_transform); - // Reset the local transformation to identity gp_Trsf identity_transform; shape->SetLocalTransformation(identity_transform); if (Shp_ptr document_shape = Shp_ptr::DownCast(shape); !document_shape.IsNull()) document_shape->update_frame_display(); - // Redisplay to update the viewer and selection m_ctx->Redisplay(shape, update_viewer); } @@ -588,11 +592,13 @@ void Occt_view::cancel(Set_parent_mode set_parent_mode) // Transform tools already return to Workbench_inspection in their reset() and re-select the operands; // a further set_mode() here would redisplay shapes and drop that selection again. case Mode::Workbench_move: + case Mode::Design_move: shp_move().cancel(); operation_canceled = true; break; case Mode::Workbench_rotate: + case Mode::Design_rotate: shp_rotate().cancel(); operation_canceled = true; break; @@ -603,6 +609,7 @@ void Occt_view::cancel(Set_parent_mode set_parent_mode) break; case Mode::Workbench_shaft_align: + case Mode::Design_shaft_align: shp_cyl_align().cancel(); operation_canceled = true; break; @@ -1077,7 +1084,7 @@ void Occt_view::ensure_current_group_valid_() if (m_current_group_id == 0) return; - Shp_ptr g = find_shape_by_id(m_current_group_id); + Shp_ptr g = find_design_shape_by_id(m_current_group_id); if (g.IsNull() || !g->is_group()) m_current_group_id = 0; } @@ -1090,7 +1097,7 @@ void Occt_view::set_current_group_id(Shape_id id) return; } - Shp_ptr g = find_shape_by_id(id); + Shp_ptr g = find_design_shape_by_id(id); if (g.IsNull() || !g->is_group()) { m_current_group_id = 0; @@ -1100,6 +1107,452 @@ void Occt_view::set_current_group_id(Shape_id id) m_current_group_id = id; } +namespace +{ +TopoDS_Shape local_geom_of_source_(const Shp& src) +{ + gp_Trsf to_local = Shp::trsf_from_frame(src.get_frame()); + to_local.Invert(); + to_local = to_local * src.LocalTransformation(); + if (to_local.Form() == gp_Identity) + return src.Shape(); + + BRepBuilderAPI_Transform xf(src.Shape(), to_local, true); + return xf.Shape(); +} + +void apply_workbench_placement_(const Shp_ptr& inst) +{ + if (inst.IsNull() || inst->is_group()) + return; + + inst->SetLocalTransformation(inst->placement_trsf()); + inst->sync_frame_display_trsf(); +} + +std::vector list_children_(const std::list& store, Shape_id parent_id) +{ + std::vector kids; + for (const Shp_ptr& s : store) + if (!s.IsNull() && s->get_parent_id() == parent_id) + kids.push_back(s); + + std::sort(kids.begin(), kids.end(), + [](const Shp_ptr& a, const Shp_ptr& b) + { + if (a->get_sibling_order() != b->get_sibling_order()) + return a->get_sibling_order() < b->get_sibling_order(); + return a->get_id() < b->get_id(); + }); + return kids; +} + +int next_order_in_store_(const std::list& store, Shape_id parent_id) +{ + int max_order = -1; + for (const Shp_ptr& s : store) + { + if (s.IsNull() || s->get_parent_id() != parent_id) + continue; + + max_order = std::max(max_order, s->get_sibling_order()); + } + return max_order + 1; +} + +std::string unique_name_in_store_(const std::list& store, const char* base_name) +{ + std::vector existing; + existing.reserve(store.size()); + for (const Shp_ptr& s : store) + if (!s.IsNull()) + existing.push_back(s->get_name()); + + return unique_sequential_name(base_name, existing); +} + +bool would_cycle_in_store_(const std::list& store, Shape_id id, Shape_id new_parent) +{ + if (new_parent == 0) + return false; + + if (new_parent == id) + return true; + + std::unordered_set seen; + Shape_id walk = new_parent; + while (walk != 0) + { + if (walk == id) + return true; + + if (!seen.insert(walk).second) + break; + + Shp_ptr p; + for (const Shp_ptr& s : store) + if (!s.IsNull() && s->get_id() == walk) + { + p = s; + break; + } + + if (p.IsNull()) + break; + + walk = p->get_parent_id(); + } + return false; +} +} // namespace + +void Occt_view::set_current_workbench_group_id(Shape_id id) +{ + if (id == 0) + { + m_current_wbk_group_id = 0; + return; + } + + Shp_ptr g = find_workbench_shape_by_id(id); + if (g.IsNull() || !g->is_group()) + { + m_current_wbk_group_id = 0; + return; + } + + m_current_wbk_group_id = id; +} + +std::vector Occt_view::workbench_children(Shape_id parent_id) const +{ + return list_children_(m_wbk_shps, parent_id); +} + +std::vector Occt_view::workbench_descendant_solids(Shape_id id) const +{ + std::vector out; + Shp_ptr root = find_workbench_shape_by_id(id); + if (root.IsNull()) + return out; + + std::unordered_set seen; + std::vector stack = {root}; + while (!stack.empty()) + { + Shp_ptr n = stack.back(); + stack.pop_back(); + if (n.IsNull() || !seen.insert(n->get_id()).second) + continue; + + if (!n->is_group()) + { + out.push_back(n); + continue; + } + + for (const Shp_ptr& c : workbench_children(n->get_id())) + stack.push_back(c); + } + return out; +} + +void Occt_view::sync_workbench_links(Shape_id source_id) +{ + bool any = false; + for (Shp_ptr& inst : m_wbk_shps) + { + if (inst.IsNull() || !inst->is_workbench_link()) + continue; + + if (source_id != 0 && inst->get_source_id() != source_id) + continue; + + Shp_ptr src = find_design_shape_by_id(inst->get_source_id()); + if (src.IsNull() || src->is_group()) + continue; + + inst->Set(local_geom_of_source_(*src)); + apply_workbench_placement_(inst); + if (!m_ctx.IsNull()) + m_ctx->Redisplay(inst, false); + + any = true; + } + + if (any && !m_ctx.IsNull()) + m_ctx->UpdateCurrentViewer(); +} + +void Occt_view::add_wbk_shp_(Shp_ptr& shp) +{ + if (shp.IsNull()) + return; + + if (shp->get_id() == 0) + shp->set_id(allocate_shape_id()); + + shp->set_is_workbench(true); + if (!shp->is_group()) + { + shp->set_selection_mode(m_shp_selection_mode); + apply_workbench_placement_(shp); + if (!m_ctx.IsNull()) + m_ctx->Redisplay(shp, false); + } + + m_wbk_shps.push_back(shp); +} + +Shp_ptr Occt_view::clone_design_to_workbench_(const Shp_ptr& src, Shape_id wbk_parent, std::vector& added) +{ + if (src.IsNull()) + return Shp_ptr(); + + Shp_ptr inst; + if (src->is_group()) + { + inst = Shp::create_group(*m_ctx, unique_name_in_store_(m_wbk_shps, src->get_name().c_str())); + inst->set_source_id(0); + } + else + { + inst = new Shp(*m_ctx, local_geom_of_source_(*src)); + inst->set_name(unique_name_in_store_(m_wbk_shps, src->get_name().c_str())); + inst->set_source_id(src->get_id()); + inst->set_frame(src->get_frame()); + inst->SetLocalTransformation(inst->placement_trsf()); + inst->SetMaterial(Graphic3d_MaterialAspect(static_cast(src->Material()))); + refresh_shape_shading_(inst); + inst->set_disp_mode(src->get_disp_mode()); + inst->set_visible(src->get_visible()); + } + + inst->set_parent_id(wbk_parent); + inst->set_sibling_order(next_order_in_store_(m_wbk_shps, wbk_parent)); + add_wbk_shp_(inst); + added.push_back(capture_shape_rec(*inst)); + + if (src->is_group()) + for (const Shp_ptr& child : shape_children(src->get_id())) + clone_design_to_workbench_(child, inst->get_id(), added); + + return inst; +} + +Status Occt_view::add_to_workbench(const std::vector& design_nodes) +{ + std::vector roots; + roots.reserve(design_nodes.size()); + for (const Shp_ptr& n : design_nodes) + { + if (n.IsNull() || n->is_workbench()) + continue; + + if (find_design_shape_by_id(n->get_id()).IsNull()) + continue; + + roots.push_back(n); + } + + if (roots.empty()) + return Status::user_error("Select a Shape List solid or group to add to the Workbench."); + + std::vector root_ids; + root_ids.reserve(roots.size()); + for (const Shp_ptr& n : roots) + root_ids.push_back(n->get_id()); + + std::vector collapsed; + collapsed.reserve(root_ids.size()); + for (Shape_id id : root_ids) + { + bool under_other = false; + for (Shape_id other : root_ids) + { + if (other == id) + continue; + + if (would_reparent_create_cycle(other, id)) + { + under_other = true; + break; + } + } + + if (!under_other) + collapsed.push_back(id); + } + + ensure_current_group_valid_(); + if (m_current_wbk_group_id != 0) + { + Shp_ptr g = find_workbench_shape_by_id(m_current_wbk_group_id); + if (g.IsNull() || !g->is_group()) + m_current_wbk_group_id = 0; + } + + const Shape_id wbk_parent = m_current_wbk_group_id; + std::vector added; + for (Shape_id id : collapsed) + { + Shp_ptr src = find_design_shape_by_id(id); + if (src.IsNull()) + continue; + + clone_design_to_workbench_(src, wbk_parent, added); + } + + if (added.empty()) + return Status::user_error("Nothing to add to the Workbench."); + + push_undo_delta(std::make_unique(std::move(added))); + sync_sketch_shape_faint_style(); + if (!m_ctx.IsNull()) + m_ctx->UpdateCurrentViewer(); + + return Status::ok(); +} + +Shp_ptr Occt_view::create_workbench_group(const std::string& name, Shape_id parent_id) +{ + if (parent_id != 0) + { + Shp_ptr p = find_workbench_shape_by_id(parent_id); + if (p.IsNull() || !p->is_group()) + parent_id = 0; + } + + Shp_ptr grp = Shp::create_group(*m_ctx, unique_name_in_store_(m_wbk_shps, name.c_str())); + grp->set_parent_id(parent_id); + grp->set_sibling_order(next_order_in_store_(m_wbk_shps, parent_id)); + add_wbk_shp_(grp); + push_undo_delta(std::make_unique(std::vector{capture_shape_rec(*grp)})); + return grp; +} + +Status Occt_view::group_workbench_shapes(const std::vector& nodes) +{ + std::vector to_group; + to_group.reserve(nodes.size()); + for (const Shp_ptr& n : nodes) + if (!n.IsNull() && n->is_workbench()) + to_group.push_back(n); + + if (to_group.empty()) + return Status::user_error("Select one or more Workbench items to group."); + + Shape_id parent = to_group[0]->get_parent_id(); + for (size_t i = 1; i < to_group.size(); ++i) + if (to_group[i]->get_parent_id() != parent) + parent = 0; + + Shp_ptr grp = Shp::create_group(*m_ctx, unique_name_in_store_(m_wbk_shps, "Group")); + grp->set_parent_id(parent); + grp->set_sibling_order(next_order_in_store_(m_wbk_shps, parent)); + add_wbk_shp_(grp); + + std::vector links; + links.reserve(to_group.size()); + int order = 0; + for (const Shp_ptr& n : to_group) + { + Shape_tree_delta::Link_change ch; + ch.id = n->get_id(); + ch.old_parent = n->get_parent_id(); + ch.old_order = n->get_sibling_order(); + ch.new_parent = grp->get_id(); + ch.new_order = order++; + links.push_back(ch); + } + + for (const Shape_tree_delta::Link_change& ch : links) + { + Shp_ptr n = find_workbench_shape_by_id(ch.id); + if (!n.IsNull()) + { + n->set_parent_id(ch.new_parent); + n->set_sibling_order(ch.new_order); + } + } + + push_undo_delta(std::make_unique(std::vector{capture_shape_rec(*grp)}, std::vector{}, + std::move(links))); + set_current_workbench_group_id(grp->get_id()); + sync_sketch_shape_faint_style(); + return Status::ok(); +} + +Status Occt_view::ungroup_workbench_shape(Shape_id group_id) +{ + Shp_ptr grp = find_workbench_shape_by_id(group_id); + if (grp.IsNull() || !grp->is_group()) + return Status::user_error("Workbench group not found."); + + const Shape_id parent = grp->get_parent_id(); + std::vector links; + int order = next_order_in_store_(m_wbk_shps, parent); + for (const Shp_ptr& c : workbench_children(group_id)) + { + Shape_tree_delta::Link_change ch; + ch.id = c->get_id(); + ch.old_parent = c->get_parent_id(); + ch.old_order = c->get_sibling_order(); + ch.new_parent = parent; + ch.new_order = order++; + links.push_back(ch); + c->set_parent_id(ch.new_parent); + c->set_sibling_order(ch.new_order); + } + + Shape_rec removed = capture_shape_rec(*grp); + if (m_current_wbk_group_id == group_id) + m_current_wbk_group_id = parent; + + remove_shape_by_id(group_id); + push_undo_delta(std::make_unique(std::vector{}, std::vector{std::move(removed)}, + std::move(links))); + sync_sketch_shape_faint_style(); + return Status::ok(); +} + +Status Occt_view::reparent_workbench_shape(Shape_id id, Shape_id new_parent, int sibling_order, bool push_undo) +{ + Shp_ptr shp = find_workbench_shape_by_id(id); + if (shp.IsNull()) + return Status::user_error("Workbench item not found."); + + if (new_parent != 0) + { + Shp_ptr p = find_workbench_shape_by_id(new_parent); + if (p.IsNull() || !p->is_group()) + return Status::user_error("Parent must be a Workbench group (or root)."); + } + + if (would_cycle_in_store_(m_wbk_shps, id, new_parent)) + return Status::user_error("Cannot reparent: would create a cycle."); + + const int new_order = (sibling_order < 0) ? next_order_in_store_(m_wbk_shps, new_parent) : sibling_order; + + Shape_tree_delta::Link_change ch; + ch.id = id; + ch.old_parent = shp->get_parent_id(); + ch.old_order = shp->get_sibling_order(); + ch.new_parent = new_parent; + ch.new_order = new_order; + + if (ch.old_parent == ch.new_parent && ch.old_order == ch.new_order) + return Status::ok(); + + shp->set_parent_id(new_parent); + shp->set_sibling_order(new_order); + if (push_undo) + push_undo_delta(std::make_unique(std::vector{}, std::vector{}, + std::vector{ch})); + sync_sketch_shape_faint_style(); + return Status::ok(); +} + Shape_id Occt_view::allocate_shape_id() { return m_next_shape_id++; } void Occt_view::adopt_shape_id(Shape_id id) @@ -1108,7 +1561,7 @@ void Occt_view::adopt_shape_id(Shape_id id) m_next_shape_id = id + 1; } -Shp_ptr Occt_view::find_shape_by_id(Shape_id id) const +Shp_ptr Occt_view::find_design_shape_by_id(Shape_id id) const { for (const Shp_ptr& s : m_shps) if (!s.IsNull() && s->get_id() == id) @@ -1117,6 +1570,24 @@ Shp_ptr Occt_view::find_shape_by_id(Shape_id id) const return Shp_ptr(); } +Shp_ptr Occt_view::find_workbench_shape_by_id(Shape_id id) const +{ + for (const Shp_ptr& s : m_wbk_shps) + if (!s.IsNull() && s->get_id() == id) + return s; + + return Shp_ptr(); +} + +Shp_ptr Occt_view::find_shape_by_id(Shape_id id) const +{ + Shp_ptr s = find_design_shape_by_id(id); + if (!s.IsNull()) + return s; + + return find_workbench_shape_by_id(id); +} + int Occt_view::next_sibling_order(Shape_id parent_id) const { int max_order = -1; @@ -1433,6 +1904,8 @@ void Occt_view::insert_shape_rec(const Shape_rec& rec) shp->set_id(rec.id); adopt_shape_id(rec.id); shp->set_name(rec.name); + shp->set_is_workbench(rec.is_workbench); + shp->set_source_id(rec.source_id); shp->set_frame(rec.frame); if (!shp->is_group()) { @@ -1446,32 +1919,73 @@ void Occt_view::insert_shape_rec(const Shape_rec& rec) if (shp->get_visible() != rec.visible) shp->set_visible(rec.visible); - m_shps.push_back(shp); + if (rec.is_workbench) + { + if (!shp->is_group() && rec.source_id != 0) + { + Shp_ptr src = find_design_shape_by_id(rec.source_id); + if (!src.IsNull()) + shp->Set(local_geom_of_source_(*src)); + } + + if (!shp->is_group()) + shp->SetLocalTransformation(shp->placement_trsf()); + + m_wbk_shps.push_back(shp); + } + else + m_shps.push_back(shp); + sync_sketch_shape_faint_style(); + if (!rec.is_workbench) + sync_workbench_links(rec.id); } void Occt_view::remove_shape_by_id(Shape_id id) { - for (auto it = m_shps.begin(); it != m_shps.end(); ++it) + auto remove_from = [&](std::list& store, bool design) { - if ((*it).IsNull() || (*it)->get_id() != id) - continue; + for (auto it = store.begin(); it != store.end(); ++it) + { + if ((*it).IsNull() || (*it)->get_id() != id) + continue; - Shp_ptr shp = *it; - if (m_shape_list_hover == shp) - set_shape_list_hover(nullptr); + Shp_ptr shp = *it; + if (m_shape_list_hover == shp) + set_shape_list_hover(nullptr); - if (!shp->is_group()) - { - shp->clear_frame_display(); - m_ctx->Remove(shp, false); + if (!shp->is_group()) + { + shp->clear_frame_display(); + m_ctx->Remove(shp, false); + } + + store.erase(it); + if (design) + { + ensure_current_group_valid_(); + std::vector dead; + for (const Shp_ptr& w : m_wbk_shps) + if (!w.IsNull() && w->get_source_id() == id) + dead.push_back(w->get_id()); + + for (Shape_id wid : dead) + remove_shape_by_id(wid); + } + else if (m_current_wbk_group_id == id) + m_current_wbk_group_id = 0; + + m_ctx->UpdateCurrentViewer(); + return true; } - m_shps.erase(it); - ensure_current_group_valid_(); - m_ctx->UpdateCurrentViewer(); + return false; + }; + + if (remove_from(m_shps, true)) return; - } + + remove_from(m_wbk_shps, false); } void Occt_view::set_shape_geom_by_id(Shape_id id, const TopoDS_Shape& geom, const gp_Ax3& frame) @@ -1482,11 +1996,19 @@ void Occt_view::set_shape_geom_by_id(Shape_id id, const TopoDS_Shape& geom, cons shp->Set(geom); shp->set_frame(frame); - gp_Trsf identity; - shp->SetLocalTransformation(identity); + if (shp->is_workbench()) + shp->SetLocalTransformation(shp->placement_trsf()); + else + { + gp_Trsf identity; + shp->SetLocalTransformation(identity); + } + shp->sync_frame_display_trsf(); m_ctx->Redisplay(shp, true); m_ctx->UpdateCurrentViewer(); + if (!shp->is_workbench()) + sync_workbench_links(id); } void Occt_view::set_shape_frame(const Shp_ptr& shp, const gp_Ax3& frame) @@ -1935,8 +2457,12 @@ void Occt_view::delete_shapes(std::vector to_delete) expanded.push_back(cur); if (cur->is_group()) - for (const Shp_ptr& c : shape_children(cur->get_id())) + { + const std::vector kids = + cur->is_workbench() ? workbench_children(cur->get_id()) : shape_children(cur->get_id()); + for (const Shp_ptr& c : kids) stack.push_back(c); + } } } to_delete = std::move(expanded); @@ -2257,13 +2783,31 @@ void Occt_view::delete_(std::vector& to_delete) if (auto wire = dynamic_cast(shp.get()); wire) wire->owner_sketch.remove_edge(*wire); + std::unordered_set deleted_design_ids; + for (const AIS_Shape_ptr& obj : to_delete) + if (Shp_ptr s = Shp_ptr::DownCast(obj); !s.IsNull() && !s->is_workbench()) + deleted_design_ids.insert(s->get_id()); + + for (const Shp_ptr& w : m_wbk_shps) + if (!w.IsNull() && w->get_source_id() != 0 && + deleted_design_ids.find(w->get_source_id()) != deleted_design_ids.end()) + to_delete.push_back(w); + for (auto itr = m_shps.begin(); itr != m_shps.end();) if (std::find(to_delete.begin(), to_delete.end(), *itr) != to_delete.end()) itr = m_shps.erase(itr); else ++itr; + for (auto itr = m_wbk_shps.begin(); itr != m_wbk_shps.end();) + if (std::find(to_delete.begin(), to_delete.end(), *itr) != to_delete.end()) + itr = m_wbk_shps.erase(itr); + else + ++itr; + ensure_current_group_valid_(); + if (m_current_wbk_group_id != 0 && find_workbench_shape_by_id(m_current_wbk_group_id).IsNull()) + m_current_wbk_group_id = 0; for (const AIS_Shape_ptr& shp : to_delete) if (m_shape_list_hover == shp) @@ -2883,10 +3427,10 @@ void Occt_view::on_mouse_button(int theButton, int theAction, int theMods) if (theButton == GLFW_MOUSE_BUTTON_LEFT) { // clang-format off - const bool finalize_transform = (get_mode() == Mode::Workbench_move && shp_move().has_operation_shps()) || - (get_mode() == Mode::Workbench_rotate && shp_rotate().has_operation_shps()) || + const bool finalize_transform = (is_move_mode(get_mode()) && shp_move().has_operation_shps()) || + (is_rotate_mode(get_mode()) && shp_rotate().has_operation_shps()) || (get_mode() == Mode::Scale && shp_scale().has_operation_shps()) || - (get_mode() == Mode::Workbench_shaft_align && shp_cyl_align().is_dragging()); + (is_shaft_align_mode(get_mode()) && shp_cyl_align().is_dragging()); // clang-format on if (finalize_transform) { @@ -3611,7 +4155,7 @@ void Occt_view::on_mode() // stay at the pre-transform pose. Disable AIS_ViewController dynamic highlight (skips MoveTo // while idle) so hover cannot paint a wireframe ghost there; orbit/pan still get mouse updates. const Mode mode = get_mode(); - const bool transform_preview = mode == Mode::Workbench_move || mode == Mode::Workbench_rotate || mode == Mode::Scale; + const bool transform_preview = is_move_mode(mode) || is_rotate_mode(mode) || mode == Mode::Scale; SetAllowHighlight(!transform_preview); if (transform_preview && !m_ctx.IsNull()) m_ctx->ClearDetected(false); @@ -3682,10 +4226,13 @@ void Occt_view::on_mode() case Mode::Sketch_from_planar_face: set_shp_selection_mode(TopAbs_FACE); break; case Mode::Shape_chamfer: on_chamfer_mode(); break; // Will update selection mode case Mode::Shape_fillet: on_fillet_mode(); break; // Will update selection mode - case Mode::Workbench_move: set_shp_selection_mode(TopAbs_SHAPE); break; - case Mode::Workbench_rotate: set_shp_selection_mode(TopAbs_SHAPE); break; + case Mode::Workbench_move: + case Mode::Design_move: set_shp_selection_mode(TopAbs_SHAPE); break; + case Mode::Workbench_rotate: + case Mode::Design_rotate: set_shp_selection_mode(TopAbs_SHAPE); break; case Mode::Scale: set_shp_selection_mode(TopAbs_SHAPE); break; - case Mode::Workbench_shaft_align: set_shp_selection_mode(TopAbs_FACE); break; + case Mode::Workbench_shaft_align: + case Mode::Design_shaft_align: set_shp_selection_mode(TopAbs_FACE); break; case Mode::Shape_set_frame: set_shp_selection_mode(TopAbs_FACE); break; case Mode::Shape_cross_section: set_shp_selection_mode(TopAbs_COMPOUND); break; default: @@ -3712,9 +4259,11 @@ void Occt_view::on_mode() switch (mode) { case Mode::Workbench_move: + case Mode::Design_move: shp_move().begin(enter_selection); break; case Mode::Workbench_rotate: + case Mode::Design_rotate: shp_rotate().begin(enter_selection); break; case Mode::Scale: @@ -3725,7 +4274,7 @@ void Occt_view::on_mode() } } - if (mode == Mode::Workbench_shaft_align) + if (is_shaft_align_mode(mode)) shp_cyl_align().begin(); if (mode == Mode::Shape_cross_section && !enter_selection.empty()) @@ -3738,7 +4287,9 @@ void Occt_view::on_mode() void Occt_view::sync_sketch_shape_faint_style() { const bool hide_all = gui().get_hide_all_shapes(); + const bool hide_wbk = gui().get_hide_all_workbench(); const bool sketch = is_sketch_mode(get_mode()); + const bool workbench = is_workbench_mode(get_mode()); const bool enabled = gui().sketch_shape_faint_enabled(); const int style = gui().sketch_shape_faint_style(); const float opacity = std::clamp(gui().sketch_shape_faint_opacity(), k_gui_sketch_shape_faint_opacity_min, @@ -3766,12 +4317,12 @@ void Occt_view::sync_sketch_shape_faint_style() // Hide all / sketch-hide are overlays: do not write get_visible(). const bool own_ok = shp->get_visible() && shape_ancestors_visible(*shp); - const bool hide_overlay = hide_all || (sketch && hide_in_sketch); + const bool hide_overlay = hide_all || workbench || (sketch && hide_in_sketch); const bool show = own_ok && !hide_overlay; // Frame AIS follow effective visibility, not only get_visible(): sketch tools, Hide all, // and a hidden ancestor group all keep axes/plane/up off (flags unchanged). - shp->set_frame_display_suppressed(sketch || !show); + shp->set_frame_display_suppressed(sketch || workbench || !show); if (faint_active && show) { @@ -3790,6 +4341,20 @@ void Occt_view::sync_sketch_shape_faint_style() } } + for (Shp_ptr& shp : m_wbk_shps) + { + if (shp.IsNull() || shp->is_group()) + continue; + + const bool own_ok = shp->get_visible() && shape_ancestors_visible(*shp); + const bool show = workbench && own_ok && !hide_wbk; + shp->set_frame_display_suppressed(!show); + shp->set_sketch_faint(false, AIS_Shaded, 0.0f); + shp->apply_context_shown(show); + if (show) + apply_workbench_placement_(shp); + } + if (!m_ctx.IsNull()) m_ctx->UpdateCurrentViewer(); } @@ -4133,6 +4698,7 @@ std::string Occt_view::to_json() const j["projectUnit"] = (m_project_unit == Project_unit::Millimeter) ? "millimeter" : "inch"; json& sketches = j["sketches"] = json::array(); json& shps = j["shapes"] = json::array(); + json& wbk = j["workbench"] = json::array(); const auto pnt_to_json = [](double x, double y, double z) { @@ -4181,6 +4747,39 @@ std::string Occt_view::to_json() const shps.push_back(shp_json); } + for (const Shp_ptr& s : m_wbk_shps) + { + json shp_json; + shp_json["id"] = s->get_id(); + shp_json["name"] = s->get_name(); + shp_json["parentId"] = s->get_parent_id(); + shp_json["order"] = s->get_sibling_order(); + shp_json["visible"] = s->get_visible(); + if (s->is_group()) + shp_json["isGroup"] = true; + else + { + shp_json["sourceId"] = s->get_source_id(); + shp_json["material"] = s->Material(); + shp_json["dispMode"] = static_cast(s->get_disp_mode()); + shp_json["frame"] = ::to_json(gp_Pln(s->get_frame())); + if (s->show_frame_axes() || s->show_frame_plane() || s->show_frame_up()) + { + json fd; + if (s->show_frame_axes()) + fd["axes"] = true; + + if (s->show_frame_plane()) + fd["plane"] = true; + + if (s->show_frame_up()) + fd["up"] = true; + shp_json["frameDisplay"] = fd; + } + } + wbk.push_back(shp_json); + } + // --------------------------------------------------------------------------- // View / camera state if (!m_view.IsNull()) @@ -4222,7 +4821,15 @@ void Occt_view::load(const std::string& json_str, bool restore_view) m_ctx->Remove(s, false); } - clear_all(m_sketches, m_cur_sketch, m_shps); + for (AIS_Shape_ptr& s : m_wbk_shps) + { + if (Shp_ptr shp = Shp_ptr::DownCast(s); !shp.IsNull()) + shp->clear_frame_display(); + m_ctx->Remove(s, false); + } + + clear_all(m_sketches, m_cur_sketch, m_shps, m_wbk_shps); + m_current_wbk_group_id = 0; if (!m_restoring) { @@ -4322,6 +4929,90 @@ void Occt_view::load(const std::string& json_str, bool restore_view) m_shps.push_back(shp); } + + if (j.contains("workbench") && j["workbench"].is_array()) + { + for (const json& s : j["workbench"]) + { + const bool is_group = s.contains("isGroup") && s["isGroup"].is_boolean() && s["isGroup"].get(); + Shp_ptr shp; + if (is_group) + shp = Shp::create_group(*m_ctx, s.value("name", "Group")); + else + { + const Shape_id source_id = s.value("sourceId", Shape_id{0}); + TopoDS_Shape local; + Shp_ptr src = find_design_shape_by_id(source_id); + if (!src.IsNull()) + local = local_geom_of_source_(*src); + else + { + TopoDS_Compound comp; + BRep_Builder().MakeCompound(comp); + local = comp; + } + + shp = new Shp(*m_ctx, local); + shp->set_source_id(source_id); + if (s.contains("frame") && s["frame"].is_object()) + shp->set_frame(from_json_pln(s["frame"]).Position()); + + if (s.contains("frameDisplay") && s["frameDisplay"].is_object()) + { + const json& fd = s["frameDisplay"]; + if (fd.contains("axes") && fd["axes"].is_boolean()) + shp->set_show_frame_axes(fd["axes"].get()); + + if (fd.contains("plane") && fd["plane"].is_boolean()) + shp->set_show_frame_plane(fd["plane"].get()); + + if (fd.contains("up") && fd["up"].is_boolean()) + shp->set_show_frame_up(fd["up"].get()); + } + + int mat_idx = static_cast(m_default_material.Name()); + if (s.contains("material") && s["material"].is_number_integer()) + mat_idx = s["material"].get(); + + const int nmat = Graphic3d_MaterialAspect::NumberOfMaterials(); + if (mat_idx < 0 || mat_idx >= nmat) + mat_idx = static_cast(m_default_material.Name()); + + shp->SetMaterial(Graphic3d_MaterialAspect(static_cast(mat_idx))); + refresh_shape_shading_(shp); + if (s.contains("dispMode") && s["dispMode"].is_number_integer()) + shp->set_disp_mode(s["dispMode"].get() == static_cast(AIS_WireFrame) ? AIS_WireFrame : AIS_Shaded); + + shp->SetLocalTransformation(shp->placement_trsf()); + } + + shp->set_is_workbench(true); + if (s.contains("id") && s["id"].is_number_unsigned()) + { + shp->set_id(s["id"].get()); + adopt_shape_id(shp->get_id()); + } + else + shp->set_id(allocate_shape_id()); + + shp->set_name(s.value("name", shp->get_name())); + if (s.contains("parentId") && s["parentId"].is_number_unsigned()) + shp->set_parent_id(s["parentId"].get()); + + if (s.contains("order") && s["order"].is_number_integer()) + shp->set_sibling_order(s["order"].get()); + + if (s.contains("visible") && s["visible"].is_boolean()) + { + const bool vis = s["visible"].get(); + if (shp->get_visible() != vis) + shp->set_visible(vis); + } + + m_wbk_shps.push_back(shp); + } + } + sync_sketch_shape_faint_style(); // --------------------------------------------------------------------------- @@ -4725,15 +5416,17 @@ void Occt_view::new_file() m_undo_stack.clear(); m_redo_stack.clear(); remove(m_shps); - clear_all(m_shps, m_sketches, m_cur_sketch); + remove(m_wbk_shps); + clear_all(m_shps, m_wbk_shps, m_sketches, m_cur_sketch); m_assets.clear(); // Keep m_shape_clipboard so Copy then New then Paste can seed a fresh document. // Live source-root ids are invalid after the document is cleared. m_shape_clipboard_source_roots.clear(); - m_next_sketch_id = 1; - m_next_shape_id = 1; - m_current_group_id = 0; - m_project_unit = m_gui.default_project_unit(); + m_next_sketch_id = 1; + m_next_shape_id = 1; + m_current_group_id = 0; + m_current_wbk_group_id = 0; + m_project_unit = m_gui.default_project_unit(); create_default_sketch_(); refresh_viewer_grid_(); @@ -4823,9 +5516,12 @@ Mode mode_for_history_restore_(Mode mode) switch (mode) { case Mode::Workbench_move: + case Mode::Design_move: case Mode::Workbench_rotate: + case Mode::Design_rotate: case Mode::Scale: case Mode::Workbench_shaft_align: + case Mode::Design_shaft_align: case Mode::Shape_set_frame: return GUI::parent_mode_of(mode); default: diff --git a/src/gui_occt_view.h b/src/gui_occt_view.h index d330cfac..83a2d65f 100644 --- a/src/gui_occt_view.h +++ b/src/gui_occt_view.h @@ -145,6 +145,8 @@ class Occt_view : protected AIS_ViewController Shape_id allocate_shape_id(); void adopt_shape_id(Shape_id id); Shp_ptr find_shape_by_id(Shape_id id) const; + Shp_ptr find_design_shape_by_id(Shape_id id) const; + Shp_ptr find_workbench_shape_by_id(Shape_id id) const; /// Insert a shape from an undo snapshot (keeps \a rec.id). Displays solids in the viewer (not groups). void insert_shape_rec(const Shape_rec& rec); /// Remove a shape by stable id (viewer + document list). @@ -182,6 +184,24 @@ class Occt_view : protected AIS_ViewController /// Set current group; \a id must be 0 or an existing group. Invalid ids clear to root. void set_current_group_id(Shape_id id); + std::list& get_workbench_shapes() { return m_wbk_shps; } + const std::list& get_workbench_shapes() const { return m_wbk_shps; } + /// Children of \a parent_id in the Workbench list (0 = workbench roots). + std::vector workbench_children(Shape_id parent_id) const; + /// Descendant leaf instances under a workbench node. + std::vector workbench_descendant_solids(Shape_id id) const; + Shape_id current_workbench_group_id() const { return m_current_wbk_group_id; } + void set_current_workbench_group_id(Shape_id id); + /// Copy Design nodes into the Workbench list as geometry links (own placement). + [[nodiscard]] Status add_to_workbench(const std::vector& design_nodes); + Shp_ptr create_workbench_group(const std::string& name, Shape_id parent_id = 0); + [[nodiscard]] Status group_workbench_shapes(const std::vector& nodes); + [[nodiscard]] Status ungroup_workbench_shape(Shape_id group_id); + [[nodiscard]] Status reparent_workbench_shape(Shape_id id, Shape_id new_parent, int sibling_order = -1, + bool push_undo = true); + /// Refresh every workbench instance that links to \a source_id (0 = all links). + void sync_workbench_links(Shape_id source_id = 0); + /// Insert a sketch from JSON for undo/redo (adopts sketch id from JSON). void undo_insert_sketch(const nlohmann::json& sketch_json, bool make_current); /// Remove a sketch by id for undo/redo (does not auto-create a default sketch). @@ -450,6 +470,8 @@ class Occt_view : protected AIS_ViewController /// Register shape. When \a use_current_group, solids still at parent 0 are placed under current_group_id(). void add_shp_(Shp_ptr& shp, bool use_current_group = false); + void add_wbk_shp_(Shp_ptr& shp); + Shp_ptr clone_design_to_workbench_(const Shp_ptr& src, Shape_id wbk_parent, std::vector& added); void ensure_current_group_valid_(); std::string unique_shape_name_(const char* base_name) const; /// Snapshot one shape for the in-app clipboard (independent BREP; local transform baked). @@ -513,6 +535,8 @@ class Occt_view : protected AIS_ViewController size_t m_next_sketch_id{1}; Shape_id m_next_shape_id{1}; Shape_id m_current_group_id{0}; + Shape_id m_current_wbk_group_id{0}; + std::list m_wbk_shps; /// In-app clipboard: forest of Shape_rec (roots have parent_id 0; independent BREP). std::vector m_shape_clipboard; /// Live document ids of clipboard roots at copy time (for paste-as-sibling when still current). diff --git a/src/gui_settings.cpp b/src/gui_settings.cpp index 47ec2c34..85719fbe 100644 --- a/src/gui_settings.cpp +++ b/src/gui_settings.cpp @@ -129,6 +129,7 @@ void GUI::save_occt_view_settings() {"show_options", m_show_options}, {"show_sketch_list", m_show_sketch_list}, {"show_shape_list", m_show_shape_list}, + {"show_workbench_list", m_show_workbench_list}, {"log_window_visible", m_log_window_visible}, {"show_settings_dialog", m_show_settings_dialog}, {"dark_mode", m_dark_mode}, @@ -337,6 +338,7 @@ void GUI::parse_gui_panes_settings_(const std::string& content) set_show_options(b("show_options", true)); set_show_sketch_list(b("show_sketch_list", true)); set_show_shape_list(b("show_shape_list", true)); + set_show_workbench_list(b("show_workbench_list", true)); set_log_window_visible(b("log_window_visible", true)); set_show_settings_dialog(b("show_settings_dialog", false)); m_dark_mode = b("dark_mode", m_dark_mode); diff --git a/src/gui_shp_pane.cpp b/src/gui_shp_pane.cpp index 3603422e..30669e7a 100644 --- a/src/gui_shp_pane.cpp +++ b/src/gui_shp_pane.cpp @@ -19,9 +19,16 @@ namespace gui_shp_detail { +enum class Shp_list_kind +{ + Design, + Workbench +}; + struct Shape_list_row_drawer { - Shape_list_row_drawer(GUI& gui, const std::vector& mat_names, int nmat, float mat_popup_w); + Shape_list_row_drawer(GUI& gui, const std::vector& mat_names, int nmat, float mat_popup_w, + Shp_list_kind kind); void draw(const Shp_ptr& shape); @@ -30,7 +37,10 @@ struct Shape_list_row_drawer Shape_id to_ungroup_id = 0; private: - [[nodiscard]] bool row_is_selected_(const Shp_ptr& shape) const; + [[nodiscard]] bool row_is_selected_(const Shp_ptr& shape) const; + [[nodiscard]] std::vector children_(Shape_id parent_id) const; + [[nodiscard]] std::vector descendant_solids_(Shape_id id) const; + [[nodiscard]] std::unordered_map& expanded_(); void select_row_(const Shp_ptr& shape); void apply_material_(const Shp_ptr& shape, int i); void draw_ctx_menu_(const Shp_ptr& shape, bool is_group); @@ -38,8 +48,9 @@ struct Shape_list_row_drawer GUI& m_gui; Occt_view& m_view; + Shp_list_kind m_kind; const std::vector& m_mat_names; - int m_nmat = 0; + int m_nmat = 0; float m_mat_popup_w = 0.0f; std::unordered_set m_selected_in_viewer; std::unordered_set m_ancestors; @@ -91,6 +102,31 @@ void GUI::shape_list_() ImGui::EndDisabled(); } + ImGui::SameLine(); + { + const std::vector sel = m_view->get_selected_shps(); + const bool can_add = !sel.empty() || m_view->current_group_id() != 0; + if (!can_add) + ImGui::BeginDisabled(); + + if (ImGui::SmallButton("Add to Workbench")) + { + std::vector nodes = sel; + if (nodes.empty() && m_view->current_group_id() != 0) + if (Shp_ptr g = m_view->find_design_shape_by_id(m_view->current_group_id()); !g.IsNull()) + nodes.push_back(g); + + const Status st = m_view->add_to_workbench(nodes); + if (!st.is_ok()) + show_status(st); + else + show_message("Added to Workbench.", Status_msg::Success); + } + + if (!can_add) + ImGui::EndDisabled(); + } + ImGui::Separator(); const std::vector& mat_names = occt_material_combo_labels_(); @@ -105,7 +141,7 @@ void GUI::shape_list_() const float check_col_w = ImGui::GetFrameHeight(); const float mat_col_w = ImGui::CalcTextSize("M").x + st_mat.FramePadding.x * 2.0f; - gui_shp_detail::Shape_list_row_drawer row(*this, mat_names, nmat, mat_popup_w); + gui_shp_detail::Shape_list_row_drawer row(*this, mat_names, nmat, mat_popup_w, gui_shp_detail::Shp_list_kind::Design); const ImGuiTableFlags table_flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_SizingFixedFit; @@ -202,6 +238,187 @@ void GUI::shape_list_() ImGui::End(); } +void GUI::workbench_list_() +{ + if (!show_workbench_list_effective()) + { + if (m_view->shape_list_hover() && m_view->shape_list_hover()->is_workbench()) + m_view->set_shape_list_hover(nullptr); + + return; + } + + if (!ImGui::Begin("Workbench List", &m_show_workbench_list, ImGuiWindowFlags_None)) + { + if (m_view->shape_list_hover() && m_view->shape_list_hover()->is_workbench()) + m_view->set_shape_list_hover(nullptr); + + ImGui::End(); + return; + } + + if (ImGui::Checkbox("Hide all", &m_hide_all_workbench)) + { + if (m_hide_all_workbench && m_view->shape_list_hover() && m_view->shape_list_hover()->is_workbench()) + m_view->set_shape_list_hover(nullptr); + + m_view->sync_sketch_shape_faint_style(); + } + + ImGui::SameLine(); + if (ImGui::SmallButton("New group")) + { + Shp_ptr grp = m_view->create_workbench_group("Group", m_view->current_workbench_group_id()); + if (!grp.IsNull()) + m_view->set_current_workbench_group_id(grp->get_id()); + } + + ImGui::SameLine(); + { + const std::vector sel = m_view->get_selected_shps(); + std::vector wbk_sel; + for (const Shp_ptr& s : sel) + if (!s.IsNull() && s->is_workbench()) + wbk_sel.push_back(s); + + const bool can_group = !wbk_sel.empty(); + if (!can_group) + ImGui::BeginDisabled(); + + if (ImGui::SmallButton("Group")) + (void)m_view->group_workbench_shapes(wbk_sel); + + if (!can_group) + ImGui::EndDisabled(); + } + + ImGui::Separator(); + + const std::vector& mat_names = occt_material_combo_labels_(); + const int nmat = static_cast(mat_names.size()); + float mat_label_w_max = 0.0f; + for (int mi = 0; mi < nmat; ++mi) + mat_label_w_max = std::max(mat_label_w_max, ImGui::CalcTextSize(mat_names[static_cast(mi)].c_str()).x); + + const ImGuiStyle& st_mat = ImGui::GetStyle(); + const float mat_popup_w = std::min(440.0f, std::max(280.0f, mat_label_w_max + st_mat.WindowPadding.x * 2.0f + + st_mat.FramePadding.x * 2.0f + st_mat.ScrollbarSize + 8.0f)); + const float check_col_w = ImGui::GetFrameHeight(); + const float mat_col_w = ImGui::CalcTextSize("M").x + st_mat.FramePadding.x * 2.0f; + + gui_shp_detail::Shape_list_row_drawer row(*this, mat_names, nmat, mat_popup_w, gui_shp_detail::Shp_list_kind::Workbench); + + const ImGuiTableFlags table_flags = + ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_SizingFixedFit; + if (ImGui::BeginTable("##workbench_outliner", 4, table_flags, ImVec2(0.f, 0.f))) + { + ImGui::TableSetupColumn( + "vis", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_IndentDisable, + check_col_w); + ImGui::TableSetupColumn( + "disp", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_IndentDisable, + check_col_w); + ImGui::TableSetupColumn( + "mat", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_IndentDisable, + mat_col_w); + ImGui::TableSetupColumn("name", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_IndentEnable, 1.0f); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + ImVec2(st_mat.FramePadding.x, std::max(1.0f, st_mat.FramePadding.y * 0.65f))); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(st_mat.CellPadding.x, std::max(1.0f, st_mat.CellPadding.y * 0.5f))); + + for (const Shp_ptr& root : m_view->workbench_children(0)) + row.draw(root); + + { + const float min_pad_h = ImGui::GetFrameHeight(); + const ImGuiWindow* inner = ImGui::GetCurrentWindow(); + const float visible_remain = inner->InnerClipRect.Max.y - ImGui::GetCursorScreenPos().y - ImGui::GetStyle().CellPadding.y; + const float pad_h = std::max(min_pad_h, visible_remain); + ImGui::TableNextRow(ImGuiTableRowFlags_None, pad_h); + ImGui::TableSetColumnIndex(0); + + const ImGuiPayload* active_payload = ImGui::GetDragDropPayload(); + const bool dragging_wbk = active_payload != nullptr && active_payload->IsDataType("EZY_WBK_ID") && + active_payload->DataSize == sizeof(Shape_id); + const bool dragging_design = active_payload != nullptr && active_payload->IsDataType("EZY_SHAPE_ID") && + active_payload->DataSize == sizeof(Shape_id); + + if (dragging_wbk || dragging_design) + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_Header, 0.20f)); + + ImGui::Selectable("##wbk_root_drop", false, + ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap | ImGuiSelectableFlags_Disabled, + ImVec2(0.0f, pad_h)); + + bool root_drop_hovered = false; + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("EZY_WBK_ID", ImGuiDragDropFlags_AcceptBeforeDelivery)) + { + root_drop_hovered = payload->Preview; + if (payload->IsDelivery()) + { + Shape_id drag_id = 0; + std::memcpy(&drag_id, payload->Data, sizeof(drag_id)); + (void)m_view->reparent_workbench_shape(drag_id, 0, -1, true); + } + } + + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("EZY_SHAPE_ID", ImGuiDragDropFlags_AcceptBeforeDelivery)) + { + root_drop_hovered = payload->Preview; + if (payload->IsDelivery()) + { + Shape_id drag_id = 0; + std::memcpy(&drag_id, payload->Data, sizeof(drag_id)); + if (Shp_ptr src = m_view->find_design_shape_by_id(drag_id); !src.IsNull()) + { + const Status st = m_view->add_to_workbench({src}); + if (!st.is_ok()) + show_status(st); + } + } + } + ImGui::EndDragDropTarget(); + } + + if (dragging_wbk || dragging_design) + { + const ImVec2 rmin = ImGui::GetItemRectMin(); + const ImVec2 rmax = ImGui::GetItemRectMax(); + if (root_drop_hovered) + ImGui::GetWindowDrawList()->AddRectFilled(rmin, rmax, ImGui::GetColorU32(ImGuiCol_HeaderHovered, 0.55f)); + + const char* hint = dragging_design ? "Add to Workbench" : "Move to root"; + const ImVec2 ts = ImGui::CalcTextSize(hint); + ImGui::GetWindowDrawList()->AddText( + ImVec2(rmin.x + (rmax.x - rmin.x - ts.x) * 0.5f, rmin.y + (rmax.y - rmin.y - ts.y) * 0.5f), + ImGui::GetColorU32(ImGuiCol_TextDisabled), hint); + } + } + + ImGui::PopStyleVar(2); + ImGui::EndTable(); + } + + if (row.hover) + m_view->set_shape_list_hover(row.hover); + else if (m_view->shape_list_hover() && m_view->shape_list_hover()->is_workbench()) + m_view->set_shape_list_hover(nullptr); + + if (row.to_ungroup_id != 0) + { + m_workbench_list_expanded.erase(row.to_ungroup_id); + (void)m_view->ungroup_workbench_shape(row.to_ungroup_id); + } + + if (row.to_delete) + m_view->delete_shapes({row.to_delete}); + + ImGui::End(); +} + void GUI::open_shape_info_(const Shp_ptr& shape) { if (shape.IsNull() || shape->is_group()) @@ -242,6 +459,14 @@ void GUI::shape_info_dialog_() break; } + if (!shape_still_exists) + for (const Shp_ptr& s : m_view->get_workbench_shapes()) + if (s == m_shape_info_shp) + { + shape_still_exists = true; + break; + } + if (!shape_still_exists) { clear_all(m_shape_info_open, m_shape_info_shp, m_shape_info_lines); @@ -360,13 +585,70 @@ void GUI::apply_shape_list_ui_from_json_(const nlohmann::json& j) m_view->set_current_group_id(sl["currentGroupId"].get()); } +nlohmann::json GUI::workbench_list_ui_to_json_() const +{ + using namespace nlohmann; + json out; + json expanded = json::object(); + for (const auto& [id, is_open] : m_workbench_list_expanded) + { + if (!is_open) + expanded[std::to_string(id)] = false; + } + if (!expanded.empty()) + out["expanded"] = std::move(expanded); + + if (m_view && m_view->current_workbench_group_id() != 0) + out["currentGroupId"] = m_view->current_workbench_group_id(); + + return out; +} + +void GUI::apply_workbench_list_ui_from_json_(const nlohmann::json& j) +{ + using namespace nlohmann; + m_workbench_list_expanded.clear(); + if (m_view) + m_view->set_current_workbench_group_id(0); + + if (!j.contains("ui") || !j["ui"].is_object()) + return; + + const json& ui = j["ui"]; + if (!ui.contains("workbenchList") || !ui["workbenchList"].is_object()) + return; + + const json& sl = ui["workbenchList"]; + if (sl.contains("expanded") && sl["expanded"].is_object()) + { + for (auto it = sl["expanded"].begin(); it != sl["expanded"].end(); ++it) + { + if (!it.value().is_boolean()) + continue; + + try + { + const Shape_id id = static_cast(std::stoull(it.key())); + m_workbench_list_expanded[id] = it.value().get(); + } + catch (...) + { + } + } + } + + if (m_view && sl.contains("currentGroupId") && sl["currentGroupId"].is_number_unsigned()) + m_view->set_current_workbench_group_id(sl["currentGroupId"].get()); +} + namespace gui_shp_detail { Shape_list_row_drawer::Shape_list_row_drawer(GUI& gui, const std::vector& mat_names, int nmat, - float mat_popup_w) + float mat_popup_w, Shp_list_kind kind) : m_gui(gui) , m_view(*gui.get_view()) + , m_kind(kind) , m_mat_names(mat_names) , m_nmat(nmat) , m_mat_popup_w(mat_popup_w) @@ -376,6 +658,21 @@ Shape_list_row_drawer::Shape_list_row_drawer(GUI& gui, const std::vector Shape_list_row_drawer::children_(Shape_id parent_id) const +{ + return m_kind == Shp_list_kind::Workbench ? m_view.workbench_children(parent_id) : m_view.shape_children(parent_id); +} + +std::vector Shape_list_row_drawer::descendant_solids_(Shape_id id) const +{ + return m_kind == Shp_list_kind::Workbench ? m_view.workbench_descendant_solids(id) : m_view.shape_descendant_solids(id); +} + +std::unordered_map& Shape_list_row_drawer::expanded_() +{ + return m_kind == Shp_list_kind::Workbench ? m_gui.m_workbench_list_expanded : m_gui.m_shape_list_expanded; +} + bool Shape_list_row_drawer::row_is_selected_(const Shp_ptr& shape) const { if (shape.IsNull()) @@ -383,7 +680,7 @@ bool Shape_list_row_drawer::row_is_selected_(const Shp_ptr& shape) const if (shape->is_group()) { - for (const Shp_ptr& leaf : m_view.shape_descendant_solids(shape->get_id())) + for (const Shp_ptr& leaf : descendant_solids_(shape->get_id())) if (m_selected_in_viewer.count(leaf.get()) != 0) return true; @@ -402,13 +699,21 @@ void Shape_list_row_drawer::select_row_(const Shp_ptr& shape) if (shape->is_group()) { - m_view.set_current_group_id(shape->get_id()); - for (const Shp_ptr& leaf : m_view.shape_descendant_solids(shape->get_id())) + if (m_kind == Shp_list_kind::Workbench) + m_view.set_current_workbench_group_id(shape->get_id()); + else + m_view.set_current_group_id(shape->get_id()); + + for (const Shp_ptr& leaf : descendant_solids_(shape->get_id())) ctx.AddOrRemoveSelected(leaf, true); } else { - m_view.set_current_group_id(shape->get_parent_id()); + if (m_kind == Shp_list_kind::Workbench) + m_view.set_current_workbench_group_id(shape->get_parent_id()); + else + m_view.set_current_group_id(shape->get_parent_id()); + ctx.AddOrRemoveSelected(shape, true); } @@ -432,7 +737,7 @@ void Shape_list_row_drawer::apply_material_(const Shp_ptr& shape, int i) void Shape_list_row_drawer::draw_ctx_menu_(const Shp_ptr& shape, bool is_group) { - const bool can_zoom = is_group ? !m_view.shape_descendant_solids(shape->get_id()).empty() : !shape->Shape().IsNull(); + const bool can_zoom = is_group ? !descendant_solids_(shape->get_id()).empty() : !shape->Shape().IsNull(); if (ImGui::MenuItem("Zoom to", nullptr, false, can_zoom)) { select_row_(shape); @@ -496,6 +801,15 @@ void Shape_list_row_drawer::draw_ctx_menu_(const Shp_ptr& shape, bool is_group) } } + if (m_kind == Shp_list_kind::Design && ImGui::MenuItem("Add to Workbench")) + { + const Status st = m_view.add_to_workbench({shape}); + if (!st.is_ok()) + m_gui.show_status(st); + else + m_gui.show_message("Added to Workbench.", Status_msg::Success); + } + if (ImGui::MenuItem("Delete")) to_delete = shape; } @@ -505,13 +819,33 @@ void Shape_list_row_drawer::accept_reparent_drop_(const Shp_ptr& shape, bool is_ if (!ImGui::BeginDragDropTarget()) return; - if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("EZY_SHAPE_ID")) + const char* payload_type = m_kind == Shp_list_kind::Workbench ? "EZY_WBK_ID" : "EZY_SHAPE_ID"; + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(payload_type)) { Shape_id drag_id = 0; std::memcpy(&drag_id, payload->Data, sizeof(drag_id)); const Shape_id new_parent = is_group ? shape->get_id() : shape->get_parent_id(); - (void)m_view.reparent_shape(drag_id, new_parent, -1, true); + if (m_kind == Shp_list_kind::Workbench) + (void)m_view.reparent_workbench_shape(drag_id, new_parent, -1, true); + else + (void)m_view.reparent_shape(drag_id, new_parent, -1, true); } + + if (m_kind == Shp_list_kind::Workbench) + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("EZY_SHAPE_ID")) + { + Shape_id drag_id = 0; + std::memcpy(&drag_id, payload->Data, sizeof(drag_id)); + if (Shp_ptr src = m_view.find_design_shape_by_id(drag_id); !src.IsNull()) + { + if (is_group) + m_view.set_current_workbench_group_id(shape->get_id()); + + const Status st = m_view.add_to_workbench({src}); + if (!st.is_ok()) + m_gui.show_status(st); + } + } ImGui::EndDragDropTarget(); } @@ -522,9 +856,11 @@ void Shape_list_row_drawer::draw(const Shp_ptr& shape) return; // Parent cycle: skip rather than hang the Shape List. const bool is_group = shape->is_group(); - const std::vector children = m_view.shape_children(shape->get_id()); + const std::vector children = children_(shape->get_id()); const bool has_children = !children.empty(); - const bool is_current_group = is_group && shape->get_id() == m_view.current_group_id(); + const Shape_id cur_group = + m_kind == Shp_list_kind::Workbench ? m_view.current_workbench_group_id() : m_view.current_group_id(); + const bool is_current_group = is_group && shape->get_id() == cur_group; // Selection highlight follows the 3D viewer only. Current group uses a distinct tint so // Alt-drag / clear-selection cannot look like the group (or its children) stayed selected. const bool row_selected = row_is_selected_(shape); @@ -638,15 +974,15 @@ void Shape_list_row_drawer::draw(const Shp_ptr& shape) bool open = true; if (has_children) { - const auto exp_it = m_gui.m_shape_list_expanded.find(shape->get_id()); - open = (exp_it == m_gui.m_shape_list_expanded.end()) ? true : exp_it->second; + const auto exp_it = expanded_().find(shape->get_id()); + open = (exp_it == expanded_().end()) ? true : exp_it->second; ImGui::SetNextItemOpen(open); } ImGui::SetNextItemAllowOverlap(); const bool node_open = ImGui::TreeNodeEx("##node", node_flags); if (has_children && node_open != open) - m_gui.m_shape_list_expanded[shape->get_id()] = node_open; + expanded_()[shape->get_id()] = node_open; if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) select_row_(shape); @@ -660,10 +996,11 @@ void Shape_list_row_drawer::draw(const Shp_ptr& shape) ImGui::SetTooltip("Current group (new shapes go here)"); } + const char* drag_type = m_kind == Shp_list_kind::Workbench ? "EZY_WBK_ID" : "EZY_SHAPE_ID"; if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { const Shape_id drag_id = shape->get_id(); - ImGui::SetDragDropPayload("EZY_SHAPE_ID", &drag_id, sizeof(drag_id)); + ImGui::SetDragDropPayload(drag_type, &drag_id, sizeof(drag_id)); ImGui::TextUnformatted(shape->get_name().c_str()); ImGui::EndDragDropSource(); } @@ -683,7 +1020,7 @@ void Shape_list_row_drawer::draw(const Shp_ptr& shape) if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { const Shape_id drag_id = shape->get_id(); - ImGui::SetDragDropPayload("EZY_SHAPE_ID", &drag_id, sizeof(drag_id)); + ImGui::SetDragDropPayload(drag_type, &drag_id, sizeof(drag_id)); ImGui::TextUnformatted(shape->get_name().c_str()); ImGui::EndDragDropSource(); } @@ -702,7 +1039,7 @@ void Shape_list_row_drawer::draw(const Shp_ptr& shape) } const ImGuiPayload* dd = ImGui::GetDragDropPayload(); - const bool dragging_row = dd != nullptr && dd->IsDataType("EZY_SHAPE_ID"); + const bool dragging_row = dd != nullptr && (dd->IsDataType("EZY_SHAPE_ID") || dd->IsDataType("EZY_WBK_ID")); if (row_hovered && !dragging_row && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) ImGui::OpenPopup("shape_row_ctx"); diff --git a/src/mode.cpp b/src/mode.cpp index 669f2a13..0bee080e 100644 --- a/src/mode.cpp +++ b/src/mode.cpp @@ -14,10 +14,10 @@ Mode mode_from_string(std::string_view name) return Mode::Sketch_inspection; if (name == "Move") - return Mode::Workbench_move; + return Mode::Design_move; if (name == "Rotate") - return Mode::Workbench_rotate; + return Mode::Design_rotate; if (name == "Workbench_scale") return Mode::Scale; @@ -73,6 +73,21 @@ bool is_workbench_mode(const Mode mode) return mode == Mode::Workbench_inspection || is_workbench_transform_mode(mode); } +bool is_move_mode(const Mode mode) +{ + return mode == Mode::Design_move || mode == Mode::Workbench_move; +} + +bool is_rotate_mode(const Mode mode) +{ + return mode == Mode::Design_rotate || mode == Mode::Workbench_rotate; +} + +bool is_shaft_align_mode(const Mode mode) +{ + return mode == Mode::Design_shaft_align || mode == Mode::Workbench_shaft_align; +} + bool is_shape_browse_mode(const Mode mode) { return mode == Mode::Design_inspection || mode == Mode::Workbench_inspection; diff --git a/src/mode.h b/src/mode.h index b1eea74d..40dbd4b7 100644 --- a/src/mode.h +++ b/src/mode.h @@ -33,7 +33,10 @@ X(Shape_cross_section) \ X(Workbench_shaft_align) \ X(Shape_set_frame) /* Shape List only: pick face for local frame */ \ - X(Workbench_inspection) /* Workbench browse/select home; not Mode::Design_inspection */ + X(Workbench_inspection) /* Workbench browse/select home; not Mode::Design_inspection */ \ + X(Design_move) /* Design CSG placement; bake BREP. Workbench_move is instance pose. */ \ + X(Design_rotate) /* Design CSG rotation; bake BREP. Workbench_rotate is instance pose. */ \ + X(Design_shaft_align) /* Design CSG shaft align; bake BREP. Workbench_shaft_align is instance pose. */ enum class Mode { @@ -125,6 +128,12 @@ bool is_sketch_mode(const Mode mode); bool is_workbench_mode(const Mode mode); /// Workbench Move / Rotate / Align shafts (not idle; Scale is Design). bool is_workbench_transform_mode(const Mode mode); +/// Design or Workbench Move (same tool, different bake / parent). +bool is_move_mode(const Mode mode); +/// Design or Workbench Rotate. +bool is_rotate_mode(const Mode mode); +/// Design or Workbench Align shafts. +bool is_shaft_align_mode(const Mode mode); /// Design Inspection or Workbench idle (solid browse / selection-filter digits). bool is_shape_browse_mode(const Mode mode); @@ -132,5 +141,5 @@ Task task_of(Mode mode); Mode idle_mode_of(Task task); /// Return Mode for a name (e.g. "Design_inspection", "Sketch_add_edge"). Returns Design_inspection if not found. -/// Also accepts retired names: Normal, Sketch_inspection_mode, Move, Rotate, Workbench_scale, Shape_shaft_align. +/// Also accepts retired names: Normal, Sketch_inspection_mode, Move (Design_move), Rotate (Design_rotate), Workbench_scale, Shape_shaft_align. Mode mode_from_string(std::string_view name); diff --git a/src/shp.cpp b/src/shp.cpp index 7c7ff352..0c36711d 100644 --- a/src/shp.cpp +++ b/src/shp.cpp @@ -161,6 +161,23 @@ void Shp::update_display_() m_ctx.UpdateCurrentViewer(); } +gp_Trsf Shp::trsf_from_frame(const gp_Ax3& frame) +{ + // Two-arg form: coordinates in `frame` -> world. Single-arg SetTransformation(frame) + // is the inverse (world -> frame) on both OCCT 7.9 and 8. + gp_Trsf t; + t.SetTransformation(frame, gp_Ax3()); + return t; +} + +gp_Trsf Shp::placement_trsf() const +{ + if (!m_is_workbench) + return gp_Trsf(); + + return trsf_from_frame(m_frame); +} + void Shp::set_frame(const gp_Ax3& frame) { m_frame = frame; @@ -259,10 +276,13 @@ void Shp::update_frame_display() if (arm <= 0.0) return; - const gp_Pnt o = m_frame.Location(); - const gp_Vec x(m_frame.XDirection()); - const gp_Vec y(m_frame.YDirection()); - const gp_Vec z(m_frame.Direction()); + // Workbench placement lives on LocalTransformation; draw the triad in local + // space so preview transforms carry the annotations. Design bakes to identity. + const gp_Ax3 draw = m_is_workbench ? gp_Ax3() : m_frame; + const gp_Pnt o = draw.Location(); + const gp_Vec x(draw.XDirection()); + const gp_Vec y(draw.YDirection()); + const gp_Vec z(draw.Direction()); const gp_Trsf trsf = LocalTransformation(); auto display_wire = [&](const TopoDS_Shape& geom, Quantity_NameOfColor color, double width) -> AIS_Shape_ptr @@ -289,7 +309,7 @@ void Shp::update_frame_display() if (m_show_frame_plane) { const double half = arm * 0.75; - const gp_Pln pln(m_frame); + const gp_Pln pln(draw); const TopoDS_Face face = BRepBuilderAPI_MakeFace(pln, -half, half, -half, half).Face(); m_frame_plane_fill_ais = new AIS_Shape(face); m_frame_plane_fill_ais->SetColor(Quantity_NOC_CYAN); diff --git a/src/shp.h b/src/shp.h index bc76f03b..58be1c54 100644 --- a/src/shp.h +++ b/src/shp.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,19 @@ class Shp : public AIS_Shape int get_sibling_order() const { return m_sibling_order; } void set_sibling_order(int order) { m_sibling_order = order; } + /// Workbench list node (instance or workbench-only group). Design nodes are false. + bool is_workbench() const { return m_is_workbench; } + void set_is_workbench(bool v) { m_is_workbench = v; } + /// Design shape this instance links to (0 = workbench group or orphan). + Shape_id get_source_id() const { return m_source_id; } + void set_source_id(Shape_id id) { m_source_id = id; } + bool is_workbench_link() const { return m_is_workbench && !m_is_group && m_source_id != 0; } + + /// World placement from get_frame() for workbench nodes; identity for Design. + gp_Trsf placement_trsf() const; + /// gp_Trsf that maps frame-local coordinates to world. + static gp_Trsf trsf_from_frame(const gp_Ax3& frame); + /// Shape-local frame metadata. New shapes default to a world-aligned frame /// centered on their bounding box. Z is the primary axis; Y is "up". const gp_Ax3& get_frame() const { return m_frame; } @@ -89,6 +103,8 @@ class Shp : public AIS_Shape bool m_sketch_faint_active{false}; AIS_DisplayMode m_faint_disp_mode{AIS_Shaded}; bool m_is_group{false}; + bool m_is_workbench{false}; + Shape_id m_source_id{0}; Shape_id m_parent_id{0}; int m_sibling_order{0}; gp_Ax3 m_frame; diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index cc5c4a5c..0f442738 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -152,7 +152,7 @@ void Shp_cyl_align::apply_preview_() const gp_Trsf trsf = cyl_align_trsf(*m_moving_axis, *m_fixed_axis, m_opts.flip_direction, offset, twist); for (const Shp_ptr& shape : m_shps) - shape->SetLocalTransformation(trsf); + shape->SetLocalTransformation(trsf * shape->placement_trsf()); redisplay_operation_shps_after_transform_(); } @@ -338,6 +338,7 @@ void Shp_cyl_align::cancel() void Shp_cyl_align::reset() { + const Mode parent = GUI::parent_mode_of(gui().get_mode()); begin(); - gui().set_mode(Mode::Workbench_inspection); + gui().set_mode(parent); } diff --git a/src/shp_delta.cpp b/src/shp_delta.cpp index ef474046..35bb9176 100644 --- a/src/shp_delta.cpp +++ b/src/shp_delta.cpp @@ -20,6 +20,8 @@ Shape_rec capture_shape_rec(const Shp& shp) rec.parent_id = shp.get_parent_id(); rec.sibling_order = shp.get_sibling_order(); rec.is_group = shp.is_group(); + rec.is_workbench = shp.is_workbench(); + rec.source_id = shp.get_source_id(); rec.visible = shp.get_visible(); rec.show_frame_axes = shp.show_frame_axes(); rec.show_frame_plane = shp.show_frame_plane(); diff --git a/src/shp_delta.h b/src/shp_delta.h index 39931755..6c511766 100644 --- a/src/shp_delta.h +++ b/src/shp_delta.h @@ -20,6 +20,8 @@ struct Shape_rec Shape_id parent_id{0}; int sibling_order{0}; bool is_group{false}; + bool is_workbench{false}; + Shape_id source_id{0}; bool visible{true}; bool show_frame_axes{false}; bool show_frame_plane{false}; diff --git a/src/shp_move.cpp b/src/shp_move.cpp index 0766d4c2..0fb46a8f 100644 --- a/src/shp_move.cpp +++ b/src/shp_move.cpp @@ -41,7 +41,7 @@ Status Shp_move::move_selected(const ScreenCoords& screen_coords) translation.SetTranslation(transform_translation(axes, m_delta.delta)); for (const Shp_ptr& shape : m_shps) - shape->SetLocalTransformation(translation); + shape->SetLocalTransformation(translation * shape->placement_trsf()); redisplay_operation_shps_after_transform_(); @@ -144,8 +144,9 @@ void Shp_move::cancel() void Shp_move::reset() { + const Mode parent = GUI::parent_mode_of(gui().get_mode()); clear_all(m_opts, m_delta, m_move_pln, m_center, m_shps); - gui().set_mode(Mode::Workbench_inspection); + gui().set_mode(parent); } Move_options& Shp_move::get_opts() { return m_opts; } diff --git a/src/shp_operation.cpp b/src/shp_operation.cpp index 26b403d6..04137bc6 100644 --- a/src/shp_operation.cpp +++ b/src/shp_operation.cpp @@ -95,7 +95,7 @@ void Shp_operation_base::operation_shps_cancel_() m_completed_shps = m_shps; for (Shp_ptr& shape : m_shps) - shape->ResetTransformation(); + shape->SetLocalTransformation(shape->placement_trsf()); } void Shp_operation_base::restore_operation_selection_() @@ -144,11 +144,14 @@ void Shp_operation_base::replace_picked_shape_(Shp_ptr& old_shp, Shp_ptr& new_sh v.get_shapes().remove(old_shp); new_shp->set_name(name); + new_shp->set_id(old_shp->get_id()); + new_shp->set_frame(old_shp->get_frame()); new_shp->set_parent_id(old_shp->get_parent_id()); new_shp->set_sibling_order(old_shp->get_sibling_order()); add_shp_(new_shp); copy_shape_material_from_(new_shp, old_shp); ctx().Display(new_shp, new_shp->get_disp_mode(), AIS_Shape::SelectionMode(v.get_shp_selection_mode()), true); + v.sync_workbench_links(new_shp->get_id()); v.redraw_view(); } diff --git a/src/shp_rotate.cpp b/src/shp_rotate.cpp index bcfc9f49..de4bdbee 100644 --- a/src/shp_rotate.cpp +++ b/src/shp_rotate.cpp @@ -229,7 +229,7 @@ void Shp_rotate::preview_rotate_() rotation.SetRotation(gp_Ax1(*m_center, current_axis_dir_()), m_angle); for (const Shp_ptr& shape : m_shps) - shape->SetLocalTransformation(rotation); + shape->SetLocalTransformation(rotation * shape->placement_trsf()); redisplay_operation_shps_after_transform_(); } @@ -287,9 +287,10 @@ void Shp_rotate::cancel() void Shp_rotate::reset() { + const Mode parent = GUI::parent_mode_of(gui().get_mode()); clear_all(m_angle, m_shps, m_initial_mouse_pos, m_rotate_pln, m_captured_axis_dir, m_center); clear_rotation_vis_(); - gui().set_mode(Mode::Workbench_inspection); + gui().set_mode(parent); } void Shp_rotate::clear_rotation_vis_() diff --git a/src/shp_scale.cpp b/src/shp_scale.cpp index 078635c4..55116586 100644 --- a/src/shp_scale.cpp +++ b/src/shp_scale.cpp @@ -75,7 +75,7 @@ void Shp_scale::preview_scale_() scale_trsf.SetScale(*m_center, m_scale_factor); for (const Shp_ptr& shape : m_shps) - shape->SetLocalTransformation(scale_trsf); + shape->SetLocalTransformation(scale_trsf * shape->placement_trsf()); redisplay_operation_shps_after_transform_(); } diff --git a/src/shp_transform.cpp b/src/shp_transform.cpp index da6e52fe..6a47f377 100644 --- a/src/shp_transform.cpp +++ b/src/shp_transform.cpp @@ -36,7 +36,8 @@ Transform_axes transform_axes_for(const std::vector& shps, Transform_sp return axes; } - axes.origin = get_shape_bbox_center(s->Shape()); + const gp_Pnt local_c = get_shape_bbox_center(s->Shape()); + axes.origin = s->is_workbench() ? local_c.Transformed(s->placement_trsf()) : local_c; return axes; } diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index 1a7c8544..08206957 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -30,6 +30,7 @@ #include "shp_rotate.h" #include "shp_scale.h" #include "shp_transform.h" +#include "utl_geom.h" #include "skt_op_recorder.h" #include "utl.h" @@ -1636,3 +1637,177 @@ TEST_F(Shp_test, Rotate_space_change_mid_drag_keeps_angle) gui().set_mode(Mode::Design_inspection); } + +// --------------------------------------------------------------------------- +// Workbench list (geometry links, own placement) +// --------------------------------------------------------------------------- + +TEST(Mode_helpers, Design_move_rotate_stay_on_design_task) +{ + EXPECT_EQ(task_of(Mode::Design_move), Task::Design); + EXPECT_EQ(task_of(Mode::Design_rotate), Task::Design); + EXPECT_EQ(task_of(Mode::Design_shaft_align), Task::Design); + EXPECT_EQ(task_of(Mode::Scale), Task::Design); + EXPECT_EQ(task_of(Mode::Workbench_move), Task::Workbench); + EXPECT_EQ(task_of(Mode::Workbench_rotate), Task::Workbench); + EXPECT_EQ(task_of(Mode::Workbench_shaft_align), Task::Workbench); + EXPECT_TRUE(is_move_mode(Mode::Design_move)); + EXPECT_TRUE(is_rotate_mode(Mode::Design_rotate)); + EXPECT_TRUE(is_shaft_align_mode(Mode::Design_shaft_align)); + EXPECT_TRUE(is_shaft_align_mode(Mode::Workbench_shaft_align)); + EXPECT_FALSE(is_workbench_mode(Mode::Design_move)); + EXPECT_FALSE(is_workbench_mode(Mode::Design_rotate)); + EXPECT_FALSE(is_workbench_mode(Mode::Design_shaft_align)); + EXPECT_EQ(GUI::parent_mode_of(Mode::Design_move), Mode::Design_inspection); + EXPECT_EQ(GUI::parent_mode_of(Mode::Design_rotate), Mode::Design_inspection); + EXPECT_EQ(GUI::parent_mode_of(Mode::Design_shaft_align), Mode::Design_inspection); + EXPECT_EQ(GUI::parent_mode_of(Mode::Workbench_move), Mode::Workbench_inspection); + EXPECT_EQ(GUI::parent_mode_of(Mode::Workbench_shaft_align), Mode::Workbench_inspection); + EXPECT_EQ(mode_from_string("Move"), Mode::Design_move); + EXPECT_EQ(mode_from_string("Rotate"), Mode::Design_rotate); +} + +TEST(Shp_frame, Trsf_from_frame_maps_local_origin_to_world) +{ + const gp_Ax3 f(gp_Pnt(10.0, 20.0, 30.0), gp_Dir(0.0, 0.0, 1.0), gp_Dir(1.0, 0.0, 0.0)); + const gp_Pnt world = gp_Pnt(0.0, 0.0, 0.0).Transformed(Shp::trsf_from_frame(f)); + EXPECT_TRUE(world.IsEqual(gp_Pnt(10.0, 20.0, 30.0), 1e-9)); +} + +TEST_F(Shp_test, Add_to_workbench_creates_geometry_link) +{ + view().add_box(0, 0, 0, 10, 10, 10); + ASSERT_EQ(view().get_shapes().size(), 1u); + Shp_ptr src = view().get_shapes().back(); + const Shape_id src_id = src->get_id(); + const double vol = volume_of(src->Shape()); + + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + ASSERT_EQ(view().get_workbench_shapes().size(), 1u); + Shp_ptr inst = view().get_workbench_shapes().back(); + EXPECT_TRUE(inst->is_workbench()); + EXPECT_TRUE(inst->is_workbench_link()); + EXPECT_EQ(inst->get_source_id(), src_id); + EXPECT_NE(inst->get_id(), src_id); + EXPECT_NEAR(volume_of(inst->Shape()), vol, 1e-6); +} + +TEST_F(Shp_test, Design_move_does_not_move_workbench_instance) +{ + view().add_box(0, 0, 0, 10, 10, 10); + Shp_ptr src = view().get_shapes().back(); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + Shp_ptr inst = view().get_workbench_shapes().back(); + const gp_Pnt inst_origin = inst->get_frame().Location(); + const gp_Pnt src_origin = src->get_frame().Location(); + + gp_Trsf move; + move.SetTranslation(gp_Vec(40.0, 0.0, 0.0)); + src->SetLocalTransformation(move); + AIS_Shape_ptr ais = src; + view().bake_transform_into_geometry(ais); + view().sync_workbench_links(src->get_id()); + + EXPECT_TRUE(src->get_frame().Location().IsEqual(src_origin.Translated(gp_Vec(40.0, 0.0, 0.0)), 1e-6)); + EXPECT_TRUE(inst->get_frame().Location().IsEqual(inst_origin, 1e-6)); + EXPECT_NEAR(volume_of(inst->Shape()), volume_of(src->Shape()), 1e-6); +} + +TEST_F(Shp_test, Workbench_move_bakes_instance_frame_only) +{ + view().add_box(0, 0, 0, 10, 10, 10); + Shp_ptr src = view().get_shapes().back(); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + Shp_ptr inst = view().get_workbench_shapes().back(); + const gp_Pnt frame0 = inst->get_frame().Location(); + const gp_Pnt disp0 = get_shape_bbox_center(inst->Shape()).Transformed(inst->placement_trsf()); + + gp_Trsf move; + move.SetTranslation(gp_Vec(25.0, 0.0, 0.0)); + inst->SetLocalTransformation(move * inst->placement_trsf()); + AIS_Shape_ptr ais = inst; + view().bake_transform_into_geometry(ais); + + EXPECT_TRUE(inst->get_frame().Location().IsEqual(frame0.Translated(gp_Vec(25.0, 0.0, 0.0)), 1e-6)); + const gp_Pnt disp1 = get_shape_bbox_center(inst->Shape()).Transformed(inst->placement_trsf()); + EXPECT_TRUE(disp1.IsEqual(disp0.Translated(gp_Vec(25.0, 0.0, 0.0)), 1e-6)); + EXPECT_NEAR(volume_of(inst->Shape()), volume_of(src->Shape()), 1e-6); + + const Transform_axes world_axes = transform_axes_for({inst}, Transform_space::World); + EXPECT_TRUE(world_axes.origin.IsEqual(disp1, 1e-6)); +} + +TEST_F(Shp_test, Design_geom_change_syncs_workbench_link) +{ + view().add_box(0, 0, 0, 10, 10, 10); + Shp_ptr src = view().get_shapes().back(); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + Shp_ptr inst = view().get_workbench_shapes().back(); + EXPECT_NEAR(volume_of(inst->Shape()), 1000.0, 1e-4); + + const TopoDS_Shape bigger = shp_create::create_box(0, 0, 0, 20, 20, 20); + view().set_shape_geom_by_id(src->get_id(), bigger, src->get_frame()); + EXPECT_NEAR(volume_of(inst->Shape()), 8000.0, 1e-3); + EXPECT_TRUE(inst->get_frame().Location().IsEqual(src->get_frame().Location(), 1e-6)); +} + +TEST_F(Shp_test, Workbench_json_round_trip_and_source_delete) +{ + view().add_box(0, 0, 0, 4, 5, 6); + Shp_ptr src = view().get_shapes().back(); + const Shape_id src_id = src->get_id(); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + Shp_ptr inst = view().get_workbench_shapes().back(); + const Shape_id inst_id = inst->get_id(); + gp_Ax3 placed = inst->get_frame(); + placed.SetLocation(gp_Pnt(15.0, 0.0, 0.0)); + inst->set_frame(placed); + + const std::string json = view().to_json(); + view().new_file(); + EXPECT_TRUE(view().get_shapes().empty()); + EXPECT_TRUE(view().get_workbench_shapes().empty()); + + view().load(json, false); + ASSERT_EQ(view().get_shapes().size(), 1u); + ASSERT_EQ(view().get_workbench_shapes().size(), 1u); + Shp_ptr loaded = view().get_workbench_shapes().back(); + EXPECT_EQ(loaded->get_id(), inst_id); + EXPECT_EQ(loaded->get_source_id(), src_id); + EXPECT_TRUE(loaded->is_workbench_link()); + EXPECT_TRUE(loaded->get_frame().Location().IsEqual(gp_Pnt(15.0, 0.0, 0.0), 1e-6)); + EXPECT_NEAR(volume_of(loaded->Shape()), volume_of(view().get_shapes().back()->Shape()), 1e-6); + + view().delete_shapes({view().get_shapes().back()}); + EXPECT_TRUE(view().get_shapes().empty()); + EXPECT_TRUE(view().get_workbench_shapes().empty()); +} + +TEST_F(Shp_test, Add_design_group_to_workbench) +{ + view().add_box(0, 0, 0, 1, 1, 1); + view().add_box(3, 0, 0, 1, 1, 1); + std::vector boxes(view().get_shapes().begin(), view().get_shapes().end()); + ASSERT_TRUE(view().group_shapes(boxes).is_ok()); + Shp_ptr grp; + for (const Shp_ptr& s : view().get_shapes()) + if (s->is_group()) + grp = s; + + ASSERT_FALSE(grp.IsNull()); + ASSERT_TRUE(view().add_to_workbench({grp}).is_ok()); + EXPECT_EQ(view().get_workbench_shapes().size(), 3u); + + size_t wbk_groups = 0; + size_t wbk_links = 0; + for (const Shp_ptr& s : view().get_workbench_shapes()) + { + if (s->is_group()) + ++wbk_groups; + else if (s->is_workbench_link()) + ++wbk_links; + } + + EXPECT_EQ(wbk_groups, 1u); + EXPECT_EQ(wbk_links, 2u); +} From 41047f063d1b3dd488cbc5825dc709a281680346 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Mon, 21 Sep 2026 17:13:03 -0600 Subject: [PATCH 04/12] Align shafts fix. --- CHANGELOG.md | 1 + src/doc/gui.md | 2 +- src/doc/shape.md | 2 +- src/gui_occt_view.cpp | 22 ++++++++++++--------- src/shp.cpp | 3 +++ src/shp_cyl_align.cpp | 7 +++++-- tests/shp_tests.cpp | 46 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26535611..1a3d955a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **Workbench toolbar**: the tools row no longer repeats the Workbench task icon; idle is the task button only. +- **Workbench Align shafts**: cylindrical face picking uses Face selection on Workbench instances (same as Design). Insert position uses the instance placement (axis direction was already correct). - **Shape List right-click**: the row context menu opens from the whole entry (checkboxes, **M**, tree arrow, name field, and padding), not only from some widgets. Right-click on the name field opens the same menu. - **Shape rotate axis**: Choosing X/Y/Z (or view-to-object) before the first drag is kept. Constrained rotation uses the plane perpendicular to that axis when the view faces it. Local space rotates about the assigned frame, not only world XYZ through the bbox. - **Shape rotate drag**: View-to-object axis and the constrained axis-vs-view plane are captured on the first mouse sample. Orbit or pan during rotate no longer changes the axis/plane against the original mouse anchor. diff --git a/src/doc/gui.md b/src/doc/gui.md index edcf0429..1a0effa4 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -64,7 +64,7 @@ Pick a default that does not collide with existing `c_actions` chords or fixed k `Occt_view::on_mode` also sets `AIS_ViewController::SetAllowHighlight(false)` for `is_move_mode` / `is_rotate_mode` / `Scale` (and `ClearDetected`) so idle mouse moves do not run dynamic `MoveTo` while transform preview leaves selection BVHs at the pre-transform pose; other modes restore highlight. Orbit/pan still receive `UpdateMousePosition` when buttons are held. When LMB finalizes an active transform (operands loaded), `on_mouse_button` skips `PressMouseButton` / `ReleaseMouseButton` for that click so AIS `SelectDetected` on release cannot replace the restored multi-selection with the single shape under the cursor. -Because the faint/selection-mode redisplay `Erase`s shapes (dropping the AIS selection), `on_mode` snapshots the entered selection for Move / Rotate / Scale / `Shape_cross_section` and restores it via `Occt_view::set_selected_shps` after `sync_sketch_shape_faint_style()`. The same helper restores the operands when a transform tool finishes (see `restore_operation_selection_` in [shape.md](shape.md)); `Occt_view::cancel` therefore treats those transform modes as already handled instead of switching mode again. Transform tools are also seeded via `Shp_move` / `Shp_rotate` / `Shp_scale::begin(enter_selection)` so multi-select operands do not depend on AIS selection surviving the mode switch (`ensure_operation_shps_` still falls back to the AIS selection when the seed was empty). Move/Rotate/Scale use `TopAbs_SHAPE` (whole-object) selection mode. +Because the faint/selection-mode redisplay `Erase`s shapes (dropping the AIS selection), `on_mode` snapshots the entered selection for Move / Rotate / Scale / `Shape_cross_section` and restores it via `Occt_view::set_selected_shps` after `sync_sketch_shape_faint_style()`. The same helper restores the operands when a transform tool finishes (see `restore_operation_selection_` in [shape.md](shape.md)); `Occt_view::cancel` therefore treats those transform modes as already handled instead of switching mode again. Transform tools are also seeded via `Shp_move` / `Shp_rotate` / `Shp_scale::begin(enter_selection)` so multi-select operands do not depend on AIS selection surviving the mode switch (`ensure_operation_shps_` still falls back to the AIS selection when the seed was empty). Move/Rotate/Scale use `TopAbs_SHAPE` (whole-object) selection mode. Align shafts uses `TopAbs_FACE`. `set_shp_selection_mode` applies AIS activation to the on-screen store (`m_wbk_shps` in Workbench, `m_shps` otherwise), including when the document filter is already that enum (so Workbench Align shafts still arms Face pick after Design FACE). `Occt_view::on_mode` ends with `sync_sketch_shape_faint_style()`: while `is_sketch_mode`, Design shapes follow **Options/Settings** `gui.sketch_shape_faint_enabled` (master) plus `gui.sketch_shape_faint_style` (**0** hide / **1** ghost / **2** wire) and `gui.sketch_shape_faint_opacity` (ghost). Workbench modes hide `m_shps` and show `m_wbk_shps`; Sketch/Design hide workbench instances. **Hide all** is per list. New shapes call the same sync from `add_shp_` / `add_wbk_shp_` / `insert_shape_rec`. The **Faint shapes** checkbox lives in `options_sketch_common_` (every sketch tool). diff --git a/src/doc/shape.md b/src/doc/shape.md index a2c06a71..902dd47a 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -156,7 +156,7 @@ Protected helpers used by all operation classes: | `shp_rotate.h` | `Shp_rotate` | Rotate about view axis or Local/World X/Y/Z through the current pivot; angle Tab entry; axis/center AIS guides (Topmost). Axis can be set before the first drag. View-to-object axis and the axis-vs-view drag plane are captured on the first mouse sample so orbit/pan does not retarget a stale anchor. Mid-drag Local/World re-seeds the pivot and re-baselines the mouse anchor so the angle does not jump. | | `shp_scale.h` | `Shp_scale` | Uniform scale from Local frame origin or World bbox center vs mouse distance; clamped factor 0.01..100. Mid-drag Local/World re-seeds the pivot and re-baselines distance so the factor does not jump. | | `shp_transform.h` | helpers | `transform_axes_for` / `transform_translation` shared by move, rotate, scale. | -| `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); coaxial `cyl_align_trsf`; drag axial depth; Options **Clock rotation** (default off) then LMB / Shift+Tab about shared axis; Options flip; bake like Move. | +| `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); map cylinder axes through AIS `LocalTransformation` (Workbench instance pose; identity on Design); coaxial `cyl_align_trsf`; drag axial depth; Options **Clock rotation** (default off) then LMB / Shift+Tab about shared axis; Options flip; bake like Move. | | `shp_set_frame.h` | `Shp_set_frame` | Shape List-only: pick planar or cylindrical face to redefine `Shp` local frame; enables Show axes. | | `shp_fillet.h` | `Shp_fillet` | `add_fillet(..., Fillet_mode)` -- `BRepFilletAPI_MakeFillet`; modes: Shape, Face, Wire, Edge (`mode.h`). | | `shp_chamfer.h` | `Shp_chamfer` | `add_chamfer(..., Chamfer_mode)` -- diagonal distance converted to setback (`dist/sqrt(2)`). | diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index 93d907c4..6363e4f6 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -4070,16 +4070,20 @@ void Occt_view::set_shp_selection_mode(const TopAbs_ShapeEnum selection_mode) { m_modes_selection_mode_map[get_mode()] = selection_mode; - if (m_shp_selection_mode == selection_mode) - return; - - m_shp_selection_mode = selection_mode; - const std::size_t idx = static_cast(selection_mode); - EZY_ASSERT(idx < c_names_TopAbs_ShapeEnum.size()); - m_gui.log_message(std::string("Selection mode: ") + std::string(c_names_TopAbs_ShapeEnum[idx])); + if (m_shp_selection_mode != selection_mode) + { + m_shp_selection_mode = selection_mode; + const std::size_t idx = static_cast(selection_mode); + EZY_ASSERT(idx < c_names_TopAbs_ShapeEnum.size()); + m_gui.log_message(std::string("Selection mode: ") + std::string(c_names_TopAbs_ShapeEnum[idx])); + } - for (auto& shp : m_shps) - shp->set_selection_mode(selection_mode); + // Apply to the on-screen store even when the document filter is unchanged + // (Design FACE then Workbench Align shafts still needs FACE on instances). + std::list& store = is_workbench_mode(get_mode()) ? m_wbk_shps : m_shps; + for (auto& shp : store) + if (!shp.IsNull() && !shp->is_group()) + shp->set_selection_mode(selection_mode); } // Material related diff --git a/src/shp.cpp b/src/shp.cpp index 0c36711d..cfbfb6fe 100644 --- a/src/shp.cpp +++ b/src/shp.cpp @@ -115,6 +115,9 @@ void Shp::apply_context_shown(bool shown) void Shp::set_selection_mode(const TopAbs_ShapeEnum mode) { + if (m_selection_mode == mode) + return; + m_selection_mode = mode; if (!m_is_group) update_display_(); diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index 0f442738..2626b980 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -65,10 +65,13 @@ Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) if (!cyl) return Status::user_error("Selected face is not cylindrical."); + // AIS face geom is local BREP. Workbench pose is LocalTransformation (identity on Design). + const gp_Ax1 world_axis = cyl->axis.Transformed(shp->LocalTransformation()); + if (m_phase == Phase::Pick_moving) { m_moving_shp = shp; - m_moving_axis = cyl->axis; + m_moving_axis = world_axis; m_moving_radius = cyl->radius; m_phase = Phase::Pick_fixed; gui().show_message("Pick the fixed cylindrical face (hole or shaft)."); @@ -80,7 +83,7 @@ Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) return Status::user_error("Pick a cylindrical face on a different shape."); m_fixed_shp = shp; - m_fixed_axis = cyl->axis; + m_fixed_axis = world_axis; m_fixed_radius = cyl->radius; if (std::abs(m_moving_radius - m_fixed_radius) > Precision::Confusion()) diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index 08206957..a6c143ed 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -1737,6 +1739,50 @@ TEST_F(Shp_test, Workbench_move_bakes_instance_frame_only) EXPECT_TRUE(world_axes.origin.IsEqual(disp1, 1e-6)); } +TEST_F(Shp_test, Workbench_cyl_align_uses_instance_placement) +{ + view().add_cylinder(0, 0, 0, 1.0, 4.0); + Shp_ptr src = view().get_shapes().back(); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + auto it = view().get_workbench_shapes().begin(); + Shp_ptr moving = *it++; + Shp_ptr fixed = *it; + ASSERT_FALSE(moving.IsNull()); + ASSERT_FALSE(fixed.IsNull()); + + gp_Ax3 placed = moving->get_frame(); + placed.SetLocation(placed.Location().Translated(gp_Vec(50.0, 0.0, 0.0))); + moving->set_frame(placed); + moving->SetLocalTransformation(moving->placement_trsf()); + + auto first_cyl = [](const TopoDS_Shape& s) -> std::optional + { + for (TopExp_Explorer ex(s, TopAbs_FACE); ex.More(); ex.Next()) + if (std::optional c = cylinder_from_face(TopoDS::Face(ex.Current()))) + return c; + + return std::nullopt; + }; + + const std::optional moving_local = first_cyl(moving->Shape()); + const std::optional fixed_local = first_cyl(fixed->Shape()); + ASSERT_TRUE(moving_local.has_value()); + ASSERT_TRUE(fixed_local.has_value()); + + const gp_Ax1 moving_world = moving_local->axis.Transformed(moving->LocalTransformation()); + const gp_Ax1 fixed_world = fixed_local->axis.Transformed(fixed->LocalTransformation()); + const gp_Trsf local_only = cyl_align_trsf(moving_local->axis, fixed_local->axis, false, 0.0, 0.0); + const gp_Trsf world_align = cyl_align_trsf(moving_world, fixed_world, false, 0.0, 0.0); + + // Same linked local geom: local-only align does not close the instance gap. + EXPECT_LT(local_only.TranslationPart().Modulus(), 1.0); + EXPECT_GT(world_align.TranslationPart().Modulus(), 40.0); + + const gp_Pnt after = moving_world.Location().Transformed(world_align); + EXPECT_NEAR(gp_Lin(fixed_world).Distance(after), 0.0, 1e-6); +} + TEST_F(Shp_test, Design_geom_change_syncs_workbench_link) { view().add_box(0, 0, 0, 10, 10, 10); From 8838b8402780163a6a9993713c6601326d54c815 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Mon, 21 Sep 2026 17:27:04 -0600 Subject: [PATCH 05/12] Workbench show axis fix. --- CHANGELOG.md | 1 + docs/usage.md | 4 ++-- src/doc/gui.md | 22 ++++++++++---------- src/doc/shape.md | 2 +- src/gui.cpp | 1 + src/gui_mode.cpp | 7 +++++-- src/gui_occt_view.cpp | 47 ++++++++++++++++++++++++++++++++++++++++--- src/gui_occt_view.h | 4 +++- src/gui_shp_pane.cpp | 9 +++++---- src/mode.cpp | 6 ++++++ src/mode.h | 7 +++++-- src/shp.cpp | 11 ++++++++-- src/shp.h | 5 +++++ src/shp_delta.cpp | 9 +++++++++ src/shp_delta.h | 4 ++++ src/shp_set_frame.cpp | 8 +++++--- src/shp_set_frame.h | 3 ++- src/shp_transform.cpp | 11 ++++++++++ tests/shp_tests.cpp | 20 ++++++++++++++++++ 19 files changed, 149 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3d955a..0e67de30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Workbench toolbar**: the tools row no longer repeats the Workbench task icon; idle is the task button only. - **Workbench Align shafts**: cylindrical face picking uses Face selection on Workbench instances (same as Design). Insert position uses the instance placement (axis direction was already correct). +- **Workbench set frame**: **Set from planar/cylindrical face** from the Workbench List stays in Workbench and updates the instance tool frame (pose unchanged). - **Shape List right-click**: the row context menu opens from the whole entry (checkboxes, **M**, tree arrow, name field, and padding), not only from some widgets. Right-click on the name field opens the same menu. - **Shape rotate axis**: Choosing X/Y/Z (or view-to-object) before the first drag is kept. Constrained rotation uses the plane perpendicular to that axis when the view faces it. Local space rotates about the assigned frame, not only world XYZ through the bbox. - **Shape rotate drag**: View-to-object axis and the constrained axis-vs-view plane are captured on the first mouse sample. Orbit or pan during rotate no longer changes the axis/plane against the original mouse anchor. diff --git a/docs/usage.md b/docs/usage.md index c17e3154..c326bdff 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -164,8 +164,8 @@ This is useful after **Revolve**, **Extrude**, booleans, or imports when you nee The **Workbench List** is a second pane for arranging solids in the Workbench task. Open it from **View -> Workbench List**. It is not a second copy of the Shape List: each solid row is a **link** to a Shape List body. - Adding a shape or group from the Shape List creates Workbench rows that share that body's geometry. Fillet, chamfer, and other Design geometry edits update the Workbench solids. Moving, rotating, or scaling a body in Design does not move its Workbench instances. -- Each Workbench solid has its own local frame. **Move** / **Rotate** / **Align shafts** in the Workbench task change that instance pose only. -- The pane matches the Shape List for **Hide all**, visibility, shaded/wireframe, material, rename, groups, drag-to-reparent, and right-click **Zoom to** / **Delete**. +- Each Workbench solid has its own instance pose and a local tool frame (Show axes / Local move). Right-click **Set from planar face...** / **Set from cylindrical face...** stays in Workbench and does not move the instance. +- The pane matches the Shape List for **Hide all**, visibility, shaded/wireframe, material, rename, groups, drag-to-reparent, and right-click **Zoom to** / **Delete** / local-frame actions. - Deleting a Shape List solid removes Workbench rows that link to it. Deleting a Workbench row does not delete the Design body. - The 3D view shows Shape List solids in Sketch and Design, and Workbench solids in the Workbench task (so the two trees do not overlap). diff --git a/src/doc/gui.md b/src/doc/gui.md index 1a0effa4..0b8a08ea 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -50,7 +50,7 @@ The chrome toolbar is one ImGui window with two regions: **task** switcher (Sket ### New mode or toolbar command (hotkeys) -When adding a `Mode` to [`mode.h`](../mode.h) (`EZY_MODE_LIST`), a toolbar button, or a one-shot `Command`, update remappable hotkeys in the **same change**. Skip only for modes that must stay toolbar-only (document that choice). `Mode::Shape_set_frame` is Shape List-only (no toolbar button / remappable hotkey). `Mode::Design_inspection`, `Mode::Workbench_inspection`, and the Sketch / Design / Workbench task buttons are toolbar-only (no remappable hotkey). +When adding a `Mode` to [`mode.h`](../mode.h) (`EZY_MODE_LIST`), a toolbar button, or a one-shot `Command`, update remappable hotkeys in the **same change**. Skip only for modes that must stay toolbar-only (document that choice). `Mode::Shape_set_frame` / `Mode::Workbench_set_frame` are list-only (no toolbar button / remappable hotkey). `Mode::Design_inspection`, `Mode::Workbench_inspection`, and the Sketch / Design / Workbench task buttons are toolbar-only (no remappable hotkey). | Step | Touch | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -185,16 +185,16 @@ See also [`src/doc/sketch.md`](sketch.md) and [`src/doc/shape.md`](shape.md) for ### Mouse move (`GUI::on_mouse_pos`) -| `Mode` | Delegate | -| --------------------------------------------------------- | -------------------------------------------------------------------- | -| `Design_move` / `Workbench_move` | `shp_move().move_selected` | -| `Design_rotate` / `Workbench_rotate` | `shp_rotate().rotate_selected` | -| `Scale` | `shp_scale().scale_selected` | -| `Design_shaft_align` / `Workbench_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | -| `Shape_set_frame` | `options_shape_set_frame_mode_` (Shape List only; no toolbar/hotkey) | -| `Shape_polar_duplicate` | `shp_polar_dup().move_point` | -| Sketch tool modes (line, arc, rect, dim, axis, bone, ...) | `curr_sketch().sketch_pt_move` | -| `Sketch_face_extrude` | `sketch_face_extrude(..., true)` | +| `Mode` | Delegate | +| --------------------------------------------------------- | -------------------------------------------------------------- | +| `Design_move` / `Workbench_move` | `shp_move().move_selected` | +| `Design_rotate` / `Workbench_rotate` | `shp_rotate().rotate_selected` | +| `Scale` | `shp_scale().scale_selected` | +| `Design_shaft_align` / `Workbench_shaft_align` | `shp_cyl_align().drag_depth` / `drag_twist` | +| `Shape_set_frame` / `Workbench_set_frame` | `options_shape_set_frame_mode_` (list-only; no toolbar/hotkey) | +| `Shape_polar_duplicate` | `shp_polar_dup().move_point` | +| Sketch tool modes (line, arc, rect, dim, axis, bone, ...) | `curr_sketch().sketch_pt_move` | +| `Sketch_face_extrude` | `sketch_face_extrude(..., true)` | Always calls `m_view->on_mouse_move(screen_coords)` first. diff --git a/src/doc/shape.md b/src/doc/shape.md index 902dd47a..487b7ce0 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -157,7 +157,7 @@ Protected helpers used by all operation classes: | `shp_scale.h` | `Shp_scale` | Uniform scale from Local frame origin or World bbox center vs mouse distance; clamped factor 0.01..100. Mid-drag Local/World re-seeds the pivot and re-baselines distance so the factor does not jump. | | `shp_transform.h` | helpers | `transform_axes_for` / `transform_translation` shared by move, rotate, scale. | | `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); map cylinder axes through AIS `LocalTransformation` (Workbench instance pose; identity on Design); coaxial `cyl_align_trsf`; drag axial depth; Options **Clock rotation** (default off) then LMB / Shift+Tab about shared axis; Options flip; bake like Move. | -| `shp_set_frame.h` | `Shp_set_frame` | Shape List-only: pick planar or cylindrical face to redefine `Shp` local frame; enables Show axes. | +| `shp_set_frame.h` | `Shp_set_frame` | Shape List (`Shape_set_frame`) or Workbench List (`Workbench_set_frame`): pick planar or cylindrical face. Design writes `get_frame()`; Workbench writes `get_local_frame()` so instance pose stays put. Enables Show axes. | | `shp_fillet.h` | `Shp_fillet` | `add_fillet(..., Fillet_mode)` -- `BRepFilletAPI_MakeFillet`; modes: Shape, Face, Wire, Edge (`mode.h`). | | `shp_chamfer.h` | `Shp_chamfer` | `add_chamfer(..., Chamfer_mode)` -- diagonal distance converted to setback (`dist/sqrt(2)`). | | `shp_polar_dup.h` | `Shp_polar_dup` | Arm on sketch plane; `dup()` copies selection at polar steps; options: rotate copies, combine into one solid. | diff --git a/src/gui.cpp b/src/gui.cpp index e6e74959..b8b97302 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -3499,6 +3499,7 @@ void GUI::on_left_click_(const ScreenCoords& screen_coords) show_status(s); break; case Mode::Shape_set_frame: + case Mode::Workbench_set_frame: if (Status s = m_view->shp_set_frame().pick(screen_coords); !s.is_ok()) show_status(s); break; diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index e0e1b797..74c5e11c 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -73,6 +73,7 @@ std::string GUI::get_doc_url_for_mode(Mode mode) {Mode::Sketch_dim_anno, "https://ezycad.readthedocs.io/en/latest/usage-sketch.html#dimension-tool"}, {Mode::Shape_cross_section, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-cross-section-tool"}, {Mode::Shape_set_frame, "https://ezycad.readthedocs.io/en/latest/usage.html#shape-list"}, + {Mode::Workbench_set_frame, "https://ezycad.readthedocs.io/en/latest/usage.html#workbench-list"}, // clang-format on }; @@ -97,7 +98,7 @@ const char* GUI::current_mode_description_() const // Modes entered only from Shape List / menus / task buttons (no tools-row button). if (m_mode == Mode::Workbench_inspection) return "Workbench"; - if (m_mode == Mode::Shape_set_frame) + if (m_mode == Mode::Shape_set_frame || m_mode == Mode::Workbench_set_frame) return "Set local frame"; EZY_ASSERT_MSG(false, "Current mode not found in toolbar buttons"); @@ -164,6 +165,7 @@ Mode GUI::parent_mode_of(Mode mode) {Mode::Sketch_dim_anno, Mode::Sketch_inspection}, {Mode::Shape_cross_section, Mode::Design_inspection}, {Mode::Shape_set_frame, Mode::Design_inspection}, + {Mode::Workbench_set_frame, Mode::Workbench_inspection}, // clang-format on }; @@ -548,6 +550,7 @@ void GUI::options_() case Mode::Shape_polar_duplicate: options_shape_polar_duplicate_mode_(); break; case Mode::Shape_cross_section: options_shape_cross_section_mode_(); break; case Mode::Shape_set_frame: options_shape_set_frame_mode_(); break; + case Mode::Workbench_set_frame: options_shape_set_frame_mode_(); break; // Sketch related modes: case Mode::Sketch_inspection: options_sketch_inspection_mode_(); break; @@ -728,7 +731,7 @@ void GUI::options_shape_shaft_align_mode_() void GUI::options_shape_set_frame_mode_() { - EZY_ASSERT(get_mode() == Mode::Shape_set_frame); + EZY_ASSERT(is_set_frame_mode(get_mode())); ImGui::TextUnformatted("Set local frame"); options_doc_help_button_(); diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index 6363e4f6..47c2193e 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -615,6 +615,7 @@ void Occt_view::cancel(Set_parent_mode set_parent_mode) break; case Mode::Shape_set_frame: + case Mode::Workbench_set_frame: shp_set_frame().cancel(); operation_canceled = true; break; @@ -1907,6 +1908,7 @@ void Occt_view::insert_shape_rec(const Shape_rec& rec) shp->set_is_workbench(rec.is_workbench); shp->set_source_id(rec.source_id); shp->set_frame(rec.frame); + shp->set_local_frame(rec.local_frame); if (!shp->is_group()) { shp->set_show_frame_axes(rec.show_frame_axes); @@ -2011,14 +2013,41 @@ void Occt_view::set_shape_geom_by_id(Shape_id id, const TopoDS_Shape& geom, cons sync_workbench_links(id); } +void Occt_view::set_shape_local_frame_by_id(Shape_id id, const gp_Ax3& local_frame) +{ + Shp_ptr shp = find_shape_by_id(id); + if (shp.IsNull()) + return; + + shp->set_local_frame(local_frame); +} + void Occt_view::set_shape_frame(const Shp_ptr& shp, const gp_Ax3& frame) { if (shp.IsNull() || shp->is_group()) return; std::vector changes; - changes.push_back(Shape_geom_delta::Geom_change{shp->get_id(), shp->Shape(), shp->Shape(), shp->get_frame(), frame}); - shp->set_frame(frame); + if (shp->is_workbench()) + { + Shape_geom_delta::Geom_change ch; + ch.id = shp->get_id(); + ch.before_geom = shp->Shape(); + ch.after_geom = shp->Shape(); + ch.before_frame = shp->get_frame(); + ch.after_frame = shp->get_frame(); + ch.has_local_frame = true; + ch.before_local_frame = shp->get_local_frame(); + ch.after_local_frame = frame; + changes.push_back(ch); + shp->set_local_frame(frame); + } + else + { + changes.push_back(Shape_geom_delta::Geom_change{shp->get_id(), shp->Shape(), shp->Shape(), shp->get_frame(), frame}); + shp->set_frame(frame); + } + push_undo_delta(std::make_unique(std::move(changes))); } @@ -4237,7 +4266,8 @@ void Occt_view::on_mode() case Mode::Scale: set_shp_selection_mode(TopAbs_SHAPE); break; case Mode::Workbench_shaft_align: case Mode::Design_shaft_align: set_shp_selection_mode(TopAbs_FACE); break; - case Mode::Shape_set_frame: set_shp_selection_mode(TopAbs_FACE); break; + case Mode::Shape_set_frame: + case Mode::Workbench_set_frame: set_shp_selection_mode(TopAbs_FACE); break; case Mode::Shape_cross_section: set_shp_selection_mode(TopAbs_COMPOUND); break; default: if(m_modes_selection_mode_map.count(get_mode())) @@ -4767,6 +4797,13 @@ std::string Occt_view::to_json() const shp_json["material"] = s->Material(); shp_json["dispMode"] = static_cast(s->get_disp_mode()); shp_json["frame"] = ::to_json(gp_Pln(s->get_frame())); + { + const gp_Ax3& lf = s->get_local_frame(); + const gp_Ax3 identity; + if (!lf.Location().IsEqual(identity.Location(), 1e-12) || !lf.Direction().IsEqual(identity.Direction(), 1e-12) || + !lf.XDirection().IsEqual(identity.XDirection(), 1e-12)) + shp_json["localFrame"] = ::to_json(gp_Pln(lf)); + } if (s->show_frame_axes() || s->show_frame_plane() || s->show_frame_up()) { json fd; @@ -4961,6 +4998,9 @@ void Occt_view::load(const std::string& json_str, bool restore_view) if (s.contains("frame") && s["frame"].is_object()) shp->set_frame(from_json_pln(s["frame"]).Position()); + if (s.contains("localFrame") && s["localFrame"].is_object()) + shp->set_local_frame(from_json_pln(s["localFrame"]).Position()); + if (s.contains("frameDisplay") && s["frameDisplay"].is_object()) { const json& fd = s["frameDisplay"]; @@ -5527,6 +5567,7 @@ Mode mode_for_history_restore_(Mode mode) case Mode::Workbench_shaft_align: case Mode::Design_shaft_align: case Mode::Shape_set_frame: + case Mode::Workbench_set_frame: return GUI::parent_mode_of(mode); default: return mode; diff --git a/src/gui_occt_view.h b/src/gui_occt_view.h index 83a2d65f..e9a3a4b0 100644 --- a/src/gui_occt_view.h +++ b/src/gui_occt_view.h @@ -153,7 +153,9 @@ class Occt_view : protected AIS_ViewController void remove_shape_by_id(Shape_id id); /// Replace BREP of an existing shape (identity local transform). void set_shape_geom_by_id(Shape_id id, const TopoDS_Shape& geom, const gp_Ax3& frame); - /// Frame-only edit with undo (geometry unchanged). Also used by Shape_set_frame. + /// Workbench tool-frame restore (pose / placement unchanged). + void set_shape_local_frame_by_id(Shape_id id, const gp_Ax3& local_frame); + /// Frame-only edit with undo (geometry unchanged). Workbench sets local tool frame. void set_shape_frame(const Shp_ptr& shp, const gp_Ax3& frame); /// Next sibling_order among children of \a parent_id (0 = document root). diff --git a/src/gui_shp_pane.cpp b/src/gui_shp_pane.cpp index 30669e7a..dc5d4bc8 100644 --- a/src/gui_shp_pane.cpp +++ b/src/gui_shp_pane.cpp @@ -770,24 +770,25 @@ void Shape_list_row_drawer::draw_ctx_menu_(const Shp_ptr& shape, bool is_group) shape->set_show_frame_axes(true); } + const Mode set_frame_mode = shape->is_workbench() ? Mode::Workbench_set_frame : Mode::Shape_set_frame; if (ImGui::MenuItem("Set from planar face...")) { select_row_(shape); m_view.shp_set_frame().begin(shape, Shp_set_frame::Pick::Planar_face); - m_gui.set_mode(Mode::Shape_set_frame); + m_gui.set_mode(set_frame_mode); } if (ImGui::MenuItem("Set from cylindrical face...")) { select_row_(shape); m_view.shp_set_frame().begin(shape, Shp_set_frame::Pick::Cylindrical_face); - m_gui.set_mode(Mode::Shape_set_frame); + m_gui.set_mode(set_frame_mode); } if (ImGui::MenuItem("Flip up")) { select_row_(shape); - gp_Ax3 f = shape->get_frame(); + gp_Ax3 f = shape->is_workbench() ? shape->get_local_frame() : shape->get_frame(); f.XReverse(); m_view.set_shape_frame(shape, f); } @@ -795,7 +796,7 @@ void Shape_list_row_drawer::draw_ctx_menu_(const Shp_ptr& shape, bool is_group) if (ImGui::MenuItem("Flip axis (Z)")) { select_row_(shape); - gp_Ax3 f = shape->get_frame(); + gp_Ax3 f = shape->is_workbench() ? shape->get_local_frame() : shape->get_frame(); f.ZReverse(); m_view.set_shape_frame(shape, f); } diff --git a/src/mode.cpp b/src/mode.cpp index 0bee080e..1924fca9 100644 --- a/src/mode.cpp +++ b/src/mode.cpp @@ -61,6 +61,7 @@ bool is_workbench_transform_mode(const Mode mode) case Mode::Workbench_move: case Mode::Workbench_rotate: case Mode::Workbench_shaft_align: + case Mode::Workbench_set_frame: return true; default: @@ -88,6 +89,11 @@ bool is_shaft_align_mode(const Mode mode) return mode == Mode::Design_shaft_align || mode == Mode::Workbench_shaft_align; } +bool is_set_frame_mode(const Mode mode) +{ + return mode == Mode::Shape_set_frame || mode == Mode::Workbench_set_frame; +} + bool is_shape_browse_mode(const Mode mode) { return mode == Mode::Design_inspection || mode == Mode::Workbench_inspection; diff --git a/src/mode.h b/src/mode.h index 40dbd4b7..2558cebe 100644 --- a/src/mode.h +++ b/src/mode.h @@ -32,11 +32,12 @@ X(Sketch_dim_anno) \ X(Shape_cross_section) \ X(Workbench_shaft_align) \ - X(Shape_set_frame) /* Shape List only: pick face for local frame */ \ + X(Shape_set_frame) /* Shape List: pick face for local frame */ \ X(Workbench_inspection) /* Workbench browse/select home; not Mode::Design_inspection */ \ X(Design_move) /* Design CSG placement; bake BREP. Workbench_move is instance pose. */ \ X(Design_rotate) /* Design CSG rotation; bake BREP. Workbench_rotate is instance pose. */ \ - X(Design_shaft_align) /* Design CSG shaft align; bake BREP. Workbench_shaft_align is instance pose. */ + X(Design_shaft_align) /* Design CSG shaft align; bake BREP. Workbench_shaft_align is instance pose. */ \ + X(Workbench_set_frame) /* Workbench List: pick face for instance local frame (pose unchanged). */ enum class Mode { @@ -134,6 +135,8 @@ bool is_move_mode(const Mode mode); bool is_rotate_mode(const Mode mode); /// Design or Workbench Align shafts. bool is_shaft_align_mode(const Mode mode); +/// Design or Workbench set-local-frame pick. +bool is_set_frame_mode(const Mode mode); /// Design Inspection or Workbench idle (solid browse / selection-filter digits). bool is_shape_browse_mode(const Mode mode); diff --git a/src/shp.cpp b/src/shp.cpp index cfbfb6fe..e06d9f38 100644 --- a/src/shp.cpp +++ b/src/shp.cpp @@ -193,6 +193,12 @@ void Shp::transform_frame(const gp_Trsf& transform) update_frame_display(); } +void Shp::set_local_frame(const gp_Ax3& frame) +{ + m_local_frame = frame; + update_frame_display(); +} + gp_Ax3 Shp::default_frame_for(const TopoDS_Shape& shape) { return default_shape_frame_(shape); } void Shp::set_show_frame_axes(bool show) @@ -280,8 +286,9 @@ void Shp::update_frame_display() return; // Workbench placement lives on LocalTransformation; draw the triad in local - // space so preview transforms carry the annotations. Design bakes to identity. - const gp_Ax3 draw = m_is_workbench ? gp_Ax3() : m_frame; + // space (m_local_frame, identity = instance origin) so preview transforms + // carry the annotations. Design bakes to identity and draws m_frame in world. + const gp_Ax3 draw = m_is_workbench ? m_local_frame : m_frame; const gp_Pnt o = draw.Location(); const gp_Vec x(draw.XDirection()); const gp_Vec y(draw.YDirection()); diff --git a/src/shp.h b/src/shp.h index 58be1c54..aa7a85ed 100644 --- a/src/shp.h +++ b/src/shp.h @@ -61,6 +61,10 @@ class Shp : public AIS_Shape void set_frame(const gp_Ax3& frame); void transform_frame(const gp_Trsf& transform); + /// Workbench tool frame in local geom (identity = instance origin). Design unused. + const gp_Ax3& get_local_frame() const { return m_local_frame; } + void set_local_frame(const gp_Ax3& frame); + /// World-aligned frame at the shape bounding-box center (same as ctor default). static gp_Ax3 default_frame_for(const TopoDS_Shape& shape); @@ -108,6 +112,7 @@ class Shp : public AIS_Shape Shape_id m_parent_id{0}; int m_sibling_order{0}; gp_Ax3 m_frame; + gp_Ax3 m_local_frame; bool m_show_frame_axes{false}; bool m_show_frame_plane{false}; bool m_show_frame_up{false}; diff --git a/src/shp_delta.cpp b/src/shp_delta.cpp index 35bb9176..33145890 100644 --- a/src/shp_delta.cpp +++ b/src/shp_delta.cpp @@ -17,6 +17,7 @@ Shape_rec capture_shape_rec(const Shp& shp) rec.material = shp.Material(); rec.geom = shp.Shape(); rec.frame = shp.get_frame(); + rec.local_frame = shp.get_local_frame(); rec.parent_id = shp.get_parent_id(); rec.sibling_order = shp.get_sibling_order(); rec.is_group = shp.is_group(); @@ -59,13 +60,21 @@ Shape_geom_delta::Shape_geom_delta(std::vector changes) void Shape_geom_delta::apply_forward(Occt_view& view) { for (const Geom_change& ch : m_changes) + { view.set_shape_geom_by_id(ch.id, ch.after_geom, ch.after_frame); + if (ch.has_local_frame) + view.set_shape_local_frame_by_id(ch.id, ch.after_local_frame); + } } void Shape_geom_delta::apply_reverse(Occt_view& view) { for (const Geom_change& ch : m_changes) + { view.set_shape_geom_by_id(ch.id, ch.before_geom, ch.before_frame); + if (ch.has_local_frame) + view.set_shape_local_frame_by_id(ch.id, ch.before_local_frame); + } } std::unique_ptr Shape_geom_delta::clone() const { return std::make_unique(m_changes); } diff --git a/src/shp_delta.h b/src/shp_delta.h index 6c511766..04dadab3 100644 --- a/src/shp_delta.h +++ b/src/shp_delta.h @@ -17,6 +17,7 @@ struct Shape_rec int material{0}; TopoDS_Shape geom; gp_Ax3 frame; + gp_Ax3 local_frame; Shape_id parent_id{0}; int sibling_order{0}; bool is_group{false}; @@ -69,6 +70,9 @@ class Shape_geom_delta : public Delta TopoDS_Shape after_geom; gp_Ax3 before_frame; gp_Ax3 after_frame; + bool has_local_frame{false}; + gp_Ax3 before_local_frame; + gp_Ax3 after_local_frame; }; explicit Shape_geom_delta(std::vector changes); diff --git a/src/shp_set_frame.cpp b/src/shp_set_frame.cpp index a326a847..c30e2053 100644 --- a/src/shp_set_frame.cpp +++ b/src/shp_set_frame.cpp @@ -105,10 +105,11 @@ Status Shp_set_frame::pick(const ScreenCoords& screen_coords) frame = gp_Ax3(gp_Ax2(origin, cyl->axis.Direction())); } - // Leave pick mode before the undo push so history stores Normal, not Shape_set_frame. + // Leave pick mode before the undo push so history stores the parent idle. const Shp_ptr target = m_target; + const Mode parent = GUI::parent_mode_of(gui().get_mode()); clear_all(m_target); - gui().set_mode(Mode::Design_inspection); + gui().set_mode(parent); view().set_shape_frame(target, *frame); target->set_show_frame_axes(true); gui().show_message("Local frame updated.", Status_msg::Success); @@ -117,6 +118,7 @@ Status Shp_set_frame::pick(const ScreenCoords& screen_coords) void Shp_set_frame::cancel() { + const Mode parent = GUI::parent_mode_of(gui().get_mode()); clear_all(m_target); - gui().set_mode(Mode::Design_inspection); + gui().set_mode(parent); } diff --git a/src/shp_set_frame.h b/src/shp_set_frame.h index e2108354..37ec098f 100644 --- a/src/shp_set_frame.h +++ b/src/shp_set_frame.h @@ -2,7 +2,8 @@ #include "shp_operation.h" -/// Pick a planar or cylindrical face to redefine a solid's local frame (Shape List only). +/// Pick a planar or cylindrical face to redefine a solid's local frame +/// (Shape List or Workbench List). class Shp_set_frame : private Shp_operation_base { public: diff --git a/src/shp_transform.cpp b/src/shp_transform.cpp index 6a47f377..438157c7 100644 --- a/src/shp_transform.cpp +++ b/src/shp_transform.cpp @@ -28,6 +28,17 @@ Transform_axes transform_axes_for(const std::vector& shps, Transform_sp if (space == Transform_space::Local) { + if (s->is_workbench()) + { + const gp_Trsf pl = s->placement_trsf(); + const gp_Ax3& loc = s->get_local_frame(); + axes.origin = loc.Location().Transformed(pl); + axes.x = loc.XDirection().Transformed(pl); + axes.y = loc.YDirection().Transformed(pl); + axes.z = loc.Direction().Transformed(pl); + return axes; + } + const gp_Ax3& f = s->get_frame(); axes.origin = f.Location(); axes.x = f.XDirection(); diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index a6c143ed..df7021d7 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -1665,6 +1665,10 @@ TEST(Mode_helpers, Design_move_rotate_stay_on_design_task) EXPECT_EQ(GUI::parent_mode_of(Mode::Design_shaft_align), Mode::Design_inspection); EXPECT_EQ(GUI::parent_mode_of(Mode::Workbench_move), Mode::Workbench_inspection); EXPECT_EQ(GUI::parent_mode_of(Mode::Workbench_shaft_align), Mode::Workbench_inspection); + EXPECT_EQ(task_of(Mode::Workbench_set_frame), Task::Workbench); + EXPECT_TRUE(is_set_frame_mode(Mode::Workbench_set_frame)); + EXPECT_TRUE(is_workbench_mode(Mode::Workbench_set_frame)); + EXPECT_EQ(GUI::parent_mode_of(Mode::Workbench_set_frame), Mode::Workbench_inspection); EXPECT_EQ(mode_from_string("Move"), Mode::Design_move); EXPECT_EQ(mode_from_string("Rotate"), Mode::Design_rotate); } @@ -1783,6 +1787,22 @@ TEST_F(Shp_test, Workbench_cyl_align_uses_instance_placement) EXPECT_NEAR(gp_Lin(fixed_world).Distance(after), 0.0, 1e-6); } +TEST_F(Shp_test, Workbench_set_frame_keeps_instance_pose) +{ + view().add_cylinder(0, 0, 0, 1.0, 4.0); + Shp_ptr src = view().get_shapes().back(); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + Shp_ptr inst = view().get_workbench_shapes().back(); + const gp_Pnt pose0 = inst->get_frame().Location(); + + const gp_Ax3 local(gp_Pnt(0.0, 0.0, 1.0), gp_Dir(0.0, 0.0, 1.0), gp_Dir(1.0, 0.0, 0.0)); + view().set_shape_frame(inst, local); + + EXPECT_TRUE(inst->get_frame().Location().IsEqual(pose0, 1e-9)); + EXPECT_TRUE(inst->get_local_frame().Location().IsEqual(local.Location(), 1e-9)); + EXPECT_TRUE(inst->get_local_frame().Direction().IsEqual(local.Direction(), 1e-9)); +} + TEST_F(Shp_test, Design_geom_change_syncs_workbench_link) { view().add_box(0, 0, 0, 10, 10, 10); From 8bbb635d377d1135a232905c51ca2044799e68e2 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Mon, 21 Sep 2026 17:38:14 -0600 Subject: [PATCH 06/12] Improve --- CHANGELOG.md | 1 + agents/plans/tasks-workbenches.md | 2 +- docs/usage.md | 4 ++-- src/doc/gui.md | 2 +- src/gui.cpp | 1 + src/gui.h | 6 ++++-- src/gui_mode.cpp | 8 ++++++++ src/gui_shp_pane.cpp | 6 ++++++ 8 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e67de30..5645f400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Workbench toolbar**: the tools row no longer repeats the Workbench task icon; idle is the task button only. - **Workbench Align shafts**: cylindrical face picking uses Face selection on Workbench instances (same as Design). Insert position uses the instance placement (axis direction was already correct). - **Workbench set frame**: **Set from planar/cylindrical face** from the Workbench List stays in Workbench and updates the instance tool frame (pose unchanged). +- **List panes**: selecting or right-clicking a Shape List / Workbench List / Sketch List row switches to that task so the 3D view matches the pane. - **Shape List right-click**: the row context menu opens from the whole entry (checkboxes, **M**, tree arrow, name field, and padding), not only from some widgets. Right-click on the name field opens the same menu. - **Shape rotate axis**: Choosing X/Y/Z (or view-to-object) before the first drag is kept. Constrained rotation uses the plane perpendicular to that axis when the view faces it. Local space rotates about the assigned frame, not only world XYZ through the bbox. - **Shape rotate drag**: View-to-object axis and the constrained axis-vs-view plane are captured on the first mouse sample. Orbit or pan during rotate no longer changes the axis/plane against the original mouse anchor. diff --git a/agents/plans/tasks-workbenches.md b/agents/plans/tasks-workbenches.md index bd1e17af..832e55c2 100644 --- a/agents/plans/tasks-workbenches.md +++ b/agents/plans/tasks-workbenches.md @@ -21,7 +21,7 @@ Two stores, one document: Design `Occt_view::m_shps` and Workbench `Occt_view::m | Design | `m_shps` / Shape List | Bodies / groups you model | | Workbench | `m_wbk_shps` / Workbench List | Linked instances with their own frame (placement) | -Copy Shape List solids or groups onto the Workbench (**Add to Workbench**, context menu, or drag). Design fillet/chamfer (same `Shape_id`) refreshes linked local geom. Design move/rotate bake does not move instances. Workbench Move/Rotate/Align update the instance frame only. Deleting a Design source removes its Workbench links. `.ezy` writes `workbench[]` (ids, `sourceId`, frame; no geom). +Copy Shape List solids or groups onto the Workbench (**Add to Workbench**, context menu, or drag). Design fillet/chamfer (same `Shape_id`) refreshes linked local geom. Design move/rotate bake does not move instances. Workbench Move/Rotate/Align update the instance frame only. Deleting a Design source removes its Workbench links. `.ezy` writes `workbench[]` (ids, `sourceId`, frame; no geom). Selecting or right-clicking a list row switches to that pane's task (`GUI::ensure_task_`) so Reset / Show axes / set-from-face run against the visible store. Polar array of **parts** can follow on Workbench after instances exist. Polar array of a body stays Design. diff --git a/docs/usage.md b/docs/usage.md index c326bdff..69244038 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -110,7 +110,7 @@ The window can be closed with its close button; use **View -> Sketch List** agai ### Shape List -The **Shape List** pane lists every **3D shape** and **group** in the current document (extrudes, imports, booleans, etc.). Open it from **View -> Shape List**. +The **Shape List** pane lists every **3D shape** and **group** in the current document (extrudes, imports, booleans, etc.). Open it from **View -> Shape List**. Selecting or right-clicking a row switches to the **Design** task so the 3D view shows those solids. At the top: @@ -161,7 +161,7 @@ This is useful after **Revolve**, **Extrude**, booleans, or imports when you nee ### Workbench List -The **Workbench List** is a second pane for arranging solids in the Workbench task. Open it from **View -> Workbench List**. It is not a second copy of the Shape List: each solid row is a **link** to a Shape List body. +The **Workbench List** is a second pane for arranging solids in the Workbench task. Open it from **View -> Workbench List**. Selecting or right-clicking a row switches to the **Workbench** task so the 3D view shows those instances. It is not a second copy of the Shape List: each solid row is a **link** to a Shape List body. - Adding a shape or group from the Shape List creates Workbench rows that share that body's geometry. Fillet, chamfer, and other Design geometry edits update the Workbench solids. Moving, rotating, or scaling a body in Design does not move its Workbench instances. - Each Workbench solid has its own instance pose and a local tool frame (Show axes / Local move). Right-click **Set from planar face...** / **Set from cylindrical face...** stays in Workbench and does not move the instance. diff --git a/src/doc/gui.md b/src/doc/gui.md index 0b8a08ea..346a8276 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -265,7 +265,7 @@ Shared sketch controls (snap, faint shapes) live in `options_sketch_shared_contr Sketch List expand **Faces**: each face row supports **`E`** and right-click **Extrude** via `GUI::sketch_list_extrude_face_` (`set_mode(Sketch_face_extrude)` + `Occt_view::begin_sketch_face_extrude` / `Shp_extrude::begin_face_extrude`). Hovering a **Faces**, **Edges**, or **Nodes** row calls `Occt_view::set_sketch_list_hover_{face,edge,node}` (temporarily displays the AIS when hidden outside sketch modes; uses `Graphic3d_ZLayerId_Topmost` so solids do not occlude the highlight). -**Workbench List:** `workbench_list_()` in `gui_shp_pane.cpp` uses the same row drawer on `m_wbk_shps` (`EZY_WBK_ID` payload; accepts `EZY_SHAPE_ID` drops as `add_to_workbench`). `.ezy` `ui.workbenchList` mirrors `ui.shapeList`. Design **Add to Workbench** (toolbar button, context menu, drag). +**Workbench List:** `workbench_list_()` in `gui_shp_pane.cpp` uses the same row drawer on `m_wbk_shps` (`EZY_WBK_ID` payload; accepts `EZY_SHAPE_ID` drops as `add_to_workbench`). `.ezy` `ui.workbenchList` mirrors `ui.shapeList`. Design **Add to Workbench** (toolbar button, context menu, drag). Selecting or right-clicking a Shape List / Workbench List row calls `GUI::ensure_task_` so the 3D view matches that pane (Design vs Workbench). Sketch List radio / context menu already enters Sketch. **Shape List outliner:** `shape_list_` (`gui_shp_pane.cpp`) draws a tree of document shapes/groups via `shape_children(0)` and recursive `gui_shp_detail::Shape_list_row_drawer` rows (`TreeNodeEx`). Fixed-width vis/disp/mat columns are on the left; the name column stretches on the right with tree indent (`IndentEnable` on name only). An empty pad row after the last item is a drag-drop target for document root (`reparent_shape(..., 0)`); it shows a "Move to root" hint while dragging. Groups support expand/collapse (`ui.shapeList.expanded`), drag-drop reparent (`EZY_SHAPE_ID` payload), Group / New group / Ungroup, and cascade delete. Clicking a group sets `Occt_view::current_group_id` (including empty groups) and selects descendant solids; clicking a solid selects it and sets current group to its parent. New primitives/extrudes/revolves parent under the current group. Ctrl+click multi-selects. Row highlight for **selection** follows AIS only; the **current group** uses a weaker tint so it is not mistaken for a selected subtree after Alt-drag rectangle select clears AIS. Copy/paste (Ctrl+C/V) deep-copies the current group subtree when the selection matches that group's descendant solids. Row context menu opens on right-click anywhere on the table row (not only `InputText` / **M**). **Zoom to** calls `Occt_view::fit_shapes_in_view` (solid or group descendant solids; keeps camera orientation). Hover uses `set_shape_list_hover` on leaf solids only. `ui.shapeList.currentGroupId` is persisted in `.ezy`. diff --git a/src/gui.cpp b/src/gui.cpp index b8b97302..1c2c4c49 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -1538,6 +1538,7 @@ void GUI::sketch_list_() // This will open a popup when you right-click on the InputText if (ImGui::BeginPopupContextItem("Sketch_InputTextContextMenu")) { + ensure_task_(Task::Sketch); if (ImGui::MenuItem("Delete")) sketch_to_delete = sketch; diff --git a/src/gui.h b/src/gui.h index d97ab6de..626cd796 100644 --- a/src/gui.h +++ b/src/gui.h @@ -372,8 +372,10 @@ class GUI 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 - void set_parent_mode(); // gui_mode.cpp + void set_mode(Mode mode); // gui_mode.cpp + /// Enter \a task idle if the current mode is not already on that task. + void ensure_task_(Task task); // gui_mode.cpp + void set_parent_mode(); // gui_mode.cpp /// Parent mode for Escape / tool exit (e.g. Workbench_move -> Workbench_inspection, sketch tools -> Sketch_inspection). static Mode parent_mode_of(Mode mode); // gui_mode.cpp void set_dist_edit(float dist, std::function&& callback, diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 74c5e11c..4b149d40 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -131,6 +131,14 @@ void GUI::set_mode(Mode mode) b.is_active = std::get(b.data) == mode; } +void GUI::ensure_task_(Task task) +{ + if (task_of(m_mode) == task) + return; + + set_mode(idle_mode_of(task)); +} + Mode GUI::parent_mode_of(Mode mode) { static const std::map parent_modes = { diff --git a/src/gui_shp_pane.cpp b/src/gui_shp_pane.cpp index dc5d4bc8..69ef6e17 100644 --- a/src/gui_shp_pane.cpp +++ b/src/gui_shp_pane.cpp @@ -692,6 +692,8 @@ bool Shape_list_row_drawer::row_is_selected_(const Shp_ptr& shape) const void Shape_list_row_drawer::select_row_(const Shp_ptr& shape) { + m_gui.ensure_task_(m_kind == Shp_list_kind::Workbench ? Task::Workbench : Task::Design); + AIS_InteractiveContext& ctx = m_view.ctx(); const bool ctrl = ImGui::GetIO().KeyCtrl; if (!ctrl) @@ -1042,10 +1044,14 @@ void Shape_list_row_drawer::draw(const Shp_ptr& shape) const ImGuiPayload* dd = ImGui::GetDragDropPayload(); const bool dragging_row = dd != nullptr && (dd->IsDataType("EZY_SHAPE_ID") || dd->IsDataType("EZY_WBK_ID")); if (row_hovered && !dragging_row && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) + { + m_gui.ensure_task_(m_kind == Shp_list_kind::Workbench ? Task::Workbench : Task::Design); ImGui::OpenPopup("shape_row_ctx"); + } if (ImGui::BeginPopup("shape_row_ctx")) { + m_gui.ensure_task_(m_kind == Shp_list_kind::Workbench ? Task::Workbench : Task::Design); draw_ctx_menu_(shape, is_group); ImGui::EndPopup(); } From f6d277958f5e17f4bccfeea0359b9425fd4ce578 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 22 Sep 2026 17:09:49 -0600 Subject: [PATCH 07/12] Align shafts flip direction fix. --- CHANGELOG.md | 1 + docs/usage-settings.md | 1 + docs/usage.md | 30 +++++++++++++++++++++--------- src/doc/shape.md | 2 +- src/gui.h | 2 ++ src/gui_mode.cpp | 10 ++++++++++ src/utl_geom.cpp | 6 +++++- src/utl_geom.h | 7 ++++--- tests/shp_tests.cpp | 16 ++++++++++++++++ 9 files changed, 61 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5645f400..87fcdf95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Workbench Align shafts**: cylindrical face picking uses Face selection on Workbench instances (same as Design). Insert position uses the instance placement (axis direction was already correct). - **Workbench set frame**: **Set from planar/cylindrical face** from the Workbench List stays in Workbench and updates the instance tool frame (pose unchanged). - **List panes**: selecting or right-clicking a Shape List / Workbench List / Sketch List row switches to that task so the 3D view matches the pane. +- **Align shafts**: default axial sense is the smaller rotation so a hole whose face parametrization is opposite does not flip the part. **Flip direction** still forces the 180°. - **Shape List right-click**: the row context menu opens from the whole entry (checkboxes, **M**, tree arrow, name field, and padding), not only from some widgets. Right-click on the name field opens the same menu. - **Shape rotate axis**: Choosing X/Y/Z (or view-to-object) before the first drag is kept. Constrained rotation uses the plane perpendicular to that axis when the view faces it. Local space rotates about the assigned frame, not only world XYZ through the bbox. - **Shape rotate drag**: View-to-object axis and the constrained axis-vs-view plane are captured on the first mouse sample. Orbit or pan during rotate no longer changes the axis/plane against the original mouse anchor. diff --git a/docs/usage-settings.md b/docs/usage-settings.md index becbb71b..6a27b9d5 100644 --- a/docs/usage-settings.md +++ b/docs/usage-settings.md @@ -110,6 +110,7 @@ Under **Material**: - Document preset for new solids that do not inherit from a clicked shape (toolbar **Box**, **polar duplicate** output, and similar). To change material on an existing solid, use the [Shape List](usage.md#shape-list). - **Move / rotate / scale**: **Local** (default) or **World** axis space. Local uses the first selected solid's frame (origin and X/Y/Z; same triad as Shape List **Show axes**). World uses global XYZ at that solid's bounding-box center. Persisted as **`gui.transform_space`**. Rotate also has view-to-object / X / Y / Z. See [Move](usage.md#shape-move-tool-g), [Rotate](usage.md#shape-rotate-tool-r), and [Scale](usage.md#shape-scale-tool). +- **Align shafts**: **Flip direction** ([usage.md#align-shafts-option-flip-direction](usage.md#align-shafts-option-flip-direction)) and **Clock rotation** ([usage.md#align-shafts-option-clock-rotation](usage.md#align-shafts-option-clock-rotation)); each has a **?** link. See [Align shafts](usage.md#align-shafts-tool-j). For other non-sketch Options content, see the matching tool section in the usage guide. In particular, **Shape cross-section** provides local **XY/XZ/YZ**, **Invert normal**, **Hide back side**, **Show section outline**, a bbox-ranged **Offset** slider (Ctrl+click to type), **Clip** (half-space cut, replaces selection), **Cross section sketch** (new sketch from section outline), and automatic preview updates when the selection changes; see [Shape cross-section tool](usage.md#shape-cross-section-tool). diff --git a/docs/usage.md b/docs/usage.md index 69244038..6b628d9f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -512,14 +512,26 @@ Place a shaft into a hole (or the reverse) by aligning two cylindrical faces, sl **Features:** -| | | -| -------------------: | -------------------------------------------------------------------------------------------------- | -| **Face picks** | Click a cylindrical face on the shape to move, then a cylindrical face on the fixed shape. | -| **Coaxial align** | The moving shape snaps so the two cylinder axes coincide. | -| **Axial depth drag** | After align, drag to set how far the moving shape slides along the shared axis. | -| **Clock rotation** | Options **Clock rotation** (default off): after depth, rotate about the shared axis to mesh teeth. | -| **Flip direction** | Options **Flip direction** reverses which way the moving axis points along the fixed axis. | -| **Radius mismatch** | Placement is still allowed when radii differ (clearance / press fits); a log warning is recorded. | +| | | +| -------------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Face picks** | Click a cylindrical face on the shape to move, then a cylindrical face on the fixed shape. | +| **Coaxial align** | The moving shape snaps so the two cylinder axes coincide. | +| **Axial depth drag** | After align, drag to set how far the moving shape slides along the shared axis. | +| **Clock rotation** | Options **Clock rotation** (default off): after depth, rotate about the shared axis to mesh teeth. See [Align shafts option: Clock rotation](#align-shafts-option-clock-rotation). | +| **Flip direction** | Default keeps the smaller rotation (cylinder faces have no preferred +/−). Options **Flip direction** forces the opposite sense (180°). See [Align shafts option: Flip direction](#align-shafts-option-flip-direction). | +| **Radius mismatch** | Placement is still allowed when radii differ (clearance / press fits); a log warning is recorded. | + +While Align shafts is active, Options shows **Flip direction** and **Clock rotation**. Each row has a small **?** button: hover for a short summary, click to open the matching section on Read the Docs. + +(align-shafts-option-flip-direction)= +##### Align shafts option: Flip direction + +Cylinder faces have no preferred +/− along the axis, so the default align keeps the **smaller** rotation. Check **Flip direction** to force the opposite sense (180°)—useful when you want to insert from the other side. + +(align-shafts-option-clock-rotation)= +##### Align shafts option: Clock rotation + +When **Clock rotation** is on (default off), after you set insert depth you can rotate the moving body about the shared axis (splines, keyways, tooth meshing). Use left mouse button or Shift+Tab to lock depth and enter clocking; drag or type an angle. With **Clock rotation** off, LMB finalizes after depth. Enter during the depth phase always finalizes immediately without clocking. **How to Use:** @@ -533,7 +545,7 @@ Place a shaft into a hole (or the reverse) by aligning two cylindrical faces, sl **Tips:** - The first face you pick is the body that moves. To move the hole body onto the shaft, pick the hole face first. -- Use **Flip direction** in Options if the shaft points the wrong way after align. +- Use **Flip direction** in Options if you want the moving part turned 180° along the axis (insert from the other side). - Pick cylindrical faces (root / major diameter or smooth lands), not spline tooth flanks. - This tool only places solids; use [Cut](#boolean-operations) afterward if you need a boolean. diff --git a/src/doc/shape.md b/src/doc/shape.md index 487b7ce0..75486730 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -156,7 +156,7 @@ Protected helpers used by all operation classes: | `shp_rotate.h` | `Shp_rotate` | Rotate about view axis or Local/World X/Y/Z through the current pivot; angle Tab entry; axis/center AIS guides (Topmost). Axis can be set before the first drag. View-to-object axis and the axis-vs-view drag plane are captured on the first mouse sample so orbit/pan does not retarget a stale anchor. Mid-drag Local/World re-seeds the pivot and re-baselines the mouse anchor so the angle does not jump. | | `shp_scale.h` | `Shp_scale` | Uniform scale from Local frame origin or World bbox center vs mouse distance; clamped factor 0.01..100. Mid-drag Local/World re-seeds the pivot and re-baselines distance so the factor does not jump. | | `shp_transform.h` | helpers | `transform_axes_for` / `transform_translation` shared by move, rotate, scale. | -| `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); map cylinder axes through AIS `LocalTransformation` (Workbench instance pose; identity on Design); coaxial `cyl_align_trsf`; drag axial depth; Options **Clock rotation** (default off) then LMB / Shift+Tab about shared axis; Options flip; bake like Move. | +| `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); map cylinder axes through AIS `LocalTransformation` (Workbench instance pose; identity on Design); coaxial `cyl_align_trsf` (default acute sense; Options flip forces 180°); drag axial depth; Options **Clock rotation** (default off) then LMB / Shift+Tab about shared axis; bake like Move. | | `shp_set_frame.h` | `Shp_set_frame` | Shape List (`Shape_set_frame`) or Workbench List (`Workbench_set_frame`): pick planar or cylindrical face. Design writes `get_frame()`; Workbench writes `get_local_frame()` so instance pose stays put. Enables Show axes. | | `shp_fillet.h` | `Shp_fillet` | `add_fillet(..., Fillet_mode)` -- `BRepFilletAPI_MakeFillet`; modes: Shape, Face, Wire, Edge (`mode.h`). | | `shp_chamfer.h` | `Shp_chamfer` | `add_chamfer(..., Chamfer_mode)` -- diagonal distance converted to setback (`dist/sqrt(2)`). | diff --git a/src/gui.h b/src/gui.h index 626cd796..4ef28a4b 100644 --- a/src/gui.h +++ b/src/gui.h @@ -256,6 +256,8 @@ inline constexpr const char* k_usage_settings_options = "https://ezycad.re inline constexpr const char* k_occt_view = "https://ezycad.readthedocs.io/en/latest/usage-occt-view.html"; inline constexpr const char* k_startup_project = "https://ezycad.readthedocs.io/en/latest/usage-settings.html#startup-project"; inline constexpr const char* k_extrude_sketch_face = "https://ezycad.readthedocs.io/en/latest/usage.html#extrude-sketch-face-tool-e"; +inline constexpr const char* k_align_shafts_flip_direction = "https://ezycad.readthedocs.io/en/latest/usage.html#align-shafts-option-flip-direction"; +inline constexpr const char* k_align_shafts_clock_rotation = "https://ezycad.readthedocs.io/en/latest/usage.html#align-shafts-option-clock-rotation"; inline constexpr const char* k_hotkeys = "https://ezycad.readthedocs.io/en/latest/usage-settings.html#keyboard-shortcuts"; // clang-format on } // namespace doc_urls diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 4b149d40..202ef170 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -729,10 +729,20 @@ void GUI::options_shape_shaft_align_mode_() if (ImGui::Checkbox("Flip direction", &opts.flip_direction)) m_view->shp_cyl_align().apply_preview(); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("Default keeps the smaller axis rotation. When on, forces the opposite sense (180 deg) so you can " + "insert from the other side. Click ? to open the user guide.", + doc_urls::k_align_shafts_flip_direction); + bool clock_rotation = opts.clock_rotation; if (ImGui::Checkbox("Clock rotation", &clock_rotation)) m_view->shp_cyl_align().set_clock_rotation_enabled(clock_rotation); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + GUI_DOC_HELP_("When on (default off), after depth you can rotate about the shared axis to mesh teeth. " + "LMB or Shift+Tab enters clocking; Enter during depth skips it. Click ? to open the user guide.", + doc_urls::k_align_shafts_clock_rotation); + ImGui::Separator(); options_orthographic_projection_(); } diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index 16942a3c..5cdad91a 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -480,8 +480,12 @@ std::optional cylinder_from_face(const TopoDS_Face& face) gp_Trsf cyl_align_trsf(const gp_Ax1& moving_axis, const gp_Ax1& fixed_axis, bool flip, double axial_offset, double twist_rad) { const gp_Dir from_dir = moving_axis.Direction(); - const gp_Dir to_dir = flip ? fixed_axis.Direction().Reversed() : fixed_axis.Direction(); const gp_Dir fixed_dir = fixed_axis.Direction(); + // OCCT cylinder dirs follow face parametrization, not part Z. Default = acute + // rotation so already-aligned solids are not flipped; Flip inverts that choice. + gp_Dir to_dir = fixed_dir; + if ((from_dir.Dot(to_dir) < 0.0) != flip) + to_dir.Reverse(); const gp_Vec to_moving(fixed_axis.Location(), moving_axis.Location()); const double param0 = to_moving.Dot(gp_Vec(fixed_dir)); diff --git a/src/utl_geom.h b/src/utl_geom.h index 8cd0fcb4..7c1d667b 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -106,9 +106,10 @@ std::optional cylinder_from_face(const TopoDS_Face& face); /// Rigid transform that maps \a moving_axis onto \a fixed_axis. /// Offset 0 places the moving origin on its projection onto the fixed axis; -/// \a axial_offset then slides along the fixed direction. \a flip reverses the -/// moving direction sense (180 deg about a perpendicular). \a twist_rad is an -/// extra rotation about the fixed axis after coaxial placement (clock rotation). +/// \a axial_offset then slides along the fixed direction. Face cylinder dirs are +/// unsigned: the default sense is the smaller rotation (acute). \a flip forces +/// the opposite (180 deg about a perpendicular). \a twist_rad is an extra +/// rotation about the fixed axis after coaxial placement (clock rotation). gp_Trsf cyl_align_trsf(const gp_Ax1& moving_axis, const gp_Ax1& fixed_axis, bool flip, double axial_offset, double twist_rad = 0.0); diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index df7021d7..5e5caa28 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -1787,6 +1787,22 @@ TEST_F(Shp_test, Workbench_cyl_align_uses_instance_placement) EXPECT_NEAR(gp_Lin(fixed_world).Distance(after), 0.0, 1e-6); } +TEST(Shp_cyl_align, Prefers_smaller_rotation) +{ + const gp_Ax1 fixed(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)); + const gp_Ax1 anti(gp_Pnt(8.0, 0.0, 0.0), gp_Dir(0.0, 0.0, -1.0)); + + const gp_Trsf keep = cyl_align_trsf(anti, fixed, false, 0.0, 0.0); + EXPECT_GT(gp_Vec(0.0, 0.0, 1.0).Transformed(keep).Z(), 0.9); + + const gp_Trsf flipped = cyl_align_trsf(anti, fixed, true, 0.0, 0.0); + EXPECT_LT(gp_Vec(0.0, 0.0, 1.0).Transformed(flipped).Z(), -0.9); + + const gp_Ax1 same(gp_Pnt(8.0, 0.0, 0.0), gp_Dir(0.0, 0.0, 1.0)); + const gp_Trsf par = cyl_align_trsf(same, fixed, false, 0.0, 0.0); + EXPECT_GT(gp_Vec(0.0, 0.0, 1.0).Transformed(par).Z(), 0.9); +} + TEST_F(Shp_test, Workbench_set_frame_keeps_instance_pose) { view().add_cylinder(0, 0, 0, 1.0, 4.0); From 799ba64b51e53e8c50e2f2f10bb10f3840ccbdfb Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 22 Sep 2026 17:39:18 -0600 Subject: [PATCH 08/12] Design to workbench prop fix. --- docs/usage.md | 6 ++--- src/doc/shape.md | 2 +- src/gui.cpp | 2 +- src/gui_mode.cpp | 2 +- src/gui_occt_view.cpp | 26 +++++++++++++------- src/gui_occt_view.h | 6 +++-- src/mode.cpp | 1 + src/shp_common.cpp | 5 +--- src/shp_cross_section.cpp | 29 ++++++++++++++++------ src/shp_cut.cpp | 5 +--- src/shp_delta.cpp | 32 +++++++++++++++++------- src/shp_fuse.cpp | 5 +--- src/shp_operation.cpp | 22 +++++++++++++++-- src/shp_operation.h | 5 +++- tests/shp_tests.cpp | 51 +++++++++++++++++++++++++++++++++++++++ 15 files changed, 150 insertions(+), 49 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 6b628d9f..64d57648 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -236,8 +236,8 @@ Press Esc to cancel the current action or step back to a broader mode - **If something is in progress:** Esc cancels it and discards the change. Examples: cancel a line you are drawing, revert an unconfirmed [move](#shape-move-tool-g)/[rotate](#shape-rotate-tool-r)/[scale](#shape-scale-tool-s), cancel [extrude](#extrude-sketch-face-tool-e) preview, clear the distance or angle input dialog. - **If nothing is in progress:** Esc steps the application to the **parent mode** (one level up): - - From a **sketch tool** (e.g. Add line, Add circle, Operation axis) -> **Sketch inspection mode**. - - From **Sketch inspection**, **Design** tools ([Move](#shape-move-tool-g), [Rotate](#shape-rotate-tool-r), [Align shafts](#align-shafts-tool-j), [Extrude](#extrude-sketch-face-tool-e), [Scale](#shape-scale-tool-s), [Chamfer](#other-feature-operations) (C), [Fillet](#other-feature-operations) (F), [Polar duplicate](#shape-polar-duplicate-tool), [Create sketch from face](usage-sketch.md#create-sketch-from-planar-face-tool)), or Design **Inspection** -> **Design inspection**. + - From a **sketch tool** (e.g. Add line, Add circle, Operation axis, [Extrude](#extrude-sketch-face-tool-e)) -> **Sketch inspection mode**. + - From **Sketch inspection**, **Design** tools ([Move](#shape-move-tool-g), [Rotate](#shape-rotate-tool-r), [Align shafts](#align-shafts-tool-j), [Scale](#shape-scale-tool-s), [Chamfer](#other-feature-operations) (C), [Fillet](#other-feature-operations) (F), [Polar duplicate](#shape-polar-duplicate-tool), [Create sketch from face](usage-sketch.md#create-sketch-from-planar-face-tool)), or Design **Inspection** -> **Design inspection**. - From a **Workbench** tool ([Move](#shape-move-tool-g), [Rotate](#shape-rotate-tool-r), [Align shafts](#align-shafts-tool-j)) -> **Workbench** idle (not Design inspection). Use the task buttons to jump between Sketch, Design, and Workbench. So repeated Esc from a sketch drawing tool first cancels the current element, then returns to Sketch inspection, then to Design inspection. Workbench tools step back to Workbench idle. @@ -974,9 +974,9 @@ Contributors should follow **[ezycad_code_style.md](ezycad_code_style.md)** for - ![Sketcher_CreateSlot](res/icons/Sketcher_CreateSlot.png) - Add slot - ![Sketcher_CreateBone](res/icons/Sketcher_CreateBone.png) - Add bone (Shift+U) - ![TechDraw_LengthDimension](res/icons/TechDraw_LengthDimension.png) - Dimension tool (D) +- ![Design456_Extrude](res/icons/Design456_Extrude.png) - Extrude sketch face (E) ### 3D Operations -- ![Design456_Extrude](res/icons/Design456_Extrude.png) - Extrude sketch face (E) - ![PartDesign_Chamfer](res/icons/PartDesign_Chamfer.png) - Chamfer (C) - ![PartDesign_Fillet](res/icons/PartDesign_Fillet.png) - Fillet (F) - ![Draft_PolarArray](res/icons/Draft_PolarArray.png) - Shape polar duplicate diff --git a/src/doc/shape.md b/src/doc/shape.md index 75486730..57fbdbdb 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -33,7 +33,7 @@ Typical uses: ### Lifetime and ownership - Design shapes are stored in `Occt_view::m_shps`. Workbench instances live in `m_wbk_shps` (separate list). Access via `get_shapes()` / `get_workbench_shapes()`. Shared `allocate_shape_id()`. -- Workbench leaves are **links**: `source_id` + local geom `Inverse(trsf_from_frame(source.frame)) * source.Shape()`, instance `gp_Ax3` as placement (`trsf_from_frame` is local-to-world; do not use single-arg `gp_Trsf::SetTransformation`). `add_to_workbench` copies Design nodes; `sync_workbench_links` refreshes after Design geom replace (fillet/chamfer keep `Shape_id`). Design bake of move/rotate does not move instances. Workbench bake updates frame only. `.ezy` `workbench[]` stores ids / `sourceId` / frame (no BREP). Deleting a Design source cascade-deletes its links. +- Workbench leaves are **links**: `source_id` + local geom `Inverse(trsf_from_frame(source.frame)) * source.Shape()`, instance `gp_Ax3` as placement (`trsf_from_frame` is local-to-world; do not use single-arg `gp_Trsf::SetTransformation`). `add_to_workbench` copies Design nodes; `sync_workbench_links` refreshes after Design geom replace. Fillet, chamfer, cut, fuse, common, and cross-section clip keep the edited solid's `Shape_id` (boolean result reuses the first operand). `Shape_replace_delta` does not cascade workbench links for an id that the same delta inserts again. Consumed operands (cut tools, fused-away solids) still cascade-delete their links. Design bake of move/rotate does not move instances. Workbench bake updates frame only. `.ezy` `workbench[]` stores ids / `sourceId` / frame (no BREP). Deleting a Design source cascade-deletes its links. - Each solid stores a `gp_Ax3` local frame (**Z** = primary axis, **Y** = up). 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. Shape List right-click toggles **Show axes** / **Show plane** / **Show up** AIS annotations (`frameDisplay` in `.ezy`), and can **Reset**, **Set from planar/cylindrical face** (`Mode::Shape_set_frame`, no toolbar/hotkey; planar origin = face area centroid), or **Flip up** / **Flip axis** via `Occt_view::set_shape_frame` (frame-only `Shape_geom_delta`). Call `clear_frame_display()` before `Remove` from the interactive context (user delete via `Occt_view::remove`, fillet/chamfer `replace_picked_shape_`, load, view dtor). `~Shp` only Nullifies those handles -- the context may already be dying. - `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`. diff --git a/src/gui.cpp b/src/gui.cpp index 1c2c4c49..ce27a15b 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -201,10 +201,10 @@ void GUI::initialize_toolbar_() {load_texture("res/icons/Sketcher_CreateSlot.png"), false, "Add slot", Mode::Sketch_add_slot, Task::Sketch}, {load_texture("res/icons/Sketcher_CreateBone.png"), false, "Add bone", Mode::Sketch_add_bone, Task::Sketch}, {load_texture("res/icons/TechDraw_LengthDimension.png"), false, "Length dimension", Mode::Sketch_dim_anno, Task::Sketch}, + {load_texture("res/icons/Design456_Extrude.png"), false, "Extrude sketch face", Mode::Sketch_face_extrude, Task::Sketch}, // Design {load_texture("res/icons/User.png"), true, "Inspection mode", Mode::Design_inspection, Task::Design}, {load_texture("res/icons/Macro_FaceToSketch_48.png"), false, "Create a sketch from planar face", Mode::Sketch_from_planar_face, Task::Design}, - {load_texture("res/icons/Design456_Extrude.png"), false, "Extrude sketch face", Mode::Sketch_face_extrude, Task::Design}, {load_texture("res/icons/Assembly_AxialMove.png"), false, "Shape move", Mode::Design_move, Task::Design}, {load_texture("res/icons/Draft_Rotate.png"), false, "Shape rotate", Mode::Design_rotate, Task::Design}, {load_texture("res/icons/Part_Scale.png"), false, "Shape Scale", Mode::Scale, Task::Design}, diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 202ef170..7b0b03a7 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -154,7 +154,7 @@ Mode GUI::parent_mode_of(Mode mode) {Mode::Design_shaft_align, Mode::Design_inspection}, {Mode::Sketch_inspection, Mode::Design_inspection}, {Mode::Sketch_from_planar_face, Mode::Design_inspection}, - {Mode::Sketch_face_extrude, Mode::Design_inspection}, + {Mode::Sketch_face_extrude, Mode::Sketch_inspection}, {Mode::Shape_chamfer, Mode::Design_inspection}, {Mode::Shape_fillet, Mode::Design_inspection}, {Mode::Shape_polar_duplicate, Mode::Design_inspection}, diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index 47c2193e..ac2292db 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -1943,7 +1943,7 @@ void Occt_view::insert_shape_rec(const Shape_rec& rec) sync_workbench_links(rec.id); } -void Occt_view::remove_shape_by_id(Shape_id id) +void Occt_view::remove_shape_by_id(Shape_id id, bool cascade_workbench_links) { auto remove_from = [&](std::list& store, bool design) { @@ -1966,13 +1966,16 @@ void Occt_view::remove_shape_by_id(Shape_id id) if (design) { ensure_current_group_valid_(); - std::vector dead; - for (const Shp_ptr& w : m_wbk_shps) - if (!w.IsNull() && w->get_source_id() == id) - dead.push_back(w->get_id()); + if (cascade_workbench_links) + { + std::vector dead; + for (const Shp_ptr& w : m_wbk_shps) + if (!w.IsNull() && w->get_source_id() == id) + dead.push_back(w->get_id()); - for (Shape_id wid : dead) - remove_shape_by_id(wid); + for (Shape_id wid : dead) + remove_shape_by_id(wid, false); + } } else if (m_current_wbk_group_id == id) m_current_wbk_group_id = 0; @@ -2803,7 +2806,7 @@ void Occt_view::remove_selected_length_dimensions_from_sketches_() break; } -void Occt_view::delete_(std::vector& to_delete) +void Occt_view::delete_(std::vector& to_delete, const std::vector& keep_workbench_sources) { for (AIS_Shape_ptr& shp : to_delete) try_remove_sketch_permanent_node_mark(shp.get()); @@ -2812,9 +2815,14 @@ void Occt_view::delete_(std::vector& to_delete) if (auto wire = dynamic_cast(shp.get()); wire) wire->owner_sketch.remove_edge(*wire); + auto keep_links = [&](Shape_id id) + { + return std::find(keep_workbench_sources.begin(), keep_workbench_sources.end(), id) != keep_workbench_sources.end(); + }; + std::unordered_set deleted_design_ids; for (const AIS_Shape_ptr& obj : to_delete) - if (Shp_ptr s = Shp_ptr::DownCast(obj); !s.IsNull() && !s->is_workbench()) + if (Shp_ptr s = Shp_ptr::DownCast(obj); !s.IsNull() && !s->is_workbench() && !keep_links(s->get_id())) deleted_design_ids.insert(s->get_id()); for (const Shp_ptr& w : m_wbk_shps) diff --git a/src/gui_occt_view.h b/src/gui_occt_view.h index e9a3a4b0..c60e2676 100644 --- a/src/gui_occt_view.h +++ b/src/gui_occt_view.h @@ -150,7 +150,8 @@ class Occt_view : protected AIS_ViewController /// Insert a shape from an undo snapshot (keeps \a rec.id). Displays solids in the viewer (not groups). void insert_shape_rec(const Shape_rec& rec); /// Remove a shape by stable id (viewer + document list). - void remove_shape_by_id(Shape_id id); + /// \a cascade_workbench_links drops Design-linked workbench copies. Pass false when this id is replaced in the same edit. + void remove_shape_by_id(Shape_id id, bool cascade_workbench_links = true); /// Replace BREP of an existing shape (identity local transform). void set_shape_geom_by_id(Shape_id id, const TopoDS_Shape& geom, const gp_Ax3& frame); /// Workbench tool-frame restore (pose / placement unchanged). @@ -230,7 +231,8 @@ class Occt_view : protected AIS_ViewController // Delete related. void delete_selected(); void delete_shapes(std::vector to_delete); - void delete_(std::vector& to_delete); + /// \a keep_workbench_sources are Design ids replaced in this delete (links stay; caller syncs them). + void delete_(std::vector& to_delete, const std::vector& keep_workbench_sources = {}); /// Copy selected solids / current-group subtree into the in-app shape clipboard. [[nodiscard]] Status copy_selected_shapes(); diff --git a/src/mode.cpp b/src/mode.cpp index 1924fca9..64c8940a 100644 --- a/src/mode.cpp +++ b/src/mode.cpp @@ -120,6 +120,7 @@ Task task_of(Mode mode) case Mode::Sketch_add_bone: case Mode::Sketch_operation_axis: case Mode::Sketch_dim_anno: + case Mode::Sketch_face_extrude: return Task::Sketch; default: diff --git a/src/shp_common.cpp b/src/shp_common.cpp index 211d0b5f..4836bc09 100644 --- a/src/shp_common.cpp +++ b/src/shp_common.cpp @@ -50,10 +50,7 @@ Shp_rslt Shp_common::common(std::vector shps) // Create a new shape from the common result Shp_ptr shp = new Shp(ctx(), result); - shp->set_name("Common"); - assign_result_parent_(shp, m_shps); - delete_operation_shps_(); - add_shp_(shp); + replace_operands_keeping_first_id_(shp, "Common"); view().push_undo_delta( std::make_unique(std::move(removed), std::vector{capture_shape_rec(*shp)})); return Shp_rslt(shp); diff --git a/src/shp_cross_section.cpp b/src/shp_cross_section.cpp index 5c260702..0d26c3ac 100644 --- a/src/shp_cross_section.cpp +++ b/src/shp_cross_section.cpp @@ -511,23 +511,36 @@ Status Shp_cross_section::clip(const std::vector& shapes) for (const Shp_ptr& shp : plane_ctx.shapes) removed.push_back(capture_shape_rec(*shp)); - std::vector added; - added.reserve(survivors.size()); - m_shps = plane_ctx.shapes; + std::vector replacements; + replacements.reserve(survivors.size()); + std::vector keep_links; + keep_links.reserve(survivors.size()); for (size_t i = 0; i < survivors.size(); ++i) { const Shp_ptr& old_shp = survivors[i]; Shp_ptr new_shp = new Shp(ctx(), clipped_geoms[i]); new_shp->set_name(old_shp->get_name()); + new_shp->set_id(old_shp->get_id()); new_shp->set_frame(frame_world_(*old_shp)); - assign_result_parent_(new_shp, std::vector{old_shp}); - add_shp_(new_shp); - copy_shape_material_from_(new_shp, old_shp); - added.push_back(capture_shape_rec(*new_shp)); + new_shp->set_parent_id(old_shp->get_parent_id()); + new_shp->set_sibling_order(old_shp->get_sibling_order()); + replacements.push_back(new_shp); + keep_links.push_back(old_shp->get_id()); } - delete_operation_shps_(); + m_shps = plane_ctx.shapes; + delete_operation_shps_(keep_links); + + std::vector added; + added.reserve(replacements.size()); + for (size_t i = 0; i < replacements.size(); ++i) + { + add_shp_(replacements[i], false); + copy_shape_material_from_(replacements[i], survivors[i]); + view().sync_workbench_links(replacements[i]->get_id()); + added.push_back(capture_shape_rec(*replacements[i])); + } view().push_undo_delta(std::make_unique(std::move(removed), std::move(added))); clear(); diff --git a/src/shp_cut.cpp b/src/shp_cut.cpp index b5f31345..99e47750 100644 --- a/src/shp_cut.cpp +++ b/src/shp_cut.cpp @@ -50,10 +50,7 @@ Shp_rslt Shp_cut::cut(std::vector shps) return Shp_rslt(Result_status::User_error, "Error: Resulting shape is null"); Shp_ptr shp = new Shp(ctx(), result_shape); - shp->set_name("Cut"); - assign_result_parent_(shp, m_shps); - delete_operation_shps_(); - add_shp_(shp); + replace_operands_keeping_first_id_(shp, "Cut"); view().push_undo_delta( std::make_unique(std::move(removed), std::vector{capture_shape_rec(*shp)})); diff --git a/src/shp_delta.cpp b/src/shp_delta.cpp index 33145890..fe775610 100644 --- a/src/shp_delta.cpp +++ b/src/shp_delta.cpp @@ -1,12 +1,24 @@ #include "shp_delta.h" +#include + #include "gui_occt_view.h" namespace { -void remove_recs_(Occt_view& view, const std::vector& recs); +void remove_recs_(Occt_view& view, const std::vector& recs, const std::unordered_set& keep_links); void insert_recs_(Occt_view& view, const std::vector& recs); void apply_links_(Occt_view& view, const std::vector& links, bool forward); + +std::unordered_set design_ids_(const std::vector& recs) +{ + std::unordered_set ids; + for (const Shape_rec& rec : recs) + if (!rec.is_workbench) + ids.insert(rec.id); + + return ids; +} } // namespace Shape_rec capture_shape_rec(const Shp& shp) @@ -37,7 +49,7 @@ Shape_add_delta::Shape_add_delta(std::vector added) void Shape_add_delta::apply_forward(Occt_view& view) { insert_recs_(view, m_added); } -void Shape_add_delta::apply_reverse(Occt_view& view) { remove_recs_(view, m_added); } +void Shape_add_delta::apply_reverse(Occt_view& view) { remove_recs_(view, m_added, {}); } std::unique_ptr Shape_add_delta::clone() const { return std::make_unique(m_added); } @@ -46,7 +58,7 @@ Shape_remove_delta::Shape_remove_delta(std::vector removed) { } -void Shape_remove_delta::apply_forward(Occt_view& view) { remove_recs_(view, m_removed); } +void Shape_remove_delta::apply_forward(Occt_view& view) { remove_recs_(view, m_removed, {}); } void Shape_remove_delta::apply_reverse(Occt_view& view) { insert_recs_(view, m_removed); } @@ -87,13 +99,15 @@ Shape_replace_delta::Shape_replace_delta(std::vector removed, std::ve void Shape_replace_delta::apply_forward(Occt_view& view) { - remove_recs_(view, m_removed); + // Ids that return in m_added are the same part (cut/fuse/common/fillet). Do not drop their workbench links + // in the gap before insert; insert_shape_rec syncs geometry. + remove_recs_(view, m_removed, design_ids_(m_added)); insert_recs_(view, m_added); } void Shape_replace_delta::apply_reverse(Occt_view& view) { - remove_recs_(view, m_added); + remove_recs_(view, m_added, design_ids_(m_removed)); insert_recs_(view, m_removed); } @@ -110,14 +124,14 @@ void Shape_tree_delta::apply_forward(Occt_view& view) { insert_recs_(view, m_added); apply_links_(view, m_links, true); - remove_recs_(view, m_removed); + remove_recs_(view, m_removed, {}); } void Shape_tree_delta::apply_reverse(Occt_view& view) { insert_recs_(view, m_removed); apply_links_(view, m_links, false); - remove_recs_(view, m_added); + remove_recs_(view, m_added, {}); } std::unique_ptr Shape_tree_delta::clone() const @@ -127,10 +141,10 @@ std::unique_ptr Shape_tree_delta::clone() const namespace { -void remove_recs_(Occt_view& view, const std::vector& recs) +void remove_recs_(Occt_view& view, const std::vector& recs, const std::unordered_set& keep_links) { for (const Shape_rec& rec : recs) - view.remove_shape_by_id(rec.id); + view.remove_shape_by_id(rec.id, keep_links.find(rec.id) == keep_links.end()); } void insert_recs_(Occt_view& view, const std::vector& recs) diff --git a/src/shp_fuse.cpp b/src/shp_fuse.cpp index fc5faf64..de5a2113 100644 --- a/src/shp_fuse.cpp +++ b/src/shp_fuse.cpp @@ -50,10 +50,7 @@ Shp_rslt Shp_fuse::fuse(std::vector shps) // Create a new shape from the union result Shp_ptr shp = new Shp(ctx(), result); - shp->set_name("Fused"); - assign_result_parent_(shp, m_shps); - delete_operation_shps_(); - add_shp_(shp); + replace_operands_keeping_first_id_(shp, "Fused"); view().push_undo_delta( std::make_unique(std::move(removed), std::vector{capture_shape_rec(*shp)})); diff --git a/src/shp_operation.cpp b/src/shp_operation.cpp index 04137bc6..bf36deda 100644 --- a/src/shp_operation.cpp +++ b/src/shp_operation.cpp @@ -66,16 +66,34 @@ void Shp_operation_base::set_operation_shps_(std::vector shps) { m_shps return Status::ok(); } -void Shp_operation_base::delete_operation_shps_() +void Shp_operation_base::delete_operation_shps_(const std::vector& keep_workbench_sources) { std::vector to_delete; for (Shp_ptr& shp : m_shps) to_delete.push_back(shp); - m_view.delete_(to_delete); + m_view.delete_(to_delete, keep_workbench_sources); m_shps.clear(); } +void Shp_operation_base::replace_operands_keeping_first_id_(Shp_ptr& result, const std::string& name) +{ + if (result.IsNull() || m_shps.empty() || m_shps.front().IsNull()) + return; + + Shp_ptr survivor = m_shps.front(); + result->set_name(name); + result->set_id(survivor->get_id()); + result->set_frame(survivor->get_frame()); + result->set_parent_id(survivor->get_parent_id()); + result->set_sibling_order(survivor->get_sibling_order()); + + delete_operation_shps_({survivor->get_id()}); + add_shp_(result, false); + copy_shape_material_from_(result, survivor); + view().sync_workbench_links(result->get_id()); +} + void Shp_operation_base::operation_shps_finalize_() { m_completed_shps = m_shps; diff --git a/src/shp_operation.h b/src/shp_operation.h index 22107695..e6dca95f 100644 --- a/src/shp_operation.h +++ b/src/shp_operation.h @@ -24,7 +24,10 @@ class Shp_operation_base void assign_result_parent_(Shp_ptr& result, const std::vector& operands); [[nodiscard]] Status ensure_operation_shps_(); [[nodiscard]] Status ensure_operation_multi_shps_(); - void delete_operation_shps_(); + /// \a keep_workbench_sources: Design ids whose workbench links must survive (the id is reused by the result). + void delete_operation_shps_(const std::vector& keep_workbench_sources = {}); + /// First operand's Shape_id, frame, and list slot stay on \a result. Other operands are deleted. + void replace_operands_keeping_first_id_(Shp_ptr& result, const std::string& name); void operation_shps_finalize_(); void operation_shps_cancel_(); /// Seed operands (e.g. selection snapshot at mode enter). Empty clears. diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index 5e5caa28..2bbdf938 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -663,6 +663,55 @@ TEST_F(Shp_test, Cut_box_from_box) EXPECT_NEAR(volume_of(view().get_shapes().back()->Shape()), 500.0, 1e-3); } +TEST_F(Shp_test, Cut_keeps_object_id_and_workbench_links) +{ + view().add_box(0, 0, 0, 10, 10, 10); + view().add_box(0, 0, 0, 5, 10, 10); + Shp_ptr object = *view().get_shapes().begin(); + Shp_ptr tool = view().get_shapes().back(); + const Shape_id object_id = object->get_id(); + ASSERT_TRUE(view().add_to_workbench({object}).is_ok()); + ASSERT_TRUE(view().add_to_workbench({object}).is_ok()); + ASSERT_TRUE(view().add_to_workbench({tool}).is_ok()); + ASSERT_EQ(view().get_workbench_shapes().size(), 3u); + + std::vector to_select(view().get_shapes().begin(), view().get_shapes().end()); + select_shapes(view(), to_select); + Status st = view().shp_cut().selected_cut(); + ASSERT_TRUE(st.is_ok()) << st.message(); + + ASSERT_EQ(view().get_shapes().size(), 1u); + EXPECT_EQ(view().get_shapes().back()->get_id(), object_id); + ASSERT_EQ(view().get_workbench_shapes().size(), 2u); + for (const Shp_ptr& inst : view().get_workbench_shapes()) + { + EXPECT_EQ(inst->get_source_id(), object_id); + EXPECT_NEAR(volume_of(inst->Shape()), 500.0, 1e-3); + } + + EXPECT_TRUE(view().undo()); + ASSERT_EQ(view().get_shapes().size(), 2u); + Shp_ptr restored; + for (const Shp_ptr& s : view().get_shapes()) + if (s->get_id() == object_id) + restored = s; + + ASSERT_FALSE(restored.IsNull()); + EXPECT_NEAR(volume_of(restored->Shape()), 1000.0, 1e-3); + ASSERT_EQ(view().get_workbench_shapes().size(), 2u); + for (const Shp_ptr& inst : view().get_workbench_shapes()) + { + EXPECT_EQ(inst->get_source_id(), object_id); + EXPECT_NEAR(volume_of(inst->Shape()), 1000.0, 1e-3); + } + + EXPECT_TRUE(view().redo()); + ASSERT_EQ(view().get_shapes().size(), 1u); + EXPECT_EQ(view().get_shapes().back()->get_id(), object_id); + ASSERT_EQ(view().get_workbench_shapes().size(), 2u); + EXPECT_NEAR(volume_of(view().get_workbench_shapes().back()->Shape()), 500.0, 1e-3); +} + TEST_F(Shp_test, Common_overlapping_boxes) { view().add_box(0, 0, 0, 10, 10, 10); @@ -1650,6 +1699,8 @@ TEST(Mode_helpers, Design_move_rotate_stay_on_design_task) EXPECT_EQ(task_of(Mode::Design_rotate), Task::Design); EXPECT_EQ(task_of(Mode::Design_shaft_align), Task::Design); EXPECT_EQ(task_of(Mode::Scale), Task::Design); + EXPECT_EQ(task_of(Mode::Sketch_face_extrude), Task::Sketch); + EXPECT_EQ(GUI::parent_mode_of(Mode::Sketch_face_extrude), Mode::Sketch_inspection); EXPECT_EQ(task_of(Mode::Workbench_move), Task::Workbench); EXPECT_EQ(task_of(Mode::Workbench_rotate), Task::Workbench); EXPECT_EQ(task_of(Mode::Workbench_shaft_align), Task::Workbench); From 97185a4beb08d7303e9864f25058a631a1e102b1 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 22 Sep 2026 17:43:58 -0600 Subject: [PATCH 09/12] Record the Workbench links issue and pull request. Co-authored-by: Cursor --- .../active/gh-268-workbench-geometry-links.md | 26 ++++++++++++++++ .../active/gh-269-workbench-geometry-links.md | 30 +++++++++++++++++++ agents/plans/tasks-workbenches.md | 2 +- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 agents/drafts/issues/active/gh-268-workbench-geometry-links.md create mode 100644 agents/drafts/prs/active/gh-269-workbench-geometry-links.md diff --git a/agents/drafts/issues/active/gh-268-workbench-geometry-links.md b/agents/drafts/issues/active/gh-268-workbench-geometry-links.md new file mode 100644 index 00000000..d0954ded --- /dev/null +++ b/agents/drafts/issues/active/gh-268-workbench-geometry-links.md @@ -0,0 +1,26 @@ +--- +github_issue: 268 +github_pr: 269 +status: open +paired_draft: ../prs/active/gh-269-workbench-geometry-links.md +--- + +# Workbench task with geometry links + +**Suggested labels:** `enhancement` + +## Title (GitHub) + +Workbench task with geometry links + +## Body (GitHub) + +Filed: https://github.com/trailcode/EzyCad/issues/268 + +### Summary + +Workbench task and list of placement instances linked to Design solids. Design cut, fuse, common, and cross-section clip keep the edited solid's `Shape_id` so those instances update. + +## Related + +- PR: https://github.com/trailcode/EzyCad/pull/269 diff --git a/agents/drafts/prs/active/gh-269-workbench-geometry-links.md b/agents/drafts/prs/active/gh-269-workbench-geometry-links.md new file mode 100644 index 00000000..a84c33ed --- /dev/null +++ b/agents/drafts/prs/active/gh-269-workbench-geometry-links.md @@ -0,0 +1,30 @@ +--- +github_issue: 268 +github_pr: 269 +status: open +paired_draft: ../issues/active/gh-268-workbench-geometry-links.md +--- + +# PR - Workbench task with geometry links + +## Title + +Workbench task with geometry links + +## Summary + +- Sketch / Design / Workbench task toolbar. Workbench List stores geometry links with their own placement. +- Cut, fuse, and common reuse the first operand's `Shape_id`. Cross-section clip keeps each survivor's id. `Shape_replace_delta` does not drop links for an id the same delta inserts again. +- Extrude sketch face is on the Sketch task. + +## Related + +- Issue: https://github.com/trailcode/EzyCad/issues/268 +- PR: https://github.com/trailcode/EzyCad/pull/269 +- Branch: `Trailcode/workbench` + +## Test Plan + +- [x] `EzyCad_tests` cut / fuse / common / workbench link filters +- [ ] Two Workbench instances, cut the Design solid, undo and redo +- [ ] Delete the Design source and confirm instances disappear diff --git a/agents/plans/tasks-workbenches.md b/agents/plans/tasks-workbenches.md index 832e55c2..79b5cb28 100644 --- a/agents/plans/tasks-workbenches.md +++ b/agents/plans/tasks-workbenches.md @@ -3,7 +3,7 @@ status: partial topic: tasks-workbenches depends_on: null blocks: [assembly-inspection-mode] -github_issue: null +github_issue: 268 --- # Tasks / workbenches (toolbar filter) From 389134feeee09b08bf10b704cee8d0fe239770b9 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Wed, 23 Sep 2026 07:21:43 -0600 Subject: [PATCH 10/12] Fix. --- CHANGELOG.md | 1 + res/icons/Workbench_Assembly.png | Bin 0 -> 3177 bytes res/icons/Workbench_Assembly.svg | 343 +++++++++++++++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 res/icons/Workbench_Assembly.png create mode 100644 res/icons/Workbench_Assembly.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 87fcdf95..a5191b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Workbench task icon**: `res/icons/Workbench_Assembly.png` is included so the task button and the usage guide can load it (Sphinx `-W` failed on the missing image). - **Workbench toolbar**: the tools row no longer repeats the Workbench task icon; idle is the task button only. - **Workbench Align shafts**: cylindrical face picking uses Face selection on Workbench instances (same as Design). Insert position uses the instance placement (axis direction was already correct). - **Workbench set frame**: **Set from planar/cylindrical face** from the Workbench List stays in Workbench and updates the instance tool frame (pose unchanged). diff --git a/res/icons/Workbench_Assembly.png b/res/icons/Workbench_Assembly.png new file mode 100644 index 0000000000000000000000000000000000000000..74df2868a8897f828ea68809644c22b3f9804273 GIT binary patch literal 3177 zcmV-v43_hWP)aLJc4Wk6)Iw%hq8=R zYoE?2gW5`GXerhf1r#lIrncJ0K|6?OX0$*-WsQQeugYS;up}Y-dY5yTbKZCUIN2P? z!qS=f!{76q=RWt`^Sj64Eh~YhntoEE|H9 zu=x5NYF^^K;q z(u?jhiHPb)TGoV6A;b@Dnr&A^Vpgd$A#+Vgd`50}b=-UeVu?xWseLKX(p&O9G{n}e zp=FgibDuuQm1|xy^X4&j#!mpyJuWVu*vEwvg{r#bqQDBnX`%QJgxcV#zFcuf0DAOe zXjrK2Ga-cY*~jyEWX|>Z-jSU+SI_*VhwhU~& zcoX?vblJts$M)mZO@h97lKM}l{L)!cGIJ54wW-9|5NaC(C?yy-B#!S+O6C50Q;Cjr z+;Gg=^+l}PP{fpxkxUpK%kn>zu;A&VcwG&Ab#yQ15AC9{wj3eA85K+4{BdOEkKy8p z{hT}aFG{VbbLCVgyZ+lX`Ie!~qI7t_^r=ibHRtv(q9L zKzNv)xzl?ycY1HC>V0h5R>JxZF7e^MO2VBQ-<^=k{*yJ7R`^)^L{Gkd|4@X`P_k_v zBWMyd43t#ZLn9e6Z62ei&&SnJ!=^QllRa!A`4eX0_4=uAbQ1^|7={I47_w7GcMi}l zN0l^5j0s`M!fci-?8o`yCf0v&nS!0A1cQo#Rhf(%GYC&p6%o-%DAl&CX`1-`K}=I3 zrNod5XJ`nqJrb}vLJ19z0id?th1;zY2pSls$Ay3N`vHHSt!+Zc=UNa1e(=Gd@$1$*h?H#rvK>JCQ+-&d9R>LPL45uIet(ca z&}ep;mz%$v znNAgU4p3RGBQ%ZP0|w%0YNWpM8ugWx7)GGAK2*C1^vvvo&F}Q7C4Ouj18Tdr=B}IyfV4BLX%Jo_}&M!|zJu+{H## z{M(o8Kh;3t@kTW59N(Lqz@nLHxV=^}E?#+z&4m@L`&mBUo8J4n`69z@%%2^{{Mm6- z)&$x3mwF1e?`YoV6$_?UvihHOOcVTGYe(buQm8F_xqV|OST_z3>^)q`bALR~f9|a2 zy`43*G~6wFD|ma=NbVV(-1&+?a)N^&FNovZmqpZ;dzm>|vf>FpMgY8Skg{!UDLbF5 zTW@3tto>d|CXPtt-hWR51b_a^*F5)n5iXBGoRBPCeuxQor!Zr3FUF5a#^Jc}F5Tn` z@c7ct`RZaL&prt=CPBalo~CBO5~7;lSWC)4sg5h53jk{aHiAKvw3?Tc5~M`4*;sI2 zZyuW62h)T-`%2mLU&R#eFK5rEye)eV4PW#o6Z8dhhX{3pcJc5bs@fM0Mn|S@W?2R8e1DNiYyVbx<3YGg{6CZH>t=AiW=iN8Y-4 zfDZqqs;uTfMK$Y^68T7)NW!@d-r7CGh%NUomM6^h&q99uWMxmrheN`pZWhFFJSGGc=XZI()-vCBT+RVzIz}V5>z1QTLEab z{CpxbgV_oe3c*ysHgZUeoIj_RHgb3xVa_m&K!DncGHS|7h)GByJ-a_6CT(HmQ!sXn z<@pusZcqFz1GKlC91_Cp%-3Kcf{{YNjC&K+gZHJ2L4#AV+3l@~KVq^4Ahg@}{2c>m zZ~6Vn(rhdef`tnDhKAUfcYjZ2{#`m5nTd>?vXudS;CylGKHsgavNOA*0PQV5kI@3= z3&BDlEG|k_gnkcx?}$hHdiq!ZE3L} P00000NkvXXu0mjfWjrd3 literal 0 HcmV?d00001 diff --git a/res/icons/Workbench_Assembly.svg b/res/icons/Workbench_Assembly.svg new file mode 100644 index 00000000..b60b7b46 --- /dev/null +++ b/res/icons/Workbench_Assembly.svg @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-Stock + 2015-07-04 + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f40e6dd6e65c012972bcc44a9889a1d43fb21486 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Wed, 23 Sep 2026 09:08:07 -0600 Subject: [PATCH 11/12] Unit test fix. --- tests/shp_tests.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index 2bbdf938..a34d4456 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -483,9 +483,12 @@ TEST_F(Shp_test, Cross_section_selection_stale_after_selection_change) const Shape_id clipped_id = boxes[1]->get_id(); ASSERT_TRUE(view().shp_cross_section().clip_selected().is_ok()); EXPECT_FALSE(view().shp_cross_section().has_preview()); - EXPECT_TRUE(view().find_shape_by_id(clipped_id).IsNull()); + // Clip reuses the solid's Shape_id so workbench links stay attached. + const Shp_ptr clipped = view().find_shape_by_id(clipped_id); + EXPECT_FALSE(clipped.IsNull()); ASSERT_FALSE(view().get_shapes().empty()); - EXPECT_TRUE(contains_solid_like(view().get_shapes().back()->Shape())); + EXPECT_TRUE(contains_solid_like(clipped->Shape())); + EXPECT_FALSE(view().find_shape_by_id(boxes[0]->get_id()).IsNull()); } TEST_F(Shp_test, Cross_section_clip_removes_fully_discarded_solids) @@ -505,8 +508,10 @@ TEST_F(Shp_test, Cross_section_clip_removes_fully_discarded_solids) const Status clip_status = view().shp_cross_section().clip_selected(); ASSERT_TRUE(clip_status.is_ok()) << clip_status.message(); EXPECT_TRUE(view().find_shape_by_id(short_id).IsNull()); - EXPECT_TRUE(view().find_shape_by_id(tall_id).IsNull()); + // The cut solid keeps its id; only the fully discarded box is removed. + EXPECT_FALSE(view().find_shape_by_id(tall_id).IsNull()); ASSERT_EQ(view().get_shapes().size(), 1u); + EXPECT_EQ(view().get_shapes().front()->get_id(), tall_id); EXPECT_TRUE(contains_solid_like(view().get_shapes().front()->Shape())); } From cdcf2d1d2b86a8a1ef1b7602c0f5dfbaf996d7f1 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Wed, 23 Sep 2026 10:12:08 -0600 Subject: [PATCH 12/12] Address PR --- docs/scripting.md | 2 +- src/doc/gui.md | 2 +- src/doc/script.md | 2 +- src/doc/shape.md | 2 +- src/gui_occt_view.cpp | 27 ++++++++++++++++++++ src/mode.cpp | 2 +- src/mode.h | 33 ++++++++++++++++--------- src/shp_common.cpp | 2 ++ src/shp_cut.cpp | 2 ++ src/shp_fuse.cpp | 2 ++ src/shp_operation.cpp | 9 +++++++ src/shp_operation.h | 2 ++ tests/shp_tests.cpp | 57 +++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 127 insertions(+), 17 deletions(-) diff --git a/docs/scripting.md b/docs/scripting.md index 6e3b6715..71692d96 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -49,7 +49,7 @@ ezy.Shp | `ezy.log(msg)` | Print to the console and the main **Log** pane | | `ezy.msg(text [, kind])` | Status toast and Log; optional kind (see below) | | `ezy.get_mode()` | Current application mode name (string) | -| `ezy.set_mode(name)` | Switch mode by name (`Design_inspection`, `Design_move`, `Design_shaft_align`, `Workbench_move`, `Scale`, ...). Retired names `Normal` / `Sketch_inspection_mode` / `Move` (Design move) / `Rotate` (Design rotate) / `Workbench_scale` / `Shape_shaft_align` still work. | +| `ezy.set_mode(name)` | Switch mode by name (`Design_inspection`, `Design_move`, `Design_shaft_align`, `Workbench_move`, `Scale`, ...). Retired names `Normal` / `Sketch_inspection_mode` / `Move` (Design move) / `Rotate` (Design rotate) / `Workbench_scale` / `Shape_shaft_align` (Design shaft align) still work. | | `ezy.save_occt_view_settings()` | Write settings JSON (including view colors) | | `ezy.occt_view_settings_json()` | Settings JSON string | | `ezy.help()` | Print binding summary | diff --git a/src/doc/gui.md b/src/doc/gui.md index 346a8276..d9c6a997 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -46,7 +46,7 @@ Typical responsibilities: The chrome toolbar is one ImGui window with two regions: **task** switcher (Sketch / Design / Workbench) then a `|` then **tools** for `task_of(get_mode())` only (option 1: hide unrelated modes). Clicking a task button calls `set_mode(idle_mode_of(task))`. Workbench idle (`Mode::Workbench_inspection`) is the task button only — not repeated on the tools row. `mode.move` / `mode.rotate` / `mode.cyl_align` pick the Design or Workbench pair from the current task. `mode.scale` is always Design `Scale`. Helpers: `task_of`, `idle_mode_of`, `is_move_mode`, `is_rotate_mode`, `is_shaft_align_mode`, `is_workbench_mode` in [`mode.h`](../mode.h). -`Mode::Design_inspection`, `Mode::Workbench_inspection`, and the three task buttons are toolbar-only (no remappable `Gui_action`). `Design_move` / `Design_rotate` / `Design_shaft_align` share `mode.move` / `mode.rotate` / `mode.cyl_align` with the Workbench pair (no extra `Gui_action`). Retired script names `Normal` / `Sketch_inspection_mode` / `Move` (`Design_move`) / `Rotate` (`Design_rotate`) / `Workbench_scale` / `Shape_shaft_align` still parse via `mode_from_string`. +`Mode::Design_inspection`, `Mode::Workbench_inspection`, and the three task buttons are toolbar-only (no remappable `Gui_action`). `Design_move` / `Design_rotate` / `Design_shaft_align` share `mode.move` / `mode.rotate` / `mode.cyl_align` with the Workbench pair (no extra `Gui_action`). Retired script names `Normal` / `Sketch_inspection_mode` / `Move` (`Design_move`) / `Rotate` (`Design_rotate`) / `Workbench_scale` / `Shape_shaft_align` (`Design_shaft_align`) still parse via `mode_from_string`. `.ezy` `mode` is the enum integer; slots 1, 3, and 24 stay `Design_move` / `Design_rotate` / `Design_shaft_align` (released Move / Rotate / Shape_shaft_align). Workbench Move / Rotate / Align shafts are later slots. ### New mode or toolbar command (hotkeys) diff --git a/src/doc/script.md b/src/doc/script.md index 7ac3595f..3073f06d 100644 --- a/src/doc/script.md +++ b/src/doc/script.md @@ -111,7 +111,7 @@ Aliases: global `view` == `ezy.view`; `Shp` == `ezy.Shp`; `view.add_sketch` / `f | `ezy.log(msg)` | `GUI::log_message` + console history | | `ezy.msg(text [, kind])` | `GUI::show_message` (toast + Log) | | `ezy.get_mode()` | `GUI::get_mode()` -> mode name string (`c_mode_strs`; Design/Workbench Move are `Design_move` / `Workbench_move`) | -| `ezy.set_mode(name)` | `GUI::set_mode(mode_from_string(...))` (aliases: `Move` / `Rotate` / `Workbench_scale` / `Shape_shaft_align`) | +| `ezy.set_mode(name)` | `GUI::set_mode(mode_from_string(...))` (`Move` / `Rotate` / `Shape_shaft_align` -> Design) | | `ezy.save_occt_view_settings()` | `GUI::save_occt_view_settings()` | | `ezy.occt_view_settings_json()` | `GUI::occt_view_settings_json()` | | `ezy.help()` / global `help()` | Prints binding summary to console | diff --git a/src/doc/shape.md b/src/doc/shape.md index 57fbdbdb..1b83adc9 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -33,7 +33,7 @@ Typical uses: ### Lifetime and ownership - Design shapes are stored in `Occt_view::m_shps`. Workbench instances live in `m_wbk_shps` (separate list). Access via `get_shapes()` / `get_workbench_shapes()`. Shared `allocate_shape_id()`. -- Workbench leaves are **links**: `source_id` + local geom `Inverse(trsf_from_frame(source.frame)) * source.Shape()`, instance `gp_Ax3` as placement (`trsf_from_frame` is local-to-world; do not use single-arg `gp_Trsf::SetTransformation`). `add_to_workbench` copies Design nodes; `sync_workbench_links` refreshes after Design geom replace. Fillet, chamfer, cut, fuse, common, and cross-section clip keep the edited solid's `Shape_id` (boolean result reuses the first operand). `Shape_replace_delta` does not cascade workbench links for an id that the same delta inserts again. Consumed operands (cut tools, fused-away solids) still cascade-delete their links. Design bake of move/rotate does not move instances. Workbench bake updates frame only. `.ezy` `workbench[]` stores ids / `sourceId` / frame (no BREP). Deleting a Design source cascade-deletes its links. +- Workbench leaves are **links**: `source_id` + local geom `Inverse(trsf_from_frame(source.frame)) * source.Shape()`, instance `gp_Ax3` as placement (`trsf_from_frame` is local-to-world; do not use single-arg `gp_Trsf::SetTransformation`). `add_to_workbench` copies Design nodes; `sync_workbench_links` refreshes after Design geom replace. Fillet, chamfer, cut, fuse, common, and cross-section clip keep the edited solid's `Shape_id` (boolean result reuses the first operand). `Shape_replace_delta` does not cascade workbench links for an id that the same delta inserts again. Consumed operands (cut tools, fused-away solids) still cascade-delete their links. User delete of a Design source records those links in `Shape_remove_delta` so undo restores them. Cut, fuse, and common reject Workbench instances (they are placements, not Design BREP). Design bake of move/rotate does not move instances. Workbench bake updates frame only. `.ezy` `workbench[]` stores ids / `sourceId` / frame (no BREP). Deleting a Design source cascade-deletes its links. - Each solid stores a `gp_Ax3` local frame (**Z** = primary axis, **Y** = up). 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. Shape List right-click toggles **Show axes** / **Show plane** / **Show up** AIS annotations (`frameDisplay` in `.ezy`), and can **Reset**, **Set from planar/cylindrical face** (`Mode::Shape_set_frame`, no toolbar/hotkey; planar origin = face area centroid), or **Flip up** / **Flip axis** via `Occt_view::set_shape_frame` (frame-only `Shape_geom_delta`). Call `clear_frame_display()` before `Remove` from the interactive context (user delete via `Occt_view::remove`, fillet/chamfer `replace_picked_shape_`, load, view dtor). `~Shp` only Nullifies those handles -- the context may already be dying. - `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`. diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index ac2292db..24d4ad53 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -2509,6 +2509,33 @@ void Occt_view::delete_shapes(std::vector to_delete) has_non_shape = true; } + // delete_ cascade-removes Workbench links of deleted Design ids. Record them or undo cannot put them back. + if (!has_non_shape) + { + std::unordered_set deleted_design_ids; + std::unordered_set captured_ids; + for (const Shape_rec& rec : removed_shapes) + { + captured_ids.insert(rec.id); + if (!rec.is_workbench) + deleted_design_ids.insert(rec.id); + } + + for (const Shp_ptr& w : m_wbk_shps) + { + if (w.IsNull() || w->get_source_id() == 0) + continue; + + if (deleted_design_ids.find(w->get_source_id()) == deleted_design_ids.end()) + continue; + + if (!captured_ids.insert(w->get_id()).second) + continue; + + removed_shapes.push_back(capture_shape_rec(*w)); + } + } + if (has_non_shape) push_undo_snapshot(); else if (!removed_shapes.empty()) diff --git a/src/mode.cpp b/src/mode.cpp index 64c8940a..b01a3b55 100644 --- a/src/mode.cpp +++ b/src/mode.cpp @@ -23,7 +23,7 @@ Mode mode_from_string(std::string_view name) return Mode::Scale; if (name == "Shape_shaft_align" || name == "Shape_cyl_align") - return Mode::Workbench_shaft_align; + return Mode::Design_shaft_align; return Mode::Design_inspection; } diff --git a/src/mode.h b/src/mode.h index 2558cebe..6c9c541a 100644 --- a/src/mode.h +++ b/src/mode.h @@ -5,12 +5,14 @@ // --- Mode ------------------------------------------------------------------ // Single source of truth: order here defines enum numeric values and c_mode_strs indices -// (mode_from_string, persistence, etc.). Do not reorder without migrating saved data. +// (mode_from_string, persistence, etc.). .ezy stores mode as this integer. +// Slots 0/1/2/3/24 match released files: Normal, Move, Scale, Rotate, Shape_shaft_align. +// Do not reorder those slots. New modes go at the end. #define EZY_MODE_LIST(X) \ - X(Design_inspection) \ - X(Workbench_move) \ + X(Design_inspection) /* 0: was Normal */ \ + X(Design_move) /* 1: was Move. Design CSG placement; bake BREP. */ \ X(Scale) \ - X(Workbench_rotate) \ + X(Design_rotate) /* 3: was Rotate. Design CSG rotation; bake BREP. */ \ X(Sketch_inspection) /* inspecting sketch elements */ \ X(Sketch_from_planar_face) /* sketch from a planar face */ \ X(Sketch_face_extrude) /* extrude a sketch face */ \ @@ -31,13 +33,13 @@ X(Sketch_add_bone) /* two centers, then r1, r2, waist clicks */ \ X(Sketch_dim_anno) \ X(Shape_cross_section) \ - X(Workbench_shaft_align) \ - X(Shape_set_frame) /* Shape List: pick face for local frame */ \ - X(Workbench_inspection) /* Workbench browse/select home; not Mode::Design_inspection */ \ - X(Design_move) /* Design CSG placement; bake BREP. Workbench_move is instance pose. */ \ - X(Design_rotate) /* Design CSG rotation; bake BREP. Workbench_rotate is instance pose. */ \ - X(Design_shaft_align) /* Design CSG shaft align; bake BREP. Workbench_shaft_align is instance pose. */ \ - X(Workbench_set_frame) /* Workbench List: pick face for instance local frame (pose unchanged). */ + X(Design_shaft_align) /* 24: was Shape_shaft_align. Design CSG shaft align; bake BREP. */ \ + X(Shape_set_frame) /* Shape List: pick face for local frame */ \ + X(Workbench_inspection) /* Workbench browse/select home; not Mode::Design_inspection */ \ + X(Workbench_move) /* instance pose; not the old Move slot */ \ + X(Workbench_rotate) /* instance pose; not the old Rotate slot */ \ + X(Workbench_shaft_align) /* instance pose; not the old Shape_shaft_align slot */ \ + X(Workbench_set_frame) /* Workbench List: pick face for instance local frame (pose unchanged). */ enum class Mode { @@ -54,6 +56,13 @@ constexpr std::array(Mode::_count)> c }; static_assert(c_mode_strs.size() == static_cast(Mode::_count)); +// Released .ezy files stored these tools at fixed integers. Keep the slots. +static_assert(static_cast(Mode::Design_inspection) == 0); +static_assert(static_cast(Mode::Design_move) == 1); +static_assert(static_cast(Mode::Scale) == 2); +static_assert(static_cast(Mode::Design_rotate) == 3); +static_assert(static_cast(Mode::Design_shaft_align) == 24); +static_assert(static_cast(Mode::Shape_set_frame) == 25); #undef EZY_MODE_LIST @@ -144,5 +153,5 @@ Task task_of(Mode mode); Mode idle_mode_of(Task task); /// Return Mode for a name (e.g. "Design_inspection", "Sketch_add_edge"). Returns Design_inspection if not found. -/// Also accepts retired names: Normal, Sketch_inspection_mode, Move (Design_move), Rotate (Design_rotate), Workbench_scale, Shape_shaft_align. +/// Also accepts retired names: Normal, Sketch_inspection_mode, Move (Design_move), Rotate (Design_rotate), Workbench_scale, Shape_shaft_align (Design_shaft_align). Mode mode_from_string(std::string_view name); diff --git a/src/shp_common.cpp b/src/shp_common.cpp index 4836bc09..5c74c58b 100644 --- a/src/shp_common.cpp +++ b/src/shp_common.cpp @@ -21,6 +21,8 @@ Shp_rslt Shp_common::common(std::vector shps) return Shp_rslt(Result_status::User_error, "common: null shape"); m_shps = std::move(shps); + if (Status st = ensure_design_operands_(); !st.is_ok()) + return Shp_rslt(st.status(), st.message()); std::vector removed; removed.reserve(m_shps.size()); diff --git a/src/shp_cut.cpp b/src/shp_cut.cpp index 99e47750..2c4cc2b0 100644 --- a/src/shp_cut.cpp +++ b/src/shp_cut.cpp @@ -22,6 +22,8 @@ Shp_rslt Shp_cut::cut(std::vector shps) return Shp_rslt(Result_status::User_error, "cut: null shape"); m_shps = std::move(shps); + if (Status st = ensure_design_operands_(); !st.is_ok()) + return Shp_rslt(st.status(), st.message()); std::vector removed; removed.reserve(m_shps.size()); diff --git a/src/shp_fuse.cpp b/src/shp_fuse.cpp index de5a2113..1031c8a5 100644 --- a/src/shp_fuse.cpp +++ b/src/shp_fuse.cpp @@ -21,6 +21,8 @@ Shp_rslt Shp_fuse::fuse(std::vector shps) return Shp_rslt(Result_status::User_error, "fuse: null shape"); m_shps = std::move(shps); + if (Status st = ensure_design_operands_(); !st.is_ok()) + return Shp_rslt(st.status(), st.message()); std::vector removed; removed.reserve(m_shps.size()); diff --git a/src/shp_operation.cpp b/src/shp_operation.cpp index bf36deda..4f0ec460 100644 --- a/src/shp_operation.cpp +++ b/src/shp_operation.cpp @@ -66,6 +66,15 @@ void Shp_operation_base::set_operation_shps_(std::vector shps) { m_shps return Status::ok(); } +Status Shp_operation_base::ensure_design_operands_() const +{ + for (const Shp_ptr& shp : m_shps) + if (!shp.IsNull() && shp->is_workbench()) + return Status::user_error("Select Design solids. Boolean operations do not edit Workbench instances."); + + return Status::ok(); +} + void Shp_operation_base::delete_operation_shps_(const std::vector& keep_workbench_sources) { std::vector to_delete; diff --git a/src/shp_operation.h b/src/shp_operation.h index e6dca95f..0e08a4b6 100644 --- a/src/shp_operation.h +++ b/src/shp_operation.h @@ -24,6 +24,8 @@ class Shp_operation_base void assign_result_parent_(Shp_ptr& result, const std::vector& operands); [[nodiscard]] Status ensure_operation_shps_(); [[nodiscard]] Status ensure_operation_multi_shps_(); + /// Cut / fuse / common edit Design BREP. Workbench rows are placements. + [[nodiscard]] Status ensure_design_operands_() const; /// \a keep_workbench_sources: Design ids whose workbench links must survive (the id is reused by the result). void delete_operation_shps_(const std::vector& keep_workbench_sources = {}); /// First operand's Shape_id, frame, and list slot stay on \a result. Other operands are deleted. diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index a34d4456..0dcd18a9 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -1727,6 +1727,13 @@ TEST(Mode_helpers, Design_move_rotate_stay_on_design_task) EXPECT_EQ(GUI::parent_mode_of(Mode::Workbench_set_frame), Mode::Workbench_inspection); EXPECT_EQ(mode_from_string("Move"), Mode::Design_move); EXPECT_EQ(mode_from_string("Rotate"), Mode::Design_rotate); + EXPECT_EQ(mode_from_string("Shape_shaft_align"), Mode::Design_shaft_align); + EXPECT_EQ(static_cast(Mode::Design_move), 1); + EXPECT_EQ(static_cast(Mode::Design_rotate), 3); + EXPECT_EQ(static_cast(Mode::Design_shaft_align), 24); + EXPECT_NE(static_cast(Mode::Workbench_move), static_cast(Mode::Design_move)); + EXPECT_NE(static_cast(Mode::Workbench_rotate), static_cast(Mode::Design_rotate)); + EXPECT_NE(static_cast(Mode::Workbench_shaft_align), static_cast(Mode::Design_shaft_align)); } TEST(Shp_frame, Trsf_from_frame_maps_local_origin_to_world) @@ -1921,6 +1928,56 @@ TEST_F(Shp_test, Workbench_json_round_trip_and_source_delete) EXPECT_TRUE(view().get_workbench_shapes().empty()); } +TEST_F(Shp_test, Undo_delete_design_restores_workbench_links) +{ + view().add_box(0, 0, 0, 4, 5, 6); + Shp_ptr src = view().get_shapes().back(); + const Shape_id src_id = src->get_id(); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + ASSERT_TRUE(view().add_to_workbench({src}).is_ok()); + ASSERT_EQ(view().get_workbench_shapes().size(), 2u); + const Shape_id inst_a = view().get_workbench_shapes().front()->get_id(); + const Shape_id inst_b = view().get_workbench_shapes().back()->get_id(); + + view().delete_shapes({src}); + EXPECT_TRUE(view().get_shapes().empty()); + EXPECT_TRUE(view().get_workbench_shapes().empty()); + + EXPECT_TRUE(view().undo()); + ASSERT_EQ(view().get_shapes().size(), 1u); + EXPECT_EQ(view().get_shapes().back()->get_id(), src_id); + ASSERT_EQ(view().get_workbench_shapes().size(), 2u); + EXPECT_EQ(view().get_workbench_shapes().front()->get_id(), inst_a); + EXPECT_EQ(view().get_workbench_shapes().back()->get_id(), inst_b); + for (const Shp_ptr& inst : view().get_workbench_shapes()) + { + EXPECT_EQ(inst->get_source_id(), src_id); + EXPECT_NEAR(volume_of(inst->Shape()), volume_of(view().get_shapes().back()->Shape()), 1e-6); + } + + EXPECT_TRUE(view().redo()); + EXPECT_TRUE(view().get_shapes().empty()); + EXPECT_TRUE(view().get_workbench_shapes().empty()); +} + +TEST_F(Shp_test, Cut_rejects_workbench_instances) +{ + view().add_box(0, 0, 0, 10, 10, 10); + view().add_box(0, 0, 0, 5, 10, 10); + std::vector design(view().get_shapes().begin(), view().get_shapes().end()); + ASSERT_TRUE(view().add_to_workbench({design[0]}).is_ok()); + ASSERT_TRUE(view().add_to_workbench({design[1]}).is_ok()); + std::vector instances(view().get_workbench_shapes().begin(), view().get_workbench_shapes().end()); + select_shapes(view(), instances); + + Status st = view().shp_cut().selected_cut(); + EXPECT_FALSE(st.is_ok()); + EXPECT_EQ(view().get_shapes().size(), 2u); + EXPECT_EQ(view().get_workbench_shapes().size(), 2u); + EXPECT_EQ(view().get_shapes().front()->get_id(), design[0]->get_id()); + EXPECT_EQ(view().get_workbench_shapes().front()->get_id(), instances[0]->get_id()); +} + TEST_F(Shp_test, Add_design_group_to_workbench) { view().add_box(0, 0, 0, 1, 1, 1);