diff --git a/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md b/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md
index a9659ac4b0..b450835daa 100644
--- a/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md
+++ b/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md
@@ -262,9 +262,9 @@ and a band is not a "gesture" in the `CadLevel::Gesture` sense because nothing h
|---|---|---|---|---|---|
| `Move` | — | `update_hover` / `update_solid_hover` (**non-consuming**, returns false) | — | passthrough | hover only asks for a repaint, `:10022-10025` |
| `LeftDown` | orbit may begin | latch press, **return false** | — | consume-or-orbit is the canvas's call | `:10058-10063`; consuming here killed orbit once already |
-| `LeftDrag` > 8 px | — | start + drive band, **consume** | — | no longer orbits in this canvas | middle-drag orbits, right-drag pans (`:10013-10016`) |
+| `LeftDrag` > 8 px | — | start + drive band, **consume**, when Shift is held or the left button has no camera action | — | otherwise the left button's drag action (Preferences > Control) | the band is Prepare's Shift+left-drag rectangle selection |
| `LeftUp` | — | commit pick **or** resolve band | — | — | `:10035-10041` |
-| `MiddleDrag` / `RightDrag` | — | must not see it | — | orbit / pan | camera gestures never reach the FSM |
+| `MiddleDrag` / `RightDrag` | — | must not see it | — | the button's drag action (Preferences > Control) | camera gestures never reach the FSM; a right press reaches the tool only once its release shows it was a click |
| `RightClick` | — | — | offer menu | — | `snaporca-xmh6` open: a right-click that only clears the sketch selection eats the offer |
| `Esc` | — | — | `escape()` ladder | — | one route whatever holds focus (`:4228-4231`) |
| `Del` / `Backspace` | — | `delete_selected_or_last_sketch_entity()` | char hook | — | `:4260` |
diff --git a/docs/HLSD/design-tab.md b/docs/HLSD/design-tab.md
index c4bb9fd297..9b8010ce2c 100644
--- a/docs/HLSD/design-tab.md
+++ b/docs/HLSD/design-tab.md
@@ -135,7 +135,7 @@ contract between them is stated in code rather than spread across handlers.
| --- | --- | --- |
| `Transient` | a value field or a popup menu | closes it; the tool stays armed |
| `Gesture` | an uncommitted delta — an entity being drawn, a body being dragged | reverts it; committed work is untouched |
-| `Tool` | a feature card, an armed sketch tool, a constrain session | exits it; drawn entities survive |
+| `Tool` | a feature card, Sketch waiting for its plane, an armed sketch tool, a constrain session | exits it; drawn entities survive |
| `Idle` | nothing transient | clears the selection, a Feature tree or Bodies row included; leaves a sketch session only if it is empty |
`cad_escape_level()` is a `constexpr` free function over a POD of four booleans rather than a
@@ -149,26 +149,66 @@ explicit selection, the sketch ribbon's Cancel, which asks first, or `Ctrl+Z`. A
*session* is deliberately not a `Tool` level; it is the environment the `Idle` level lives in,
which makes the destructive path unrepresentable rather than merely unlikely.
+A body Move is the one `Gesture` that outlives the press: its gizmo stays up between drags until
+Confirm keeps the placement or `Esc` or Cancel puts the body back. Until then the selection is
+held — a click off the gizmo only steers the camera — and undo is refused, since the placement
+is not in the history. Anything that starts another edit (a feature card, a sketch, placing
+imported art or text, another body's Move, a rebuild) keeps the placement, as switching gizmos
+keeps a move in Prepare. The panel ends the Move in one place (`DesignPanel::end_body_move`), so
+the gizmo, the Move / Rotate card and the ✓/✗ cannot outlive one another.
+
Right-click is read at button-up against one budget, 3 px of drift, applied to the whole press
rather than to its end points: a press that wandered past the budget at any moment is
navigation, even if it comes back to where it started, which is what stops a slow, careful
orbit from ending in a menu. There is no time budget — a gesture that means something different
when it is slow is exactly what the interaction charter rules out. The raycast uses the press
-position, not the release. An armed sketch tool that already consumed the right
-button (to terminate a chain, say) declines to also open a menu, through a read-and-clear flag.
-Past either budget the event is navigation, and navigation does not transition the state
-machine.
+position, not the release. The sketch tool sees a right press only once the release has shown it
+was a click: the press itself goes to the camera, which may pan or orbit with that button, and the
+canvas replays it to the tool on a stationary release. A tool that uses the click (to terminate a
+chain, say) keeps the menu closed. Past either budget the event is navigation, and navigation
+does not transition the state machine.
Navigation itself is Prepare's: the camera reads the drag actions set in Preferences > Control
-for each button. The left button is shared with picking, so a whole body is swept with a
-rectangle on plain left-drag only while no camera action is assigned to it, and with
-Shift+left-drag otherwise — Prepare's own rectangle selection.
+for each button, and in the Touchpad camera style a move with Alt held orbits and one with Shift
+held pans, whatever tool is armed. The left button is shared with picking and drawing, so a tool
+handle or a press that draws takes it first, as a gizmo does in Prepare; a whole body is swept
+with a rectangle on plain left-drag only while no camera action is assigned to the left button,
+and with Shift+left-drag otherwise — Prepare's own rectangle selection.
-Entering a sketch changes three things at once so the mode is legible: a banner above the
+Entering a sketch changes two things at once so the mode is legible: a banner above the
canvas (a sibling of the canvas, not a child over it — on GTK a child window over a
-`wxGLCanvas` is a native window and does not reliably stack over GL), the printer bed muted so
-a plate grid is never read as a sketch grid, and `N` to look normal to the plane. Code that
-changes any of the three belongs with a change to this section.
+`wxGLCanvas` is a native window and does not reliably stack over GL), and `N` to look normal to
+the plane. The printer bed stays: there is no sketch grid, so the plate grid is the only ground
+reference a sketch has. Code that changes either belongs with a change to this section.
+
+Sketch mode is never entered without a plane under it, so the banner, the sketch keys and the
+sketch offer always have a session to act on. Sketch on a picked flat face or reference plane opens
+the session on it at once. With nothing picked it stays in Feature mode and waits for one — an
+armed `Tool`, left with `Esc` or ✗, and ended by anything that starts another edit — and the
+reference plane or flat face clicked next opens the session. A picked plane is a selection like a
+face: the sketch on it uses it up, and `Esc` or a click on nothing lets go of it, so a plane that
+can no longer be seen never decides where the next sketch goes.
+
+The reference planes — XY, XZ and YZ through the modeling origin, with their half-axes — are
+drawn on demand, because three translucent squares over every model are noise once they are not
+the thing being picked. Sketch brings them up while it waits for a plane, which is exactly when
+they are picked, and the session the pick opens takes them away; a live session draws none. The
+Feature tree's Origin row keeps them up outside a sketch. Its state is a view preference in
+AppConfig rather than part of the recipe, so it costs the project format nothing. The Plane tool
+keeps its own rule: the planes and the datums as Offset bases, and nothing for the other methods,
+where a click on a plane would rewrite the datum's references. The `P` and `A` keys are a
+separate, unpickable view helper and do not follow the Origin row.
+
+The Bed row, under the Origin row, is the printer bed's switch in the same way: it draws or hides
+the bed and its plate grid in every mode. It is a view preference in AppConfig too, and the bed is
+shown until it is turned off.
+
+The two rows are view switches, not history, and the tree says so: they sit unframed on the Feature
+tree's card, above the features' own framed list, and stay put while the features scroll. A click
+never selects either row, since a selected Origin or Bed would have nothing to edit, move or
+delete; the eye and the right-click menu are the only targets, and a row's label dims while its
+thing is hidden, as a hidden body's does. Because the block never takes the focus, `Ctrl+Shift+O`
+and `Ctrl+Shift+B` flip the Origin and the Bed from the keyboard.
## Rendering the bodies
@@ -189,6 +229,15 @@ degenerate edges are left out (`GeometryEngine::display_edges`), and the polylin
once per shape, keyed by its `TShape`, because a recompute that leaves a body unchanged is the
common case.
+While a feature card is open, its preview ghost is the whole model the candidate would produce,
+drawn translucent over the bodies, so every face the feature leaves alone is in both at the same
+depth. The ghost is drawn with a depth bias that pushes it back (`GLVolume::depth_bias`), so on a shared face
+the body always wins instead of the two copies z-fighting, and the ghost shows only where the
+result reaches past the bodies. Material a feature removes lies inside the old solid and would not
+show at all, so the tools whose result mostly coincides with the body — Fillet/Chamfer, Draft,
+Hole and the Mate hover — hide the bodies once the preview is valid and draw the result alone,
+opaque.
+
## Showing what is selected
A selection is drawn on the faces it names, never as a tint over the body: a translucent
@@ -214,7 +263,17 @@ replay costs up to a recompute, so the panel finds the faces once per row and to
generation, off the UI thread, and only while no feature card is open. One selection is live at
a time: a viewport pick clears the feature row and a feature row clears the viewport pick, as the
Feature tree and Bodies list do between themselves. `Esc`, a click on empty space and an
-empty rubber band all let go of it, whichever list or pick made it.
+empty rubber band all let go of it, whichever list or pick made it — except while a body Move is
+open, which holds the selection until it ends (see the interaction contract).
+
+Zoom to selection, on a Feature tree or Bodies row and in its right-click menu, frames one thing
+along the current view direction, as the canvas's Fit button frames the selection. On a body it
+frames the body whole, hidden or not, from its display mesh: the viewport never selects a hidden
+body, so the selection cannot stand in for it. On a sketch it frames the sketch's own geometry,
+drawn, consumed or suppressed; on any other feature, the faces the feature made, found as the row
+highlight finds them and from the same cache. From the offer it frames whatever the selection is,
+a body again included. A feature that is not a sketch and makes no faces, such as a datum plane
+or a suppressed Extrude, has no Zoom to selection.
## Following the app
@@ -231,9 +290,21 @@ The tab is a page of Orca's main window and answers to the same settings as Prep
- **Sidebar icons.** Every clickable icon in the sidebar shows a hover chip. The card-header and
constraint-row buttons are Orca's self-painted `Button`, because a native button cannot take a
hover background on macOS. The Feature tree and Bodies lists are a custom-drawn
- `DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Edit,
- Show/hide and Delete on a feature, Move, Show/hide and Delete on a body — and the eye shows
- whether that row is hidden.
+ `DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Zoom to
+ selection, Edit, Show/hide and Delete on a feature, Zoom to selection, Move, Show/hide and
+ Delete on a body, and only Show/hide on the Origin and Bed rows, a separate non-selectable list
+ above the features — and the eye shows whether that row is hidden. A feature with nothing to
+ frame keeps a blank cell where Zoom to selection would be, so every icon stays in its column.
+- **Plates.** This is the one thing the tab does not follow. The canvas has a bed of its own at
+ the printer bed's home position, whichever plate Prepare has current, and a new document's
+ modeling origin is that bed's centre. A bed that followed the current plate would slide out
+ from under a design: the origin is fixed once per document, baked into every sketch plane and
+ saved in the recipe, while the current plate can change between visits. Commit to Plate does
+ not need it either, since the committed object is placed on an empty spot of the current
+ plate. What the canvas does read from the plate is moved onto its bed: the exclude areas, the
+ plate box the camera orbits about when nothing is picked (`GLCanvas3D::_current_plate_box`),
+ and the first view, which starts from Prepare's camera turned to the tab's iso view: the CAD
+ isometric from the front-right corner that Home returns to, not Prepare's front-left one.
- **Viewport text.** The status line and the active tool's values are drawn by the canvas in
its ImGui pass, so they go with the canvas: a top-level window over GL does not follow its
frame and was left floating over other applications.
@@ -256,12 +327,12 @@ permanent row index, verbs that do not apply shown disabled **in place with thei
rather than removed. The invariant is that a verb's row index is identical in every selection
where it appears and that adding a verb never moves an existing one — the hand learns the
position, so the menu is never re-sorted, compacted or adaptively ordered. Above the families
-sits one *flat* row, holding Rename and Color — what a selection is opened for most: its verbs are
-items of their own at the top of the menu rather than a family's submenu. It is appended after
-the eight, so it moved no existing index, and it reads the same from the viewport and from a row
-of the Bodies list.
+sits one *flat* row, holding Rename, Color and Zoom to selection — what a selection is opened for
+most: its verbs are items of their own at the top of the menu rather than a family's submenu. It
+is appended after the eight, so it moved no existing index, and it reads the same from the
+viewport and from a row of the Bodies list.
-An invariant across 92 verbs and 20 selection kinds does not survive by review, so the map
+An invariant across 93 verbs and 20 selection kinds does not survive by review, so the map
exists once, as data: `scripts/CAD/tool_atlas.json` carries every verb with its row, key, icon,
accepted selections, preconditions and refusal string, and `scripts/CAD/gen_offer_table.py`
emits `src/slic3r/GUI/CAD/DesignOffer.hpp` from it. The header is checked in and never
diff --git a/resources/images/design_bed.svg b/resources/images/design_bed.svg
new file mode 100644
index 0000000000..a9441e7b5d
--- /dev/null
+++ b/resources/images/design_bed.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_export.svg b/resources/images/design_export.svg
new file mode 100644
index 0000000000..b998ea19fc
--- /dev/null
+++ b/resources/images/design_export.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_import_mesh.svg b/resources/images/design_import_mesh.svg
new file mode 100644
index 0000000000..7517578578
--- /dev/null
+++ b/resources/images/design_import_mesh.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_new.svg b/resources/images/design_new.svg
new file mode 100644
index 0000000000..dfe7425de4
--- /dev/null
+++ b/resources/images/design_new.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_new_sketch.svg b/resources/images/design_new_sketch.svg
new file mode 100644
index 0000000000..a645cf1dff
--- /dev/null
+++ b/resources/images/design_new_sketch.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_step.svg b/resources/images/design_step.svg
index a4a92b7b20..45974dfd77 100644
--- a/resources/images/design_step.svg
+++ b/resources/images/design_step.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_zoom.svg b/resources/images/design_zoom.svg
new file mode 100644
index 0000000000..e9048dcff4
--- /dev/null
+++ b/resources/images/design_zoom.svg
@@ -0,0 +1 @@
+
diff --git a/scripts/CAD/check-gui-click-edit.py b/scripts/CAD/check-gui-click-edit.py
index 159bdb19f1..5eb9b3c4f2 100755
--- a/scripts/CAD/check-gui-click-edit.py
+++ b/scripts/CAD/check-gui-click-edit.py
@@ -147,6 +147,7 @@ def seed_datadir(datadir):
# ~90% case expects. A ladder whose result depends on the developer's own preferences is not
# a gate.
app["auto_close_sketch_loops"] = True
+ app["design_show_bed"] = True
# SILENCE THE NETWORK PLUGIN PROMPT. Without this, GUI_App::post_init() re-raises "Bambu
# Network Plug-in Required" from an IDLE event — after any modal sweep this driver does at
# startup — and ShowModal() then runs a nested event loop. The app is alive, its window is
@@ -480,10 +481,14 @@ def type_into_open_field(value, mark):
def enter_sketch(timeout=180):
"""Open a real sketch on a real plane, and PROVE it with the socket before drawing anything.
- THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. Shift+S enters sketch MODE and pops the plane
- offer; the offer must be dismissed; and the plane itself is chosen by clicking it in the
- viewport BEFORE Shift+S. check-gui-sketching.py has always done all four steps. This ladder
- did two of them — Design tab, then Shift+S — and went straight to the tool letters.
+ THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. The reference planes are hidden until Shift+S,
+ with nothing picked, brings them up to choose from, so the plane is clicked AFTER it. That
+ click opens the session on the plane and pops the tool offer, and a click outside a popup
+ menu only closes it, so the spot is clicked twice, apart: the second click closes the offer,
+ and inside the session it is empty canvas. No Escape — with no offer up, Escape in an empty
+ sketch leaves it. Then a tool letter arms the tool. check-gui-sketching.py does the same
+ steps. This ladder once did two of them — Design tab, then Shift+S — and went straight to
+ the tool letters.
That intermediate state is the trap. `is_sketching` reads 1, every tool key is accepted and
traced, and not one click draws anything, because there is no plane under them. The ladder
@@ -499,10 +504,10 @@ def enter_sketch(timeout=180):
click(132, 53) # Design tab
time.sleep(2.0)
dismiss_modals()
- click(*PLANE_PX) # pick the plane IN THE VIEWPORT — before Shift+S
- key("shift+s", 1.0)
- key("Escape", 0.5) # entering sketch mode pops the offer; dismiss it
- key("p", 0.6) # any sketch tool starts the session on that plane
+ key("shift+s", 1.0) # nothing picked: the reference planes come up
+ click(*PLANE_PX, pause=0.8) # picks the plane IN THE VIEWPORT: session open, offer up...
+ click(*PLANE_PX) # ...and this closes the offer
+ key("p", 0.6) # arms Point in the session
if try_call("sketch_describe") is not None:
# NO Escape here. Every rung already opens with one to drop whatever tool the last
# one left armed, and Escape in the Design tab walks a LIFO: first press drops the
@@ -511,14 +516,15 @@ def enter_sketch(timeout=180):
# reported all eleven checks failed with "nothing opened" — the tools were arming
# into an empty Feature-mode document.
return
- die("no sketch opened after plane click + Shift+S within "
+ die("no sketch opened after Shift+S + plane click within "
f"{timeout}s — sketch_describe never answered on {A.sock} (trace {TRACE})")
# tool key, the clicks that draw it, and one distinct value per queued field. The values are
# deliberately nothing like the as-drawn size, so a committed prefill cannot coincide with them.
-# Where the plane label sits in the viewport before a sketch is open. Same constant the gesture
-# ladder uses; it is a label on the 3D view, not a widget, so it moves only if the camera does.
+# Where the plane label sits in the viewport once Shift+S has brought the planes up. Same
+# constant the gesture ladder uses; it is a label on the 3D view, not a widget, so it moves only
+# if the camera does.
PLANE_PX = (913, 359)
# Every coordinate below stays inside 1000..1400 x 500..760 — the box check-gui-sketching.py's
diff --git a/scripts/CAD/check-gui-sketching.py b/scripts/CAD/check-gui-sketching.py
index d4ebd1f1ca..df83803819 100644
--- a/scripts/CAD/check-gui-sketching.py
+++ b/scripts/CAD/check-gui-sketching.py
@@ -229,7 +229,20 @@ CHROME_DY = int(os.environ.get("ORCA_CAD_CHROME_DY", "26"))
DESIGN_TAB = (128, 29 + CHROME_DY)
-# Feature-tree rows, measured on the rig at 1920x1080: first row centre, then 23 px apart.
+# The ribbon's document group lost its Bed checkbox and label when the bed toggle moved to the
+# Feature tree, and every group to its right moved left by their width: wxLEFT 6 + the 18 px
+# CheckBox + wxLEFT 4 + the "Bed" label (~24 px) + wxRIGHT 4, about 56 px. The x of
+# CONSTRUCTION_CHECKBOX, CON_BTN and CON_BTN_SKETCH below is the one measured before that, plus
+# this. DERIVED, NOT MEASURED: re-measure on the rig and fold it in. CONFIRM_BTN sits after the
+# ribbon's stretch spacer and did not move.
+RIBBON_DX = -56
+
+# Feature-tree rows, measured on the rig at 1920x1080: the list's first row is centred at y=215,
+# then 23 px apart. The first two rows are the Origin and the Bed, unframed on the card above the
+# features' own framed list, so the first FEATURE row sits two rows and TREE_GAP further down;
+# reset_document and the reopening double-click both need a feature. TREE_GAP is the list's
+# 12 px top margin plus its 1 px frame, derived rather than measured -- re-measure on the rig, like
+# RIBBON_DX.
# x=300, not the label: a second click ON the label opens the inline rename, and Delete then
# edits the text instead of removing the feature.
#
@@ -237,7 +250,8 @@ DESIGN_TAB = (128, 29 + CHROME_DY)
# unshifted click lands 26 px BELOW the first row -- just past its 23 px height -- so the row is
# never selected, Delete does nothing, and reset_document spends 40 rounds on it before dying
# with "could not empty the feature tree". That names the feature tree, which is not the fault.
-TREE_ROW0 = (300, 215 + CHROME_DY)
+TREE_GAP = 12 + 1
+TREE_ROW0 = (300, 215 + 2 * 23 + TREE_GAP + CHROME_DY)
def go_design():
@@ -266,18 +280,21 @@ def reset_document():
def enter_sketch(tool_key, plane_px=(913, 359)):
"""Enter a sketch the way the design law says: pick the plane in the viewport, then the tool.
- Shift+S enters sketch MODE and pops the offer; Escape dismisses it; the tool letter then
- starts the session on the plane the click selected. All four steps are real input — nothing
- here goes through the socket.
+ leave_sketch()'s Escapes let go of every pick, so Shift+S finds nothing to sketch on and only
+ brings the reference planes up; the click on one opens the session on it and pops the tool
+ offer. A click outside a popup menu only closes it, so the same spot is clicked again, apart:
+ inside the session it is empty canvas, so it selects nothing if the offer is slow to appear.
+ No Escape: with no offer up, Escape in an empty sketch leaves it. The tool letter then arms
+ the tool. All of it is real input — nothing here goes through the socket.
"""
leave_sketch()
- click(*plane_px)
key("shift+s", 0.8)
- key("Escape", 0.4) # entering sketch mode pops the offer; dismiss it
+ click(*plane_px, pause=0.8) # picks the plane: the session opens and the offer pops...
+ click(*plane_px) # ...and this closes the offer
key("p", 0.6)
if try_call("sketch_describe") is None:
shot("/shots/gl-enter-failed.png")
- die("no sketch opened after plane click + Shift+S (see /shots/gl-enter-failed.png)")
+ die("no sketch opened after Shift+S + plane click (see /shots/gl-enter-failed.png)")
calibrate_here() # THIS sketch's own camera map, on THIS sketch's own plane
key(tool_key, 0.6)
@@ -737,7 +754,7 @@ def corner_pair(ents):
die("no adjacent pair in what should be a rectangle")
-CONSTRUCTION_CHECKBOX = (419, 75 + CHROME_DY)
+CONSTRUCTION_CHECKBOX = (419 + RIBBON_DX, 75 + CHROME_DY)
def draw_line(x0, y0, x1, y1, length, angle):
@@ -866,7 +883,7 @@ def rung_extend():
# because the ladder only ever clicks "perpendicular" (3) and "equal" (5), both of which sit
# before the first insertion. The next rung to use "tangent" would have clicked "collinear".
CON_BTN_Y = 76 + CHROME_DY
-CON_BTN = {n: (449 + 42 * i, CON_BTN_Y) for i, n in enumerate(
+CON_BTN = {n: (449 + RIBBON_DX + 42 * i, CON_BTN_Y) for i, n in enumerate(
["horizontal", "vertical", "parallel", "perpendicular", "coincident", "equal",
"equal_radius", "collinear", "concentric", "tangent", "midpoint", "symmetric",
"sym_v", "sym_h", "angle", "radius", "diameter", "fix", "dist_x", "dist_y"])}
@@ -880,7 +897,7 @@ CON_BTN = {n: (449 + 42 * i, CON_BTN_Y) for i, n in enumerate(
# which already carries CHROME_DY), but the X start depends on how wide the sketch toolbar to the
# left of this group renders, and this fork keeps mainline's top row. Re-measure before trusting
# D11 here: screenshot in sketch mode and detect the icon columns, do not derive it by offset.
-CON_BTN_SKETCH = {n: (677 + 42 * i, CON_BTN_Y) for i, n in enumerate(
+CON_BTN_SKETCH = {n: (677 + RIBBON_DX + 42 * i, CON_BTN_Y) for i, n in enumerate(
["horizontal", "vertical", "parallel", "perpendicular", "coincident", "equal",
"equal_radius", "collinear", "concentric", "tangent", "midpoint", "symmetric",
"sym_v", "sym_h", "angle", "radius", "diameter", "fix", "dist_x", "dist_y"])}
diff --git a/scripts/CAD/tool_atlas.json b/scripts/CAD/tool_atlas.json
index 1e7f1bf968..af0653feff 100644
--- a/scripts/CAD/tool_atlas.json
+++ b/scripts/CAD/tool_atlas.json
@@ -229,11 +229,11 @@
"none"
],
"needs": {},
- "refusal": "Click a face or a reference plane in the viewport, then a sketch tool",
+ "refusal": "Select a flat face or a reference plane to sketch on",
"gui": true,
"action": "key:S+S",
"icon": "design_sketch",
- "hint": "Click a face or a reference plane, then pick a drawing tool"
+ "hint": "Sketch on the selected flat face or plane, or click one next"
},
{
"id": "extrude",
@@ -1113,6 +1113,32 @@
"icon": "color_palette",
"hint": "Set the selected body's display color"
},
+ {
+ "id": "zoom_to",
+ "name": "Zoom to selection",
+ "slot": "top",
+ "key": null,
+ "feature": null,
+ "mcp": null,
+ "mode": "model",
+ "accepts": [
+ "body_solid",
+ "body_sheet",
+ "face_planar",
+ "face_cyl",
+ "face_other",
+ "edge_str",
+ "edge_circ",
+ "vertex",
+ "sk_loop"
+ ],
+ "needs": {},
+ "refusal": null,
+ "gui": true,
+ "action": "btn:zoom_to",
+ "icon": "design_zoom",
+ "hint": "Frame the selection in the view, keeping the view direction"
+ },
{
"id": "delete",
"name": "Delete",
diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp
index ebaa17a4b7..984524b0ba 100644
--- a/src/libslic3r/AppConfig.cpp
+++ b/src/libslic3r/AppConfig.cpp
@@ -370,6 +370,10 @@ void AppConfig::set_defaults()
// restores the conventional CAD representation for users who expect it (x0kd).
if (get("design_connector_face_glyph").empty())
set_bool("design_connector_face_glyph", true);
+
+ // Design tab: draw the printer bed and its plate grid (the Feature tree's Bed row).
+ if (get("design_show_bed").empty())
+ set_bool("design_show_bed", true);
#endif
//#ifdef SUPPORT_SHOW_HINTS
diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp
index 597b960710..77010cbf9d 100644
--- a/src/slic3r/GUI/3DScene.cpp
+++ b/src/slic3r/GUI/3DScene.cpp
@@ -310,6 +310,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
, force_native_color(false)
, force_neutral_color(false)
, force_sinking_contours(false)
+ , depth_bias(false)
, picking(false)
, tverts_range(0, size_t(-1))
{
@@ -1332,11 +1333,17 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
shader->set_uniform("projection_matrix", projection_matrix);
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
+ if (volume.first->depth_bias) {
+ glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
+ glsafe(::glPolygonOffset(1.0f, 1.0f));
+ }
//BBS: add outline related logic
if (volume.first->selected && shader_can_outline && GUI::wxGetApp().show_outline())
volume.first->render_with_outline(cnv_size);
else
volume.first->render();
+ if (volume.first->depth_bias)
+ glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
#if ENABLE_ENVIRONMENT_MAP
if (use_environment_texture)
diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp
index 663979690f..664cd109cf 100644
--- a/src/slic3r/GUI/3DScene.hpp
+++ b/src/slic3r/GUI/3DScene.hpp
@@ -232,6 +232,10 @@ public:
bool force_neutral_color : 1;
// Whether or not to force rendering of sinking contours
bool force_sinking_contours : 1;
+ // Orca: draw this volume with a positive depth bias (glPolygonOffset, pushed away from the
+ // camera), so on a surface it shares with another volume the other volume wins the depth
+ // test instead of z-fighting it
+ bool depth_bias : 1;
// Is render for picking
bool picking : 1;
// slice error
diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp
index 0e7296ccd4..0489df35c1 100644
--- a/src/slic3r/GUI/CAD/DesignCanvas.cpp
+++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp
@@ -8,6 +8,7 @@
#include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
+#include "slic3r/GUI/PartPlate.hpp" // the current plate's origin: the first view's offset
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLToolbar.hpp"
#include "slic3r/GUI/Event.hpp"
@@ -58,6 +59,16 @@ extern wxPopupWindow* wxCurrentPopupWindow;
namespace Slic3r {
namespace GUI {
+// The Design tab's iso view is the CAD one: a true isometric from the front-right corner, which
+// shows the navigator's Front, Right and Top faces. Camera's own "iso" looks from the front-left
+// at 45 degrees and stays Prepare's and the thumbnails'.
+static void select_iso_view(Camera& camera)
+{
+ const Vec3d target = camera.get_target();
+ camera.look_at(target + camera.get_distance() * Vec3d(1., -1., 1.).normalized(), target, Vec3d::UnitZ());
+ camera.auto_type(Camera::EType::Perspective);
+}
+
DesignCanvas::DesignCanvas(wxWindow* parent)
: wxPanel()
{
@@ -91,7 +102,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
m_canvas->enable_plate_chrome(false);
m_canvas->enable_labels(false);
m_canvas->enable_sinking_contours(false); // they would be sliced from the plater's meshes
- m_canvas->set_axes_at_bed_center(true); // triad at bed centre = modeling origin
+ m_canvas->set_design_canvas(true); // home-position bed, triad and CAD grid at the modeling origin
m_canvas->set_design_sketch_tool(&m_sketch_tool);
m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) {
@@ -190,8 +201,13 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
refresh_bed();
// The view this canvas opens on. Built lazily, on the way into the Design tab, so this
- // is the view the user is looking at right now.
+ // is the view the user is looking at right now — moved off the current plate onto the
+ // Design bed, which stays at the printer bed's home whichever plate is current — turned to
+ // the iso view Home returns to, keeping its target and zoom.
m_parked_camera = wxGetApp().plater()->get_camera();
+ if (PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate())
+ m_parked_camera.translate_world(-plate->get_origin());
+ select_iso_view(m_parked_camera);
// Before any of this class's own Binds below: wx calls dynamically bound handlers in
// reverse order of binding, and GLCanvas3D swallows several events without skipping them —
@@ -345,7 +361,7 @@ void DesignCanvas::repaint_now()
m_canvas_widget->Update(); // service the pending paint immediately (a modal popup owns the loop)
}
-void DesignCanvas::reload(bool keep_view)
+void DesignCanvas::reload()
{
m_canvas->reset_volumes();
@@ -378,21 +394,19 @@ void DesignCanvas::reload(bool keep_view)
v->set_color(c);
}
} else if (obj_idx == 1) {
- // The ghost is normally a faint blue overlay on the visible body. In preview-only
- // mode it IS the result (base bodies hidden), so render it opaque so it reads as a
- // finished solid rather than a see-through hint.
+ // The ghost is the whole resulting model, normally drawn as a faint blue overlay on
+ // the visible bodies. Every face the feature leaves alone is in both, at the same
+ // depth, so the ghost is drawn with a depth bias: the bodies win on those faces instead
+ // of the two copies z-fighting, and the ghost shows only where the result reaches past
+ // the bodies. In preview-only mode it IS the result (base bodies hidden), so render it
+ // opaque so it reads as a finished solid rather than a see-through hint.
v->set_color(m_body_hidden ? ColorRGBA(0.40f, 0.82f, 1.0f, 1.0f) : ghost);
+ v->depth_bias = true;
}
}
- if (!keep_view) {
- if (m_first_frame && !m_model.objects.empty()) {
- m_canvas->select_view("iso");
- m_canvas->zoom_to_volumes();
- m_first_frame = false;
- }
- }
-
+ // The camera stays where the user left it, even for the first body: Home and a double-click
+ // fit on demand.
m_canvas->set_as_dirty();
if (m_canvas_widget)
m_canvas_widget->Refresh();
@@ -406,18 +420,21 @@ void DesignCanvas::set_bodies(const std::vector* body_meshes,
if (body_meshes == nullptr || body_meshes->empty()) { clear_mesh(); return; }
m_body_meshes = body_meshes;
+ m_sketch_tool.refresh_body_edges(); // of the bodies set_solid_pick() pointed the tool at
m_lit_faces = m_sketch_tool.selected_faces();
rebuild_bodies();
- reload(!m_first_frame);
+ reload();
}
void DesignCanvas::clear_mesh()
{
m_body_meshes = nullptr;
m_volumes.clear();
+ // No solid, so no edge lines, pick or hover either, as after a rebuild that leaves no body.
+ m_sketch_tool.set_solid_pick(nullptr, nullptr, nullptr, nullptr);
if (!m_model.objects.empty()) {
m_model.delete_object((size_t)0);
- reload(true);
+ reload();
}
}
@@ -431,14 +448,14 @@ void DesignCanvas::set_preview_mesh(const TriangleMesh& mesh)
obj->add_volume(mesh);
obj->add_instance();
- reload(true);
+ reload();
}
void DesignCanvas::clear_preview()
{
if (m_model.objects.size() > 1) {
m_model.delete_object((size_t)1);
- reload(true);
+ reload();
}
}
@@ -452,10 +469,23 @@ void DesignCanvas::fit_view()
}
}
+bool DesignCanvas::zoom_to_box(BoundingBoxf3 box)
+{
+ if (m_canvas == nullptr || !box.defined)
+ return false;
+ DesignSketchTool::pad_box(box);
+ m_canvas->zoom_to_box(box);
+ request_repaint();
+ return true;
+}
+
void DesignCanvas::set_view(const std::string& view_name)
{
if (m_canvas) {
- m_canvas->select_view(view_name);
+ if (view_name == "iso")
+ select_iso_view(wxGetApp().plater()->get_camera()); // the Design camera while the tab is shown
+ else
+ m_canvas->select_view(view_name);
m_canvas->zoom_to_volumes();
m_canvas->set_as_dirty();
if (m_canvas_widget)
@@ -470,13 +500,6 @@ void DesignCanvas::begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode
if (m_canvas_widget) m_canvas_widget->Refresh();
}
-void DesignCanvas::set_sketch_plane(const SketchPlane& plane)
-{
- m_sketch_tool.set_plane(plane); // keeps the 2D entities; only the carrier plane changes
- if (m_canvas) m_canvas->set_as_dirty();
- if (m_canvas_widget) m_canvas_widget->Refresh();
-}
-
void DesignCanvas::edit_sketch(const std::vector& entities,
const std::vector& constraints,
const SketchPlane& plane)
@@ -550,6 +573,7 @@ void DesignCanvas::refresh_bed()
double printable_height = 100.0;
const auto* ph_opt = config->opt("printable_height");
if (ph_opt) printable_height = ph_opt->value;
+ // No position: the Design bed stays at the printer bed's home, whichever plate is current.
m_bed.set_shape(bed_shape_opt->values, printable_height, {}, {}, "", false); // mainline added extruder_areas/heights params
}
@@ -731,8 +755,6 @@ void DesignCanvas::clear_move_gizmo()
request_repaint();
}
-bool DesignCanvas::moving_body() const { return m_sketch_tool.moving_body(); }
-
void DesignCanvas::set_on_body_move_changed(std::function cb)
{
m_sketch_tool.on_body_move_changed = std::move(cb);
@@ -1025,6 +1047,11 @@ void DesignCanvas::set_on_datum_base_picked(std::function cb)
m_sketch_tool.on_datum_base_picked = std::move(cb);
}
+void DesignCanvas::set_selected_base(std::function cb)
+{
+ m_sketch_tool.selected_base = std::move(cb);
+}
+
void DesignCanvas::set_on_sketch_exit(std::function cb)
{
m_sketch_tool.on_exit = std::move(cb);
@@ -1047,15 +1074,18 @@ void DesignCanvas::set_on_context_menu(std::function cb)
return;
m_ctx_bound = true;
// Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event.
- // It only consumes when it actually opens the offer; every other right-click still falls
- // through to the polyline-chain end and the move gizmo, which were there first.
- // Right-drag pans. Without remembering where the press landed, every pan ended by popping
- // the offer over wherever the camera stopped — the menu appearing as the reward for moving
- // the view. The offer is the release of a STATIONARY right-click (kCadRightClickDriftPx).
+ // It only consumes when it actually opens the offer.
+ // Right-drag may pan or orbit (Preferences > Control). Without remembering where the press
+ // landed, every such drag ended by popping the offer over wherever the camera stopped — the
+ // menu appearing as the reward for moving the view. A right-click is the release of a
+ // STATIONARY press (kCadRightClickDriftPx); only that reaches the sketch tool or the offer.
m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
m_ctx_press = e.GetPosition();
m_ctx_travelled = false;
- e.Skip(); // the canvas still needs the press to seed the orbit
+ // Drop any press the tool still keeps (only a click's release takes it, so a pan's stays)
+ // before the canvas offers it this one, which ImGui may take instead.
+ m_sketch_tool.drop_right_click();
+ e.Skip(); // the canvas still needs the press to seed a pan or an orbit
});
m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
if (e.RightIsDown()) {
@@ -1066,12 +1096,13 @@ void DesignCanvas::set_on_context_menu(std::function cb)
});
m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) {
const wxPoint d = e.GetPosition() - m_ctx_press;
- // Always read-and-clear, even when another guard already rules the offer out, or a
- // terminator recorded under one condition would still be pending under the next.
- const bool terminated = m_sketch_tool.take_right_consumed();
- // Click, or navigation? A press that travelled orbited; one that did not, did not.
+ // Click, or navigation? A press that travelled panned or orbited; one that did not, did not.
const bool is_click = !m_ctx_travelled && std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx;
- if (m_on_context_menu && !terminated && !inline_busy() && is_click) {
+ // Ending a chain or abandoning an anchor uses the click up.
+ const bool terminated = is_click && m_canvas && m_sketch_tool.take_right_click(*m_canvas);
+ if (terminated)
+ m_canvas->set_as_dirty(); // drawn by the canvas's own RightUp (e.Skip below) or at idle
+ else if (m_on_context_menu && !inline_busy() && is_click) {
// The menu belongs to what you POINTED AT — and pointing happened at the PRESS, not
// at the release, so the raycast uses the press position. Within a 3 px budget the
// two are the same pixel in practice; using the press is what makes that a
@@ -1167,9 +1198,6 @@ void DesignCanvas::set_readout(const std::string& text)
if (m_canvas) m_canvas->set_as_dirty(); // drawn by the next frame (the tool feeds this from one)
}
-// Clear of the view cube and the two round view buttons, which own the bottom-left corner.
-static constexpr float kStatusHudLeftInset = 190.f;
-
void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour)
{
if (text == m_status_hud_last && colour == m_status_hud_colour) return;
@@ -1204,8 +1232,11 @@ void DesignCanvas::render_hud()
ImGuiWrapper::pop_common_window_style();
};
if (!m_status_hud_last.IsEmpty()) {
+ // Past the view cube and the round view buttons, which own the bottom-left corner. Asked
+ // of the canvas, which lays them out: they follow the monitor's DPI on Windows, where `em`
+ // does not, so a fixed inset in `em` let them cover the start of the line at 150%.
// A sentence can be a sentence: it wraps to the room left of the readout chip.
- const float left = kStatusHudLeftInset * em;
+ const float left = m_canvas->get_canvas_toolbar_right() + margin;
const ImVec4 col = m_status_hud_colour.IsOk()
? ImVec4(m_status_hud_colour.Red() / 255.f, m_status_hud_colour.Green() / 255.f,
m_status_hud_colour.Blue() / 255.f, 1.f)
@@ -1241,7 +1272,7 @@ void DesignCanvas::sync_selected_faces()
if (m_body_meshes == nullptr || m_model.objects.empty())
return;
rebuild_bodies();
- reload(true);
+ reload();
});
}
@@ -1293,7 +1324,7 @@ void DesignCanvas::set_operand_bodies(int target_body, int tool_body)
if (m_hl_body_target == target_body && m_hl_body_tool == tool_body) return;
m_hl_body_target = target_body;
m_hl_body_tool = tool_body;
- reload(true); // recolours the body volumes
+ reload(); // recolours the body volumes
}
void DesignCanvas::set_highlight_sketches(std::vector> hl)
@@ -1306,7 +1337,7 @@ void DesignCanvas::set_body_translucent(bool on)
{
if (m_body_translucent == on) return;
m_body_translucent = on;
- reload(true); // re-applies object-0 alpha so the solid fades for the fillet preview
+ reload(); // re-applies object-0 alpha so the solid fades for the fillet preview
}
void DesignCanvas::set_xray_focus(int body)
@@ -1314,7 +1345,7 @@ void DesignCanvas::set_xray_focus(int body)
if (m_xray_focus == body) return;
m_xray_focus = body;
m_sketch_tool.set_pick_only_body(body);
- reload(true); // re-applies per-body alpha so the non-focused bodies fade
+ reload(); // re-applies per-body alpha so the non-focused bodies fade
}
void DesignCanvas::set_body_hidden(bool on)
@@ -1322,7 +1353,7 @@ void DesignCanvas::set_body_hidden(bool on)
if (m_body_hidden == on) return;
m_body_hidden = on;
m_sketch_tool.set_body_edges_hidden(on);
- reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
+ reload(); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
}
bool DesignCanvas::delete_selected_sketch_entities()
diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp
index 9584550980..a7413dd370 100644
--- a/src/slic3r/GUI/CAD/DesignCanvas.hpp
+++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp
@@ -3,6 +3,8 @@
#include
#include "libslic3r/Point.hpp"
+#include "libslic3r/BoundingBox.hpp"
+#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Color.hpp"
#include
#include
@@ -48,16 +50,26 @@ public:
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
// `visible` (optional, indexed by body) hides bodies whose flag is false. `body_meshes` is kept
- // by address (a stable panel member) and read again whenever the selection changes.
+ // by address (a stable panel member) and read again whenever the selection changes. The
+ // bodies' edge lines are refreshed with them (DesignSketchTool::refresh_body_edges).
void set_bodies(const std::vector* body_meshes,
const std::vector& visible = {});
- void clear_mesh();
+ void clear_mesh(); // no bodies: drops their volumes and the tool's edge lines, pick and selection
void set_preview_mesh(const TriangleMesh& mesh);
void clear_preview();
void fit_view();
void set_view(const std::string& view_name);
+ // Frame `box` along the current view direction, as the canvas's Fit button frames a selection.
+ // False, the camera left alone, when there is nothing to frame: an undefined box.
+ bool zoom_to_box(BoundingBoxf3 box);
+ // Boxes to hand zoom_to_box: the selection and body faces (see DesignSketchTool).
+ BoundingBoxf3 selection_box() const { return m_sketch_tool.selection_box(); }
+ BoundingBoxf3 faces_box(std::vector> faces) const
+ {
+ return m_sketch_tool.faces_box(std::move(faces));
+ }
void begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode);
// Re-open a committed entity sketch for full in-canvas editing (load geometry +
@@ -66,7 +78,6 @@ public:
const std::vector& constraints,
const SketchPlane& plane);
void set_sketch_tool(DesignSketchTool::Mode mode);
- void set_sketch_plane(const SketchPlane& plane); // re-plane the live sketch when a reference plane is clicked in 3D
void set_sketch_construction(bool c);
// Flip the sketch selection between construction and real geometry; returns the
// number of entities changed (0 = nothing selected, caller falls back to the mode).
@@ -83,6 +94,8 @@ public:
void finish_sketch();
bool is_sketching() const;
void refresh_bed(); // re-sync the bed to the current printer (call on tab activation)
+ // Centre of the Design bed: the printer bed at its home position, whichever plate is current.
+ Vec2d bed_center() const { return m_bed.build_volume().bed_center(); }
// The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no
// per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs
// show. Exactly one of the two views is live at a time, so entering and leaving are the
@@ -157,7 +170,6 @@ public:
void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform,
double body_radius);
void clear_move_gizmo();
- bool moving_body() const;
void set_on_body_move_changed(std::function cb);
// Visual Fillet/Chamfer radius gizmo: when a solid edge is picked, anchor a radius arrow on
// it; drag/edit fire the radius callback. Returns false if no edge is currently picked.
@@ -234,6 +246,7 @@ public:
std::vector labels = {}); // clickable labelled reference planes
void clear_base_pick();
void set_on_datum_base_picked(std::function cb);
+ void set_selected_base(std::function cb); // the reference plane drawn selected, or -1
void set_on_sketch_exit(std::function cb); // Esc -> exit the tool
void set_on_sketch_exit_refused(std::function cb); // Esc declined: sketch has work
void set_on_sketch_notice(std::function cb); // tool refusals/side effects
@@ -380,7 +393,7 @@ public:
void repaint_now();
private:
- void reload(bool keep_view);
+ void reload();
void swap_camera(); // enter_viewport / leave_viewport, in the one direction they share
// Selected faces are filled by the canvas: each body's selected faces become a volume of their
@@ -415,7 +428,6 @@ private:
Camera m_parked_camera;
bool m_camera_swapped{false}; // guards a leave without an enter, and the reverse
Model m_model;
- bool m_first_frame{true};
int m_hl_body_target{-1};
int m_hl_body_tool{-1};
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
diff --git a/src/slic3r/GUI/CAD/DesignOffer.hpp b/src/slic3r/GUI/CAD/DesignOffer.hpp
index 846a55d72f..2c2d40d5fc 100644
--- a/src/slic3r/GUI/CAD/DesignOffer.hpp
+++ b/src/slic3r/GUI/CAD/DesignOffer.hpp
@@ -102,7 +102,7 @@ static const bool kOfferRowFlat[] = {
};
static const OfferVerb kOfferVerbs[] = {
- {"sketch", L_CONTEXT("Sketch", "Design"), 0, "Shift+S", "key:S+S", L("Click a face or a reference plane in the viewport, then a sketch tool"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Click a face or a reference plane, then pick a drawing tool")},
+ {"sketch", L_CONTEXT("Sketch", "Design"), 0, "Shift+S", "key:S+S", L("Select a flat face or a reference plane to sketch on"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Sketch on the selected flat face or plane, or click one next")},
{"extrude", L_CONTEXT("Extrude", "Design"), 1, "Shift+E", "key:S+E", L("Create a sketch, or pick a solid face, first"), 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", L("Extrude a sketch profile, or push/pull a picked face")},
{"revolve", L_CONTEXT("Revolve", "Design"), 1, "Shift+R", "key:S+R", L("Create a sketch profile to revolve first"), 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", L("Revolve a profile about an axis")},
{"sweep", L_CONTEXT("Sweep", "Design"), 1, "Shift+W", "key:S+W", L("Create a profile sketch to sweep first"), 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", L("Sweep a profile along a path")},
@@ -145,6 +145,7 @@ static const OfferVerb kOfferVerbs[] = {
{"rename", L_CONTEXT("Rename…", "Design"), 8, "F2", "btn:rename", L("Select a feature, or a body, to rename it"), 0x00004080u, 0, 0, false, false, nullptr, nullptr, L("Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)")},
{"delete_face", L_CONTEXT("Delete Face", "Design"), 7, nullptr, "fly:dressup#3", L("Delete Face needs a body — add or import one first"), 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", L("Remove faces from a body and heal the solid")},
{"colour", L_CONTEXT("Color", "Design"), 8, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", L("Set the selected body's display color")},
+ {"zoom_to", L_CONTEXT("Zoom to selection", "Design"), 8, nullptr, "btn:zoom_to", nullptr, 0x000041feu, 0, 0, false, false, nullptr, "design_zoom", L("Frame the selection in the view, keeping the view direction")},
{"delete", L_CONTEXT("Delete", "Design"), 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", L("Delete what is selected")},
{"delete_body", L_CONTEXT("Delete Body", "Design"), 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", L("Delete this whole body — removes the feature it was made from")},
{"sk_line_t", L_CONTEXT("Line", "Design"), 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, L("Line"), "design_line", L("Line — click start, then end")},
@@ -204,7 +205,7 @@ static const OfferVerb kOfferVerbs[] = {
{"sk_radius", L_CONTEXT("Radius / diameter…", "Design"), 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the radius of this arc, or the diameter of this circle")},
{"sk_angdist", L_CONTEXT("Angle / distance…", "Design"), 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the angle between two lines, or the distance between the two picks")},
};
-static const int kOfferVerbCount = 92;
+static const int kOfferVerbCount = 93;
}} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp
index e21fc18085..a98bdad3b4 100644
--- a/src/slic3r/GUI/CAD/DesignPanel.cpp
+++ b/src/slic3r/GUI/CAD/DesignPanel.cpp
@@ -9,6 +9,7 @@
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor
+#include "libslic3r/AppConfig.hpp" // design_* view preferences
#include "libslic3r/TriangleMesh.hpp" // mesh import: STL/OBJ -> indexed_triangle_set
#include "libslic3r/Format/OBJ.hpp"
#include "libslic3r/Format/bbs_3mf.hpp" // put_other_changes: mark the project dirty outside the undo stack
@@ -35,6 +36,7 @@
#include "libslic3r/Point.hpp"
#include
#include
+#include
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/CAD/SketchSolver.hpp"
@@ -104,7 +106,6 @@
#include "libslic3r/Model.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
-#include "libslic3r/BuildVolume.hpp"
#include "slic3r/GUI/MainFrame.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
@@ -275,7 +276,9 @@ static std::string commit_icon(bool bodies)
}
// The icons on each Feature tree and Bodies row (DesignRowList::Action::id).
-enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete };
+enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete, RowZoom };
+// The rows of the Feature tree's pinned block, above the features.
+static constexpr int kOriginRow = 0, kBedRow = 1;
// A row's eye shows the state the row is in; its tip names what a click does.
static DesignRowList::Action eye_action(bool shown)
@@ -283,6 +286,25 @@ static DesignRowList::Action eye_action(bool shown)
return { RowVisibility, shown ? "design_eye" : "design_eye_off", shown ? _L("Hide") : _L("Show") };
}
+// What the Origin row's eye and its menu item do.
+static wxString origin_toggle_text(bool shown)
+{
+ return shown ? _L("Hide reference planes and axes") : _L("Show reference planes and axes");
+}
+
+// What the Bed row's eye and its menu item do.
+static wxString bed_toggle_text(bool shown)
+{
+ return shown ? _L("Hide the printer bed and its plate grid")
+ : _L("Show the printer bed and its plate grid");
+}
+
+// What Sketch asks for while it waits for a plane (DesignPanel::start_sketch).
+static wxString sketch_plane_prompt()
+{
+ return _L("Click a reference plane or a flat face to sketch on it — Esc cancels");
+}
+
// Prepare outlines every numeric field (rounded, #4A4A51 on dark). wxSpinCtrlDouble is a
// native GTK control that cannot draw that, and Orca's own SpinInput is int-only — it would
// silently truncate a 2.5 mm radius. So the double spin keeps its arrows and validation and
@@ -471,22 +493,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_construction->SetForegroundColour(dp_ctl_text());
auto select_tool = [this](DesignSketchTool::Mode mode, const wxString& hint) {
if (!m_viewport) return;
- if (!m_viewport->is_sketching()) {
- wxString on;
- const SketchPlane plane = sketch_plane_from_selection(on);
- m_viewport->begin_sketch(plane, mode);
- m_construction->SetValue(false); // a fresh session starts non-construction
- m_sketch_on = on; // shown with the tool hint, so the target is visible
- // The face has been CONSUMED as the sketch plane, so drop the pick. Leaving it live
- // meant the next Extrude saw a selected face and push/pulled it instead of extruding
- // the sketch just drawn — the same trap the imported-art path already guards against.
- if (m_sel_solid_face >= 0 || m_pick_face >= 0) {
- m_sel_solid_face = m_sel_solid_edge = m_sel_solid_body = -1;
- m_pick_face = m_pick_face_body = -1;
- }
- } else {
- m_viewport->set_sketch_tool(mode);
- }
+ // Sketch mode opens with its session (start_sketch_on_target); this covers a mode a
+ // failed Constrain left without one.
+ if (!m_viewport->is_sketching())
+ start_sketch_on_target(false);
+ m_viewport->set_sketch_tool(mode);
m_viewport->set_sketch_construction(m_construction->GetValue());
set_status(StatusKind::Info, m_sketch_on.IsEmpty() ? hint
: wxString::Format(_L("%s · plane: %s"), hint, m_sketch_on));
@@ -597,17 +608,19 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(_L("Axonometric view, fitted"));
};
- // Commit to Plate and the bed toggle were mouse-only: a toolbar button and a checkbox with
- // no accelerator between them, so neither could be reached from the keyboard at all, nor by
- // anything driving the keyboard. Ctrl+Shift+P is Plate, Ctrl+Shift+B is Bed; neither
- // collides with Orca's own Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate).
+ // Commit to Plate and the bed and origin toggles are mouse targets — a toolbar button and the
+ // Feature tree's Bed and Origin rows, which never take the focus — so without an accelerator
+ // none could be reached from the keyboard, nor by anything driving the keyboard. Ctrl+Shift+P
+ // is Plate, Ctrl+Shift+B is Bed, Ctrl+Shift+O is Origin; none collides with Orca's own
+ // Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate).
m_keys_feature['P' | SC_SHIFT | SC_CTRL] = [this] { on_commit(); };
m_keys_feature['B' | SC_SHIFT | SC_CTRL] = [this] {
- if (!m_show_bed) return;
- const bool show = !m_show_bed->GetValue();
- m_show_bed->SetValue(show);
- if (m_viewport) m_viewport->set_show_bed(show);
- set_status(show ? _L("Bed shown") : _L("Bed hidden"));
+ toggle_bed();
+ set_status(m_show_bed ? _L("Bed shown") : _L("Bed hidden"));
+ };
+ m_keys_feature['O' | SC_SHIFT | SC_CTRL] = [this] {
+ toggle_origin();
+ set_status(m_show_origin ? _L("Reference planes shown") : _L("Reference planes hidden"));
};
// Flyout rows show the design_* glyphs as they are: drawn in Orca's icon grey (#949494), which
@@ -625,14 +638,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
// the addresses are created inside the widget-building loops, so not building would silently
// delete 42 verbs from the offer while they still rendered. 7ih records the cleanup
// that lets the construction go away too.
- // What stays: the two doc-row imports (consumed by add_doc below) and the view controls,
- // which are chrome_only in the atlas and so have no offer row to fall back on.
- static const std::set kBarKeep = { "step", "mesh", "place", "section", "flip" };
+ // What stays: the view controls, which are chrome_only in the atlas and so have no offer row
+ // to fall back on, and Sketch, the group's lead. All four are built with the doc buttons.
+ static const std::set kBarKeep = { "place", "section", "flip" };
auto fadd = [&tb_slot](const char* id, wxWindow* w) {
if (kBarKeep.count(id) == 0) { w->Hide(); return; }
tb_slot[id].push_back(w);
};
- m_tb_feature->Add(caption(_L("FEATURES")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
{
// Onshape-style FEATURE flyouts: same themed-DropDown pattern as the sketch toolbar
// (tinted glyphs, Body_14 measure, content-width popup) but each entry runs an
@@ -710,36 +722,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
fadd(id, chev);
};
- auto* b_sketch = icon_btn("design_sketch", _L("Sketch"));
- // Sketch means a tool. Pressing it used to set the mode and then ask, unconditionally,
- // for the very thing the user had just done — click XZ, read "XZ plane selected, press
- // Sketch", press Sketch, and be told to click a reference plane. The plane was never
- // lost (m_ref_plane holds it and begin_sketch captures it when the first tool is armed);
- // the sentence was simply false, and with right-click excluded in sketch mode there was
- // no door to the tools at all, so the only way on was the toolbar this tab is retiring.
- std::function act_sketch = [this] {
- set_ui_mode(UiMode::Sketch);
- wxString where;
- const bool have_plane = sketch_plane_target(where);
- set_status(StatusKind::Info, have_plane
- ? wxString::Format(_L("Sketch plane: %s — pick a tool"), where)
- : _L("Click a face or a reference plane in the viewport, then a sketch tool"));
- if (m_sketch_hint) { // the card must agree with the status line, not argue with it
- m_sketch_hint->SetLabel(have_plane
- ? wxString::Format(_L("Sketch plane: %s.\nPick a tool, or press Menu for the list."), where)
- : _L("Click a face or a reference plane, then a sketch tool."));
- m_sketch_hint->Refresh();
- m_cards->Layout();
- }
- // Hand over the tools rather than naming them in a status line. CallAfter so the
- // mode change has settled before a modal menu takes the loop; the menu carries each
- // tool's shortcut, so pressing the key instead of picking a row costs nothing.
- if (have_plane)
- CallAfter([this] { show_offer_menu(offer_anchor()); });
- };
- b_sketch->Bind(wxEVT_BUTTON, [act_sketch](wxCommandEvent&) { act_sketch(); });
- m_keys_feature[SHIFT('S')] = act_sketch;
- fadd("sketch", b_sketch);
+ // Sketch opens a sketch on the face or plane already picked, or asks for one first
+ // (start_sketch): sketch mode is never entered without a plane under it. Its button is
+ // built with the doc buttons below.
+ m_keys_feature[SHIFT('S')] = [this] { start_sketch(); };
// Add material: every feature that grows new solid material — from a profile
// (extrude/revolve/sweep/loft), from a face (thicken) or from a line (rib).
feat_dropdown("material", "design_extrude", _L("Add material (extrude / revolve / sweep / loft / thicken / rib)"), {
@@ -1080,6 +1066,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
b_color->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_set_body_color(); });
fadd("color", b_color);
m_verb_actions["btn:colour"] = [this] { on_set_body_color(); };
+ m_verb_actions["btn:zoom_to"] = [this] { zoom_to_selection(); };
m_verb_actions["btn:delete"] = [this] { on_delete_feature(); };
// A row's double-click is Edit, so renaming needs a door of its own. The offer is this
// tab's only tool vocabulary and the row IS the object, so rename sits in the offer, in
@@ -1091,8 +1078,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
// is editable now (CadBody::user_name), so the verb opens the editor there.
if (const int b = tree_body_selection(); b >= 0)
m_parts->begin_rename(b);
- else if (const int sel = tree_selection(); sel != wxNOT_FOUND)
- m_tree->begin_rename(sel);
+ else if (tree_selection() != wxNOT_FOUND)
+ m_tree->begin_rename(m_tree->selection());
else
set_status(_L("Select a feature, or a body, first — then rename it")); // not an error
};
@@ -1240,18 +1227,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
}, SHIFT('T')},
});
// Text / SVG insert tools live in the SKETCH toolbar (they produce 2D profiles =
- // sketches), not here. STEP stays in Features: it imports a whole B-rep solid.
- // Import STEP — standalone: a STEP comes in as a whole editable B-rep body, not a profile.
- auto* b_step = icon_btn("design_step", _L("Import STEP (editable B-rep solid)"));
- b_step->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_step(); });
- m_keys_feature[SHIFT('I')] = [this] { on_import_step(); };
- fadd("step", b_step);
- // Import mesh — same destination as STEP (an editable B-rep body), but the geometry has
- // to be reconstructed from triangles first (GeometryEngine::mesh_to_brep).
- auto* b_mesh = icon_btn("param_triangles", _L("Import mesh (STL/OBJ) as an editable B-rep solid"));
- b_mesh->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_mesh(); });
- m_keys_feature[SHIFT('M')] = [this] { on_import_mesh(); };
- fadd("mesh", b_mesh);
+ // sketches), not here. Import STEP / mesh are document actions: see the doc row.
auto* b_constrain = icon_btn("design_constrain", _L("Constrain selected sketch"));
b_constrain->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
on_begin_constrain();
@@ -1398,12 +1374,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
// the sketch tools, not in the generic Features strip. Each places the art
// in-canvas, then commits via the Insert card's Confirm.
{
- // In Sketch MODE the art must land in the sketch — but begin_sketch() does not run
- // until the first tool is armed, so pressing Sketch and then Text would find no
- // session and commit a separate feature. Arm Select first: it starts the session on
- // the picked plane without drawing anything, so Text behaves the same whether or not
- // you had already drawn a line. (MODE vs SESSION — the distinction that has bitten
- // this panel before.)
+ // In Sketch MODE the art must land in the sketch. The mode opens with its session
+ // (start_sketch_on_target), but a failed Constrain can leave it without one, and Text
+ // would then find no session and commit a separate feature. Arm Select first: it
+ // starts the session without drawing anything. (MODE vs SESSION — the distinction
+ // that has bitten this panel before.)
auto ensure_sketch = [this, select_tool] {
if (m_ui_mode == UiMode::Sketch && m_viewport && !m_viewport->is_sketching())
select_tool(DesignSketchTool::Mode::Select,
@@ -1540,10 +1515,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
m_viewport->set_sketch_construction(m_construction->GetValue()); });
// STAYS on the bar. Construction is not a tool, it is a persistent MODE — the same kind
- // of thing as the Bed checkbox — and the sketch bar is already shown only in Sketch mode,
- // so it appears exactly while it can apply. Hiding it left Q and the offer's Construction
- // row still toggling a checkbox nobody could see: you could not tell whether the next
- // line would be construction geometry. A stateful toggle has to show its state.
+ // of thing as the Feature tree's Bed row — and the sketch bar is already shown only in
+ // Sketch mode, so it appears exactly while it can apply. Hiding it left Q and the offer's
+ // Construction row still toggling a checkbox nobody could see: you could not tell whether
+ // the next line would be construction geometry. A stateful toggle has to show its state.
sadd_bar(m_construction);
add_sep(m_tb_sketch);
auto* b_del = icon_btn("design_delete", _L("Delete selected"));
@@ -1633,31 +1608,29 @@ DesignPanel::DesignPanel(wxWindow* parent)
auto add_doc = [this](ScalableButton* b) {
m_tb_doc->Add(b, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); };
- auto* b_new = doc_btn("add", _L("New Design — clear the feature tree"));
+ auto* b_new = doc_btn("design_new", _L("New Design — clear the feature tree"));
b_new->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_new_design(); });
add_doc(b_new);
- add_doc(static_cast(tb_slot["step"][0])); // 2. Import STEP
- add_doc(static_cast(tb_slot["mesh"][0])); // 3. Import mesh
- auto* b_export = doc_btn("save", _L("Export STEP…"));
+ // Import STEP — a STEP comes in as a whole editable B-rep body, not a profile.
+ auto* b_step = doc_btn("design_step", _L("Import STEP (editable B-rep solid)"));
+ b_step->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_step(); });
+ m_keys_feature[SHIFT('I')] = [this] { on_import_step(); };
+ add_doc(b_step);
+ // Import mesh — same destination as STEP (an editable B-rep body), but the geometry has
+ // to be reconstructed from triangles first (GeometryEngine::mesh_to_brep).
+ auto* b_mesh = doc_btn("design_import_mesh", _L("Import mesh (STL/OBJ) as an editable B-rep solid"));
+ b_mesh->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_mesh(); });
+ m_keys_feature[SHIFT('M')] = [this] { on_import_mesh(); };
+ add_doc(b_mesh);
+ auto* b_export = doc_btn("design_export", _L("Export STEP…"));
b_export->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_export_step(); });
add_doc(b_export);
- // View option, not a document action: hide the printer bed to model without it. Lives in
- // this row because it must stay reachable with no tool open — a card would come and go.
- m_show_bed = new CheckBox(m_toolbar);
- m_show_bed->SetValue(true); // bed visible by default, as the tab opens today
- m_show_bed->SetToolTip(_L("Show the printer bed and its plate grid"));
- // wxEVT_TOGGLEBUTTON, NOT wxEVT_CHECKBOX: Orca's CheckBox derives from
- // wxBitmapToggleButton (Widgets/CheckBox.hpp), so a wxEVT_CHECKBOX handler never fires.
- // Read the control rather than the event so the state cannot disagree with the glyph.
- m_show_bed->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
- if (m_viewport) m_viewport->set_show_bed(m_show_bed->GetValue());
- e.Skip();
- });
- auto* bed_lbl = new wxStaticText(m_toolbar, wxID_ANY, _L("Bed"));
- bed_lbl->SetForegroundColour(dp_sec_text());
- m_tb_doc->Add(m_show_bed, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 6);
- m_tb_doc->Add(bed_lbl, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 4);
+ // Sketch leads the feature group. Momentary like the imports: start_sketch() may only ask
+ // for a plane, or refuse, and a teal active-tool state would outlive either.
+ auto* b_sketch = doc_btn("design_new_sketch", _L("Create a new sketch"));
+ b_sketch->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { start_sketch(); });
+ tb_slot["sketch"].push_back(b_sketch);
// These act on bodies / the view, so they ride in the feature group, in the slots
// the user assigned them (9, 11bis, 16).
@@ -3111,11 +3084,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// looking and pointing. A combo duplicated that decision somewhere the geometry could not
// see it, and once a face could be picked it went further and displayed a stale row that
// contradicted the real target. e1p.
- // Kept as a member, not a local: the card has to be able to STOP saying this. It asked
- // for a plane even when one had just been picked, directly contradicting the status line
- // two inches below it, which by then read "Sketching on XZ".
- m_sketch_hint = new wxStaticText(m_cards, wxID_ANY,
- _L("Click a face or a reference plane, then a sketch tool."));
+ // A member, not a local: it names the plane the session opened on, which is known only
+ // when it opens (start_sketch_on_target writes it), so the card and the status line agree.
+ m_sketch_hint = new wxStaticText(m_cards, wxID_ANY, _L("Pick a tool, or press Menu for the list."));
m_sketch_hint->SetForegroundColour(dp_sec_text());
m_box_sketch_session->Add(m_sketch_hint, 0, wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 12);
}
@@ -3159,11 +3130,34 @@ DesignPanel::DesignPanel(wxWindow* parent)
FromDIP(SidebarProps::ContentMargin()));
tree_inner->Add(new wxStaticLine(m_tree_box), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP,
FromDIP(SidebarProps::TitlebarMargin()));
- // Sized to its rows, so a short history wastes no block, and scrolling past 9.
+ // The Origin and Bed switches sit on the card itself, unframed, above the features' own framed
+ // list: view switches, not history. A list of their own so they stay put while the features
+ // scroll, and a non-selectable one because selecting them would do nothing: only their eyes
+ // answer. 13 = the feature list's margin plus its frame, so the two lists' icons line up.
+ m_pinned = new DesignRowList(m_tree_box, 2, false, wxBORDER_NONE);
+ m_pinned->SetBackgroundColour(dp_panel_bg());
+ tree_inner->Add(m_pinned, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 13);
+ // Sized to its rows, so a short history wastes no block, and scrolling past 9 features.
m_tree = new DesignRowList(m_tree_box, 9);
m_tree->SetBackgroundColour(dp_panel_bg());
tree_inner->Add(m_tree, 0, wxEXPAND | wxALL, 12);
+ m_pinned->on_action = [this](int row, int) {
+ if (row == kBedRow)
+ toggle_bed();
+ else if (row == kOriginRow)
+ toggle_origin();
+ };
+ // Exactly what the eye does, named.
+ m_pinned->on_menu = [this](int row, const wxPoint& screen) {
+ const bool bed = (row == kBedRow);
+ wxMenu menu;
+ const int id_toggle = wxWindow::NewControlId();
+ menu.Append(id_toggle, bed ? bed_toggle_text(m_show_bed) : origin_toggle_text(m_show_origin));
+ menu.Bind(wxEVT_MENU, [this, bed](wxCommandEvent&) { bed ? toggle_bed() : toggle_origin(); }, id_toggle);
+ m_pinned->PopupMenu(&menu, m_pinned->ScreenToClient(screen));
+ };
+
// Selecting a feature that leaves a body (Extrude, Fillet, Chamfer, Hole, ...) lights the
// faces it made in the viewport — the fillet's round, not the whole part it sits on.
m_tree->on_select = [this] {
@@ -3172,7 +3166,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// "the target" — but ONLY when this tree has a selection. Each list notifies on every
// change, so clicking a body row runs apply_body_row, whose m_tree->unselect() fires
// THIS handler, which would otherwise clear the body row the user had just clicked.
- if (m_parts && tree_selection() != wxNOT_FOUND) m_parts->unselect();
+ if (m_parts && m_tree->selection() != wxNOT_FOUND) m_parts->unselect();
const int sel = tree_selection();
// Likewise a viewport pick, which would be drawn just like the feature's faces. Not while
// a card is open: the card reads that pick.
@@ -3194,14 +3188,17 @@ DesignPanel::DesignPanel(wxWindow* parent)
};
// Double-click a row = Edit, the same gesture that re-opens a committed sketch on the canvas.
- m_tree->on_activate = [this] { on_edit_feature(); };
+ m_tree->on_activate = [this] {
+ if (tree_selection() != wxNOT_FOUND) on_edit_feature();
+ };
- // The row's own Edit / Show-hide / Delete, on the row the click selected. The body list is
- // cleared here too, not left to on_select, which re-clicking the selected row does not run:
- // a body row still selected would be what on_toggle_visibility acts on.
- m_tree->on_action = [this](int, int id) {
+ // The row's own Zoom to selection / Edit / Show-hide / Delete, on the row the click selected.
+ // The body list is cleared here too, not left to on_select, which re-clicking the selected row
+ // does not run: a body row still selected would be what on_toggle_visibility acts on.
+ m_tree->on_action = [this](int row, int id) {
if (m_parts) m_parts->unselect();
switch (id) {
+ case RowZoom: zoom_to_feature(row); break;
case RowEdit: on_edit_feature(); break;
case RowVisibility: on_toggle_visibility(); break;
case RowDelete: on_delete_feature(); break;
@@ -3222,6 +3219,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
const int id_up = wxWindow::NewControlId();
const int id_down = wxWindow::NewControlId();
const int id_vis = wxWindow::NewControlId();
+ const int id_zoom = wxWindow::NewControlId();
const int id_art = wxWindow::NewControlId();
const int id_del = wxWindow::NewControlId();
menu.Append(id_rename, _L("Rename\tF2"));
@@ -3236,6 +3234,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
menu.Append(id_up, _L("Move up"));
menu.Append(id_down, _L("Move down"));
menu.Append(id_vis, _L("Show / hide"));
+ // Offered where the row's own icon is: on a row with something to frame.
+ const bool zoom = can_zoom_to_feature(row);
+ if (zoom) menu.Append(id_zoom, _L("Zoom to selection"));
menu.AppendSeparator();
menu.Append(id_del, _L("Delete"));
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { m_verb_actions["btn:rename"](); }, id_rename);
@@ -3246,6 +3247,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_delete_feature(); }, id_del);
if (art)
menu.Bind(wxEVT_MENU, [this, row](wxCommandEvent&) { on_transform_imported(row); }, id_art);
+ if (zoom)
+ menu.Bind(wxEVT_MENU, [this, row](wxCommandEvent&) { zoom_to_feature(row); }, id_zoom);
m_tree->PopupMenu(&menu, m_tree->ScreenToClient(screen));
};
@@ -3347,12 +3350,14 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(StatusKind::Info, wxString::Format(_L("Body %d selected — right-click for what applies to it"), b + 1));
};
m_parts->on_select = [this, apply_body_row] { apply_body_row(tree_body_selection()); };
- // The row's own Move / Show-hide / Delete. apply_body_row runs unconditionally, as for the
- // menu below: a face picked in the viewport since the row was selected has moved
- // m_sel_solid_body, which is the body on_move_body and on_delete_body act on.
+ // The row's own Zoom to selection / Move / Show-hide / Delete. apply_body_row runs
+ // unconditionally, as for the menu below: a face picked in the viewport since the row was
+ // selected has moved m_sel_solid_body, which is the body on_move_body and on_delete_body act
+ // on.
m_parts->on_action = [this, apply_body_row](int row, int id) {
apply_body_row(row);
switch (id) {
+ case RowZoom: zoom_to_body(row); break;
case RowMove: on_move_body(); break;
case RowVisibility: on_toggle_visibility(); break;
case RowDelete: on_delete_body(); break;
@@ -3365,9 +3370,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
// been promising this right-click since before it existed.
m_parts->on_menu = [this, apply_body_row](int row, const wxPoint& screen) {
apply_body_row(row); // unconditional — see above
- // Let the modal menu take the loop after this handler returns — same CallAfter as the
- // sketch path, which learned it the hard way.
- CallAfter([this, screen] { show_offer_menu(screen); });
+ // From inside the event, as the feature rows do, never CallAfter'd: wxGTK sends the
+ // context menu on the right PRESS, and a menu GTK pops after that event has finished
+ // has no trigger time, so the button's release closes it the moment it opens.
+ show_offer_menu(screen);
};
// Renaming a BODY names the body itself. It does NOT rename the feature that created it:
// an Extrude, a Cut and a Fillet all land on one body, so source_feature is one operation in
@@ -3699,8 +3705,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
// One selection at a time: a pick replaces the Feature tree row. Not while a card is open:
// the feature being edited keeps its row while the card's picks are made.
if (level >= 1 && m_active == Tool::None && m_tree) m_tree->unselect();
- // A pick that fell through the move gizmo (clicked off the arrows) exits move mode.
- if (m_viewport->moving_body()) m_viewport->clear_move_gizmo();
// Remember which body + face/edge so Extrude / dress-up target the RIGHT body.
m_sel_solid_body = (level >= 1) ? body : -1;
m_sel_solid_face = (level == 2) ? face : -1; // 4 = Vertex: a corner is not its face
@@ -3712,8 +3716,29 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_pick_face_body = (level >= 1) ? body : -1;
m_pick_face = (level >= 1) ? face : -1;
// Last pick wins: a leftover loop pick would block Extrude's face push/pull branch, so
- // Extrude would extrude a sketch instead of push/pulling the clicked face.
- if (level >= 1) m_viewport->clear_loop_pick();
+ // Extrude would extrude a sketch instead of push/pulling the clicked face. And a reference
+ // plane picked before stops being the one chosen, as a plane pick drops a face: otherwise it
+ // would come back as the sketch plane, and be drawn selected, once this pick is let go.
+ if (level >= 1) {
+ m_viewport->clear_loop_pick();
+ m_plane_picked = false;
+ }
+ // Sketch is waiting for a plane: a flat face is one. m_pick_face is the face the click
+ // landed on whether it took the face, one of its edges or a corner — the rule Sketch
+ // applies to a face picked beforehand. A curved face says why it was not taken; a swept
+ // body or a click that let go of the pick leaves the prompt up.
+ if (sketch_plane_pick_live()) {
+ wxString on;
+ if (level >= 1 && sketch_plane_target(on)) {
+ start_sketch_on_target(true);
+ return;
+ }
+ set_status(StatusKind::Info, level >= 1 && m_pick_face >= 0
+ ? _L("That face is not flat — click a flat face or a reference plane")
+ : sketch_plane_prompt());
+ m_status->Refresh();
+ return;
+ }
// Say what got picked. Without this the ONLY feedback is the viewport highlight, so a
// pick that registers but draws faintly is indistinguishable from one that never
// happened — which is precisely how this failure was reported and why it resisted
@@ -3909,7 +3934,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// A click on nothing drops a list row as it drops a pick. Not while a card is open: the
// feature being edited keeps its row.
m_viewport->set_on_empty_pick([this] {
- if (m_active == Tool::None && deselect_rows())
+ if (m_active != Tool::None) return;
+ const bool row = deselect_rows();
+ if (drop_plane_pick() || row)
set_status(StatusKind::Info, _L("Nothing selected"));
});
@@ -3987,28 +4014,37 @@ DesignPanel::DesignPanel(wxWindow* parent)
refresh_plane_labels();
refresh_preview(); // re-resolve the frame + move the gizmo/ghosts to the new base
} else {
- // Clicking a reference plane in 3D IS how a sketch plane is chosen now. Record it and,
- // when a session is already live, re-plane it immediately: begin_sketch captures the
- // plane at first-tool-pick, so without this the entities would stay on the old plane
- // while the committed feature landed on the new one.
+ // Clicking a reference plane in 3D IS how a sketch plane is chosen. The planes are
+ // only pickable outside a session (DesignSketchTool hit-tests them while no session
+ // is live), and the canvas reports the click after clearing the solid selection on the
+ // same miss, so the face pick is already empty and the sketch falls back to this plane.
if (base >= 0) {
m_ref_plane = base;
m_plane_picked = true; // chosen, not merely defaulted to
m_pick_face = m_pick_face_body = -1; // last pick wins: a plane beats a stale face
- if (m_viewport && m_viewport->is_sketching())
- m_viewport->set_sketch_plane(plane_from_choice(m_ref_plane));
+ }
+ if (base >= 0 && sketch_plane_pick_live()) {
+ start_sketch_on_target(true); // Sketch was waiting for exactly this
+ return;
}
const wxString nm = ref_plane_name(base);
- // Both halves named the TOOLBAR, which no longer carries either button: the tools
- // moved to the offer. Name the gesture that actually works in each mode, and say
- // what a plain click does, since the two are easy to confuse on a plane.
- set_status(StatusKind::Ok, m_ui_mode == UiMode::Sketch
- ? wxString::Format(_L("%s plane selected — right-click for the drawing tools"), nm)
- : wxString::Format(_L("%s plane selected — right-click to sketch on it, "
- "or click an object to select it"), nm));
+ // Name the gesture that works, and what a plain click does, since the two are easy to
+ // confuse on a plane.
+ set_status(StatusKind::Ok,
+ wxString::Format(_L("%s plane selected — right-click to sketch on it, "
+ "or click an object to select it"), nm));
m_status->Refresh();
}
});
+ // The reference plane drawn selected: the Plane card's base, or else the plane a sketch would go
+ // on — chosen, and not overridden by a picked face (sketch_plane_target's test, without its
+ // per-face OCCT lookup, as this is asked every frame).
+ m_viewport->set_selected_base([this] {
+ if (m_active == Tool::Plane)
+ return m_plane_base != nullptr && m_pl_faceA < 0 ? m_plane_base->GetSelection() : -1;
+ const bool face = (m_sel_solid_face >= 0 && m_sel_solid_body >= 0) || (m_pick_face >= 0 && m_pick_face_body >= 0);
+ return m_plane_picked && !face ? m_ref_plane : -1;
+ });
// Move-body gizmo (M5): each drag/edit reports the body's new translation. Store it as a
// display-only per-body transform and re-feed the moved meshes (the OCCT shape is untouched,
@@ -4144,13 +4180,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
const int key = e.GetKeyCode();
const bool ctrl = e.ControlDown() || e.CmdDown();
const bool sketching = (m_ui_mode == UiMode::Sketch) && m_viewport && m_viewport->is_sketching();
- // Which key MAP applies is a question about the MODE, not about whether a session is
- // already running. Gating the sketch map on is_sketching() made it unreachable by
- // keyboard: is_sketching() only turns true inside select_tool()'s begin_sketch(), and
- // select_tool() is what the sketch keys call — so the first letter after entering
- // sketch mode fell through to the feature map, matched nothing (feature keys are
- // Shift+letter), and did nothing. The mouse worked only because the toolbar flyout
- // reaches select_tool() directly. That is why all 17 keys read as dead. 0ud.
+ // Which key MAP applies is a question about the MODE, not about the canvas tool:
+ // is_sketching() is true in Constrain too, which keeps the feature map. 0ud.
const bool sketch_mode = (m_ui_mode == UiMode::Sketch);
// Never steal editing keys from a focused text field or an open in-canvas value field —
// Delete/Ctrl+Z there must edit the text, not the model.
@@ -4416,7 +4447,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
assert(dead.empty()); // debug builds stop here; release ships the log line
}
+ m_show_origin = wxGetApp().app_config->get_bool("design_show_origin");
+ m_show_bed = wxGetApp().app_config->get_bool("design_show_bed");
+ if (m_viewport) m_viewport->set_show_bed(m_show_bed);
set_ui_mode(UiMode::Feature);
+ // The Origin and Bed rows from the first paint: nothing else builds them before the first
+ // edit, project load or theme switch.
+ refresh_pinned();
build_phase("wiring");
BOOST_LOG_TRIVIAL(info) << "Design tab build: total " << build_clock.Time() << " ms";
}
@@ -4460,7 +4497,13 @@ void DesignPanel::apply_dof_status(int dof, bool ok, bool has_constraints)
void DesignPanel::set_ui_mode(UiMode m)
{
+ // A session's ✓/✗ and Esc are its own; a Move left open would answer them instead, unseen,
+ // since the gizmo is not drawn in a sketch.
+ if (m != UiMode::Feature) end_body_move(true);
m_ui_mode = m;
+ // Any mode change ends a plane choice under way; the refreshes below put the planes and the
+ // ✓/✗ right for it.
+ m_choosing_sketch_plane = false;
if (m != UiMode::Sketch) m_sketch_on.clear(); // no stale "on the picked face" on the next hint
// A committed loop picked before the session means nothing to the sketch map, but it would
// still count as a selection and swallow the first Esc. The double-click that opens a sketch
@@ -4507,9 +4550,8 @@ void DesignPanel::set_ui_mode(UiMode m)
m_form->FitInside();
}
update_action_bar(); // Sketch/Constrain modes show the unified ✓/✗; Feature idle hides it
- // The origin planes follow the mode: entering Sketch offers them even when a body exists,
- // leaving it takes them back. Without this they would only refresh on the next tree
- // rebuild, which is not an event that happens when you merely press Sketch.
+ // The reference planes follow the plane choice the mode change just ended
+ // (update_reference_planes); without this they would wait for the next tree rebuild.
update_reference_planes();
// Say where you are, in words, across the top of the viewport.
@@ -4519,8 +4561,8 @@ void DesignPanel::set_ui_mode(UiMode m)
// Seen on the rig: pick XY, arm Line, and the viewport is an empty grey field — no bed, no
// grid, no origin, nothing to judge a length or a direction against. The plate grid was
// carrying the ground reference for the whole tab. The banner already says where you are;
- // taking the floor away as well only made the sketch harder to draw. The Bed checkbox is the
- // one thing that governs the bed, in every mode.
+ // taking the floor away as well only made the sketch harder to draw. The Feature tree's Bed
+ // row is the one thing that governs the bed, in every mode.
if (m_sketch_banner != nullptr) {
const bool sketching = (m == UiMode::Sketch);
if (sketching && m_sketch_banner_txt != nullptr)
@@ -4546,7 +4588,9 @@ void DesignPanel::set_status_ok()
set_status(wxString::Format(_L("OK — %zu triangles"),
m_doc.display_mesh.its.indices.size()));
if (m_viewport != nullptr) {
- m_viewport->clear_move_gizmo(); // a recompute invalidates the gizmo's body centroid
+ // A recompute invalidates the gizmo's body centroid; a Move ends with it, keeping its pose.
+ if (body_move_pending()) end_body_move(true);
+ else m_viewport->clear_move_gizmo(); // the Transform card's, if up
rebuild_disp_meshes(); // apply per-body Move transforms to the display/pick meshes
// Point the solid-pick at the fresh body + TRANSFORMED pick mesh (stable address) + the
// per-body xform vector (for edge sampling). Resets the whole/face/edge selection, whose
@@ -4657,6 +4701,8 @@ void DesignPanel::on_add_text()
void DesignPanel::open_text_dialog(int feat)
{
if (m_text_dlg != nullptr) { m_text_dlg->Raise(); return; }
+ end_body_move(true);
+ end_sketch_plane_choice(); // the text goes on the plane picked before, not on the next click
m_text_editing = feat >= 0 && feat < int(m_doc.features.size()) && m_doc.features[feat].is_text();
m_text_feat = m_text_editing ? feat : -1;
m_text_face_body = -1;
@@ -5116,7 +5162,9 @@ void DesignPanel::add_imported_sketch(
// Show the Insert Confirm/Cancel card while the imported art is being placed/sized.
void DesignPanel::open_insert_card(const wxString& base_name)
{
+ end_body_move(true);
m_active = Tool::Insert;
+ end_sketch_plane_choice();
if (m_hdr_insert) m_hdr_insert->SetLabel(base_name);
wxSizer* s = m_cards->GetSizer();
s->Show(m_box_insert, true, true);
@@ -5161,6 +5209,8 @@ void DesignPanel::on_transform_imported(int feat_idx)
const CadFeature& f = m_doc.features[feat_idx];
if (f.imported_regions.empty())
return;
+ end_body_move(true); // the art's placement gizmo and ✓/✗ take over from a Move left open
+ end_sketch_plane_choice(); // ...and from a plane choice: Scale artwork reaches here directly
// In-canvas bbox handles (replaces the Move/Scale dialog): drag a corner to scale,
// the centre to move. Values stream back via set_on_imported_transform.
m_viewport->begin_imported_transform(feat_idx, f.imported_regions, f.plane,
@@ -5688,7 +5738,6 @@ void DesignPanel::on_add_transform()
// below does not — without this the body would keep showing the
// dragged pose after a Transform that failed to build.
m_xf_gizmo_body = -1;
- m_move_body = -1;
}
if (m_doc.bodies.empty()) {
set_status(StatusKind::Warning, _L("Transform needs a body — add or import one first"));
@@ -7247,7 +7296,7 @@ wxString DesignPanel::idle_hint() const
{
return m_doc.features.empty()
? _L("Nothing yet — import a STEP or a mesh from the toolbar,\n"
- "or click a reference plane and right-click it to start a sketch.")
+ "or press Sketch and click a reference plane to start a sketch.")
: _L("No solid yet — select a sketch and right-click it to Extrude.");
}
@@ -7275,7 +7324,7 @@ void DesignPanel::on_tab_shown()
if (m_viewport) m_viewport->refresh_bed();
hydrate_from_model();
- update_reference_planes(); // entering the Design tab: show the XY/XZ/YZ planes if no object yet
+ update_reference_planes(); // entering the Design tab
if (show_clock.Time() > 100) // a slow first show is what users report; the usual one is not news
BOOST_LOG_TRIVIAL(info) << "Design tab shown: bed, project recipe and planes in " << show_clock.Time() << " ms";
m_laid_out = true;
@@ -7336,6 +7385,7 @@ void DesignPanel::on_sys_color_changed()
wxGetApp().UpdateDarkUIWin(this);
refresh_icons();
refresh_tree(); // the rows carry their own text colours
+ refresh_pinned();
Refresh();
}
@@ -7354,8 +7404,12 @@ void DesignPanel::hydrate_from_model()
// that has none yet. A design in progress keeps its origin: its sketches have it baked into
// their planes, so moving it under them (a printer change between visits) would slide the
// datum planes off the geometry. A loaded project brings its own (load_recipe).
+ // The centre of the Design bed, not the plater's: the plater moves its bed onto the current
+ // plate, so on plate 2+ the origin would land one plate stride away from the bed drawn here.
+ // Re-synced first because the control socket calls this without on_tab_shown().
if (!m_doc.origin_from_recipe) {
- const Vec2d bc = plater->build_volume().bed_center();
+ m_viewport->refresh_bed();
+ const Vec2d bc = m_viewport->bed_center();
m_doc.modeling_origin = Vec3d(bc.x(), bc.y(), 0.0);
// A document started here takes the weld rule from the preference; a loaded one
// brings its own (load_recipe).
@@ -7503,12 +7557,12 @@ void DesignPanel::refresh_tree()
// Preserve the selected row across the rebuild — set_rows() drops the selection, which made
// every edit/add feel like it "lost" the selection (and broke Edit/Move/Delete on the
// just-touched feature).
- const int keep = tree_selection();
+ const int keep = m_tree->selection();
// Datum/reference planes carry no solid; feed them to the viewport so they render as
// translucent rectangles (otherwise a Plane feature is invisible in the canvas).
refresh_datum_planes();
- update_reference_planes(); // body added/removed -> show/hide the XY/XZ/YZ origin planes
+ update_reference_planes();
std::vector rows;
rows.reserve(m_doc.features.size());
for (size_t fi = 0; fi < m_doc.features.size(); ++fi) {
@@ -7529,7 +7583,9 @@ void DesignPanel::refresh_tree()
row.colour = !f.enabled ? dp_item_dim()
: mate_conflict_reason(int(fi)) != nullptr ? wxColour(235, 110, 110)
: dp_item_text();
+ // A row with nothing to frame keeps a blank cell, so the icons stay in their columns.
row.actions = {
+ { RowZoom, can_zoom_to_feature(int(fi)) ? "design_zoom" : "", _L("Zoom to selection") },
{ RowEdit, "design_edit", _L("Edit") },
eye_action(f.enabled),
{ RowDelete, "design_delete", _L("Delete") },
@@ -7573,6 +7629,7 @@ void DesignPanel::refresh_parts()
// Hidden bodies are greyed and their eye is closed, so the state reads at a glance.
row.colour = vis ? dp_item_text() : dp_item_dim();
row.actions = {
+ { RowZoom, "design_zoom", _L("Zoom to selection") },
{ RowMove, "design_move", _L("Move") },
eye_action(vis),
{ RowDelete, "design_delete", _L("Delete") },
@@ -7605,12 +7662,22 @@ int DesignPanel::tree_body_selection() const
// if a row was selected.
bool DesignPanel::deselect_rows()
{
- const bool any = (m_tree && tree_selection() != wxNOT_FOUND) || tree_body_selection() >= 0;
+ const bool any = (m_tree && m_tree->selection() != wxNOT_FOUND) || tree_body_selection() >= 0;
if (m_tree) m_tree->unselect();
if (m_parts) m_parts->unselect();
return any;
}
+// A picked reference plane is a selection like a face: the next Sketch opens on it, so letting go
+// of everything has to let go of it too. The viewport asks for it every frame (set_selected_base).
+bool DesignPanel::drop_plane_pick()
+{
+ if (!m_plane_picked) return false;
+ m_plane_picked = false;
+ if (m_viewport) m_viewport->request_repaint();
+ return true;
+}
+
void DesignPanel::update_section_flip_btn()
{
if (m_section_flip_btn) m_section_flip_btn->Enable(m_section_on);
@@ -7725,23 +7792,108 @@ void DesignPanel::update_feature_highlight()
const int sel = tree_selection();
// Hidden while a feature card is open: the card's ghost and picks are what the view is about.
const bool wanted = sel >= 0 && sel < int(m_doc.features.size()) && m_active == Tool::None;
- if (wanted && (sel != m_hl_feature || m_hl_generation != m_doc.topo_generation)) {
+ // Re-sending the same faces is cheap: the viewport reuses what it has.
+ m_viewport->set_highlight_faces(wanted ? feature_faces(sel) : std::vector>{});
+}
+
+const std::vector>& DesignPanel::feature_faces(int f)
+{
+ if (f < 0 || f >= int(m_doc.features.size())) {
+ static const std::vector> none;
+ return none;
+ }
+ if (f != m_hl_feature || m_hl_generation != m_doc.topo_generation) {
std::vector> faces;
- const CadFeature& f = m_doc.features[sel];
- if (f.enabled && CadDocument::produces_body(f.type)) // the rest make no faces
- run_off_ui_thread(this, _L("Finding the feature's faces…"), [this, sel, &faces] {
+ const CadFeature& feat = m_doc.features[f];
+ if (feat.enabled && CadDocument::produces_body(feat.type)) // the rest make no faces
+ run_off_ui_thread(this, _L("Finding the feature's faces…"), [this, f, &faces] {
try {
- faces = m_doc.faces_made_by(sel);
+ faces = m_doc.faces_made_by(f);
} catch (...) {
faces.clear(); // a highlight is not worth an escaped exception
}
});
m_hl_faces = std::move(faces);
- m_hl_feature = sel;
+ m_hl_feature = f;
m_hl_generation = m_doc.topo_generation;
}
- // Re-sending the same faces is cheap: the viewport reuses what it has.
- m_viewport->set_highlight_faces(wanted ? m_hl_faces : std::vector>{});
+ return m_hl_faces;
+}
+
+bool DesignPanel::can_zoom_to_feature(int f) const
+{
+ if (f < 0 || f >= int(m_doc.features.size()))
+ return false;
+ const CadFeature& feat = m_doc.features[f];
+ return feat.type == CadFeatureType::Sketch || (feat.enabled && CadDocument::produces_body(feat.type));
+}
+
+void DesignPanel::zoom_to_feature(int f)
+{
+ // A rebuild's busy loop runs queued clicks while its worker owns the document.
+ if (s_doc_worker_busy.load() > 0) {
+ set_status(StatusKind::Info, _L("The model is being rebuilt — zoom again when it finishes"));
+ return;
+ }
+ if (m_viewport == nullptr || !can_zoom_to_feature(f))
+ return;
+ const CadFeature& feat = m_doc.features[f];
+ const wxString name = wxString::FromUTF8(feat.name); // before finding faces runs the event loop
+ BoundingBoxf3 box;
+ if (feat.type == CadFeatureType::Sketch) {
+ // Whichever geometry the sketch keeps, drawn or not: a sketch an Extrude consumed is
+ // still where the Extrude starts.
+ const auto add = [&box, &feat](const Vec2d& p) { box.merge(feat.plane.to_world(p)); };
+ if (!feat.entities.empty()) {
+ box = DesignSketchTool::sketch_box(feat.entities, feat.plane);
+ } else if (!feat.imported_regions.empty()) {
+ for (const auto& region : transform_regions(feat.imported_regions, feat.import_offset,
+ feat.import_scale_x, feat.import_scale_y))
+ for (const auto& contour : region)
+ for (const Vec2d& p : contour)
+ add(p);
+ } else if (!feat.profile.points.empty()) {
+ for (const Vec2d& p : feat.profile.points)
+ add(p);
+ } else {
+ // The legacy shape, centred on the plane origin (CadDocument::build_sketch_wire).
+ const Vec2d h = feat.shape == SketchShape::Circle ? Vec2d(feat.radius, feat.radius)
+ : Vec2d(feat.width, feat.height) * 0.5;
+ for (const Vec2d& p : { Vec2d(-h.x(), -h.y()), Vec2d(h.x(), -h.y()), Vec2d(h.x(), h.y()), Vec2d(-h.x(), h.y()) })
+ add(p);
+ }
+ } else {
+ box = m_viewport->faces_box(feature_faces(f));
+ }
+ if (!m_viewport->zoom_to_box(box))
+ set_status(StatusKind::Info, wxString::Format(_L("%s has nothing on show to zoom to"), name));
+}
+
+void DesignPanel::zoom_to_body(int b)
+{
+ if (m_viewport == nullptr)
+ return;
+ // From the body's display mesh, Move applied, rather than the selection: the viewport selects
+ // no hidden body (DesignSketchTool::select_body). An empty mesh's box still reads defined.
+ BoundingBoxf3 box;
+ if (b >= 0 && b < int(m_disp_body_meshes.size()) && !m_disp_body_meshes[b].empty())
+ box = m_disp_body_meshes[b].bounding_box();
+ if (!m_viewport->zoom_to_box(box))
+ set_status(StatusKind::Error, _L("That body has no display mesh yet — recompute first"));
+}
+
+void DesignPanel::zoom_to_selection()
+{
+ if (m_viewport == nullptr)
+ return;
+ // A body, as its row frames it; the kind has checked the index.
+ const OfferSel kind = OfferSel(offer_selection_kind());
+ if (kind == OfferSel::BodySolid || kind == OfferSel::BodySheet) {
+ zoom_to_body(m_sel_solid_body);
+ return;
+ }
+ if (!m_viewport->zoom_to_box(m_viewport->selection_box()))
+ set_status(StatusKind::Info, _L("Select something to zoom to"));
}
// Boolean (combine bodies) — one gate for every door onto the tool. A body-body operation
@@ -7761,6 +7913,12 @@ void DesignPanel::on_move_body()
{
const int b = m_sel_solid_body;
if (m_viewport == nullptr) return;
+ // Whatever is open owns the canvas and the ✓/✗, so a Move armed under it could not be left.
+ // The Bodies list stays clickable meanwhile, hence the check here.
+ if (m_active != Tool::None || m_ui_mode != UiMode::Feature || m_text_dlg != nullptr) {
+ set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
+ return;
+ }
if (b < 0 || b >= int(m_doc.display_body_meshes.size())) {
// Never fail silently here: the caller gates on bodies.size() while this needs a
// tessellated per-body mesh, and when those disagreed the click did nothing at all.
@@ -7768,6 +7926,14 @@ void DesignPanel::on_move_body()
: _L("That body has no display mesh yet — recompute first"));
return;
}
+ const wxString hint = _L("Drag the arrows to move, the rings to rotate — then Confirm (Esc cancels)");
+ // Already moving it: Cancel still goes back to where it began. Another body re-arms below,
+ // and the one being moved keeps its placement.
+ if (body_move_pending() && b == m_move_body) {
+ set_status(StatusKind::Info, hint);
+ return;
+ }
+ end_sketch_plane_choice(); // the gizmo takes the clicks a plane choice was waiting for
sync_body_xform();
// Delta gizmo: pivot at the body's CURRENT world centroid; the tool composes the drag deltas
// onto its current pose, so move + rotate both work (incl. on an already place-on-face'd body).
@@ -7788,7 +7954,25 @@ void DesignPanel::on_move_body()
if (m_move_angle) m_move_angle->SetValue(0.0);
show_move_card(true);
update_action_bar(); // surface the unified ✓/✗ while moving
- set_status(StatusKind::Info, _L("Drag the arrows to move, the rings to rotate — then Confirm (Esc cancels)"));
+ set_status(StatusKind::Info, hint);
+}
+
+// The one way out of the Move button's session, so the gizmo, the Move / Rotate card and the ✓/✗
+// go together. keep = true leaves the pose on screen, as starting another edit does; only Esc and
+// Cancel put the body back. A no-op when no Move is up.
+void DesignPanel::end_body_move(bool keep)
+{
+ if (!body_move_pending()) return;
+ if (!keep) {
+ sync_body_xform();
+ if (m_move_body < int(m_body_xform.size()))
+ m_body_xform[m_move_body] = m_move_prev;
+ feed_bodies(); // re-render the reverted placement
+ }
+ m_viewport->clear_move_gizmo();
+ m_move_body = -1;
+ show_move_card(false);
+ update_action_bar();
}
// Transform card (add mode): arm the same move gizmo on the card's body so the geometry-first
@@ -7809,8 +7993,6 @@ void DesignPanel::arm_transform_gizmo()
if (m_viewport) m_viewport->begin_move_body(b, pivot, base, radius);
m_xf_gizmo_body = b;
m_xf_gizmo_base = base;
- m_move_body = b; // the existing revert paths key off these two
- m_move_prev = base;
// Write the pivot into the card so the parametric feature reproduces what was dragged;
// dx/dy/dz and angle start at zero (the gizmo reports deltas from this pose).
if (m_xf_pivot_x) m_xf_pivot_x->SetValue(pivot.x());
@@ -7869,6 +8051,7 @@ bool DesignPanel::place_on_face()
}
const TopoDS_Face face = GeometryEngine::face_by_index(m_doc.bodies[b].shape, m_sel_solid_face);
if (face.IsNull()) return false;
+ end_sketch_plane_choice(); // the body moves; a sketch was not started
sync_body_xform();
const Transform3d old_x = m_body_xform[b];
// Outward face normal in the body's CURRENT displayed orientation.
@@ -7902,8 +8085,7 @@ void DesignPanel::set_tree_selection(int row)
// The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and
// features by index, so replacing or renumbering the feature list (undo/redo, New Design, load,
-// delete, reorder) drops it, the viewport's highlights with it. The solid highlight too: a rebuild
-// that leaves no body never reaches set_solid_pick. The callers repaint.
+// delete, reorder) drops it, the viewport's highlights with it. The callers repaint.
void DesignPanel::drop_selection()
{
if (m_viewport != nullptr)
@@ -7930,7 +8112,7 @@ void DesignPanel::drop_solid_pick()
bool DesignPanel::begin_renumber()
{
if (m_ui_mode != UiMode::Feature || m_text_dlg != nullptr || m_active == Tool::Insert
- || (m_active == Tool::None && m_viewport != nullptr && m_viewport->moving_body())) {
+ || body_move_pending()) {
set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
return false;
}
@@ -7942,6 +8124,9 @@ bool DesignPanel::begin_renumber()
void DesignPanel::after_tree_edit(bool ok)
{
+ // Any edit of the history (undo, delete, show/hide, a socket call...) writes the status line
+ // over the plane prompt, so it ends the choice rather than leave it waiting unannounced.
+ end_sketch_plane_choice();
update_undo_redo_buttons();
refresh_tree();
refresh_variables();
@@ -7993,13 +8178,12 @@ void DesignPanel::on_new_design()
void DesignPanel::clear_document()
{
tool_cancel(); // leave any active tool / sketch / constrain cleanly
+ end_body_move(true); // ...and a Move, which tool_cancel leaves while a value is pending
m_doc.clear(); // features + bodies + meshes + history
m_doc.auto_close_loops = wxGetApp().is_auto_close_sketch_loops(); // a new design: today's preference
Slic3r::set_sketch_auto_close(m_doc.auto_close_loops);
drop_selection();
m_edit_index = -1;
- m_move_body = -1;
- show_move_card(false);
m_body_xform.clear();
if (m_viewport) { m_viewport->clear_move_gizmo(); m_viewport->clear_mesh(); }
after_tree_edit(true); // rebuild the (now empty) tree + clear the viewport
@@ -8083,6 +8267,11 @@ void DesignPanel::on_toggle_visibility()
return;
}
+ // Hiding or showing a feature can renumber the bodies, and the Move holds a body index.
+ if (body_move_pending()) {
+ set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
+ return;
+ }
int sel = tree_selection();
if (sel == wxNOT_FOUND || sel >= int(m_doc.features.size())) {
set_status(StatusKind::Error, _L("Select a feature in the tree first"));
@@ -8099,8 +8288,18 @@ void DesignPanel::on_toggle_visibility()
refresh_tree(); // greys the row
set_tree_selection(sel); // keep the toggled feature selected
if (m_viewport != nullptr) {
- if (m_doc.display_mesh.its.indices.empty()) m_viewport->clear_mesh();
- else feed_bodies();
+ if (m_doc.display_mesh.its.indices.empty()) {
+ m_viewport->clear_mesh();
+ } else {
+ // Not set_status_ok(), which would take the gizmo from an open Transform card.
+ feed_bodies();
+ // Hiding or showing a feature rebuilds the bodies and can renumber them, so the pick is
+ // re-pointed and its selection dropped. That also brings picking back for a body shown
+ // after everything was hidden (clear_mesh() dropped it).
+ m_viewport->set_solid_pick(&m_doc.bodies, &m_disp_pick_mesh,
+ &m_doc.display_tri_face, &m_doc.display_tri_body,
+ &m_body_visible, &m_body_xform);
+ }
}
sync_sketch_display(); // skips the hidden sketch + direct-renders
set_status(StatusKind::Info, shown ? _L("Feature shown") : _L("Feature hidden"));
@@ -10008,6 +10207,7 @@ void DesignPanel::on_commit()
// is shown on screen, not the pre-feature solid. (confirm_tool() applies + closes.)
if (m_active != Tool::None)
confirm_tool();
+ end_sketch_plane_choice(); // the bodies leave for the plate; a sketch was not started
if (m_doc.display_mesh.its.indices.empty()) {
set_status(_L("Nothing to commit — add a feature first"));
@@ -10990,10 +11190,9 @@ void DesignPanel::update_rib_gizmo()
m_rib_thickness ? m_rib_thickness->GetValue() : 0.0);
}
-// Onshape default planes: the XY/XZ/YZ reference planes are persistent, transparent, labelled, and
-// larger than the bed — shown as the FALLBACK when there is no object yet. When the Plane tool is
-// open they additionally surface existing datums so a base can be picked. Single authority for the
-// reference-plane overlay (set/clear_base_pick).
+// The XY/XZ/YZ reference planes and their axes. Single authority for the overlay
+// (set/clear_base_pick): the Plane tool offers them with the datums as Offset bases; otherwise they
+// are up while the Feature tree's Origin row shows them, and while a sketch plane is being chosen.
void DesignPanel::update_reference_planes()
{
if (!m_viewport) return;
@@ -11020,26 +11219,117 @@ void DesignPanel::update_reference_planes()
}
return;
}
- // Fallback (Onshape default planes): show the 3 reference planes while there is no SOLID body
- // yet — so they persist through the 2D-sketch phase and reappear after a sketch is confirmed
- // (a sketch creates no body). They no longer block selection: clicking existing geometry wins,
- // a base-plane pick only fires on a click that hit nothing else (see on_mouse fall-through).
- // Available while there is no solid yet OR while the user is actually choosing a sketch
- // plane. The second half fixes a dead end: delete a sketch on a document that still has a
- // body, press Sketch, and act_sketch says "click a face or a reference plane" — with the
- // reference planes already taken away, because a body existed. The instruction was
- // impossible to follow and there was no way to start a sketch at all short of finding a
- // face to click.
- //
- // Not simply always-on: m_dbp_active both RENDERS and picks, so three translucent planes
- // would otherwise float over every finished model. Tying them to Sketch mode shows them
- // exactly when they are the thing being chosen, and hides them again on Finish.
- if (m_doc.bodies.empty() || m_ui_mode == UiMode::Sketch)
+ // Up while Sketch waits for a plane to be picked (start_sketch), which the session it opens
+ // takes away; a live session would draw none anyway (DesignSketchTool::draws_reference_axes).
+ // They never block selection: clicking existing geometry wins, and a base-plane pick only
+ // fires on a click that hit nothing else (see on_mouse fall-through).
+ if (m_show_origin || m_choosing_sketch_plane)
m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl));
else
m_viewport->clear_base_pick();
}
+void DesignPanel::toggle_origin()
+{
+ m_show_origin = !m_show_origin;
+ wxGetApp().app_config->set_bool("design_show_origin", m_show_origin);
+ // A plane picked while they were up is not a selection once nobody can see it.
+ if (!m_show_origin) drop_plane_pick();
+ // Not an edit: a plane choice under way stays, and keeps its planes up.
+ update_reference_planes(); // repaints
+ refresh_pinned(); // the Origin row's eye
+}
+
+void DesignPanel::toggle_bed()
+{
+ m_show_bed = !m_show_bed;
+ wxGetApp().app_config->set_bool("design_show_bed", m_show_bed);
+ if (m_viewport) m_viewport->set_show_bed(m_show_bed); // repaints
+ refresh_pinned(); // the Bed row's eye
+}
+
+// View switches, not history: a view toggle rebuilds these two rows and leaves the features
+// alone. The label dims while its thing is hidden, as a hidden body's does.
+void DesignPanel::refresh_pinned()
+{
+ std::vector rows;
+ auto add = [&rows](const char* icon, const wxString& label, bool shown, const wxString& tip) {
+ DesignRowList::Action eye = eye_action(shown);
+ eye.tip = tip;
+ // A blank cell under the features' Delete, so the eye sits in the features' eye column.
+ rows.push_back({ icon, label, {}, shown ? dp_item_text() : dp_item_dim(), { eye, {} } });
+ };
+ // In kOriginRow / kBedRow order.
+ add("design_plane", _L("Origin"), m_show_origin, origin_toggle_text(m_show_origin));
+ add("design_bed", _L("Bed"), m_show_bed, bed_toggle_text(m_show_bed));
+ m_pinned->set_rows(std::move(rows));
+}
+
+void DesignPanel::start_sketch()
+{
+ if (m_ui_mode != UiMode::Feature) return; // already sketching or constraining
+ if (m_active != Tool::None || m_text_dlg != nullptr || body_move_pending()) {
+ set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
+ return;
+ }
+ wxString where;
+ if (sketch_plane_target(where)) {
+ start_sketch_on_target(true);
+ return;
+ }
+ // Nothing to sketch on yet: offer the planes and stay in Feature mode until one is picked
+ // (set_on_datum_base_picked, set_on_solid_selection_changed).
+ m_choosing_sketch_plane = true;
+ update_reference_planes();
+ update_action_bar(); // ✗ leaves the choice
+ set_status(StatusKind::Info, sketch_plane_prompt());
+}
+
+void DesignPanel::start_sketch_on_target(bool offer_tools)
+{
+ if (!m_viewport) return;
+ wxString on;
+ const SketchPlane plane = sketch_plane_from_selection(on);
+ m_viewport->begin_sketch(plane, DesignSketchTool::Mode::Select);
+ // The face or plane has been CONSUMED as the sketch plane, so drop the pick. A face left live
+ // meant the next Extrude saw a selected face and push/pulled it instead of extruding the
+ // sketch just drawn — the same trap the imported-art path already guards against — and a
+ // plane left picked would open the next sketch on it, unseen once the planes are hidden.
+ // The canvas's half goes too: left lit there, the next click on that face read as a second
+ // click on it and took the whole body, which has no plane to sketch on.
+ drop_solid_pick();
+ m_plane_picked = false;
+ m_construction->SetValue(false); // a fresh session starts non-construction
+ set_ui_mode(UiMode::Sketch); // ends the plane choice, so the planes go
+ m_sketch_on = on; // shown with the tool hint, so the target is visible
+ set_status(StatusKind::Info, wxString::Format(_L("Sketch plane: %s — pick a tool"), on));
+ if (m_sketch_hint) { // the card must agree with the status line, not argue with it
+ m_sketch_hint->SetLabel(wxString::Format(_L("Sketch plane: %s.\nPick a tool, or press Menu for the list."), on));
+ m_sketch_hint->Refresh();
+ m_cards->Layout();
+ }
+ // Hand over the tools rather than naming them in a status line: the sketch bar carries none.
+ // CallAfter so the mode change has settled, and the pick that got here has returned, before a
+ // modal menu takes the loop; the menu carries each tool's shortcut, so pressing the key
+ // instead of picking a row costs nothing.
+ if (offer_tools)
+ CallAfter([this] { show_offer_menu(offer_anchor()); });
+}
+
+void DesignPanel::end_sketch_plane_choice()
+{
+ if (!m_choosing_sketch_plane) return;
+ m_choosing_sketch_plane = false;
+ update_reference_planes();
+ update_action_bar();
+}
+
+bool DesignPanel::sketch_plane_pick_live() const
+{
+ return m_choosing_sketch_plane && m_ui_mode == UiMode::Feature && m_active == Tool::None
+ && m_text_dlg == nullptr && !body_move_pending();
+}
+
TriangleMesh DesignPanel::ghost_from_bodies(const std::vector& per_body) const
{
TriangleMesh out;
@@ -11197,10 +11487,12 @@ void DesignPanel::refresh_preview()
// Confirm so the user sees the gate before clicking; the red status says why.
m_candidate_ok = ok;
update_confirm_button();
- // Fillet/Chamfer/Draft: once the target edge/face yields a valid result, show ONLY the
+ // Fillet/Chamfer/Draft/Hole: once the target edge/face yields a valid result, show ONLY the
// preview (hide the base bodies) so the user sees the finished shape, not the old solid
- // doubled with the ghost. Before a valid pick the body stays visible so it can be picked.
- m_viewport->set_body_hidden((m_active == Tool::Dressup || m_active == Tool::Draft) && ok);
+ // doubled with the ghost. A hole's cut lies inside the old solid, so without this the body
+ // would hide it entirely. Before a valid pick the body stays visible so it can be picked.
+ m_viewport->set_body_hidden((m_active == Tool::Dressup || m_active == Tool::Draft ||
+ m_active == Tool::Hole) && ok);
m_status->Refresh();
// Refresh the in-canvas Extrude depth arrow (self-gates: only while the Extrude card is open).
@@ -11292,7 +11584,9 @@ void DesignPanel::push_polygon_params()
void DesignPanel::open_tool(Tool t)
{
+ end_body_move(true); // first: arm_transform_gizmo below takes over the same gizmo
m_active = t;
+ end_sketch_plane_choice(); // after m_active: the Plane card keeps its planes
m_candidate_ok = true; // a fresh card is confirmable until its preview says otherwise
// Fillet/Chamfer/Draft no longer fade the body see-through; instead, once a valid target
// is picked, refresh_preview hides the base bodies entirely (preview-only). Keep it opaque
@@ -11590,7 +11884,6 @@ void DesignPanel::close_tool()
if (m_viewport) m_viewport->clear_move_gizmo();
feed_bodies();
m_xf_gizmo_body = -1;
- m_move_body = -1;
}
if (m_viewport) { m_viewport->set_body_translucent(false); m_viewport->set_body_hidden(false); m_viewport->set_xray_focus(-1); } // restore the opaque solid
wxSizer* s = m_cards->GetSizer();
@@ -11639,7 +11932,7 @@ void DesignPanel::close_tool()
m_viewport->clear_datum_gizmo();
m_viewport->set_operand_bodies(-1, -1);
m_viewport->set_highlight_sketches({});
- update_reference_planes(); // back to no-tool: show the origin planes if there is no object yet
+ update_reference_planes(); // back to no-tool
update_cards_frame(); m_form->Layout();
m_form->FitInside();
update_action_bar(); // no feature tool active -> hide the bar (unless a mode keeps it)
@@ -11714,11 +12007,8 @@ void DesignPanel::cancel_tool()
void DesignPanel::tool_confirm()
{
if (m_value_cont) { confirm_value(); return; } // value card owns ribbon ✓ while a value is pending
- if (m_active == Tool::None && m_viewport && m_viewport->moving_body()) { // keep the placement, drop the gizmo
- m_viewport->clear_move_gizmo();
- m_move_body = -1;
- show_move_card(false);
- update_action_bar();
+ if (body_move_pending()) { // keep the placement, drop the gizmo
+ end_body_move(true);
set_status_ok();
return;
}
@@ -11746,20 +12036,18 @@ void DesignPanel::tool_confirm()
void DesignPanel::tool_cancel()
{
if (m_value_cont) { cancel_value(); return; } // value card owns ribbon ✗ while a value is pending
- if (m_active == Tool::None && m_viewport && m_viewport->moving_body()) { // revert to the pose at move-start
- sync_body_xform();
- if (m_move_body >= 0 && m_move_body < int(m_body_xform.size()))
- m_body_xform[m_move_body] = m_move_prev;
- m_viewport->clear_move_gizmo();
- m_move_body = -1;
- show_move_card(false);
- feed_bodies(); // re-render the reverted placement
- update_action_bar();
+ if (body_move_pending()) { // revert to the pose at move-start
+ end_body_move(false);
set_status(StatusKind::Info, _L("Move canceled"));
return;
}
if (m_active == Tool::Insert) { cancel_insert(); return; }
if (m_active != Tool::None) { cancel_tool(); return; }
+ if (m_choosing_sketch_plane) { // Sketch was waiting for a plane: no sketch, planes away
+ end_sketch_plane_choice();
+ set_status(StatusKind::Info, wxString());
+ return;
+ }
if (m_ui_mode == UiMode::Sketch) {
// THE explicit discard. Esc no longer arrives here at all (it routes through escape(),
// which cannot destroy anything), so this button is now the only way a drawn sketch is
@@ -11795,8 +12083,7 @@ bool DesignPanel::confirm_enabled() const
{
if (m_value_cont) return true;
if (m_active == Tool::None)
- return (m_viewport && m_viewport->moving_body())
- || m_ui_mode == UiMode::Sketch || m_ui_mode == UiMode::Constrain;
+ return body_move_pending() || m_ui_mode == UiMode::Sketch || m_ui_mode == UiMode::Constrain;
if (m_active == Tool::Insert) return true;
return m_candidate_ok;
}
@@ -11820,12 +12107,12 @@ CadLevel DesignPanel::escape_level() const
|| (m_viewport && m_viewport->inline_busy());
// An uncommitted delta: clicks are down on an entity that does not exist yet, or a body is
// being moved by a gizmo that has not been confirmed.
- st.gesture_active = m_viewport
- && (m_viewport->drawing_in_progress()
- || (m_active == Tool::None && m_viewport->moving_body()));
- // Something is armed and waiting for input: a feature card, a sketch draw tool, Constrain.
- // A sketch SESSION is deliberately not in this list — see escape().
+ st.gesture_active = body_move_pending()
+ || (m_viewport && m_viewport->drawing_in_progress());
+ // Something is armed and waiting for input: a feature card, Sketch waiting for its plane, a
+ // sketch draw tool, Constrain. A sketch SESSION is deliberately not in this list — see escape().
st.tool_armed = (m_active != Tool::None)
+ || m_choosing_sketch_plane
|| m_ui_mode == UiMode::Constrain
|| (m_ui_mode == UiMode::Sketch && m_viewport && !m_viewport->sketch_is_selecting());
st.has_selection = m_viewport && m_viewport->has_any_selection();
@@ -11862,7 +12149,10 @@ void DesignPanel::escape()
// CANDIDATE (never a committed feature — in edit mode reset_edit_state only forgets which
// feature was being edited, the feature itself is untouched); an armed sketch tool falls
// back to Select, leaving every entity already drawn exactly where it is.
- if (m_active != Tool::None || m_ui_mode == UiMode::Constrain) { tool_cancel(); return; }
+ if (m_active != Tool::None || m_choosing_sketch_plane || m_ui_mode == UiMode::Constrain) {
+ tool_cancel();
+ return;
+ }
if (m_viewport && m_viewport->sketch_disarm_tool()) {
set_status(StatusKind::Info, _L("Select"));
}
@@ -11873,7 +12163,8 @@ void DesignPanel::escape()
// both of which say which one they are, and never through a key pressed on the way out of
// something else. The Feature tree and Bodies rows count as selections too.
const bool row = deselect_rows();
- if ((m_viewport && m_viewport->clear_any_selection()) || row) {
+ const bool plane = drop_plane_pick();
+ if ((m_viewport && m_viewport->clear_any_selection()) || row || plane) {
set_status(StatusKind::Info, wxString());
return;
}
@@ -11896,7 +12187,7 @@ bool DesignPanel::menu_can_undo_redo(bool redo) const
{
if (m_ui_mode == UiMode::Sketch && m_viewport && m_viewport->is_sketching())
return redo ? m_viewport->can_redo_sketch_entity() : m_viewport->can_undo_sketch_entity();
- if (m_ui_mode != UiMode::Feature || m_active != Tool::None) return false;
+ if (m_ui_mode != UiMode::Feature || m_active != Tool::None || body_move_pending()) return false;
return redo ? m_doc.can_redo() : m_doc.can_undo();
}
@@ -11916,9 +12207,10 @@ void DesignPanel::update_action_bar()
wxSizer* s = m_toolbar->GetSizer();
if (s == nullptr) return;
const bool active = (m_active != Tool::None)
+ || m_choosing_sketch_plane // ✗ only: ✓ has nothing to confirm
|| m_ui_mode == UiMode::Sketch
|| m_ui_mode == UiMode::Constrain
- || (m_viewport && m_viewport->moving_body());
+ || body_move_pending();
s->Show(m_tb_action, active, true);
// HIDING THE BAR ORPHANS THE KEYBOARD, and that is the "app does not consent to sketch"
// report. The ✓/✗ live in this bar, so the click that confirms a feature leaves focus on a
@@ -11954,7 +12246,8 @@ void DesignPanel::do_undo_redo(bool redo)
// v1: act only in Feature mode. While authoring/constraining a sketch (m_ui_mode) or
// with a feature dialog open (m_active), Esc/Cancel is the way out — popping committed
// history mid-tool would be ambiguous (and could orphan the tool's referenced feature).
- if (m_ui_mode != UiMode::Feature || m_active != Tool::None) {
+ // A Move is not in the history at all, so an undo under it would keep it without asking.
+ if (m_ui_mode != UiMode::Feature || m_active != Tool::None || body_move_pending()) {
set_status(StatusKind::Info, _L("Finish or cancel the current tool first (Esc)"));
return;
}
diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp
index 5e39362fad..823282bb57 100644
--- a/src/slic3r/GUI/CAD/DesignPanel.hpp
+++ b/src/slic3r/GUI/CAD/DesignPanel.hpp
@@ -383,7 +383,16 @@ private:
void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card)
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
- void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
+ void update_reference_planes(); // the XY/XZ/YZ planes + axes: Origin row, plane choice, Plane tool
+ void toggle_origin(); // the Origin row's eye (and Ctrl+Shift+O): keep the reference planes up
+ void toggle_bed(); // the Bed row's eye (and Ctrl+Shift+B): draw the printer bed, or not
+ void refresh_pinned(); // rebuild the Origin and Bed rows (their eyes and label colours)
+ // Sketch: open a session on the picked face or plane, or with none picked, put the planes up
+ // and wait for one (m_choosing_sketch_plane).
+ void start_sketch();
+ void start_sketch_on_target(bool offer_tools); // a session on sketch_plane_from_selection()
+ void end_sketch_plane_choice(); // leave the plane choice without a sketch; no-op outside it
+ bool sketch_plane_pick_live() const; // may a pick made now still open the sketch?
CadDocument m_doc;
@@ -413,7 +422,6 @@ private:
ScalableButton* m_commit_btn{nullptr}; // main face: runs the current commit mode
DropDown* m_commit_drop{nullptr}; // commit-mode choices, owned via m_flyout_keepalive
wxSizer* m_tb_doc{nullptr}; // toolbar document/view actions (new, commit, export, section, place)
- CheckBox* m_show_bed{nullptr}; // view option: draw the printer bed + plate grid, or not
wxSizer* m_box_move{nullptr}; // Move/Rotate numeric options (distance, axis, angle)
wxSizer* m_box_sketch{nullptr};
wxSizer* m_box_extrude{nullptr};
@@ -571,8 +579,20 @@ private:
// deliberately no dropdown for it. e1p.
int m_ref_plane{0};
// m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY"
- // from "nobody has chosen anything yet". This does.
+ // from "nobody has chosen anything yet". This does. A selection like a picked face: the sketch
+ // opened on it uses it up, and Esc or a click on nothing lets go of it.
bool m_plane_picked{false};
+ // Sketch was pressed with nothing to sketch on: the reference planes are up and the next
+ // reference plane or flat face clicked opens the sketch on it. Still Feature mode — sketch
+ // mode is entered only with the session (start_sketch_on_target).
+ bool m_choosing_sketch_plane{false};
+ // The Feature tree's Origin row: keeps the reference planes and their axes up
+ // (update_reference_planes). A view preference (AppConfig "design_show_origin"), not part of
+ // the recipe.
+ bool m_show_origin{false};
+ // The Feature tree's Bed row: draws the printer bed and its plate grid. A view preference
+ // (AppConfig "design_show_bed"), not part of the recipe.
+ bool m_show_bed{true};
ComboBox* m_shape{nullptr};
ComboBox* m_mode{nullptr};
wxSpinCtrlDouble* m_width{nullptr};
@@ -913,12 +933,16 @@ private:
std::function m_value_cont; // deferred apply, run on Confirm
std::function m_value_cancel; // optional action when the card is cancelled
- // Feature tree: one row per feature, in feature order, with a per-type icon and the row's
- // own Edit / Show-hide / Delete icons. Callers use row indices via
- // tree_selection()/set_tree_selection(); refresh_tree() rebuilds the rows.
+ // Feature tree: the Origin and Bed rows, a fixed block of view switches that never scrolls
+ // and selects nothing, then, in a frame of its own below them, one row per feature, in feature
+ // order, with a per-type icon and the row's own Zoom to selection / Edit / Show-hide / Delete
+ // icons. Callers use row indices via tree_selection()/set_tree_selection(); refresh_tree()
+ // rebuilds the rows.
+ DesignRowList* m_pinned{nullptr};
DesignRowList* m_tree{nullptr};
- // The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected.
- // Finding them replays the history, so they are kept per row and topology generation.
+ // The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected, and
+ // what Zoom to selection frames for a row. Finding them replays the history, so they are kept
+ // per row and topology generation (feature_faces).
// request_feature_highlight() refreshes them after the current event; every change of row,
// card or topology calls it.
int m_hl_feature{-1}; // the row m_hl_faces were found for...
@@ -927,7 +951,10 @@ private:
bool m_hl_pending{false};
void request_feature_highlight();
void update_feature_highlight();
+ // The faces feature `f` made, found again when the cached ones are another row's or topology's.
+ const std::vector>& feature_faces(int f);
bool deselect_rows(); // Esc / a click on nothing: drop the tree and Bodies rows
+ bool drop_plane_pick(); // ...and a picked reference plane; true if one was picked
// Bodies list under the feature tree: one row per body (parallel to m_doc.bodies). Selecting
// one highlights that body and makes it the target for the next op.
DesignRowList* m_parts{nullptr};
@@ -964,8 +991,17 @@ private:
void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform
void feed_bodies(); // push m_disp_* + visibility/xform to the viewport
void on_move_body(); // start the move gizmo on the selected body
+ bool body_move_pending() const { return m_move_body >= 0; } // the Move button's session is open
+ void end_body_move(bool keep); // leave it: keep the dragged pose, or put the body back
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
+ // Zoom to selection: frame one thing along the current view, as the canvas's Fit button frames
+ // a selection. A body is framed whole, hidden or not; a feature by its sketch or the faces it
+ // made; anything else the offer is opened on, by what the Fit button frames for it.
+ void zoom_to_body(int b);
+ void zoom_to_feature(int f);
+ void zoom_to_selection();
+ bool can_zoom_to_feature(int f) const; // a sketch, or a shown feature that makes faces
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1
diff --git a/src/slic3r/GUI/CAD/DesignRowList.cpp b/src/slic3r/GUI/CAD/DesignRowList.cpp
index facf0aa5ed..1c39009cd5 100644
--- a/src/slic3r/GUI/CAD/DesignRowList.cpp
+++ b/src/slic3r/GUI/CAD/DesignRowList.cpp
@@ -29,11 +29,19 @@ static constexpr int kCellDip = 20; // square action cell, hover chip included
static constexpr int kPadDip = 4; // row edges, and the gap before the action cells
static constexpr int kGapDip = 6; // type icon to label
-DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
- // wxVListBox defaults to wxBORDER_THEME; the sidebar's lists take a simple frame.
- : wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE)
+DesignRowList::DesignRowList(wxWindow* parent, int max_visible, bool selectable, long style)
+ // wxVListBox defaults to wxBORDER_THEME; `style` defaults to the simple frame the sidebar's
+ // lists take, and a list meant to sit unframed on its card passes wxBORDER_NONE.
+ : wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, style)
, m_max_visible(std::max(max_visible, 1))
+ , m_selectable(selectable)
{
+ // A non-selectable list takes no focus: from the mouse because it consumes the presses that
+ // would focus it, below, and from the keyboard through AcceptsFocus(). wxGTK reads that once,
+ // while the base class is being built, before the override exists, so it is told here.
+ if (!m_selectable)
+ SetCanFocus(false);
+
Bind(wxEVT_LISTBOX, [this](wxCommandEvent&) { if (on_select) on_select(); });
Bind(wxEVT_LISTBOX_DCLICK, [this](wxCommandEvent&) { if (on_activate) on_activate(); });
@@ -46,18 +54,20 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
m_pressed = Hit{};
e.Skip();
});
- // A press on an action cell is remembered and skipped, so the list still selects the row.
+ // A press on an action cell is remembered and skipped, so the list still selects the row. A
+ // non-selectable list keeps every press from wxVListBox, which would select the row and take
+ // the focus.
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
m_pressed = h.cell >= 0 ? h : Hit{};
- e.Skip();
+ if (m_selectable) e.Skip();
});
// The second press of a double-click on a cell is a press too, and is not skipped: the list
// would otherwise turn it into a row double-click, so a quick double toggle of the eye would
// also open the feature for editing.
Bind(wxEVT_LEFT_DCLICK, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
- if (h.cell < 0) { m_pressed = Hit{}; e.Skip(); return; }
+ if (h.cell < 0) { m_pressed = Hit{}; if (m_selectable) e.Skip(); return; }
m_pressed = h;
});
Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
@@ -69,9 +79,9 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
const int id = m_rows[pressed.row].actions[pressed.cell].id;
// After the click has finished dispatching: the action may rebuild these rows. The row
// must still exist and still be the selected one, or the click is dropped rather than
- // applied to whatever row took its place.
+ // applied to whatever row took its place. A non-selectable list has no selected row.
CallAfter([this, row = pressed.row, id] {
- if (row < int(GetItemCount()) && row == GetSelection() && on_action)
+ if (row < int(GetItemCount()) && (!m_selectable || row == GetSelection()) && on_action)
on_action(row, id);
});
});
@@ -79,7 +89,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
// press suppresses the wxEVT_CONTEXT_MENU that follows it.
Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
- if (h.row != wxNOT_FOUND) select(h.row);
+ if (m_selectable && h.row != wxNOT_FOUND) select(h.row);
e.Skip();
});
Bind(wxEVT_CONTEXT_MENU, [this](wxContextMenuEvent& e) {
@@ -95,7 +105,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
return;
// On MSW the menu comes with the button's release, and the pointer may have moved to
// another row since the press selected one: the menu is for the row it opens over.
- select(row);
+ if (m_selectable) select(row);
if (on_menu) on_menu(row, screen);
});
// wxVListBox takes every wheel event, even with nothing to scroll, so a short list would stop
@@ -254,7 +264,7 @@ void DesignRowList::OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) con
wxColour bg;
if (IsSelected(n))
bg = StateColor::darkModeColorFor(wxColour("#BFE1DE"));
- else if (int(n) == m_hover.row)
+ else if (m_selectable && int(n) == m_hover.row) // a non-selectable row lights only its icons
bg = StateColor::darkModeColorFor(wxColour("#E5F0EE"));
if (!bg.IsOk())
return; // the list already cleared to its background colour
@@ -314,7 +324,7 @@ DesignRowList::Hit DesignRowList::hit_test(const wxPoint& pt) const
const wxRect rect = GetItemRect(h.row);
const auto& acts = m_rows[h.row].actions;
for (size_t i = 0; i < acts.size(); ++i)
- if (cell_rect(rect, acts.size(), i).Contains(pt)) {
+ if (!acts[i].icon.empty() && cell_rect(rect, acts.size(), i).Contains(pt)) {
h.cell = int(i);
break;
}
diff --git a/src/slic3r/GUI/CAD/DesignRowList.hpp b/src/slic3r/GUI/CAD/DesignRowList.hpp
index ff1ecc482a..7c2c524691 100644
--- a/src/slic3r/GUI/CAD/DesignRowList.hpp
+++ b/src/slic3r/GUI/CAD/DesignRowList.hpp
@@ -29,12 +29,16 @@ namespace Slic3r { namespace GUI {
// Selection follows wxTreeCtrl's contract, which DesignPanel's mutual exclusion between the two
// lists relies on: on_select runs on every change, whether the user or select() made it, and
// selecting the row that is already selected changes nothing and notifies nobody.
+//
+// A list built non-selectable is a row of switches, not of objects: a click never selects a row,
+// the pointer highlights only the action icon under it, and the list never takes the focus. Its
+// action icons and its context menu are the only things that answer.
class DesignRowList : public wxVListBox
{
public:
struct Action {
int id; // the owner's code for it, handed back to on_action
- std::string icon; // icon name, e.g. "design_eye"
+ std::string icon; // icon name, e.g. "design_eye"; empty for a blank cell that only holds a column
wxString tip;
};
struct Row {
@@ -50,7 +54,8 @@ public:
static constexpr const char* hover_chip = "#D4D4D4";
// The list is as tall as its rows, at least one and at most `max_visible`; past that it scrolls.
- DesignRowList(wxWindow* parent, int max_visible);
+ // `style` is its frame: wxBORDER_NONE for a list that sits unframed on its card.
+ DesignRowList(wxWindow* parent, int max_visible, bool selectable = true, long style = wxBORDER_SIMPLE);
// Replace every row. Clears the selection without notifying and cancels a rename in progress;
// the owner re-selects the row it keeps, which notifies.
@@ -67,15 +72,20 @@ public:
std::function on_select; // the selection changed
std::function on_activate; // the selected row was double-clicked
- // A row's action icon was clicked. Runs after the click has finished dispatching, and only
- // while that row is still the selected one; the click itself selected it.
+ // A row's action icon was clicked. Runs after the click has finished dispatching, while the
+ // row still exists and — in a selectable list, where the click itself selected it — is still
+ // the selected one.
std::function on_action;
- // Context menu on a row, at a screen position; the row under the pointer is selected first.
+ // Context menu on a row, at a screen position; the row under the pointer is selected first,
+ // unless the list is non-selectable. Pop the menu from inside the call: wxGTK sends it on the
+ // right press, and a menu deferred past that event closes on the button's release.
std::function on_menu;
// The editor committed `name` for `row`, trimmed and never empty (an empty commit cancels).
// Runs after the editor's own events have finished, so the owner may rebuild the rows here.
std::function on_rename;
+ bool AcceptsFocus() const override { return m_selectable && wxVListBox::AcceptsFocus(); }
+
protected:
void OnDrawItem(wxDC& dc, const wxRect& rect, size_t n) const override;
void OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) const override;
@@ -103,6 +113,7 @@ private:
std::vector m_rows;
std::map m_icons;
int m_max_visible;
+ bool m_selectable;
int m_row_h{0}; // every row's height, from measure_row()
Hit m_hover;
Hit m_pressed; // action cell under the last left press
diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp
index 4d6851b473..d5007239c1 100644
--- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp
+++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp
@@ -30,7 +30,7 @@
#include