Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Shape copy / paste** (<kbd>Ctrl</kbd>+<kbd>C</kbd> / <kbd>Ctrl</kbd>+<kbd>V</kbd>): 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** (<kbd>J</kbd>): pick two cylindrical faces (first moves, second is fixed), coaxially align, then drag insert depth along the shared axis (or <kbd>Tab</kbd> 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`**.

- **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.
Expand Down
80 changes: 80 additions & 0 deletions agents/drafts/issues/active/gh-242-shape-copy-paste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
github_issue: 242
github_pr: 243
status: active
paired_draft: ../prs/active/gh-243-shape-copy-paste.md
---

# In-app shape copy and paste (Ctrl+C / Ctrl+V)

**Suggested labels:** `enhancement`, `ui`

---

## Title (GitHub)

In-app shape copy and paste (Ctrl+C / Ctrl+V)

## Body (GitHub)

### Summary

Add remappable **Ctrl+C** / **Ctrl+V** for an in-app clipboard that deep-copies selected solids and Shape List group subtrees, with undo via `Shape_add_delta`. Sketch entities and the OS clipboard are out of scope.

### Problem

- No CAD-level copy/paste existed; closest tools were polar duplicate, sketch mirror, and ImGui text clipboard.
- Users need to duplicate solids/groups and seed a new project from a selection (copy -> New -> paste).

### Implemented scope

**Code:**

- `Gui_action::Edit_copy` / `Edit_paste` (`edit.copy` / `edit.paste`, defaults Ctrl+C / Ctrl+V)
- `Occt_view::copy_selected_shapes` / `paste_clipboard_shapes` — session clipboard; group subtree when selection matches current group's descendant solids; paste under `current_group_id` with new ids and uniquified names
- Clipboard survives **New** (not cleared on `new_file`)

**Docs / tests:**

- `docs/usage.md`, `docs/usage-settings.md`, `CHANGELOG.md`, `src/doc/gui.md`, `src/doc/shape.md`
- Unit tests in `tests/shp_tests.cpp` (`Copy_*`, `New_file_keeps_shape_clipboard`)

### Acceptance criteria

- [x] Ctrl+C / Ctrl+V remappable; work when ImGui does not want text input
- [x] Copy solids; copy group subtree when Shape List group selection matches all descendants
- [x] Paste deep-copies under current group; one undo step; same pose
- [x] Clipboard survives New project
- [x] Unit tests pass; user docs and CHANGELOG updated

### Out of scope

- OS / cross-app clipboard
- Sketch edge copy/paste
- Ctrl+X cut
- Hold-key duplicate-translate (`edit.duplicate_translate`)

### Files touched

- `src/gui_hotkeys.h`, `src/gui_hotkeys.cpp`, `src/gui_mode.cpp`
- `src/gui_occt_view.h`, `src/gui_occt_view.cpp`
- `res/ezycad_settings.json`
- `tests/shp_tests.cpp`
- `docs/usage.md`, `docs/usage-settings.md`, `CHANGELOG.md`
- `src/doc/gui.md`, `src/doc/shape.md`
- `agents/drafts/issues/active/gh-242-shape-copy-paste.md` (this draft)

### Related

- Issue: https://github.com/trailcode/EzyCad/issues/242
- PR: https://github.com/trailcode/EzyCad/pull/243
- Branch: `copy-paste`
- Follow-up: hold-key duplicate translate (configurable-hotkeys plan)
- Shape List duplicate group (hierarchy phase 2, #215)

### Test plan

- [x] `EzyCad_tests --gtest_filter=Shp_test.Copy*:Shp_test.New_file*`
- [ ] Manual: select solids Ctrl+C / Ctrl+V; Shape List group copy; copy -> New -> paste
- [ ] Settings Keyboard shortcuts shows Copy / Paste
- [ ] Script console text Ctrl+C/V still works (WantTextInput)
51 changes: 51 additions & 0 deletions agents/drafts/prs/active/gh-243-shape-copy-paste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
github_issue: 242
github_pr: 243
status: active
paired_draft: ../issues/active/gh-242-shape-copy-paste.md
---

# PR - copy-paste

## Title

In-app shape copy and paste (Ctrl+C / Ctrl+V)

## Summary

- Adds remappable **Ctrl+C** / **Ctrl+V** (`edit.copy` / `edit.paste`) for an in-app shape clipboard.
- Deep-copies selected solids, or a Shape List group subtree when the selection matches that group's descendant solids.
- Paste inserts under the current group (new ids, uniquified names, same pose) with one `Shape_add_delta`; clipboard survives **New**.
- Docs, settings defaults, and unit tests included.

## Files Changed

- `src/gui_hotkeys.h`, `src/gui_hotkeys.cpp`, `src/gui_mode.cpp`
- `src/gui_occt_view.h`, `src/gui_occt_view.cpp`
- `res/ezycad_settings.json`
- `tests/shp_tests.cpp`
- `docs/usage.md`, `docs/usage-settings.md`, `CHANGELOG.md`
- `src/doc/gui.md`, `src/doc/shape.md`
- `agents/drafts/issues/active/gh-242-shape-copy-paste.md`
- `agents/drafts/prs/active/gh-243-shape-copy-paste.md` (this draft)

## Related

- Issue: https://github.com/trailcode/EzyCad/issues/242
- PR: https://github.com/trailcode/EzyCad/pull/243
- Branch: `copy-paste`

## Test Plan

- [x] `EzyCad_tests --gtest_filter=Shp_test.Copy*:Shp_test.New_file*`
- [ ] Manual: select solids → Ctrl+C / Ctrl+V (in place, undo)
- [ ] Manual: click Shape List group → Ctrl+C → paste preserves nesting
- [ ] Manual: copy → New → paste into blank project
- [ ] Settings → Keyboard shortcuts shows Copy / Paste (Ctrl+C / Ctrl+V)
- [ ] Script console text Ctrl+C/V still works while typing
- [ ] Spot-check `docs/usage.md` / `usage-settings.md` / CHANGELOG

## Notes

- Closes #242
- Out of scope: OS clipboard, sketch copy/paste, cut, hold-key duplicate-translate
6 changes: 3 additions & 3 deletions docs/usage-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Between those, the pane has collapsible sections. Expand a section to see its co

**WebAssembly build** — Open CASCADE line-width controls (`SetWidth` / `Prs3d` line width) have no visible effect in the browser (WebGL/GLES). The **Settings** pane hides **Edge thickness**, **Dimension line width**, and **Snap guide line width** on the web build. Saved JSON may still contain those keys from a desktop session; they are not shown as editable rows in the browser UI.

7. **Keyboard shortcuts** — Remappable chords for modeling and sketch tools, booleans, Delete, New/Open/Save, and Undo/Redo. See [Keyboard shortcuts](#keyboard-shortcuts).
7. **Keyboard shortcuts** — Remappable chords for modeling and sketch tools, booleans, Delete, Copy/Paste, New/Open/Save, and Undo/Redo. See [Keyboard shortcuts](#keyboard-shortcuts).

8. **Startup project** — **Desktop only:** **Load last opened on startup** (checkbox, with **?**), then **Last opened path:** … or **(No path saved yet.)** Then **Save current as startup project**, **Clear saved startup** (with **?**). **WebAssembly:** no load-last row; only the two buttons and **?**. See [Startup project](#startup-project).

Expand Down Expand Up @@ -147,7 +147,7 @@ Similar to Blender's startup file: EzyCad can load a **default document** when i

## Keyboard shortcuts

Remap modeling and sketch tool chords, boolean commands, Delete, New/Open/Save, and Undo/Redo in **View -> Settings -> Keyboard shortcuts**. Default key lists live in [usage.md -> Hotkeys](usage.md#hotkeys) (and [usage-sketch.md -> Hotkeys](usage-sketch.md#hotkeys) for sketch-focused summaries). Toolbar tooltips for remappable modes and boolean commands follow the current bindings.
Remap modeling and sketch tool chords, boolean commands, Delete, Copy/Paste, New/Open/Save, and Undo/Redo in **View -> Settings -> Keyboard shortcuts**. Default key lists live in [usage.md -> Hotkeys](usage.md#hotkeys) (and [usage-sketch.md -> Hotkeys](usage-sketch.md#hotkeys) for sketch-focused summaries). Toolbar tooltips for remappable modes and boolean commands follow the current bindings.

**How to remap**

Expand Down Expand Up @@ -267,7 +267,7 @@ If saved layout text has no `[Docking]` section (older installs), a default dock
| `default_2d_view_height` | number | Vertical sketch-plane span, stored in **inches** (allowed range **0.1** to **1000**; default **3**). Settings UI shows this in **`default_project_unit`**. |
| `load_last_opened_on_startup` | boolean | Desktop: open the last `.ezy` on launch. **Legacy:** `load_last_saved_on_startup` is read as a fallback if the newer key is absent. |
| `last_opened_project_path` | string | Path of the last opened project for the option above. **Legacy:** `last_saved_project_path` is accepted if the newer key is missing. |
| `hotkeys` | object | Remappable keyboard shortcuts: action id string keys to human-readable chord strings (for example `"mode.move": "G"`, `"mode.add_edge": "L"`, `"cmd.shape_cut": "Ctrl+Shift+C"`, `"edit.delete": "Shift+D"`). Missing keys merge to built-in defaults. See [Keyboard shortcuts](#keyboard-shortcuts) and [usage.md#hotkeys](usage.md#hotkeys). |
| `hotkeys` | object | Remappable keyboard shortcuts: action id string keys to human-readable chord strings (for example `"mode.move": "G"`, `"mode.add_edge": "L"`, `"cmd.shape_cut": "Ctrl+Shift+C"`, `"edit.delete": "Shift+D"`, `"edit.copy": "Ctrl+C"`, `"edit.paste": "Ctrl+V"`). Missing keys merge to built-in defaults. See [Keyboard shortcuts](#keyboard-shortcuts) and [usage.md#hotkeys](usage.md#hotkeys). |

Each **`imgui_style_dark`** / **`imgui_style_light`** object may contain:

Expand Down
3 changes: 3 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Each row (left to right):
- **Solid / wire** - Checkbox (solids only) to switch **shaded** or **wireframe**.
- **M** - Solids only: material popup; right-click for **Shape info...** or **Delete**.
- **Name** - Expandable tree row with an editable name. Click the row to select that solid (or all descendant solids for a group) and update the current group. **Ctrl+click** toggles multi-select. Drag to reparent (empty area below the list = document root).
- **Copy / Paste** - <kbd>Ctrl</kbd>+<kbd>C</kbd> / <kbd>Ctrl</kbd>+<kbd>V</kbd> (remappable) copy selected solids, or a whole group subtree when you clicked that group (all its descendant solids are selected). Paste inserts an independent deep copy under the current group, at the same pose. If you still have that copied group as the current group, paste places a sibling copy beside it (not nested inside it). This is an in-app clipboard (survives **New**; it does not use the system clipboard). Sketch edges are not copied.
- **Right-click the name** - Solids: **Shape info...** / **Delete**. Groups: **Ungroup** (moves **all** direct children to the group's parent, then removes the group) / **Delete** (cascade-deletes the whole subtree).

Boolean results stay under the shared parent of their inputs when all inputs share one parent; otherwise they are placed at the document root. **File -> Import** STEP assemblies use **Import as** (default **Preserve hierarchy**) to keep product/assembly groups in the tree, import **Flat solids** at the root, or **Union shapes** into one solid.
Expand Down Expand Up @@ -735,6 +736,8 @@ Mode, file, and edit chords in the **General Operations** and **Modeling Shortcu
| <kbd>Tab</kbd> | Distance/dimension input |
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | Angle input (for line edges with angle constraint) |
| <kbd>Shift</kbd>+<kbd>D</kbd>, <kbd>Delete</kbd>, or <kbd>Backspace</kbd> | Remove selected elements |
| <kbd>Ctrl</kbd>+<kbd>C</kbd> | Copy selected shapes (or current group subtree) to the in-app clipboard |
| <kbd>Ctrl</kbd>+<kbd>V</kbd> | Paste clipboard shapes under the current group (same pose; undoable) |

### Modeling Shortcuts

Expand Down
2 changes: 2 additions & 0 deletions res/ezycad_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"cmd.shape_fuse": "Ctrl+Shift+F",
"cmd.shape_common": "Ctrl+Shift+M",
"edit.delete": "Shift+D",
"edit.copy": "Ctrl+C",
"edit.paste": "Ctrl+V",
"file.new": "Ctrl+N",
"file.open": "Ctrl+O",
"file.save": "Ctrl+S",
Expand Down
8 changes: 5 additions & 3 deletions src/doc/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,14 @@ Remappable chords live in `Gui_hotkeys` (`gui_hotkeys.h` / `.cpp`), owned by `GU
| 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, file, undo/redo) |
| 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 |

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+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)).
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)).

**In-app shape clipboard** (`edit.copy` / `edit.paste`): `Occt_view::copy_selected_shapes` / `paste_clipboard_shapes` hold a session `Shape_rec` forest (not the OS clipboard). Copy roots are selected solids, or the current group when its descendant solids match the selection exactly (Shape List group click). Paste deep-copies under `current_group_id` with new ids, uniquified names, and a single `Shape_add_delta`. When the current group is still a copied group root, paste inserts as a **sibling** of that group (not nested under it). Survives `new_file` (copy -> New -> paste); live source-root ids are cleared on New. Sketch geometry is out of scope; ImGui text widgets keep OS text clipboard via `WantTextInput`.

See also [`src/doc/sketch.md`](sketch.md) and [`src/doc/shape.md`](shape.md) for per-mode mouse routing after `GUI` delegates to `Occt_view`.

Expand Down Expand Up @@ -237,7 +239,7 @@ Shared sketch controls (snap, midpoint nodes, place-from-center) live in `option

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).

**Shape List outliner:** `shape_list_` draws a tree of document shapes/groups via `shape_children(0)` and recursive `TreeNodeEx` rows. 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. Hover uses `set_shape_list_hover` on leaf solids only. `ui.shapeList.currentGroupId` is persisted in `.ezy`.
**Shape List outliner:** `shape_list_` draws a tree of document shapes/groups via `shape_children(0)` and recursive `TreeNodeEx` rows. 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. Copy/paste (Ctrl+C/V) deep-copies the current group subtree when the selection matches that group's descendant solids. 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.

Expand Down
Loading
Loading