From a80852c842a2be6aebf1c583617df7944894e9d2 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 6 Aug 2026 18:25:54 -0600 Subject: [PATCH 1/3] Fix --- CHANGELOG.md | 6 + .../gh-220-wasm-alt-drag-multiselect.md | 19 +- agents/plans/README.md | 2 +- .../plans/{ => done}/configurable-hotkeys.md | 0 agents/plans/{ => done}/cross-section-tool.md | 0 agents/plans/wasm-alt-drag-multiselect.md | 35 ++- docs/usage-occt-view.md | 1 + docs/usage.md | 5 +- src/doc/gui.md | 6 +- src/gui.cpp | 17 +- src/gui_occt_view.cpp | 203 +++++++++++++++++- src/gui_occt_view.h | 10 + tests/shp_tests.cpp | 35 +++ 13 files changed, 296 insertions(+), 43 deletions(-) rename agents/plans/{ => done}/configurable-hotkeys.md (100%) rename agents/plans/{ => done}/cross-section-tool.md (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01defac..c75f2430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **WASM Alt+LMB rectangle select**: multi-select via Alt+left-drag works again. WASM never created `Occt_glfw_win`, so live modifier polling during mouse move always returned no Alt and OCCT rebound the gesture to orbit. `Occt_view` now keeps a non-owning `GLFWwindow*` for modifiers and cursor (desktop unchanged). + +- **Alt+LMB rectangle select misses some solids**: after OCCT `SelectRectangle`, also select displayed document solids whose projected AABB intersects the rubber band. Fixes complex / STEP parts (often under Shape List groups) that stock sensitive-entity picking skipped while simple root boxes were selected. On WASM, map projections from OCCT window pixels into GLFW cursor space (canvas size can drift). + +- **Shape List current group highlight**: the current group row no longer uses the same strong selection tint as AIS-selected solids. That made Alt-drag / cleared selection look like grouped shapes stayed selected when only the current-group marker was lit. + - **Sketch from face profile wire**: the dark-red originating-face boundary no longer stays drawn after leaving sketch mode (e.g. after Revolve returns to Normal). It follows sketch edge visibility and only appears while sketch tools (or polar duplicate) show the sketch. - **`gui.hotkeys` load**: duplicate-chord cleanup no longer leaves two actions on the same key when the later row's factory chord is the colliding key (e.g. Move and Rotate both `"R"`). Earlier remaps that steal a later action's factory chord are restored to defaults so each binding stays unique. diff --git a/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md b/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md index d49da5d6..1e867fb1 100644 --- a/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md +++ b/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md @@ -21,18 +21,23 @@ See https://github.com/trailcode/EzyCad/issues/220 ### Summary -Desktop: **Alt + LMB drag** rubber-band multi-selects shapes via OCCT `AIS_ViewController`. WASM: same gesture does not select multiple shapes. +Desktop: **Alt + LMB drag** rubber-band multi-selects shapes via OCCT `AIS_ViewController`. WASM: same gesture did not select multiple shapes because live Alt was never polled (`m_occt_window` null on Emscripten). + +### Fix (working tree) + +- `Occt_view` stores non-owning `GLFWwindow* m_glfw_window` for modifier/cursor polling. +- Docs: `usage.md`, `usage-occt-view.md`, `gui.md`, CHANGELOG, plan `wasm-alt-drag-multiselect.md`. ### Acceptance criteria -- [ ] WASM Alt + LMB drag multi-selects like desktop -- [ ] Desktop unchanged -- [ ] Docs note Alt+drag (and any Web caveats) -- [ ] Plan updated: `agents/plans/wasm-alt-drag-multiselect.md` +- [x] Code path: WASM can poll Alt during drag (same as desktop) +- [ ] Manual WASM: Alt + LMB drag multi-selects like desktop +- [x] Desktop gesture map unchanged +- [x] Docs note Alt+drag (and Web Alt caveat) +- [x] Plan updated: `agents/plans/wasm-alt-drag-multiselect.md` ### Related - Issue: https://github.com/trailcode/EzyCad/issues/220 -- Plan: `agents/plans/wasm-alt-drag-multiselect.md` (tracking on PR #219; bug not fixed there) +- Plan: `agents/plans/wasm-alt-drag-multiselect.md` - Related: #93 / `gh-93-emscripten-web-hotkeys-followup.md` -- PR that added tracking: https://github.com/trailcode/EzyCad/pull/219 diff --git a/agents/plans/README.md b/agents/plans/README.md index 4dcd0361..3e7a0789 100644 --- a/agents/plans/README.md +++ b/agents/plans/README.md @@ -35,5 +35,5 @@ sketch-mode-shape-faint (done; parallel UX) | [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) | deferred | parent transform inheritance, Parts/planes, Boolean history (#214) | | [assembly-inspection-mode.md](assembly-inspection-mode.md) | deferred | assembly idle/inspection mode, Part vs arrange context, Move semantics | | [wasm-multithreading.md](wasm-multithreading.md) | planning | WASM/Emscripten pthreads, SharedArrayBuffer, parallel OCCT on web | -| [wasm-alt-drag-multiselect.md](wasm-alt-drag-multiselect.md) | planning | WASM Alt+LMB drag rectangle multi-select broken (#220) | +| [wasm-alt-drag-multiselect.md](wasm-alt-drag-multiselect.md) | implemented | WASM Alt+LMB drag rectangle multi-select (#220); verify on WASM then close | | [configurable-hotkeys.md](configurable-hotkeys.md) | done | remappable shortcuts, keybindings, `gui.hotkeys`, free Dimension off D | diff --git a/agents/plans/configurable-hotkeys.md b/agents/plans/done/configurable-hotkeys.md similarity index 100% rename from agents/plans/configurable-hotkeys.md rename to agents/plans/done/configurable-hotkeys.md diff --git a/agents/plans/cross-section-tool.md b/agents/plans/done/cross-section-tool.md similarity index 100% rename from agents/plans/cross-section-tool.md rename to agents/plans/done/cross-section-tool.md diff --git a/agents/plans/wasm-alt-drag-multiselect.md b/agents/plans/wasm-alt-drag-multiselect.md index 463fab43..12f6a08d 100644 --- a/agents/plans/wasm-alt-drag-multiselect.md +++ b/agents/plans/wasm-alt-drag-multiselect.md @@ -1,5 +1,5 @@ --- -status: planning +status: implemented topic: wasm-alt-drag-multiselect depends_on: [] blocks: [] @@ -16,13 +16,13 @@ github_issue: 220 On desktop, holding **Alt** and **left-dragging** in the 3D view activates OCCT `AIS_ViewController` rectangle selection (`SelectRectangle` / rubber band) and multi-selects shapes under the box. -On the WASM build the same gesture does **not** multi-select. +On the WASM build the same gesture did **not** multi-select. -EzyCad already maps modifiers into OCCT flags: +EzyCad maps modifiers into OCCT flags: - `Occt_view::key_flags_from_glfw_` — `GLFW_MOD_ALT` → `Aspect_VKeyFlags_ALT` - Mouse press/release: `PressMouseButton` / `ReleaseMouseButton` with `theMods` -- Mouse move: `UpdateMousePosition(..., key_flags_from_glfw_window_(), ...)` which polls `glfwGetKey(... LEFT/RIGHT_ALT ...)` +- Mouse move: `UpdateMousePosition(..., key_flags_from_glfw_window_(), ...)` which polls Alt via `glfwGetKey` Related input/hotkey tracking: [#93](https://github.com/trailcode/EzyCad/issues/93), draft `agents/drafts/issues/active/gh-93-emscripten-web-hotkeys-followup.md`. OCCT desktop vs wasm kit: [occt-wasm-dual-version](../conventions/occt-wasm-dual-version.md). @@ -30,32 +30,29 @@ Related input/hotkey tracking: [#93](https://github.com/trailcode/EzyCad/issues/ Make **Alt + LMB drag** multi-select on WASM match desktop, without changing desktop gesture mapping or selection schemes. -## Likely causes (investigate in order) +## Root cause (confirmed in code) -1. **Alt not present on pointer events under Emscripten/GLFW** — browser menu focus, `preventDefault`, or incomplete `mods` on `glfwSetMouseButtonCallback`. -2. **`glfwGetKey(ALT)` false during drag** — rubber-band gesture needs Alt on move updates (`key_flags_from_glfw_window_`), not only on button press. -3. **ImGui / focus** — canvas loses keyboard focus when Alt is pressed; Alt never reaches GLFW. -4. **OCCT 7.9.3 wasm kit** — confirm default `MouseGestureMap` still binds Alt+LMB to select-rectangle (unlikely difference, but verify if input looks correct). +WASM `init_window` skipped creating `Occt_glfw_win` (correct: `Close()` would destroy the shared canvas). `key_flags_from_glfw_window_()` then always returned `NONE` because it only read modifiers from `m_occt_window`. OCCT rebinds when modifiers drop (`myMouseModifiers != theModifiers`), so Alt+LMB `SelectRectangle` became plain LMB orbit on the next move. -## Approach +## Approach (landed) ### Phase 0 — reproduce and instrument -- [ ] Confirm desktop Alt+LMB rubber band still works. -- [ ] On WASM, log (temporary) `mods` on LMB press/release and Alt from `key_flags_from_glfw_window_` during drag. -- [ ] Note browser (Chrome/Firefox/Edge) and whether the OS/browser steals Alt. +- [x] Root cause identified from code path (null `m_occt_window` on WASM → no Alt on move). +- [ ] Manual WASM retest after fix (Chrome/Firefox): Alt+LMB box select; Ctrl+click still works; desktop unchanged. ### Phase 1 — fix input path -- [ ] Ensure Alt is forwarded for press, move, and release while the gesture is active (Emscripten/GLFW and/or synthetic modifier from `event.altKey` if GLFW is incomplete). -- [ ] Avoid focusing browser chrome on Alt when the canvas has focus (as far as the platform allows). -- [ ] Keep `GUI::on_mouse_button` / sketch `mods == 0` click paths unchanged for unmodified LMB. +- [x] Store non-owning `GLFWwindow* m_glfw_window` in `Occt_view::init_window`. +- [x] Poll modifiers and cursor from `m_glfw_window` (`key_flags_from_glfw_window_`, `cursor_position_`). +- [x] Do not wrap WASM window in owning `Occt_glfw_win`. ### Phase 2 — docs and parity -- [ ] Document Alt+drag multi-select in `docs/usage.md` and/or `docs/usage-occt-view.md`; call out any remaining Web limitation. -- [ ] Manual check: Ctrl+click multi-select still works on WASM if it already does; Alt+drag matches desktop. -- [ ] Close #220 when done; update this plan status. +- [x] Document Alt+drag in `docs/usage.md` and `docs/usage-occt-view.md`; note web Alt/menu caveat. +- [x] `src/doc/gui.md` notes non-owning GLFW pointer for WASM modifier polling. +- [x] `CHANGELOG.md` `[Unreleased]` Fixed entry. +- [ ] Close #220 when verified on WASM. ## Out of scope diff --git a/docs/usage-occt-view.md b/docs/usage-occt-view.md index c7c4cf3c..9860c9af 100644 --- a/docs/usage-occt-view.md +++ b/docs/usage-occt-view.md @@ -5,6 +5,7 @@ EzyCad's 3D viewport uses **Open CASCADE Technology (OCCT)** for displaying soli ## Navigation - **Orbit, pan, zoom** — See [Mouse Controls](usage.md#mouse-controls) in the usage guide. +- **Rectangle multi-select** — Hold Alt and left-drag to rubber-band select shapes (same Open CASCADE `AIS_ViewController` gesture as desktop). On the web build, click the canvas first if the browser steals Alt for its menu. - **Num Lock** — Num Lock off is recommended for all NumPad view shortcuts (orbit, roll, zoom, axis snap). With Num Lock on, the OS may remap the keypad so those keys no longer match the docs; use main-row alternatives listed in [View navigation](usage.md#view-navigation). - **View orbit / roll** — NumPad 8/2/4/6 orbit (same axes as LMB drag); Shift+NumPad 4/6, Shift+main 4/6, or Shift+Left/Right roll around the screen axis (repeat while held). Step size is **Settings → 3D view navigation** (**View rotation step**). See [View navigation](usage.md#view-navigation). - **Zoom** — mouse wheel, **right-drag**, NumPad +/-, Shift+=, and main - share one path. Strength is **Zoom scroll scale** in Settings (`gui.view_zoom_scroll_scale`, default **4**). Hold Shift while zooming for a Blender-style finer step (**x0.1**). See [View Controls](usage.md#view-controls). diff --git a/docs/usage.md b/docs/usage.md index 3dd141e0..5a33d1f1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -682,7 +682,7 @@ These tools are in the main toolbar (after the polar duplicate button). They are **How to Use:** 1. Make sure you are in **Normal** (inspection) mode. -2. **Select two or more shapes** in the 3D viewer. Hold Ctrl (or the platform equivalent) to multi-select, or drag a selection box. Use the selection filter if you only want to pick whole solids. +2. **Select two or more shapes** in the 3D viewer. Hold Ctrl (or the platform equivalent) to multi-select, or hold Alt and drag a selection box. Use the selection filter if you only want to pick whole solids. 3. Click the appropriate toolbar button: - ![Part_Cut](res/icons/Part_Cut.png) **Shape cut** for difference/subtract - ![Part_Fuse](res/icons/Part_Fuse.png) **Shape fuse** for union @@ -823,11 +823,14 @@ Open or close the **Lua** or **Python** consoles from **View -> Lua Console** or | -------------------------------------------------------------------------------------: | ---------------------------------------------------------------------------------- | | **Left Click** | Select object | | **Left drag** | Orbit view | +| Alt+**Left drag** | Rectangle multi-select (shapes inside the box) | | **Middle drag** | Pan view | | **Right drag** | Zoom | | **Scroll Wheel** | Zoom in/out (**Zoom scroll scale** in Settings; hold **Shift** for finer steps) | | NumPad + / NumPad -, Shift+=, - | Zoom in/out ([keyboard](#view-navigation); settings scale; Shift finer) | +On the **web (WASM)** build, keep the canvas focused while holding Alt. Some browsers use Alt for the menu bar; if rectangle select fails, click the 3D view first, or use Ctrl+click to multi-select. + ### View orbit (NumPad) Press NumPad 8, NumPad 2, NumPad 4, or NumPad 6 (without Shift) to orbit the camera in steps, using the same axes as **left-drag orbit** (Open CASCADE `AIS_ViewController` convention: yaw about camera up, pitch about camera side). NumPad 8 / NumPad 2 pitch up or down; NumPad 4 / NumPad 6 yaw left or right. The default step is **45** degrees per key press. **Num Lock off** is recommended so the keypad sends these **NumPad** codes (see [View navigation](#view-navigation) above). diff --git a/src/doc/gui.md b/src/doc/gui.md index 031274c0..948be798 100644 --- a/src/doc/gui.md +++ b/src/doc/gui.md @@ -129,6 +129,10 @@ Initialization in [`main.cpp`](../main.cpp): config flags, native-only `UpdatePl With `ViewportsEnable`, ImGui `MousePos` is in screen coordinates; OCCT picking uses GLFW client-area coordinates via `GUI::cursor_screen_coords()` (see `on_mouse_button` and the `main` cursor callback). Do not pass ImGui `MousePos` to `Occt_view` on native builds. +`Occt_view` keeps a non-owning `GLFWwindow*` (`m_glfw_window`, set in `init_window`) for `key_flags_from_glfw_window_()` and cursor polling. WASM does not wrap that window in `Occt_glfw_win` (its `Close()` would `glfwDestroyWindow` the shared canvas). Live Alt/Shift/Ctrl during drag (needed for OCCT Alt+LMB rectangle select) therefore poll via `m_glfw_window`, not `m_occt_window`. + +`Occt_view::handleSelectionPoly` overrides the base rubber-band apply: after OCCT `SelectRectangle`, `select_shps_intersecting_screen_rect_` adds any displayed non-group `Shp` whose projected AABB intersects the band (overlap). Stock picking often misses complex imported BREPs even when simple root solids are selected. The rect comes from gesture mouse points (GLFW space); projected corners are scaled from OCCT window size to GLFW window size so WASM `Wasm_Window` / canvas size drift cannot drop hits. + In [`main.cpp`](../main.cpp), GLFW **mouse-move** callbacks always forward to `GUI` (sketch rubber-band and OCCT hover must not stop when a float edit or docked panel is hovered). **Mouse-button press** and **scroll** forward only when the cursor is in the dock central passthrough region and no ImGui window is hovered (so toolbar clicks do not clear OCCT selection). A press that is forwarded sets per-button capture so the matching **release** is still sent to `GUI` / OCCT even if the cursor is over a pane (ends view orbit / AIS button state). Releases with no matching view press stay ImGui-only. Each frame, [`gui.cpp`](../gui.cpp) `dock_space_()` sets the OCCT passthrough rectangle via `DockSpaceOverViewport` with `ImGuiDockNodeFlags_PassthruCentralNode`, then reads the central node bounds. `dock_space_()` calls `SetNextFrameWantCaptureMouse(false)` when the cursor is over the passthrough region. @@ -239,7 +243,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. Copy/paste (Ctrl+C/V) deep-copies the current group subtree when the selection matches that group's descendant solids. Context menu **Zoom to** calls `Occt_view::fit_shapes_in_view` (solid or group descendant solids; keeps camera orientation). Hover uses `set_shape_list_hover` on leaf solids only. `ui.shapeList.currentGroupId` is persisted in `.ezy`. +**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. Row highlight for **selection** follows AIS only; the **current group** uses a weaker tint so it is not mistaken for a selected subtree after Alt-drag rectangle select clears AIS. Copy/paste (Ctrl+C/V) deep-copies the current group subtree when the selection matches that group's descendant solids. Context menu **Zoom to** calls `Occt_view::fit_shapes_in_view` (solid or group descendant solids; keeps camera orientation). Hover uses `set_shape_list_hover` on leaf solids only. `ui.shapeList.currentGroupId` is persisted in `.ezy`. **Sketch List UI in the project file:** `GUI::serialized_project_json_` writes `ui.sketchList` (scroll Y plus per-sketch `rows` keyed by sketch `id`: `expanded`, `dimensions`, `nodes`, `edges`, `faces`). `GUI::on_file` restores via `apply_sketch_list_ui_from_json_`. Subsection open state is app-owned (`Sketch_list_row_ui` + `SetNextItemOpen`), not ImGui ini storage. diff --git a/src/gui.cpp b/src/gui.cpp index fc6f8aea..4f101daf 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -2622,13 +2622,17 @@ void GUI::shape_list_() const std::vector children = m_view->shape_children(shape->get_id()); const bool has_children = !children.empty(); const bool is_current_group = is_group && shape->get_id() == m_view->current_group_id(); - const bool row_selected = is_current_group || row_is_selected(shape); - bool row_hovered = false; + // Selection highlight follows the 3D viewer only. Current group uses a distinct tint so + // Alt-drag / clear-selection cannot look like the group (or its children) stayed selected. + const bool row_selected = row_is_selected(shape); + bool row_hovered = false; ImGui::PushID(static_cast(shape->get_id())); ImGui::TableNextRow(); if (row_selected) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_Header, 0.45f)); + else if (is_current_group) + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::GetColorU32(ImGuiCol_Header, 0.18f)); char name_buffer[1024]; safe_cstr_copy(name_buffer, sizeof(name_buffer), shape->get_name().c_str()); @@ -2767,8 +2771,13 @@ void GUI::shape_list_() select_shape_row(shape); row_hovered |= ImGui::IsItemHovered(); - if (row_selected && ui_show_contextual_help() && ImGui::IsItemHovered()) - ImGui::SetTooltip(is_current_group ? "Current group (new shapes go here)" : "Selected in 3D viewer"); + if (ui_show_contextual_help() && ImGui::IsItemHovered()) + { + if (row_selected) + ImGui::SetTooltip("Selected in 3D viewer"); + else if (is_current_group) + ImGui::SetTooltip("Current group (new shapes go here)"); + } if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { diff --git a/src/gui_occt_view.cpp b/src/gui_occt_view.cpp index 4dc8c0c3..498c8055 100644 --- a/src/gui_occt_view.cpp +++ b/src/gui_occt_view.cpp @@ -1,6 +1,7 @@ #include "gui_occt_view.h" #include +#include #include #include #include @@ -111,6 +112,8 @@ Occt_view::~Occt_view() {} // Initialization related. void Occt_view::init_window(GLFWwindow* GlfwWindow) { + // Non-owning: needed on WASM where Occt_glfw_win is not created (Close would destroy the shared canvas). + m_glfw_window = GlfwWindow; #ifndef __EMSCRIPTEN__ m_occt_window = new Occt_glfw_win(GlfwWindow); #endif @@ -2574,6 +2577,174 @@ void Occt_view::flush_view_events() } } +void Occt_view::handleSelectionPoly(const Handle(AIS_InteractiveContext)& theCtx, + const Handle(V3d_View)& theView) +{ + // Capture apply state and rect before the base class clears the rubber band / ToApplyTool. + const bool to_apply = myGL.Selection.ToApplyTool; + const AIS_ViewSelectionTool tool = myGL.Selection.Tool; + int xmin = 0; + int ymin = 0; + int xmax = 0; + int ymax = 0; + bool have_rubber = false; + + auto set_rect_from_points = [&](int x0, int y0, int x1, int y1) + { + xmin = std::min(x0, x1); + xmax = std::max(x0, x1); + ymin = std::min(y0, y1); + ymax = std::max(y0, y1); + have_rubber = xmax > xmin && ymax > ymin; + }; + + if (to_apply && tool == AIS_ViewSelectionTool_RubberBand) + { + // Prefer gesture mouse points (GLFW / UpdateMousePosition space). On WASM these stay + // aligned with cursor coords even when OCCT Wasm_Window size drifts from GLFW. + set_rect_from_points(myMousePressPoint.x(), myMousePressPoint.y(), myMouseProgressPoint.x(), + myMouseProgressPoint.y()); + + if (!have_rubber && !myRubberBand.IsNull() && theCtx->IsDisplayed(myRubberBand)) + { + const NCollection_Sequence>& pts = myRubberBand->Points(); + if (pts.Size() >= 2) + { + int rx0 = pts.First().x(); + int ry0 = -pts.First().y(); // Rubber-band stores Y negated. + int rx1 = rx0; + int ry1 = ry0; + for (NCollection_Sequence>::Iterator it(pts); it.More(); it.Next()) + { + const int x = it.Value().x(); + const int y = -it.Value().y(); + rx0 = std::min(rx0, x); + rx1 = std::max(rx1, x); + ry0 = std::min(ry0, y); + ry1 = std::max(ry1, y); + } + set_rect_from_points(rx0, ry0, rx1, ry1); + } + } + } + + AIS_ViewController::handleSelectionPoly(theCtx, theView); + + if (have_rubber) + select_shps_intersecting_screen_rect_(xmin, ymin, xmax, ymax); +} + +void Occt_view::select_shps_intersecting_screen_rect_(int xmin, int ymin, int xmax, int ymax) +{ + if (m_ctx.IsNull() || m_view.IsNull() || is_headless()) + return; + +#ifdef __EMSCRIPTEN__ + // Keep Wasm_Window size in sync with the shared canvas before projecting. + if (!m_view->Window().IsNull()) + { + m_view->Window()->DoResize(); + m_view->MustBeResized(); + } +#endif + + // Map V3d_View::Convert pixels into the same space as the rubber-band / GLFW cursor. + // On WASM, Wasm_Window canvas size can disagree with the GLFW window ImGui sized (CSS*DPR). + int occt_w = 0; + int occt_h = 0; + if (!m_view->Window().IsNull()) + m_view->Window()->Size(occt_w, occt_h); + + int glfw_w = occt_w; + int glfw_h = occt_h; + if (m_glfw_window != nullptr) + glfwGetWindowSize(m_glfw_window, &glfw_w, &glfw_h); + + const double scale_x = (occt_w > 0) ? double(glfw_w) / double(occt_w) : 1.0; + const double scale_y = (occt_h > 0) ? double(glfw_h) / double(occt_h) : 1.0; + + bool added = false; + for (const Shp_ptr& shp : m_shps) + { + if (shp.IsNull() || shp->is_group() || shp->sketch_faint_active()) + continue; + + if (!m_ctx->IsDisplayed(shp) || m_ctx->IsSelected(shp)) + continue; + + const TopoDS_Shape& geom = shp->Shape(); + if (geom.IsNull()) + continue; + + Bnd_Box local; + BRepBndLib::Add(geom, local); + if (local.IsVoid()) + continue; + + const gp_Trsf& tr = shp->LocalTransformation(); + if (tr.Form() != gp_Identity) + local = local.Transformed(tr); + + double x0 = 0.0; + double y0 = 0.0; + double z0 = 0.0; + double x1 = 0.0; + double y1 = 0.0; + double z1 = 0.0; + local.Get(x0, y0, z0, x1, y1, z1); + + int sx_min = 0; + int sy_min = 0; + int sx_max = 0; + int sy_max = 0; + bool have_pt = false; + // clang-format off + const gp_Pnt corners[8] = { + gp_Pnt(x0, y0, z0), gp_Pnt(x1, y0, z0), gp_Pnt(x0, y1, z0), gp_Pnt(x1, y1, z0), + gp_Pnt(x0, y0, z1), gp_Pnt(x1, y0, z1), gp_Pnt(x0, y1, z1), gp_Pnt(x1, y1, z1), + }; + // clang-format on + for (const gp_Pnt& p : corners) + { + int sx = 0; + int sy = 0; + m_view->Convert(p.X(), p.Y(), p.Z(), sx, sy); + sx = static_cast(std::lround(sx * scale_x)); + sy = static_cast(std::lround(sy * scale_y)); + if (!have_pt) + { + sx_min = sx_max = sx; + sy_min = sy_max = sy; + have_pt = true; + } + else + { + sx_min = std::min(sx_min, sx); + sx_max = std::max(sx_max, sx); + sy_min = std::min(sy_min, sy); + sy_max = std::max(sy_max, sy); + } + } + + if (!have_pt) + continue; + + // Overlap: any part of the projected AABB inside the rubber band. + if (sx_max < xmin || sx_min > xmax || sy_max < ymin || sy_min > ymax) + continue; + + // AddOrRemoveSelected (not AddSelect): works when GlobalSelOwner is unset but local mode is active. + m_ctx->AddOrRemoveSelected(shp, false); + added = true; + } + + if (added) + { + m_ctx->HilightSelected(true); + m_ctx->UpdateCurrentViewer(); + } +} + void Occt_view::do_frame() { flush_view_events(); @@ -2628,7 +2799,7 @@ void Occt_view::on_mouse_scroll(double theOffsetX, double theOffsetY, bool shift { (void)theOffsetX; if (!m_view.IsNull()) - UpdateZoom(Aspect_ScrollDelta(m_occt_window->CursorPosition(), zoom_scroll_delta_int_(theOffsetY, shift_finer_zoom))); + UpdateZoom(Aspect_ScrollDelta(cursor_position_(), zoom_scroll_delta_int_(theOffsetY, shift_finer_zoom))); } void Occt_view::zoom_view_wheel_notches(double wheel_notches, bool shift_finer_zoom) @@ -2636,7 +2807,7 @@ void Occt_view::zoom_view_wheel_notches(double wheel_notches, bool shift_finer_z if (m_view.IsNull()) return; - UpdateZoom(Aspect_ScrollDelta(m_occt_window->CursorPosition(), zoom_scroll_delta_int_(wheel_notches, shift_finer_zoom))); + UpdateZoom(Aspect_ScrollDelta(cursor_position_(), zoom_scroll_delta_int_(wheel_notches, shift_finer_zoom))); } void Occt_view::on_mouse_button(int theButton, int theAction, int theMods) @@ -2644,7 +2815,7 @@ void Occt_view::on_mouse_button(int theButton, int theAction, int theMods) if (m_view.IsNull()) return; - const NCollection_Vec2 pos = m_occt_window->CursorPosition(); + const NCollection_Vec2 pos = cursor_position_(); if (theAction == GLFW_PRESS) { // Planar-face picking uses InteractiveContext::MoveTo() in get_face_(). Run it before @@ -3516,26 +3687,38 @@ Aspect_VKeyFlags Occt_view::key_flags_from_glfw_(int theFlags) Aspect_VKeyFlags Occt_view::key_flags_from_glfw_window_() const { - if (m_occt_window.IsNull() || m_occt_window->getGlfwWindow() == nullptr) + if (m_glfw_window == nullptr) return Aspect_VKeyFlags_NONE; - GLFWwindow* const window = m_occt_window->getGlfwWindow(); - int mods = 0; - if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS) + int mods = 0; + if (glfwGetKey(m_glfw_window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || + glfwGetKey(m_glfw_window, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS) mods |= GLFW_MOD_SHIFT; - if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS) + if (glfwGetKey(m_glfw_window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || + glfwGetKey(m_glfw_window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS) mods |= GLFW_MOD_CONTROL; - if (glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS) + if (glfwGetKey(m_glfw_window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || + glfwGetKey(m_glfw_window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS) mods |= GLFW_MOD_ALT; - if (glfwGetKey(window, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS) + if (glfwGetKey(m_glfw_window, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS || + glfwGetKey(m_glfw_window, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS) mods |= GLFW_MOD_SUPER; return key_flags_from_glfw_(mods); } +NCollection_Vec2 Occt_view::cursor_position_() const +{ + EZY_ASSERT(m_glfw_window != nullptr); + double x = 0.0; + double y = 0.0; + glfwGetCursorPos(m_glfw_window, &x, &y); + return NCollection_Vec2(static_cast(x), static_cast(y)); +} + Occt_view::Sketch_list& Occt_view::get_sketches() { return m_sketches; } const Occt_view::Sketch_list& Occt_view::get_sketches() const { return m_sketches; } diff --git a/src/gui_occt_view.h b/src/gui_occt_view.h index 671f0960..d28df866 100644 --- a/src/gui_occt_view.h +++ b/src/gui_occt_view.h @@ -467,6 +467,14 @@ class Occt_view : protected AIS_ViewController static Aspect_VKeyMouse mouse_button_from_glfw_(int theButton); static Aspect_VKeyFlags key_flags_from_glfw_(int theFlags); Aspect_VKeyFlags key_flags_from_glfw_window_() const; + /// Cursor in GLFW client pixels (same space as mouse-move ScreenCoords). + NCollection_Vec2 cursor_position_() const; + + /// After OCCT rubber-band SelectRectangle, add any displayed document solid whose + /// projected AABB intersects the rect (OCCT can miss complex / imported BREPs). + void handleSelectionPoly(const Handle(AIS_InteractiveContext)& theCtx, + const Handle(V3d_View)& theView) override; + void select_shps_intersecting_screen_rect_(int xmin, int ymin, int xmax, int ymax); /// Maps wheel delta to OCCT zoom units using \ref m_zoom_scroll_scale and optional Shift (x0.1). int zoom_scroll_delta_int_(double wheel_y, bool shift_finer_zoom) const; @@ -474,6 +482,8 @@ class Occt_view : protected AIS_ViewController GUI& m_gui; AIS_InteractiveContext_ptr m_ctx; V3d_View_ptr m_view; + /// Non-owning GLFW window for modifier/cursor polling (WASM has no Occt_glfw_win). + GLFWwindow* m_glfw_window{nullptr}; Occt_glfw_win_ptr m_occt_window; // Undo / redo static constexpr size_t k_max_undo{50}; diff --git a/tests/shp_tests.cpp b/tests/shp_tests.cpp index 58dadff0..ee84a872 100644 --- a/tests/shp_tests.cpp +++ b/tests/shp_tests.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -1179,3 +1180,37 @@ TEST_F(Shp_test, New_file_keeps_shape_clipboard) EXPECT_EQ(leaves, 1u); } + +TEST_F(Shp_test, Grouped_solids_stay_displayed_and_selectable) +{ + view().add_box(0, 0, 0, 1, 1, 1); + view().add_box(3, 0, 0, 1, 1, 1); + + std::vector boxes; + for (const Shp_ptr& s : view().get_shapes()) + if (!s.IsNull() && !s->is_group()) + boxes.push_back(s); + + ASSERT_EQ(boxes.size(), 2u); + ASSERT_TRUE(view().group_shapes(boxes).is_ok()); + + AIS_InteractiveContext& ctx = view().ctx(); + for (const Shp_ptr& s : boxes) + { + EXPECT_NE(s->get_parent_id(), 0u); + EXPECT_TRUE(ctx.IsDisplayed(s)); + NCollection_List modes; + ctx.ActivatedModes(s, modes); + bool has_shape_mode = false; + for (NCollection_List::Iterator it(modes); it.More(); it.Next()) + if (it.Value() == AIS_Shape::SelectionMode(TopAbs_SHAPE)) + has_shape_mode = true; + + EXPECT_TRUE(has_shape_mode) << "id=" << s->get_id(); + + ctx.ClearSelected(true); + ctx.AddOrRemoveSelected(s, true); + EXPECT_EQ(view().get_selected_shps().size(), 1u); + EXPECT_EQ(view().get_selected_shps().front()->get_id(), s->get_id()); + } +} From b6aec51b78d54c7e41f46988832f3b6096c850a4 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 6 Aug 2026 18:28:47 -0600 Subject: [PATCH 2/3] Doc update --- .../gh-220-wasm-alt-drag-multiselect.md | 11 ++-- agents/plans/README.md | 2 +- .../{ => done}/sketch-mode-shape-faint.md | 0 agents/plans/wasm-alt-drag-multiselect.md | 59 ++++++++----------- 4 files changed, 31 insertions(+), 41 deletions(-) rename agents/plans/{ => done}/sketch-mode-shape-faint.md (100%) diff --git a/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md b/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md index 1e867fb1..264fc0f9 100644 --- a/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md +++ b/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md @@ -21,20 +21,23 @@ See https://github.com/trailcode/EzyCad/issues/220 ### Summary -Desktop: **Alt + LMB drag** rubber-band multi-selects shapes via OCCT `AIS_ViewController`. WASM: same gesture did not select multiple shapes because live Alt was never polled (`m_occt_window` null on Emscripten). +Desktop: **Alt + LMB drag** rubber-band multi-selects shapes via OCCT `AIS_ViewController`. WASM: same gesture did not multi-select because live Alt was never polled (`m_occt_window` null on Emscripten). Follow-up: stock `SelectRectangle` also missed some complex / STEP solids; Shape List current-group tint looked like selection. ### Fix (working tree) -- `Occt_view` stores non-owning `GLFWwindow* m_glfw_window` for modifier/cursor polling. -- Docs: `usage.md`, `usage-occt-view.md`, `gui.md`, CHANGELOG, plan `wasm-alt-drag-multiselect.md`. +- Non-owning `GLFWwindow* m_glfw_window` for modifier/cursor polling on WASM. +- After `SelectRectangle`, select displayed solids whose projected AABB intersects the band (GLFW/OCCT size mapping on WASM). +- Shape List: AIS selection tint vs weaker current-group tint. +- Docs: `usage.md`, `usage-occt-view.md`, `gui.md`, CHANGELOG, plan. ### Acceptance criteria - [x] Code path: WASM can poll Alt during drag (same as desktop) -- [ ] Manual WASM: Alt + LMB drag multi-selects like desktop +- [x] Manual WASM: Alt + LMB drag multi-selects like desktop (incl. complex solids under groups) - [x] Desktop gesture map unchanged - [x] Docs note Alt+drag (and Web Alt caveat) - [x] Plan updated: `agents/plans/wasm-alt-drag-multiselect.md` +- [ ] Close GitHub #220 ### Related diff --git a/agents/plans/README.md b/agents/plans/README.md index 3e7a0789..fe27ad0a 100644 --- a/agents/plans/README.md +++ b/agents/plans/README.md @@ -35,5 +35,5 @@ sketch-mode-shape-faint (done; parallel UX) | [shape-list-hierarchy-phase3.md](shape-list-hierarchy-phase3.md) | deferred | parent transform inheritance, Parts/planes, Boolean history (#214) | | [assembly-inspection-mode.md](assembly-inspection-mode.md) | deferred | assembly idle/inspection mode, Part vs arrange context, Move semantics | | [wasm-multithreading.md](wasm-multithreading.md) | planning | WASM/Emscripten pthreads, SharedArrayBuffer, parallel OCCT on web | -| [wasm-alt-drag-multiselect.md](wasm-alt-drag-multiselect.md) | implemented | WASM Alt+LMB drag rectangle multi-select (#220); verify on WASM then close | +| [wasm-alt-drag-multiselect.md](wasm-alt-drag-multiselect.md) | implemented | WASM Alt+LMB multi-select (#220); AABB pick supplement; close issue when ready | | [configurable-hotkeys.md](configurable-hotkeys.md) | done | remappable shortcuts, keybindings, `gui.hotkeys`, free Dimension off D | diff --git a/agents/plans/sketch-mode-shape-faint.md b/agents/plans/done/sketch-mode-shape-faint.md similarity index 100% rename from agents/plans/sketch-mode-shape-faint.md rename to agents/plans/done/sketch-mode-shape-faint.md diff --git a/agents/plans/wasm-alt-drag-multiselect.md b/agents/plans/wasm-alt-drag-multiselect.md index 12f6a08d..aca84428 100644 --- a/agents/plans/wasm-alt-drag-multiselect.md +++ b/agents/plans/wasm-alt-drag-multiselect.md @@ -16,55 +16,42 @@ github_issue: 220 On desktop, holding **Alt** and **left-dragging** in the 3D view activates OCCT `AIS_ViewController` rectangle selection (`SelectRectangle` / rubber band) and multi-selects shapes under the box. -On the WASM build the same gesture did **not** multi-select. +On the WASM build the same gesture did **not** multi-select (Alt never reached OCCT on mouse-move). Separately, stock `SelectRectangle` could miss complex / STEP solids even when simple root boxes were selected (desktop and web). -EzyCad maps modifiers into OCCT flags: +## Root causes (confirmed) -- `Occt_view::key_flags_from_glfw_` — `GLFW_MOD_ALT` → `Aspect_VKeyFlags_ALT` -- Mouse press/release: `PressMouseButton` / `ReleaseMouseButton` with `theMods` -- Mouse move: `UpdateMousePosition(..., key_flags_from_glfw_window_(), ...)` which polls Alt via `glfwGetKey` - -Related input/hotkey tracking: [#93](https://github.com/trailcode/EzyCad/issues/93), draft `agents/drafts/issues/active/gh-93-emscripten-web-hotkeys-followup.md`. OCCT desktop vs wasm kit: [occt-wasm-dual-version](../conventions/occt-wasm-dual-version.md). - -## Goal - -Make **Alt + LMB drag** multi-select on WASM match desktop, without changing desktop gesture mapping or selection schemes. - -## Root cause (confirmed in code) - -WASM `init_window` skipped creating `Occt_glfw_win` (correct: `Close()` would destroy the shared canvas). `key_flags_from_glfw_window_()` then always returned `NONE` because it only read modifiers from `m_occt_window`. OCCT rebinds when modifiers drop (`myMouseModifiers != theModifiers`), so Alt+LMB `SelectRectangle` became plain LMB orbit on the next move. +1. **WASM Alt polling** — `init_window` skipped `Occt_glfw_win` (correct: `Close()` would destroy the shared canvas). `key_flags_from_glfw_window_()` always returned `NONE`, so OCCT rebound Alt+LMB `SelectRectangle` to orbit on the next move. +2. **Sensitive-entity miss** — OCCT rubber-band pick skipped some imported BREPs; fixed by projecting document-solid AABBs into screen space after `SelectRectangle`. +3. **WASM pixel space** — map projected corners from OCCT/`Wasm_Window` size into GLFW cursor space; keep canvas size synced before project. +4. **Shape List UX** — current-group row used the same strong tint as AIS selection (looked like grouped solids stayed selected). ## Approach (landed) -### Phase 0 — reproduce and instrument - -- [x] Root cause identified from code path (null `m_occt_window` on WASM → no Alt on move). -- [ ] Manual WASM retest after fix (Chrome/Firefox): Alt+LMB box select; Ctrl+click still works; desktop unchanged. - -### Phase 1 — fix input path +### Phase 1 — input path -- [x] Store non-owning `GLFWwindow* m_glfw_window` in `Occt_view::init_window`. -- [x] Poll modifiers and cursor from `m_glfw_window` (`key_flags_from_glfw_window_`, `cursor_position_`). -- [x] Do not wrap WASM window in owning `Occt_glfw_win`. +- [x] Non-owning `GLFWwindow* m_glfw_window` for modifier/cursor polling (no owning `Occt_glfw_win` on WASM). +- [x] `handleSelectionPoly` + `select_shps_intersecting_screen_rect_` (overlap AABB; `Handle(...)` for OCCT 7.9.3 / 8). +- [x] Shape List: selection tint from AIS only; weaker tint for current group. ### Phase 2 — docs and parity -- [x] Document Alt+drag in `docs/usage.md` and `docs/usage-occt-view.md`; note web Alt/menu caveat. -- [x] `src/doc/gui.md` notes non-owning GLFW pointer for WASM modifier polling. -- [x] `CHANGELOG.md` `[Unreleased]` Fixed entry. -- [ ] Close #220 when verified on WASM. +- [x] `docs/usage.md`, `docs/usage-occt-view.md` (Alt+drag + web Alt caveat). +- [x] `src/doc/gui.md` (GLFW pointer, AABB supplement, Shape List tint). +- [x] `CHANGELOG.md` `[Unreleased]`. +- [x] Manual: native + WASM Alt+LMB (including complex solids under groups). +- [ ] Close #220 on GitHub when ready. ## Out of scope -- WASM pthreads / parallel OCCT ([wasm-multithreading.md](wasm-multithreading.md)). -- Broader #93 hotkey parity beyond what this gesture needs. +- WASM pthreads ([wasm-multithreading.md](wasm-multithreading.md)). +- Broader #93 hotkey parity. - Changing default OCCT mouse gesture map on desktop. ## Related code -| Area | Path | -| ---------------------------- | ----------------------------------------- | -| Modifier → OCCT flags | `src/gui_occt_view.cpp` (`key_flags_*`) | -| Mouse → view controller | `src/gui_occt_view.cpp` (`on_mouse_*`) | -| GLFW → GUI | `src/main.cpp`, `src/gui.cpp` | -| Draft | `agents/drafts/issues/active/gh-220-*.md` | +| Area | Path | +| ----------------------- | ------------------------------------------------------- | +| Modifier / cursor | `src/gui_occt_view.cpp` (`key_flags_*`, `cursor_position_`) | +| Rubber-band supplement | `handleSelectionPoly`, `select_shps_intersecting_screen_rect_` | +| Shape List tint | `src/gui.cpp` (`shape_list_`) | +| Draft | `agents/drafts/issues/active/gh-220-*.md` | From d7e25619687acc830b53d785e9fc4bace9f0a2d8 Mon Sep 17 00:00:00 2001 From: Trailcode Date: Thu, 6 Aug 2026 18:32:08 -0600 Subject: [PATCH 3/3] Doc --- .../gh-220-wasm-alt-drag-multiselect.md | 6 +++- .../gh-250-wasm-alt-drag-multiselect.md | 33 +++++++++++++++++++ agents/plans/wasm-alt-drag-multiselect.md | 3 +- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 agents/drafts/prs/active/gh-250-wasm-alt-drag-multiselect.md diff --git a/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md b/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md index 264fc0f9..d0bd556d 100644 --- a/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md +++ b/agents/drafts/issues/active/gh-220-wasm-alt-drag-multiselect.md @@ -1,6 +1,8 @@ --- github_issue: 220 +github_pr: 250 status: active +paired_draft: ../prs/active/gh-250-wasm-alt-drag-multiselect.md --- # WASM: Alt + LMB drag does not multi-select shapes @@ -37,10 +39,12 @@ Desktop: **Alt + LMB drag** rubber-band multi-selects shapes via OCCT `AIS_ViewC - [x] Desktop gesture map unchanged - [x] Docs note Alt+drag (and Web Alt caveat) - [x] Plan updated: `agents/plans/wasm-alt-drag-multiselect.md` -- [ ] Close GitHub #220 +- [x] PR opened: https://github.com/trailcode/EzyCad/pull/250 (`Fixes #220`) +- [ ] Close GitHub #220 (on PR merge) ### Related - Issue: https://github.com/trailcode/EzyCad/issues/220 +- PR: https://github.com/trailcode/EzyCad/pull/250 - Plan: `agents/plans/wasm-alt-drag-multiselect.md` - Related: #93 / `gh-93-emscripten-web-hotkeys-followup.md` diff --git a/agents/drafts/prs/active/gh-250-wasm-alt-drag-multiselect.md b/agents/drafts/prs/active/gh-250-wasm-alt-drag-multiselect.md new file mode 100644 index 00000000..0f1340d3 --- /dev/null +++ b/agents/drafts/prs/active/gh-250-wasm-alt-drag-multiselect.md @@ -0,0 +1,33 @@ +--- +github_issue: 220 +github_pr: 250 +status: active +paired_draft: ../issues/active/gh-220-wasm-alt-drag-multiselect.md +--- + +# PR - Trailcode/wasm-box-select-fix + +## Title + +Fix WASM Alt+LMB rectangle multi-select (#220) + +## Summary + +- WASM Alt+LMB rubber-band multi-select via non-owning `GLFWwindow*` modifier/cursor polling. +- AABB screen-rect supplement after `SelectRectangle` (complex / STEP solids; WASM OCCT↔GLFW mapping). +- Shape List current-group tint distinct from AIS selection. +- Docs + plan updated. + +## Related + +- Issue: https://github.com/trailcode/EzyCad/issues/220 +- PR: https://github.com/trailcode/EzyCad/pull/250 +- Branch: `Trailcode/wasm-box-select-fix` +- Plan: `agents/plans/wasm-alt-drag-multiselect.md` + +## Test Plan + +- [x] Desktop Alt+LMB (incl. complex solids under groups) +- [x] WASM Alt+LMB; Ctrl+click still works +- [x] Shape List current-group vs selection tint +- [ ] CI / `Grouped_solids_stay_displayed_and_selectable` diff --git a/agents/plans/wasm-alt-drag-multiselect.md b/agents/plans/wasm-alt-drag-multiselect.md index aca84428..3a7d3443 100644 --- a/agents/plans/wasm-alt-drag-multiselect.md +++ b/agents/plans/wasm-alt-drag-multiselect.md @@ -39,7 +39,8 @@ On the WASM build the same gesture did **not** multi-select (Alt never reached O - [x] `src/doc/gui.md` (GLFW pointer, AABB supplement, Shape List tint). - [x] `CHANGELOG.md` `[Unreleased]`. - [x] Manual: native + WASM Alt+LMB (including complex solids under groups). -- [ ] Close #220 on GitHub when ready. +- [x] PR: https://github.com/trailcode/EzyCad/pull/250 (`Fixes #220`). +- [ ] Close #220 on merge. ## Out of scope