From 79426f53bdc9e57de10fe10c921c6708ba694333 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 4 Aug 2026 17:49:00 -0600 Subject: [PATCH 1/5] Start --- CHANGELOG.md | 2 +- docs/usage.md | 79 ++++++++++++++-------------- src/doc/gui.md | 64 +++++++++++------------ src/doc/shape.md | 76 +++++++++++++-------------- src/gui.cpp | 9 +++- src/gui_mode.cpp | 11 ++-- src/shp_cyl_align.cpp | 117 ++++++++++++++++++++++++++++++++++++++---- src/shp_cyl_align.h | 18 +++++-- src/utl_geom.cpp | 15 +++++- src/utl_geom.h | 9 +++- 10 files changed, 270 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c6f04..3e9cc0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Shape copy / paste** (Ctrl+C / Ctrl+V): in-app clipboard for selected solids, or a Shape List group subtree when the selection matches that group's descendant solids. Paste deep-copies under the current group (new ids, uniquified names, same pose) with one undo step. Survives **New** so you can copy, start a blank project, and paste. Remappable as **`edit.copy`** / **`edit.paste`**. Does not use the OS clipboard; sketch geometry is not copied. -- **Align cylinders** (J): pick two cylindrical faces (first moves, second is fixed), coaxially align, then drag insert depth along the shared axis (or Tab for an exact depth). Options **Flip direction** reverses axis sense. Bakes like Move/Rotate (`Shape_geom_delta`); radius mismatch logs a warning but still places. Remappable as **`mode.cyl_align`**. +- **Align cylinders** (J): pick two cylindrical faces (first moves, second is fixed), coaxially align, drag axial insert depth, then twist about the shared axis for tooth clocking (or Enter during depth to skip twist). Tab edits depth or twist angle by phase. Options **Flip direction** reverses axis sense. Bakes like Move/Rotate (`Shape_geom_delta`); radius mismatch logs a warning but still places. Remappable as **`mode.cyl_align`**. - **STEP Import as**: the Import dialog replaces the **Union shapes** checkbox with an **Import as** combo (**Preserve hierarchy** default, **Flat solids**, **Union shapes**). Hierarchy keeps XCAF assembly groups and product/instance names; flat adds leaf solids at the document root; union fuses into one solid. diff --git a/docs/usage.md b/docs/usage.md index b46b7c7..25fb9b5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -481,17 +481,18 @@ The shape scale tool allows you to uniformly scale selected shapes around a comp ![Align Cylinders Tool](res/icons/Assembly_Move.png) -Place a shaft into a hole (or the reverse) by aligning two cylindrical faces, then sliding along the shared axis for insert depth. 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, then twisting about that axis for tooth clocking (splines, keyways). This is a one-shot transform that bakes into the solid geometry (same as Move / Rotate), not a persistent assembly mate. **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. | -| **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. | +| **Twist / clocking** | After depth, drag to rotate about the shared axis (mesh splines by eye). | +| **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. | **How to Use:** @@ -499,12 +500,14 @@ Place a shaft into a hole (or the reverse) by aligning two cylindrical faces, th 2. **Pick moving face:** Click a cylindrical face on the body that should move (for example the outer face of a rod). 3. **Pick fixed face:** Click a cylindrical face on a different body (for example the inner face of a hole). 4. **Set depth:** Drag along the axis. Optionally press Tab to type an exact depth. -5. **Finalize or cancel:** left mouse button or Enter to bake; Esc to cancel. +5. **Twist:** left mouse button locks depth and enters twist; drag about the axis to clock teeth. Optionally press Tab to type an exact angle. +6. **Finalize or cancel:** left mouse button or Enter to bake; Esc to cancel. Enter during the depth phase finalizes immediately with no twist. **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. +- 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. ## Feature Operations @@ -737,38 +740,38 @@ Mode, file, and edit chords in the **General Operations** and **Modeling Shortcu | Shift+Tab | Angle input (for line edges with angle constraint) | | Shift+D, Delete, or Backspace | Remove selected elements | | Ctrl+C | Copy selected shapes (or current group subtree) to the in-app clipboard | -| Ctrl+V | Paste clipboard shapes under the current group (same pose; undoable) | +| Ctrl+V | Paste clipboard shapes under the current group (same pose; undoable) | ### Modeling Shortcuts -| | | -| ------------------------------------------------:| ------------------------------ | -| G | Move mode | -| R | Rotate mode | -| S | Scale mode | -| J | Align cylinders 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+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 | +| R | Rotate mode | +| S | Scale mode | +| J | Align cylinders 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+P | Polar duplicate | +| Shift+X | Cross-section | +| Ctrl+Shift+C | Shape cut | +| Ctrl+Shift+F | Shape fuse | +| Ctrl+Shift+M | Shape common | ### View navigation diff --git a/src/doc/gui.md b/src/doc/gui.md index 8cb653b..13274e8 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -46,12 +46,12 @@ Typical responsibilities: 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). -| Step | Touch | -| ---- | -------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | `Gui_action` + default chord in [`gui_hotkeys.h`](../gui_hotkeys.h) / [`gui_hotkeys.cpp`](../gui_hotkeys.cpp) (`c_actions`; enum order) | -| 2 | `GUI::dispatch_hotkey_action_` in [`gui_mode.cpp`](../gui_mode.cpp) | -| 3 | `sync_toolbar_hotkey_tooltips_` in [`gui.cpp`](../gui.cpp) | -| 4 | `gui.hotkeys` entry in [`res/ezycad_settings.json`](../../res/ezycad_settings.json) | +| Step | Touch | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1 | `Gui_action` + default chord in [`gui_hotkeys.h`](../gui_hotkeys.h) / [`gui_hotkeys.cpp`](../gui_hotkeys.cpp) (`c_actions`; enum order) | +| 2 | `GUI::dispatch_hotkey_action_` in [`gui_mode.cpp`](../gui_mode.cpp) | +| 3 | `sync_toolbar_hotkey_tooltips_` in [`gui.cpp`](../gui.cpp) | +| 4 | `gui.hotkeys` entry in [`res/ezycad_settings.json`](../../res/ezycad_settings.json) | | 5 | User docs: [usage.md](../../docs/usage.md#hotkeys) Modeling table; sketch tools also [usage-sketch.md](../../docs/usage-sketch.md#hotkeys); [usage-settings.md](../../docs/usage-settings.md) if labels change; `CHANGELOG.md` | 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. @@ -145,24 +145,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 | `Occt_view::dimension_input`; Move/Rotate: `break` into mode handlers | -| Shift+Tab | not Move/Rotate | `Occt_view::angle_input`; fixed | -| Enter | not Rotate | hide edits, `Occt_view::on_enter`; Rotate: `break` into `on_key_rotate_mode_` (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 | -| Cyl-align keys | `Mode::Shape_cyl_align` | `on_key_cyl_align_mode_` (Tab depth, 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` | `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 | `Occt_view::dimension_input`; Move/Rotate: `break` into mode handlers | +| Shift+Tab | not Move/Rotate | `Occt_view::angle_input`; fixed | +| Enter | not Rotate | hide edits, `Occt_view::on_enter`; Rotate: `break` into `on_key_rotate_mode_` (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 | +| Cyl-align keys | `Mode::Shape_cyl_align` | `on_key_cyl_align_mode_` (Tab depth or twist 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)). @@ -177,7 +177,7 @@ See also [`src/doc/sketch.md`](sketch.md) and [`src/doc/shape.md`](shape.md) for | `Move` | `shp_move().move_selected` | | `Rotate` | `shp_rotate().rotate_selected` | | `Scale` | `shp_scale().scale_selected` | -| `Shape_cyl_align` | `shp_cyl_align().drag_depth` | +| `Shape_cyl_align` | `drag_depth` / `drag_twist` | | `Shape_polar_duplicate` | `shp_polar_dup().move_point` | | Sketch tool modes (line, arc, rect, dim, axis, ...) | `curr_sketch().sketch_pt_move` | | `Sketch_face_extrude` | `sketch_face_extrude(..., true)` | @@ -186,12 +186,12 @@ Always calls `m_view->on_mouse_move(screen_coords)` first. ### Mouse buttons (`GUI::on_mouse_button` + `on_left_click_`) -| Event | Handler | -| --------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| LMB (underlay calib active) | `try_underlay_calib_click_` (early return) | -| LMB | `m_view->on_mouse_button` then `on_left_click_` (skipped when extrude LMB already advanced/finalized the session) | -| RMB press | `finalize_elm` for line / multi-line sketch modes | -| LMB in `on_left_click_` | Mode-specific: transform finalize, cyl-align face pick / finalize, sketch `add_sketch_pt`, fillet/chamfer click, polar dup `add_point`, extrude pick | +| Event | Handler | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| LMB (underlay calib active) | `try_underlay_calib_click_` (early return) | +| LMB | `m_view->on_mouse_button` then `on_left_click_` (skipped when extrude LMB already advanced/finalized the session) | +| RMB press | `finalize_elm` for line / multi-line sketch modes | +| LMB in `on_left_click_` | Mode-specific: transform finalize, cyl-align face pick / depth->twist / finalize, sketch `add_sketch_pt`, fillet/chamfer click, polar dup `add_point`, extrude pick | Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse position. @@ -210,7 +210,7 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `Normal` | `options_normal_mode_` (selection filter, orthographic) | | `Move` / `Rotate` / `Scale` | `options_*_mode_` (constraints, axis, material) | -| `Shape_cyl_align` | Flip direction; short pick/drag help | +| `Shape_cyl_align` | Flip direction; pick / depth / twist 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 | diff --git a/src/doc/shape.md b/src/doc/shape.md index 4d6ff36..f50dfad 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -143,42 +143,42 @@ Protected helpers used by all operation classes: ## Operation modules -| File | Type | Behavior | -| --------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `shp_create.h` | `namespace shp_create` | Pure functions: `create_box`, `create_pyramid`, `create_sphere`, `create_cylinder`, `create_cone`, `create_torus` -> `TopoDS_Shape`. Called from `Occt_view::add_*` helpers. | +| File | Type | Behavior | +| --------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `shp_create.h` | `namespace shp_create` | Pure functions: `create_box`, `create_pyramid`, `create_sphere`, `create_cylinder`, `create_cone`, `create_torus` -> `TopoDS_Shape`. Called from `Occt_view::add_*` helpers. | | `shp_extrude.h` | `Shp_extrude` | Extrude of `Sketch_face_shp`. Optional Options **Twist**: two-phase (lock height, then twist angle about face centroid). Live preview: shaded `MakePrism` when twist ~0; `BRepOffsetAPI_ThruSections` (ruled, `CheckCompatibility(false)`, intermediates every ~45 deg) when twisted; face holes lofted and cut so the bore survives; both-sides + twist uses mid-plane unrotated and ends at +/- half angle. Dense faces can use lite face-copy preview (`gui.extrude_fast_preview`): AIS translate, plus rotate about centroid when Twist is on. `finalize` bakes solid + `try_make_solid`; tmp length dimension; optional both-sides. | -| `shp_fuse.h` | `Shp_fuse` | `selected_fuse()` -- sequential `BRepAlgoAPI_Fuse` on all selected shapes -> one new `Shp`. | -| `shp_cut.h` | `Shp_cut` | `selected_cut()` -- first selected = blank, rest = tools (`BRepAlgoAPI_Cut`). | -| `shp_common.h` | `Shp_common` | `selected_common()` -- sequential `BRepAlgoAPI_Common` (intersection). | -| `shp_move.h` | `Shp_move` | Drag on view plane; axis constraints (`Move_options`); Tab distance entry; finalize bakes translation. | -| `shp_rotate.h` | `Shp_rotate` | Rotate about view axis, global X/Y/Z, or view-to-object; angle Tab entry; optional axis/center AIS guides. | -| `shp_scale.h` | `Shp_scale` | Uniform scale from bbox center vs mouse distance; clamped factor 0.01..100. | -| `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); coaxial `cyl_align_trsf`; drag axial depth; Options flip; bake like Move. | -| `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. | -| `shp_cross_section.h` | `Shp_cross_section` | Shared cutting-plane preview: immediate yellow plane AIS; cyan section wires via async job (desktop `std::async` + per-solid pool; WASM one-solid-per-`poll` chunks); running+latest-pending cancel/coalesce; optional hide-back AIS clip; **Show section outline** (default off) toggles cyan wires without recompute; **Clip** half-space-commons and replaces inputs (fully discarded solids are removed only); **Cross section sketch** imports cached section line/circle edges into a new sketch. | -| `shp_info.h` | `namespace shp_info` | `collect(TopoDS_Shape, Display_meta*)` -> labeled lines for Shape info dialog. | +| `shp_fuse.h` | `Shp_fuse` | `selected_fuse()` -- sequential `BRepAlgoAPI_Fuse` on all selected shapes -> one new `Shp`. | +| `shp_cut.h` | `Shp_cut` | `selected_cut()` -- first selected = blank, rest = tools (`BRepAlgoAPI_Cut`). | +| `shp_common.h` | `Shp_common` | `selected_common()` -- sequential `BRepAlgoAPI_Common` (intersection). | +| `shp_move.h` | `Shp_move` | Drag on view plane; axis constraints (`Move_options`); Tab distance entry; finalize bakes translation. | +| `shp_rotate.h` | `Shp_rotate` | Rotate about view axis, global X/Y/Z, or view-to-object; angle Tab entry; optional axis/center AIS guides. | +| `shp_scale.h` | `Shp_scale` | Uniform scale from bbox center vs mouse distance; clamped factor 0.01..100. | +| `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); coaxial `cyl_align_trsf`; drag axial depth; LMB enters twist about shared axis; Options flip; bake like Move. | +| `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. | +| `shp_cross_section.h` | `Shp_cross_section` | Shared cutting-plane preview: immediate yellow plane AIS; cyan section wires via async job (desktop `std::async` + per-solid pool; WASM one-solid-per-`poll` chunks); running+latest-pending cancel/coalesce; optional hide-back AIS clip; **Show section outline** (default off) toggles cyan wires without recompute; **Clip** half-space-commons and replaces inputs (fully discarded solids are removed only); **Cross section sketch** imports cached section line/circle edges into a new sketch. | +| `shp_info.h` | `namespace shp_info` | `collect(TopoDS_Shape, Display_meta*)` -> labeled lines for Shape info dialog. | ## Input routing (from UI / `Occt_view`) `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_cyl_align` | `shp_cyl_align().drag_depth` | Pick faces, then `finalize` while dragging | `show_depth_edit` / 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::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_cyl_align` | `drag_depth` / `drag_twist` | Pick faces; LMB depth->twist; LMB/Enter finalize | Tab depth or twist angle; Enter finalize (skips twist) | `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()` | -- | -- | 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). @@ -254,11 +254,11 @@ auto lines = shp_info::collect(shp->Shape(), &meta); ## Related code outside `src/shp_*` -| Location | Role | -| ----------------------------------- | -------------------------------------------------------------------------------- | -| `occt_view.h` / `gui_occt_view.cpp` | Shape list, `add_shp_`, primitives, I/O, operation member objects | -| `skt_ais.h` | `Sketch_face_shp` extrusion source | -| `skt_operations.cpp` | `Sketch::revolve_selected` -> `Shp_rslt` | -| `mode.h` | `Fillet_mode`, `Chamfer_mode`, tool modes | -| `gui.h` / `gui.cpp` | Toolbar, Shape List, fillet/chamfer mode, material UI | -| `utl_geom.h` | Plane projection, bbox center, `cylinder_from_face` / `cyl_align_trsf`, rotation helpers | +| Location | Role | +| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `occt_view.h` / `gui_occt_view.cpp` | Shape list, `add_shp_`, primitives, I/O, operation member objects | +| `skt_ais.h` | `Sketch_face_shp` extrusion source | +| `skt_operations.cpp` | `Sketch::revolve_selected` -> `Shp_rslt` | +| `mode.h` | `Fillet_mode`, `Chamfer_mode`, tool modes | +| `gui.h` / `gui.cpp` | Toolbar, Shape List, fillet/chamfer mode, material UI | +| `utl_geom.h` | Plane projection, bbox center, `cylinder_from_face` / `cyl_align_trsf` (coaxial + twist), rotation helpers | diff --git a/src/gui.cpp b/src/gui.cpp index a699670..d60a0a7 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -3849,7 +3849,12 @@ void GUI::on_mouse_pos(const ScreenCoords& screen_coords) break; case Mode::Shape_cyl_align: - if (Status s = m_view->shp_cyl_align().drag_depth(screen_coords); !s.is_ok()) + if (m_view->shp_cyl_align().is_twist_phase()) + { + if (Status s = m_view->shp_cyl_align().drag_twist(screen_coords); !s.is_ok()) + show_message(s.message()); + } + else if (Status s = m_view->shp_cyl_align().drag_depth(screen_coords); !s.is_ok()) show_message(s.message()); break; @@ -3891,7 +3896,7 @@ void GUI::on_left_click_(const ScreenCoords& screen_coords) case Mode::Scale: m_view->shp_scale().finalize(); break; case Mode::Shape_cyl_align: if (m_view->shp_cyl_align().is_dragging()) - m_view->shp_cyl_align().finalize(); + m_view->shp_cyl_align().on_left_click(); else if (Status s = m_view->shp_cyl_align().pick(screen_coords); !s.is_ok()) show_message(s.message()); break; diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 5abf888..4eb59dd 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -304,7 +304,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) case GLFW_KEY_TAB: { - // Move / Rotate / Align cylinders handle Tab in their mode key handlers (distance / angle / depth). + // Move / Rotate / Align cylinders 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_cyl_align) break; @@ -643,7 +643,8 @@ void GUI::options_shape_cyl_align_mode_() ImGui::TextWrapped( "Pick a cylindrical face on the shape to move, then a cylindrical face on the fixed shape. " - "Drag along the shared axis for insert depth. First pick moves; pick the hole first to move the hole onto the shaft."); + "Drag insert depth, LMB to twist about the shared axis (tooth clocking), then LMB or Enter to confirm. " + "Enter during depth skips twist. First pick moves; pick the hole first to move the hole onto the shaft."); Cyl_align_options& opts = m_view->shp_cyl_align().get_opts(); if (ImGui::Checkbox("Flip direction", &opts.flip_direction)) @@ -1428,12 +1429,16 @@ void GUI::on_key_cyl_align_mode_(int key) case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: + // Enter finalizes immediately (skips twist if still in depth phase). if (m_view->shp_cyl_align().is_dragging()) m_view->shp_cyl_align().finalize(); break; case GLFW_KEY_TAB: - m_view->shp_cyl_align().show_depth_edit(screen_coords); + if (m_view->shp_cyl_align().is_twist_phase()) + m_view->shp_cyl_align().show_twist_edit(screen_coords); + else + m_view->shp_cyl_align().show_depth_edit(screen_coords); break; default: diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index a5a1ac9..1323c0b 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -1,6 +1,8 @@ #include "shp_cyl_align.h" #include +#include +#include #include "gui.h" #include "gui_occt_view.h" @@ -17,10 +19,15 @@ Shp_cyl_align::Shp_cyl_align(Occt_view& view) void Shp_cyl_align::begin() { clear_all(m_phase, m_opts, m_axial_offset, m_moving_radius, m_fixed_radius, m_depth_override, m_moving_shp, m_fixed_shp, - m_moving_axis, m_fixed_axis, m_drag_pln, m_shps); + m_moving_axis, m_fixed_axis, m_drag_pln, m_twist_angle, m_twist_override, m_twist_angle0, m_twist_pln, m_shps); } -bool Shp_cyl_align::is_dragging() const { return m_phase == Phase::Drag_depth && !m_shps.empty(); } +bool Shp_cyl_align::is_dragging() const +{ + return (m_phase == Phase::Drag_depth || m_phase == Phase::Drag_twist) && !m_shps.empty(); +} + +bool Shp_cyl_align::is_twist_phase() const { return m_phase == Phase::Drag_twist && !m_shps.empty(); } Cyl_align_options& Shp_cyl_align::get_opts() { return m_opts; } @@ -32,7 +39,7 @@ void Shp_cyl_align::apply_preview() Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) { - if (m_phase == Phase::Drag_depth) + if (is_dragging()) return Status::ok(); Shp_ptr shp = Shp_ptr::DownCast(get_shape_(screen_coords)); @@ -70,7 +77,7 @@ Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) std::to_string(m_fixed_radius) + "); placement still allowed."); enter_drag_(); - gui().show_message("Drag along the axis for insert depth, then LMB or Enter to confirm."); + gui().show_message("Drag along the axis for insert depth, then LMB to twist (or Enter to finish)."); return Status::ok(); } @@ -79,20 +86,43 @@ void Shp_cyl_align::enter_drag_() EZY_ASSERT(!m_moving_shp.IsNull()); EZY_ASSERT(m_moving_axis.has_value() && m_fixed_axis.has_value()); - clear_all(m_axial_offset, m_depth_override, m_drag_pln); + clear_all(m_axial_offset, m_depth_override, m_drag_pln, m_twist_angle, m_twist_override, m_twist_angle0, m_twist_pln); set_operation_shps_({m_moving_shp}); m_phase = Phase::Drag_depth; view().set_dynamic_highlight_enabled(false); apply_preview_(); } +void Shp_cyl_align::enter_twist_() +{ + EZY_ASSERT(m_phase == Phase::Drag_depth); + EZY_ASSERT(m_fixed_axis.has_value() && m_moving_axis.has_value()); + + // Commit any typed depth override into the live offset, then clear overrides. + if (m_depth_override.has_value()) + m_axial_offset = *m_depth_override; + + clear_all(m_depth_override, m_twist_angle, m_twist_override, m_twist_angle0, m_twist_pln); + + const gp_Dir& fixed_dir = m_fixed_axis->Direction(); + const gp_Vec to_moving(m_fixed_axis->Location(), m_moving_axis->Location()); + const double param0 = to_moving.Dot(gp_Vec(fixed_dir)); + const gp_Pnt seed = m_fixed_axis->Location().Translated(gp_Vec(fixed_dir) * (param0 + m_axial_offset)); + m_twist_pln = gp_Pln(seed, fixed_dir); + + m_phase = Phase::Drag_twist; + gui().show_message("Drag to twist about the axis for tooth clocking, then LMB or Enter to confirm."); + apply_preview_(); +} + void Shp_cyl_align::apply_preview_() { EZY_ASSERT(m_moving_axis.has_value() && m_fixed_axis.has_value()); EZY_ASSERT(!m_shps.empty()); const double offset = m_depth_override.value_or(m_axial_offset); - const gp_Trsf trsf = cyl_align_trsf(*m_moving_axis, *m_fixed_axis, m_opts.flip_direction, offset); + const double twist = m_twist_override.value_or(m_twist_angle); + 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); @@ -102,7 +132,7 @@ void Shp_cyl_align::apply_preview_() Status Shp_cyl_align::drag_depth(const ScreenCoords& screen_coords) { - if (!is_dragging()) + if (m_phase != Phase::Drag_depth) return Status::ok(); EZY_ASSERT(m_fixed_axis.has_value() && m_moving_axis.has_value()); @@ -132,9 +162,52 @@ Status Shp_cyl_align::drag_depth(const ScreenCoords& screen_coords) return Status::ok(); } +Status Shp_cyl_align::drag_twist(const ScreenCoords& screen_coords) +{ + if (m_phase != Phase::Drag_twist) + return Status::ok(); + + EZY_ASSERT(m_fixed_axis.has_value()); + EZY_ASSERT(m_twist_pln.has_value()); + + if (m_twist_override.has_value()) + { + apply_preview_(); + return Status::ok(); + } + + const std::optional mouse_wc = view().pt3d_on_plane(screen_coords, *m_twist_pln); + if (!mouse_wc) + return Status::user_error("Adjust view, cannot get point on plane."); + + const gp_Ax2 ax2(m_twist_pln->Location(), m_fixed_axis->Direction()); + const gp_Vec v(m_twist_pln->Location(), *mouse_wc); + const double vx = v.Dot(gp_Vec(ax2.XDirection())); + const double vy = v.Dot(gp_Vec(ax2.YDirection())); + if (std::hypot(vx, vy) <= Precision::Confusion()) + return Status::ok(); + + const double ang = std::atan2(vy, vx); + if (!m_twist_angle0.has_value()) + m_twist_angle0 = ang; + + m_twist_angle = ang - *m_twist_angle0; + apply_preview_(); + + return Status::ok(); +} + +void Shp_cyl_align::on_left_click() +{ + if (m_phase == Phase::Drag_depth) + enter_twist_(); + else if (m_phase == Phase::Drag_twist) + finalize(); +} + void Shp_cyl_align::show_depth_edit(const ScreenCoords& screen_coords) { - if (!is_dragging()) + if (m_phase != Phase::Drag_depth) return; auto depth_edit = [&, screen_coords](float new_dist, bool is_final) @@ -142,7 +215,7 @@ void Shp_cyl_align::show_depth_edit(const ScreenCoords& screen_coords) m_depth_override = new_dist * view().get_display_to_model_scale(); EZY_ASSERT(drag_depth(screen_coords).is_ok()); if (is_final) - finalize(); + enter_twist_(); }; const double cur = m_depth_override.value_or(m_axial_offset); @@ -150,11 +223,37 @@ void Shp_cyl_align::show_depth_edit(const ScreenCoords& screen_coords) std::move(std::function(depth_edit))); } +void Shp_cyl_align::show_twist_edit(const ScreenCoords& screen_coords) +{ + if (m_phase != Phase::Drag_twist) + return; + + auto twist_edit = [&, screen_coords](float new_angle_deg, bool is_final) + { + m_twist_override = to_radians(static_cast(new_angle_deg)); + m_twist_angle = *m_twist_override; + EZY_ASSERT(drag_twist(screen_coords).is_ok()); + if (is_final) + finalize(); + }; + + const double cur = m_twist_override.value_or(m_twist_angle); + gui().set_angle_edit(float(to_degrees(cur)), std::move(std::function(twist_edit)), screen_coords); +} + void Shp_cyl_align::finalize() { if (!is_dragging()) return; + // Commit typed overrides before bake. + if (m_depth_override.has_value()) + m_axial_offset = *m_depth_override; + if (m_twist_override.has_value()) + m_twist_angle = *m_twist_override; + clear_all(m_depth_override, m_twist_override); + apply_preview_(); + std::vector changes; changes.reserve(m_shps.size()); for (const Shp_ptr& shape : m_shps) diff --git a/src/shp_cyl_align.h b/src/shp_cyl_align.h index 003aa26..ec25d88 100644 --- a/src/shp_cyl_align.h +++ b/src/shp_cyl_align.h @@ -11,18 +11,24 @@ struct Cyl_align_options bool flip_direction{false}; }; -/// One-shot cylindrical insert align: pick moving face, pick fixed face, drag depth, bake. +/// One-shot cylindrical insert align: pick moving face, pick fixed face, drag depth, +/// twist about the shared axis, bake. class Shp_cyl_align : private Shp_operation_base { public: Shp_cyl_align(Occt_view& view); void begin(); - /// True while depth-drag preview is active (LMB finalizes). + /// True while depth or twist preview is active (LMB advances / finalizes). [[nodiscard]] bool is_dragging() const; + [[nodiscard]] bool is_twist_phase() const; [[nodiscard]] Status pick(const ScreenCoords& screen_coords); [[nodiscard]] Status drag_depth(const ScreenCoords& screen_coords); + [[nodiscard]] Status drag_twist(const ScreenCoords& screen_coords); + /// Depth phase: LMB locks depth and enters twist. Twist phase: finalize. + void on_left_click(); void show_depth_edit(const ScreenCoords& screen_coords); + void show_twist_edit(const ScreenCoords& screen_coords); void finalize(); void cancel(); void reset(); @@ -36,11 +42,13 @@ class Shp_cyl_align : private Shp_operation_base { Pick_moving, Pick_fixed, - Drag_depth + Drag_depth, + Drag_twist }; void apply_preview_(); void enter_drag_(); + void enter_twist_(); Phase m_phase{Phase::Pick_moving}; Cyl_align_options m_opts; @@ -53,4 +61,8 @@ class Shp_cyl_align : private Shp_operation_base double m_axial_offset{0}; std::optional m_depth_override; std::optional m_drag_pln; + double m_twist_angle{0}; // radians about fixed axis after coaxial + std::optional m_twist_override; + std::optional m_twist_angle0; // first-drag reference for relative twist + std::optional m_twist_pln; }; diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index e8cf2b5..b5d63fa 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -474,7 +474,11 @@ std::optional cylinder_from_face(const TopoDS_Face& face) return info; } -gp_Trsf cyl_align_trsf(const gp_Ax1& moving_axis, const gp_Ax1& fixed_axis, bool flip, double axial_offset) +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(); @@ -504,7 +508,14 @@ gp_Trsf cyl_align_trsf(const gp_Ax1& moving_axis, const gp_Ax1& fixed_axis, bool gp_Trsf trans; trans.SetTranslation(gp_Vec(moved_loc, target)); - return trans * rot; + const gp_Trsf coaxial = trans * rot; + if (std::fabs(twist_rad) <= Precision::Angular()) + return coaxial; + + // Twist about the fixed axis after coaxial placement (keeps axes coincident). + gp_Trsf twist; + twist.SetRotation(fixed_axis, twist_rad); + return twist * coaxial; } bool planes_equal(const gp_Pln& plane1, const gp_Pln& plane2) diff --git a/src/utl_geom.h b/src/utl_geom.h index 152eca7..29b7170 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -106,8 +106,13 @@ 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). -gp_Trsf cyl_align_trsf(const gp_Ax1& moving_axis, const gp_Ax1& fixed_axis, bool flip, double axial_offset); +/// moving direction sense (180 deg about a perpendicular). \a twist_rad is an +/// extra rotation about the fixed axis after coaxial placement (tooth clocking). +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); bool planes_equal(const gp_Pln& plane1, const gp_Pln& plane2); From 1e636fdb2733e1cd131d5ea06d71561fb97fb53d Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 4 Aug 2026 18:08:00 -0600 Subject: [PATCH 2/5] Improve --- CHANGELOG.md | 2 +- docs/usage.md | 22 ++++++++-------- src/doc/gui.md | 6 ++--- src/doc/shape.md | 20 +++++++-------- src/gui.h | 2 +- src/gui_mode.cpp | 18 ++++++++----- src/shp_cyl_align.cpp | 59 +++++++++++++++++++++++++++++++++++++++---- src/shp_cyl_align.h | 13 +++++++--- src/utl_geom.cpp | 2 +- src/utl_geom.h | 2 +- 10 files changed, 104 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9cc0c..851127d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Shape copy / paste** (Ctrl+C / Ctrl+V): in-app clipboard for selected solids, or a Shape List group subtree when the selection matches that group's descendant solids. Paste deep-copies under the current group (new ids, uniquified names, same pose) with one undo step. Survives **New** so you can copy, start a blank project, and paste. Remappable as **`edit.copy`** / **`edit.paste`**. Does not use the OS clipboard; sketch geometry is not copied. -- **Align cylinders** (J): pick two cylindrical faces (first moves, second is fixed), coaxially align, drag axial insert depth, then twist about the shared axis for tooth clocking (or Enter during depth to skip twist). Tab edits depth or twist angle by phase. Options **Flip direction** reverses axis sense. Bakes like Move/Rotate (`Shape_geom_delta`); radius mismatch logs a warning but still places. Remappable as **`mode.cyl_align`**. +- **Align cylinders** (J): pick two cylindrical faces (first moves, second is fixed), coaxially align, drag axial insert depth; with Options **Clock rotation** (default off), LMB or Shift+Tab rotates about the shared axis for tooth clocking (Enter during depth skips clocking). Tab edits depth or angle by phase. Options **Flip direction** reverses axis sense. Bakes like Move/Rotate (`Shape_geom_delta`); radius mismatch logs a warning but still places. Remappable as **`mode.cyl_align`**. - **STEP Import as**: the Import dialog replaces the **Union shapes** checkbox with an **Import as** combo (**Preserve hierarchy** default, **Flat solids**, **Union shapes**). Hierarchy keeps XCAF assembly groups and product/instance names; flat adds leaf solids at the document root; union fuses into one solid. diff --git a/docs/usage.md b/docs/usage.md index 25fb9b5..df222a2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -481,18 +481,18 @@ The shape scale tool allows you to uniformly scale selected shapes around a comp ![Align Cylinders 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, then twisting about that axis for tooth clocking (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). This is a one-shot transform that bakes into the solid geometry (same as Move / Rotate), not a persistent assembly mate. **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. | -| **Twist / clocking** | After depth, drag to rotate about the shared axis (mesh splines by eye). | -| **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. | +| **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. | **How to Use:** @@ -500,8 +500,8 @@ Place a shaft into a hole (or the reverse) by aligning two cylindrical faces, sl 2. **Pick moving face:** Click a cylindrical face on the body that should move (for example the outer face of a rod). 3. **Pick fixed face:** Click a cylindrical face on a different body (for example the inner face of a hole). 4. **Set depth:** Drag along the axis. Optionally press Tab to type an exact depth. -5. **Twist:** left mouse button locks depth and enters twist; drag about the axis to clock teeth. Optionally press Tab to type an exact angle. -6. **Finalize or cancel:** left mouse button or Enter to bake; Esc to cancel. Enter during the depth phase finalizes immediately with no twist. +5. **Clock rotation (optional):** Enable Options **Clock rotation**. Then left mouse button or Shift+Tab locks depth and enters clocking; drag about the axis (or type an angle). With **Clock rotation** off, LMB finalizes after depth. +6. **Finalize or cancel:** left mouse button or Enter to bake; Esc to cancel. Enter during the depth phase finalizes immediately without clocking. **Tips:** diff --git a/src/doc/gui.md b/src/doc/gui.md index 13274e8..f5c41a0 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -162,7 +162,7 @@ Remappable chords live in `Gui_hotkeys` (`gui_hotkeys.h` / `.cpp`), owned by `GU | 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 | -| Cyl-align keys | `Mode::Shape_cyl_align` | `on_key_cyl_align_mode_` (Tab depth or twist angle, Enter finalize); hardcoded | +| Cyl-align keys | `Mode::Shape_cyl_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)). @@ -191,7 +191,7 @@ Always calls `m_view->on_mouse_move(screen_coords)` first. | LMB (underlay calib active) | `try_underlay_calib_click_` (early return) | | LMB | `m_view->on_mouse_button` then `on_left_click_` (skipped when extrude LMB already advanced/finalized the session) | | RMB press | `finalize_elm` for line / multi-line sketch modes | -| LMB in `on_left_click_` | Mode-specific: transform finalize, cyl-align face pick / depth->twist / finalize, sketch `add_sketch_pt`, fillet/chamfer click, polar dup `add_point`, extrude pick | +| LMB in `on_left_click_` | Mode-specific: transform finalize, cyl-align face pick / depth->clock / finalize, sketch `add_sketch_pt`, fillet/chamfer click, polar dup `add_point`, extrude pick | Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse position. @@ -210,7 +210,7 @@ Tests use `sketch_left_click` to simulate sketch LMB without ImGui mouse positio | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `Normal` | `options_normal_mode_` (selection filter, orthographic) | | `Move` / `Rotate` / `Scale` | `options_*_mode_` (constraints, axis, material) | -| `Shape_cyl_align` | Flip direction; pick / depth / twist help | +| `Shape_cyl_align` | 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 | diff --git a/src/doc/shape.md b/src/doc/shape.md index f50dfad..a565a06 100644 --- a/src/doc/shape.md +++ b/src/doc/shape.md @@ -153,7 +153,7 @@ Protected helpers used by all operation classes: | `shp_move.h` | `Shp_move` | Drag on view plane; axis constraints (`Move_options`); Tab distance entry; finalize bakes translation. | | `shp_rotate.h` | `Shp_rotate` | Rotate about view axis, global X/Y/Z, or view-to-object; angle Tab entry; optional axis/center AIS guides. | | `shp_scale.h` | `Shp_scale` | Uniform scale from bbox center vs mouse distance; clamped factor 0.01..100. | -| `shp_cyl_align.h` | `Shp_cyl_align` | Pick two cylindrical faces (first moves); coaxial `cyl_align_trsf`; drag axial depth; LMB enters twist about shared axis; Options flip; bake like Move. | +| `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_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. | @@ -169,7 +169,7 @@ Protected helpers used by all operation classes: | `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_cyl_align` | `drag_depth` / `drag_twist` | Pick faces; LMB depth->twist; LMB/Enter finalize | Tab depth or twist angle; Enter finalize (skips twist) | `shp_cyl_align().cancel` -> `Normal` | +| `Mode::Shape_cyl_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)` | -- | -- | @@ -254,11 +254,11 @@ auto lines = shp_info::collect(shp->Shape(), &meta); ## Related code outside `src/shp_*` -| Location | Role | -| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| `occt_view.h` / `gui_occt_view.cpp` | Shape list, `add_shp_`, primitives, I/O, operation member objects | -| `skt_ais.h` | `Sketch_face_shp` extrusion source | -| `skt_operations.cpp` | `Sketch::revolve_selected` -> `Shp_rslt` | -| `mode.h` | `Fillet_mode`, `Chamfer_mode`, tool modes | -| `gui.h` / `gui.cpp` | Toolbar, Shape List, fillet/chamfer mode, material UI | -| `utl_geom.h` | Plane projection, bbox center, `cylinder_from_face` / `cyl_align_trsf` (coaxial + twist), rotation helpers | +| Location | Role | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `occt_view.h` / `gui_occt_view.cpp` | Shape list, `add_shp_`, primitives, I/O, operation member objects | +| `skt_ais.h` | `Sketch_face_shp` extrusion source | +| `skt_operations.cpp` | `Sketch::revolve_selected` -> `Shp_rslt` | +| `mode.h` | `Fillet_mode`, `Chamfer_mode`, tool modes | +| `gui.h` / `gui.cpp` | Toolbar, Shape List, fillet/chamfer mode, material UI | +| `utl_geom.h` | Plane projection, bbox center, `cylinder_from_face` / `cyl_align_trsf` (coaxial + clock rotation), rotation helpers | diff --git a/src/gui.h b/src/gui.h index 26329c8..4bad3f6 100644 --- a/src/gui.h +++ b/src/gui.h @@ -488,7 +488,7 @@ class GUI void on_key_move_mode_(int key); void on_key_rotate_mode_(int key); - void on_key_cyl_align_mode_(int key); + void on_key_cyl_align_mode_(int key, int mods); void dispatch_hotkey_action_(Gui_action action); void sync_toolbar_hotkey_tooltips_(); bool try_capture_hotkey_press_(int key, int mods); diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index 4eb59dd..c432f33 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -362,7 +362,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) break; case Mode::Shape_cyl_align: - on_key_cyl_align_mode_(key); + on_key_cyl_align_mode_(key, mods); break; default: @@ -643,13 +643,17 @@ void GUI::options_shape_cyl_align_mode_() ImGui::TextWrapped( "Pick a cylindrical face on the shape to move, then a cylindrical face on the fixed shape. " - "Drag insert depth, LMB to twist about the shared axis (tooth clocking), then LMB or Enter to confirm. " - "Enter during depth skips twist. First pick moves; pick the hole first to move the hole onto the shaft."); + "Drag insert depth. With Clock rotation on, LMB or Shift+Tab rotates about the shared axis; Enter finalizes. " + "First pick moves; pick the hole first to move the hole onto the shaft."); Cyl_align_options& opts = m_view->shp_cyl_align().get_opts(); if (ImGui::Checkbox("Flip direction", &opts.flip_direction)) m_view->shp_cyl_align().apply_preview(); + 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::Separator(); options_orthographic_projection_(); } @@ -1417,7 +1421,7 @@ float GUI::options_sketch_label_col_w_() const return sketch_label_col_w; } -void GUI::on_key_cyl_align_mode_(int key) +void GUI::on_key_cyl_align_mode_(int key, int mods) { const ScreenCoords screen_coords = cursor_screen_coords(); @@ -1429,13 +1433,15 @@ void GUI::on_key_cyl_align_mode_(int key) case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: - // Enter finalizes immediately (skips twist if still in depth phase). + // Enter finalizes immediately (skips clock rotation if still in depth phase). if (m_view->shp_cyl_align().is_dragging()) m_view->shp_cyl_align().finalize(); break; case GLFW_KEY_TAB: - if (m_view->shp_cyl_align().is_twist_phase()) + if ((mods & GLFW_MOD_SHIFT) != 0) + m_view->shp_cyl_align().begin_twist_input(screen_coords); + else if (m_view->shp_cyl_align().is_twist_phase()) m_view->shp_cyl_align().show_twist_edit(screen_coords); else m_view->shp_cyl_align().show_depth_edit(screen_coords); diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index 1323c0b..3bca8db 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -18,7 +18,8 @@ Shp_cyl_align::Shp_cyl_align(Occt_view& view) void Shp_cyl_align::begin() { - clear_all(m_phase, m_opts, m_axial_offset, m_moving_radius, m_fixed_radius, m_depth_override, m_moving_shp, m_fixed_shp, + // Keep m_opts (Flip / Clock rotation) across sessions, like Extrude Twist. + clear_all(m_phase, m_axial_offset, m_moving_radius, m_fixed_radius, m_depth_override, m_moving_shp, m_fixed_shp, m_moving_axis, m_fixed_axis, m_drag_pln, m_twist_angle, m_twist_override, m_twist_angle0, m_twist_pln, m_shps); } @@ -31,6 +32,16 @@ bool Shp_cyl_align::is_twist_phase() const { return m_phase == Phase::Drag_twist Cyl_align_options& Shp_cyl_align::get_opts() { return m_opts; } +void Shp_cyl_align::set_clock_rotation_enabled(bool enabled) +{ + if (m_opts.clock_rotation == enabled) + return; + + m_opts.clock_rotation = enabled; + if (!enabled && m_phase == Phase::Drag_twist) + exit_twist_to_depth_(); +} + void Shp_cyl_align::apply_preview() { if (is_dragging()) @@ -77,7 +88,11 @@ Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) std::to_string(m_fixed_radius) + "); placement still allowed."); enter_drag_(); - gui().show_message("Drag along the axis for insert depth, then LMB to twist (or Enter to finish)."); + if (m_opts.clock_rotation) + gui().show_message( + "Drag along the axis for insert depth, then LMB or Shift+Tab for clock rotation (or Enter to finish)."); + else + gui().show_message("Drag along the axis for insert depth, then LMB or Enter to confirm."); return Status::ok(); } @@ -96,6 +111,7 @@ void Shp_cyl_align::enter_drag_() void Shp_cyl_align::enter_twist_() { EZY_ASSERT(m_phase == Phase::Drag_depth); + EZY_ASSERT(m_opts.clock_rotation); EZY_ASSERT(m_fixed_axis.has_value() && m_moving_axis.has_value()); // Commit any typed depth override into the live offset, then clear overrides. @@ -103,6 +119,7 @@ void Shp_cyl_align::enter_twist_() m_axial_offset = *m_depth_override; clear_all(m_depth_override, m_twist_angle, m_twist_override, m_twist_angle0, m_twist_pln); + gui().hide_dist_edit(false); const gp_Dir& fixed_dir = m_fixed_axis->Direction(); const gp_Vec to_moving(m_fixed_axis->Location(), m_moving_axis->Location()); @@ -111,10 +128,20 @@ void Shp_cyl_align::enter_twist_() m_twist_pln = gp_Pln(seed, fixed_dir); m_phase = Phase::Drag_twist; - gui().show_message("Drag to twist about the axis for tooth clocking, then LMB or Enter to confirm."); + gui().show_message("Drag to clock about the axis, then LMB or Enter to confirm."); apply_preview_(); } +void Shp_cyl_align::exit_twist_to_depth_() +{ + EZY_ASSERT(m_phase == Phase::Drag_twist); + clear_all(m_twist_angle, m_twist_override, m_twist_angle0, m_twist_pln); + gui().hide_angle_edit(false); + m_phase = Phase::Drag_depth; + apply_preview_(); + gui().show_message("Drag along the axis for insert depth, then LMB or Enter to confirm."); +} + void Shp_cyl_align::apply_preview_() { EZY_ASSERT(m_moving_axis.has_value() && m_fixed_axis.has_value()); @@ -200,11 +227,28 @@ Status Shp_cyl_align::drag_twist(const ScreenCoords& screen_coords) void Shp_cyl_align::on_left_click() { if (m_phase == Phase::Drag_depth) - enter_twist_(); + { + if (m_opts.clock_rotation) + enter_twist_(); + else + finalize(); + } else if (m_phase == Phase::Drag_twist) finalize(); } +void Shp_cyl_align::begin_twist_input(const ScreenCoords& screen_coords) +{ + if (!m_opts.clock_rotation || !is_dragging()) + return; + + if (m_phase == Phase::Drag_depth) + enter_twist_(); + + if (m_phase == Phase::Drag_twist) + show_twist_edit(screen_coords); +} + void Shp_cyl_align::show_depth_edit(const ScreenCoords& screen_coords) { if (m_phase != Phase::Drag_depth) @@ -215,7 +259,12 @@ void Shp_cyl_align::show_depth_edit(const ScreenCoords& screen_coords) m_depth_override = new_dist * view().get_display_to_model_scale(); EZY_ASSERT(drag_depth(screen_coords).is_ok()); if (is_final) - enter_twist_(); + { + if (m_opts.clock_rotation) + enter_twist_(); + else + finalize(); + } }; const double cur = m_depth_override.value_or(m_axial_offset); diff --git a/src/shp_cyl_align.h b/src/shp_cyl_align.h index ec25d88..6b38d32 100644 --- a/src/shp_cyl_align.h +++ b/src/shp_cyl_align.h @@ -9,24 +9,28 @@ struct Cyl_align_options { bool flip_direction{false}; + /// When true, LMB after depth enters clock rotation; when false, LMB finalizes. + bool clock_rotation{false}; }; /// One-shot cylindrical insert align: pick moving face, pick fixed face, drag depth, -/// twist about the shared axis, bake. +/// optional clock rotation about the shared axis, bake. class Shp_cyl_align : private Shp_operation_base { public: Shp_cyl_align(Occt_view& view); void begin(); - /// True while depth or twist preview is active (LMB advances / finalizes). + /// True while depth or clock-rotation preview is active (LMB advances / finalizes). [[nodiscard]] bool is_dragging() const; [[nodiscard]] bool is_twist_phase() const; [[nodiscard]] Status pick(const ScreenCoords& screen_coords); [[nodiscard]] Status drag_depth(const ScreenCoords& screen_coords); [[nodiscard]] Status drag_twist(const ScreenCoords& screen_coords); - /// Depth phase: LMB locks depth and enters twist. Twist phase: finalize. + /// Depth + Clock rotation on: enter clock phase. Depth + off: finalize. Clock phase: finalize. void on_left_click(); + /// Depth -> clock rotation (requires Options Clock rotation), then open angle edit. + void begin_twist_input(const ScreenCoords& screen_coords); void show_depth_edit(const ScreenCoords& screen_coords); void show_twist_edit(const ScreenCoords& screen_coords); void finalize(); @@ -34,6 +38,8 @@ class Shp_cyl_align : private Shp_operation_base void reset(); Cyl_align_options& get_opts(); + /// Apply Options Clock rotation toggle (exits clock phase if turned off). + void set_clock_rotation_enabled(bool enabled); /// Re-apply preview after Options flip toggle (no-op unless dragging). void apply_preview(); @@ -49,6 +55,7 @@ class Shp_cyl_align : private Shp_operation_base void apply_preview_(); void enter_drag_(); void enter_twist_(); + void exit_twist_to_depth_(); Phase m_phase{Phase::Pick_moving}; Cyl_align_options m_opts; diff --git a/src/utl_geom.cpp b/src/utl_geom.cpp index b5d63fa..538de95 100644 --- a/src/utl_geom.cpp +++ b/src/utl_geom.cpp @@ -512,7 +512,7 @@ gp_Trsf cyl_align_trsf(const gp_Ax1& moving_axis, if (std::fabs(twist_rad) <= Precision::Angular()) return coaxial; - // Twist about the fixed axis after coaxial placement (keeps axes coincident). + // Twist about the fixed axis after coaxial placement (clock rotation; keeps axes coincident). gp_Trsf twist; twist.SetRotation(fixed_axis, twist_rad); return twist * coaxial; diff --git a/src/utl_geom.h b/src/utl_geom.h index 29b7170..76c80d0 100644 --- a/src/utl_geom.h +++ b/src/utl_geom.h @@ -107,7 +107,7 @@ std::optional cylinder_from_face(const TopoDS_Face& face); /// 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 (tooth clocking). +/// 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, From 0c52123258eece4034a2a1c1876c410a0eac1955 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 4 Aug 2026 18:23:20 -0600 Subject: [PATCH 3/5] Rename --- CHANGELOG.md | 2 +- agents/plans/assembly-inspection-mode.md | 2 +- docs/usage.md | 12 ++++++------ src/doc/gui.md | 2 +- src/gui.cpp | 4 ++-- src/gui_hotkeys.cpp | 2 +- src/gui_mode.cpp | 6 +++--- src/shp_cyl_align.cpp | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 851127d..8e18795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Shape copy / paste** (Ctrl+C / Ctrl+V): in-app clipboard for selected solids, or a Shape List group subtree when the selection matches that group's descendant solids. Paste deep-copies under the current group (new ids, uniquified names, same pose) with one undo step. Survives **New** so you can copy, start a blank project, and paste. Remappable as **`edit.copy`** / **`edit.paste`**. Does not use the OS clipboard; sketch geometry is not copied. -- **Align cylinders** (J): pick two cylindrical faces (first moves, second is fixed), coaxially align, drag axial insert depth; with Options **Clock rotation** (default off), LMB or Shift+Tab rotates about the shared axis for tooth clocking (Enter during depth skips clocking). Tab edits depth or angle by phase. Options **Flip direction** reverses axis sense. Bakes like Move/Rotate (`Shape_geom_delta`); radius mismatch logs a warning but still places. Remappable as **`mode.cyl_align`**. +- **Align shafts** (J): pick two cylindrical faces (first moves, second is fixed), coaxially align, drag axial insert depth; with Options **Clock rotation** (default off), LMB or Shift+Tab rotates about the shared axis for tooth clocking (Enter during depth skips clocking). Tab edits depth or angle by phase. Options **Flip direction** reverses axis sense. Bakes like Move/Rotate (`Shape_geom_delta`); radius mismatch logs a warning but still places. Remappable as **`mode.cyl_align`**. - **STEP Import as**: the Import dialog replaces the **Union shapes** checkbox with an **Import as** combo (**Preserve hierarchy** default, **Flat solids**, **Union shapes**). Hierarchy keeps XCAF assembly groups and product/instance names; flat adds leaf solids at the document root; union fuses into one solid. diff --git a/agents/plans/assembly-inspection-mode.md b/agents/plans/assembly-inspection-mode.md index 3b4c611..8090932 100644 --- a/agents/plans/assembly-inspection-mode.md +++ b/agents/plans/assembly-inspection-mode.md @@ -53,7 +53,7 @@ flowchart TB Esc ladder (see [docs/usage.md](../../docs/usage.md)): sketch tool → sketch inspection → Normal. -**Not first-class yet:** Parts, instances, mates, inherited parent transforms. Shape List **groups** are organizational (STEP XCAF hierarchy on import). Move / Rotate / Scale / Align cylinders **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. +**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. Planned model: [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) (#214) — relative placement, typed nodes (Body, Group, Part, …), non-baking transforms. diff --git a/docs/usage.md b/docs/usage.md index df222a2..37ceff2 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -233,7 +233,7 @@ The typical modeling workflow in EzyCad follows these steps: 3. **Create a Sketch from a Planar Face**: Once you have 3D shapes, use [Create sketch from planar face](usage-sketch.md#create-sketch-from-planar-face-tool) to pick a flat face on a solid. EzyCad extracts the face boundary into a new sketch aligned with that face (Origin at the face bounding-box center). Edit the sketch, then extrude again to add or cut features on the existing model. -4. **Modify 3D Shapes**: Use [3D Modeling tools](#3d-modeling) to transform shapes ([move](#shape-move-tool-g), [rotate](#shape-rotate-tool-r), [scale](#shape-scale-tool-s), [align cylinders](#align-cylinders-tool-j)) or create patterns ([polar duplicate](#shape-polar-duplicate-tool)). +4. **Modify 3D Shapes**: Use [3D Modeling tools](#3d-modeling) to transform shapes ([move](#shape-move-tool-g), [rotate](#shape-rotate-tool-r), [scale](#shape-scale-tool-s), [align shafts](#align-shafts-tool-j)) or create patterns ([polar duplicate](#shape-polar-duplicate-tool)). 5. **Apply Feature Operations**: Use [boolean operations](#boolean-operations) (cut, fuse, common) or edge-based feature operations (chamfer with C, fillet with F) to refine your 3D model. @@ -367,7 +367,7 @@ More detail: [Sketch snapping](usage-sketch.md#sketch-snapping) in the sketch gu - ![Shape Move Tool](res/icons/Assembly_AxialMove.png) [Move shapes (G)](#shape-move-tool-g) - ![Shape Rotate Tool](res/icons/Draft_Rotate.png) [Rotate objects (R)](#shape-rotate-tool-r) - ![Shape Scale Tool](res/icons/Part_Scale.png) [Scale elements (S)](#shape-scale-tool) - - ![Align Cylinders Tool](res/icons/Assembly_Move.png) [Align cylinders (J)](#align-cylinders-tool-j) + - ![Align Shafts Tool](res/icons/Assembly_Move.png) [Align shafts (J)](#align-shafts-tool-j) - ![Polar Duplicate Tool](res/icons/Draft_PolarArray.png) [Polar duplicate](#shape-polar-duplicate-tool) - ![Cross-section Tool](res/icons/Curves_ExtractSubshape.png) [Preview a local-plane cross-section](#shape-cross-section-tool) @@ -477,9 +477,9 @@ The shape scale tool allows you to uniformly scale selected shapes around a comp - Scale works best when the view direction makes the movement relative to the center easy to see (avoid looking exactly edge-on at the objects). - Because the operation is undoable, you can experiment with different scale amounts and step back with Ctrl+Z if needed. -#### Align Cylinders Tool (J) +#### Align Shafts Tool (J) -![Align Cylinders Tool](res/icons/Assembly_Move.png) +![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. @@ -749,7 +749,7 @@ Mode, file, and edit chords in the **General Operations** and **Modeling Shortcu | G | Move mode | | R | Rotate mode | | S | Scale mode | -| J | Align cylinders mode | +| J | Align shafts mode | | E | Extrude mode | | C | Chamfer mode | | F | Fillet mode | @@ -904,7 +904,7 @@ Contributors should follow **[ezycad_code_style.md](ezycad_code_style.md)** for - ![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 cylinders (J) +- ![Assembly_Move](res/icons/Assembly_Move.png) - Align shafts (J) ### Sketch Tools - ![Workbench_Sketcher_none](res/icons/Workbench_Sketcher_none.png) - Sketch inspection mode diff --git a/src/doc/gui.md b/src/doc/gui.md index f5c41a0..3f213a5 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -162,7 +162,7 @@ Remappable chords live in `Gui_hotkeys` (`gui_hotkeys.h` / `.cpp`), owned by `GU | 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 | -| Cyl-align keys | `Mode::Shape_cyl_align` | `on_key_cyl_align_mode_` (Tab depth, Shift+Tab clock/angle, Enter finalize); hardcoded | +| Align-shafts keys | `Mode::Shape_cyl_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)). diff --git a/src/gui.cpp b/src/gui.cpp index d60a0a7..2c5ccf9 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -184,7 +184,7 @@ void GUI::initialize_toolbar_() {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 cylinders", Mode::Shape_cyl_align}, + {load_texture("res/icons/Assembly_Move.png"), false, "Align shafts", Mode::Shape_cyl_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}, @@ -240,7 +240,7 @@ void GUI::sync_toolbar_hotkey_tooltips_() tip_mode(Mode::Move, "Shape move", Gui_action::Mode_move); tip_mode(Mode::Rotate, "Shape rotate", Gui_action::Mode_rotate); tip_mode(Mode::Scale, "Shape Scale", Gui_action::Mode_scale); - tip_mode(Mode::Shape_cyl_align, "Align cylinders", Gui_action::Mode_cyl_align); + tip_mode(Mode::Shape_cyl_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); diff --git a/src/gui_hotkeys.cpp b/src/gui_hotkeys.cpp index 30d3e58..3d5ac88 100644 --- a/src/gui_hotkeys.cpp +++ b/src/gui_hotkeys.cpp @@ -44,7 +44,7 @@ constexpr Action_meta c_actions[] = { {Gui_action::Mode_add_slot, "mode.add_slot", "Add slot", {GLFW_KEY_U, 0}}, {Gui_action::Mode_polar_duplicate, "mode.polar_duplicate", "Polar duplicate", {GLFW_KEY_P, GLFW_MOD_SHIFT}}, {Gui_action::Mode_cross_section, "mode.cross_section", "Cross-section", {GLFW_KEY_X, GLFW_MOD_SHIFT}}, - {Gui_action::Mode_cyl_align, "mode.cyl_align", "Align cylinders", {GLFW_KEY_J, 0}}, + {Gui_action::Mode_cyl_align, "mode.cyl_align", "Align shafts", {GLFW_KEY_J, 0}}, {Gui_action::Cmd_shape_cut, "cmd.shape_cut", "Shape cut", {GLFW_KEY_C, GLFW_MOD_CONTROL | GLFW_MOD_SHIFT}}, {Gui_action::Cmd_shape_fuse, "cmd.shape_fuse", "Shape fuse", {GLFW_KEY_F, GLFW_MOD_CONTROL | GLFW_MOD_SHIFT}}, {Gui_action::Cmd_shape_common, "cmd.shape_common", "Shape common", {GLFW_KEY_M, GLFW_MOD_CONTROL | GLFW_MOD_SHIFT}}, diff --git a/src/gui_mode.cpp b/src/gui_mode.cpp index c432f33..d77e8e9 100644 --- a/src/gui_mode.cpp +++ b/src/gui_mode.cpp @@ -46,7 +46,7 @@ std::string GUI::get_doc_url_for_mode(Mode mode) {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_cyl_align, "https://ezycad.readthedocs.io/en/latest/usage.html#align-cylinders-tool-j"}, + {Mode::Shape_cyl_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::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"}, @@ -304,7 +304,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) case GLFW_KEY_TAB: { - // Move / Rotate / Align cylinders handle Tab in their mode key handlers (distance / angle / depth / twist). + // 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_cyl_align) break; @@ -318,7 +318,7 @@ void GUI::on_key(int key, int scancode, int action, int mods) } case GLFW_KEY_ENTER: - // Rotate / Align cylinders finalize on Enter in their mode key handlers. + // Rotate / Align shafts finalize on Enter in their mode key handlers. if (get_mode() == Mode::Rotate || get_mode() == Mode::Shape_cyl_align) break; hide_sketch_origin_set_edit(true); diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index 3bca8db..43c0ca8 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -84,7 +84,7 @@ Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) m_fixed_radius = cyl->radius; if (std::abs(m_moving_radius - m_fixed_radius) > Precision::Confusion()) - gui().log_message("Align cylinders: radii differ (moving " + std::to_string(m_moving_radius) + ", fixed " + + gui().log_message("Align shafts: radii differ (moving " + std::to_string(m_moving_radius) + ", fixed " + std::to_string(m_fixed_radius) + "); placement still allowed."); enter_drag_(); From 183fd5e63beb33a29fa7901bea8dd7b51d57dd0b Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 4 Aug 2026 18:27:44 -0600 Subject: [PATCH 4/5] WIP --- .../gh-246-align-shafts-clock-rotation.md | 29 +++++++++++++++++++ .../gh-247-align-shafts-clock-rotation.md | 27 +++++++++++++++++ src/shp_cyl_align.cpp | 7 ++--- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 agents/drafts/issues/active/gh-246-align-shafts-clock-rotation.md create mode 100644 agents/drafts/prs/active/gh-247-align-shafts-clock-rotation.md diff --git a/agents/drafts/issues/active/gh-246-align-shafts-clock-rotation.md b/agents/drafts/issues/active/gh-246-align-shafts-clock-rotation.md new file mode 100644 index 0000000..5e555cc --- /dev/null +++ b/agents/drafts/issues/active/gh-246-align-shafts-clock-rotation.md @@ -0,0 +1,29 @@ +--- +github_issue: 246 +github_pr: 247 +status: open +--- + +# Align shafts: clock rotation for tooth clocking + +**Suggested labels:** `enhancement` + +## Title (GitHub) + +Align shafts: clock rotation for tooth clocking + +## Body (GitHub) + +Filed: https://github.com/trailcode/EzyCad/issues/246 + +### Summary + +Extend Align shafts (formerly Align cylinders) with optional Clock rotation after coaxial depth; rename user-facing tool. + +### Follow-ups + +- Deterministic Clocking pick (planar tooth flanks) — revisited later. + +## Related + +- PR: https://github.com/trailcode/EzyCad/pull/247 diff --git a/agents/drafts/prs/active/gh-247-align-shafts-clock-rotation.md b/agents/drafts/prs/active/gh-247-align-shafts-clock-rotation.md new file mode 100644 index 0000000..fc77690 --- /dev/null +++ b/agents/drafts/prs/active/gh-247-align-shafts-clock-rotation.md @@ -0,0 +1,27 @@ +--- +github_issue: 246 +github_pr: 247 +status: open +--- + +# PR - Align shafts: clock rotation and rename + +## Title + +Align shafts: clock rotation and rename + +## Summary + +- Rename Align cylinders -> Align shafts (user-facing; `mode.cyl_align` unchanged). +- Options Clock rotation (default off) for roll about shared axis after depth. +- Docs + CHANGELOG. + +## Related + +- Issue: https://github.com/trailcode/EzyCad/issues/246 +- PR: https://github.com/trailcode/EzyCad/pull/247 +- Branch: `Trailcode/allign_shafts` + +## Test Plan + +- [ ] Build Release; exercise Clock rotation off/on, Flip, Tab/Shift+Tab, rename in UI/docs. diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index 43c0ca8..aa4a6ab 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -89,8 +89,7 @@ Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) enter_drag_(); if (m_opts.clock_rotation) - gui().show_message( - "Drag along the axis for insert depth, then LMB or Shift+Tab for clock rotation (or Enter to finish)."); + gui().show_message("Drag along the axis for insert depth, then LMB or Shift+Tab for clock rotation (or Enter to finish)."); else gui().show_message("Drag along the axis for insert depth, then LMB or Enter to confirm."); return Status::ok(); @@ -170,8 +169,8 @@ Status Shp_cyl_align::drag_depth(const ScreenCoords& screen_coords) return Status::ok(); } - const gp_Dir& fixed_dir = m_fixed_axis->Direction(); - const gp_Vec to_moving(m_fixed_axis->Location(), m_moving_axis->Location()); + const gp_Dir& fixed_dir = m_fixed_axis->Direction(); + const gp_Vec to_moving = {m_fixed_axis->Location(), m_moving_axis->Location()}; const double param0 = to_moving.Dot(gp_Vec(fixed_dir)); const gp_Pnt seed_on_ax = m_fixed_axis->Location().Translated(gp_Vec(fixed_dir) * param0); From aafc6072de8bfe1f32e6a8da30399689ac25d611 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Tue, 4 Aug 2026 18:28:43 -0600 Subject: [PATCH 5/5] WIP --- src/shp_cyl_align.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shp_cyl_align.cpp b/src/shp_cyl_align.cpp index aa4a6ab..f627594 100644 --- a/src/shp_cyl_align.cpp +++ b/src/shp_cyl_align.cpp @@ -92,6 +92,7 @@ Status Shp_cyl_align::pick(const ScreenCoords& screen_coords) gui().show_message("Drag along the axis for insert depth, then LMB or Shift+Tab for clock rotation (or Enter to finish)."); else gui().show_message("Drag along the axis for insert depth, then LMB or Enter to confirm."); + return Status::ok(); } @@ -297,8 +298,10 @@ void Shp_cyl_align::finalize() // Commit typed overrides before bake. if (m_depth_override.has_value()) m_axial_offset = *m_depth_override; + if (m_twist_override.has_value()) m_twist_angle = *m_twist_override; + clear_all(m_depth_override, m_twist_override); apply_preview_();