From fd1bc092d82875d761b063e55db0c4b9961fd346 Mon Sep 17 00:00:00 2001
From: Tommaso Bianchi
Date: Sat, 1 Aug 2026 06:21:08 +0200
Subject: [PATCH] Design: slot Radius caption, keyboard offer, plane combo
removal, mass props, docs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Mirror of snaporca ac85277bac..0e7cb3ec78 (six changes, applied as a patch to
DesignPanel.cpp rather than copied, so this fork's 30 permitted divergent lines survive
— parity re-checked afterwards: the five shared files are byte-identical, DesignCanvas.cpp
and DesignPanel.cpp differ by exactly 16 and 30 lines).
- The straight slot's inline field says Radius, which is what it sets. It stores the
half-width and passed the typed number through unchanged, so 30 produced a 60 mm slot.
- The offer opens from the keyboard (Menu, Shift+F10), anchored on the viewport rather
than wherever the pointer happens to be. The card hint names the new route.
- The sketch card's Plane combo is gone; the plane comes from the viewport. Also stops
build_candidate collapsing a face plane to a base plane while editing.
- Mass properties and the dead Edit row are wired into the offer; DesignOffer.hpp is
regenerated from tool_atlas.json, verified by re-running the generator and diffing.
- docs/rig_build_traps.md + scripts/rig-build.sh, which derives its fork identity from
project() so it cannot be pointed at the other fork's image or volume.
- docs/design_tab.md refreshed (44 commits stale) + a PR description, with this fork's
own merge-base and diff shape rather than snaporca's.
Built green in the deps container with the new script and verified on the rig: Menu and
Shift+F10 both open the offer at the viewport centre with the pointer parked off-canvas,
and the sketch card shows no Plane row.
Co-Authored-By: Claude Opus 5 (1M context)
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
---
docs/design_tab.md | 152 +++++++++++++++++++++++++---
docs/design_tab_pr_description.md | 73 +++++++++++++
docs/rig_build_traps.md | 123 ++++++++++++++++++++++
docs/ux/offer_atlas.html | 4 +-
docs/ux/offer_atlas_inline.html | 4 +-
docs/ux/tool_atlas.json | 6 +-
scripts/docker-iter-build.sh | 1 +
scripts/kernel-test.sh | 1 +
scripts/rig-build.sh | 76 ++++++++++++++
src/slic3r/GUI/DesignCanvas.hpp | 2 +-
src/slic3r/GUI/DesignOffer.hpp | 4 +-
src/slic3r/GUI/DesignPanel.cpp | 83 ++++++++++++---
src/slic3r/GUI/DesignPanel.hpp | 7 +-
src/slic3r/GUI/DesignSketchTool.cpp | 4 +-
src/slic3r/GUI/DesignSketchTool.hpp | 2 +-
15 files changed, 499 insertions(+), 43 deletions(-)
create mode 100644 docs/design_tab_pr_description.md
create mode 100644 docs/rig_build_traps.md
create mode 100755 scripts/rig-build.sh
diff --git a/docs/design_tab.md b/docs/design_tab.md
index f65dfa4d28..f9ad40cacb 100644
--- a/docs/design_tab.md
+++ b/docs/design_tab.md
@@ -1,8 +1,9 @@
# The Design tab
-A parametric CAD modeller inside the slicer. Draw a sketch, turn it into a solid, refine it,
-and send it straight to Prepare — without leaving for another application and coming back
-through an STL.
+Object-driven parametric CAD inside the slicer. Point at geometry; the geometry offers the
+verbs that apply to it. Selection comes first and the tool consumes it. Draw a sketch,
+constrain it, turn it into a solid, refine it, and send it straight to Prepare — without
+leaving for another application and coming back through an STL.
The model is a **recipe**, not a mesh. Every action becomes a feature in a tree that is
replayed from the start whenever anything changes, so editing a dimension you set twenty
@@ -14,19 +15,142 @@ already ships for STEP import.
## Getting started
1. Open the **Design** tab.
-2. Pick a plane — XY, XZ or YZ — and press **Sketch**.
-3. Draw a closed profile, then press **✓** to finish the sketch.
-4. Select the sketch and press **Extrude** (`Shift+E`).
+2. Click a face or a reference plane in the viewport, then press `Shift+S` (Sketch). The offer
+ opens with the sketch tools on it.
+3. Draw a closed profile, then press **✓ Confirm** in the floating action bar.
+4. With the sketch selected, press `Shift+E` (Extrude).
5. Press **Commit to Plate** to hand the solid to Prepare.
The status line under the toolbar is the thing to watch: it says what the current tool is
-waiting for, and it is where a refusal explains itself.
+waiting for. When no plane is picked it reads *"Click a face or a reference plane in the
+viewport, then a sketch tool"*; once one is picked it reads *"Sketching on — pick a
+tool"*. It is also where a refusal explains itself.
+
+---
+
+## Selecting
+
+- One left-click selects what is under the cursor. There is no click-cycling through
+ face → edge → body.
+- A click near a corner takes the corner, not the face behind it.
+- Left-drag sweeps a rubber band, and a rubber band takes the whole body.
+- An open sketch line can be clicked, even where it bounds a region.
+- Double-click a sketch stroke to edit it — the gesture belongs on the geometry.
+- Editing a dimension's value **updates** that dimension instead of adding a second one next
+ to it.
+- The floating chrome that belongs to a sketch leaves with the sketch when it ends.
+- Sketching happens on the face you clicked, first click.
+- A sketch whose entities form no wire **fails** instead of extruding a default box. A
+ subtraction that removes nothing is reported as an error instead of a silent success.
+
+---
+
+## The offer
+
+Right-click on the geometry, released without moving the mouse (an 8 px budget — a
+right-drag that orbits the camera does not open it). Left-click still only selects, so
+pointing at things stays quiet.
+
+The offer also opens by itself the moment you press Sketch on a face or plane, showing the
+sketch tools — the app hands you the tools directly.
+
+**Eight families, always in this fixed order:** Create, Add material, Remove, Dress-up,
+Repeat, Transform, Reference, Modify.
+
+- A family with at least one applicable verb shows it. Several applicable verbs collapse
+ into a submenu under the family name.
+- A family with nothing applicable is **shown greyed in place, with the reason** — e.g.
+ *"Create — Click a face or a reference plane in the viewport, then a sketch tool"*. It is
+ not hidden. A control that cannot be used still says what it is and what you would have to
+ do first.
+- Inside a sketch the offer shows the sketch verbs; outside it shows the feature verbs.
+
+**Document-level actions never enter the offer**, because they act on the document and not
+on a selection: Import STEP, Import mesh, Text, SVG, Export STEP, Commit to Plate, Undo,
+Redo, Variables, Section view, Origin planes, World axes. They live in the toolbar.
+
+---
+
+## Keyboard
+
+Single letters drive sketch tools **while a sketch is open**; Shift+letter drives feature
+tools and single letters drive view toggles **when no sketch is open**. The two maps are
+selected by the mode, not by whether a sketch session is running.
+
+### Sketch (while a sketch is open)
+
+| Key | Tool |
+|---|---|
+| `L` | Line — click start, then end |
+| `R` | Rectangle — click two opposite corners |
+| `C` | Circle — click centre, then radius |
+| `A` | Arc — click start, end, then a point |
+| `S` | Slot — two centreline ends, then width |
+| `E` | Ellipse — centre, major end, minor point |
+| `B` | Spline — click control points |
+| `P` | Point — click to place |
+| `G` | Polygon — click centre, then a vertex |
+| `D` | Dimension — click 2 points or an entity |
+| `T` | Trim — click a segment to trim it |
+| `X` | Extend — click a line/arc to extend it |
+| `O` | Offset — pick an entity, drag the distance |
+| `M` | Mirror — pick axis, then entities |
+| `F` | Fillet — pick two lines, set the radius |
+| `H` | Chamfer — pick two lines, set the distance |
+| `K` | Constrain — finish the live sketch and enter constrain |
+| `Q` | Construction toggle — draw the next entity as construction geometry |
+| `Del` | Delete the selected sketch entity |
+| `Esc` | Cancel the live tool |
+
+### Feature (when no sketch is open)
+
+| Key | Tool |
+|---|---|
+| `Shift+S` | Sketch |
+| `Shift+E` | Extrude — extrude a profile, or push/pull a picked face |
+| `Shift+R` | Revolve |
+| `Shift+W` | Sweep |
+| `Shift+L` | Loft |
+| `Shift+N` | Pattern |
+| `Shift+G` | Surface Extrude |
+| `Shift+J` | Surface Revolve |
+| `Shift+O` | Surface Loft |
+| `Shift+Q` | Surface Fill |
+| `Shift+U` | Surface Offset |
+| `Shift+V` | Thicken Surface |
+| `Shift+P` | Plane |
+| `Shift+A` | Axis |
+| `Shift+C` | Coord Sys |
+| `Shift+Y` | Transform |
+| `Shift+Z` | Mirror |
+| `Shift+B` | Boolean |
+| `Shift+X` | Cut |
+| `Shift+F` | Fillet / Chamfer |
+| `Shift+D` | Draft |
+| `Shift+K` | Shell |
+| `Shift+H` | Hole |
+| `Shift+T` | Thread |
+| `Shift+I` | Import STEP |
+| `Shift+M` | Import mesh |
+
+### View toggles (single letters, when no sketch is open)
+
+| Key | Action |
+|---|---|
+| `P` | Origin planes on/off |
+| `A` | World axes on/off |
+| `X` | Section view on/off |
+
+While the section is on: `PageUp` / `PageDown` move the cut plane, `F` flips which half is
+kept. With no section on, `F` is Place on Face — lay the picked face flat on the bed.
---
## Sketching
A sketch is a closed (or open) 2D profile on a plane or on a flat face of an existing body.
+Press `Shift+S`, click the face or plane you want to sketch on, and draw. The toolbar and
+the offer both carry the sketch tools.
**Entities:** line, polyline, rectangle (corner / centre / oblique / rounded), circle
(centre-radius / 2-point / 3-point), arc (centre-point / 3-point / tangent), ellipse and
@@ -168,10 +292,10 @@ mesh.
## View controls
-**Section view** hides half the model so you can see inside — `PageUp`/`PageDown` move the
-plane, Flip shows the other half, `Delete` removes it. **Place on Face** (`F`) lays a picked
-face flat on the bed. The bed and its grid can be toggled off when they get in the way, and
-origin planes and world axes can be shown while you orient yourself.
+**Section view** (`X`) hides half the model so you can see inside — `PageUp`/`PageDown` move
+the plane, `F` flips which half is kept. **Place on Face** (`F`, when section is off) lays a
+picked face flat on the bed. Origin planes (`P`) and world axes (`A`) can be toggled on while
+you orient yourself.
---
@@ -182,12 +306,12 @@ Being straight about the edges, so nobody discovers them the hard way:
- **Rib** needs a sketch containing an explicit open line. A parametric rectangle sketch
carries no individual entities, so Rib cannot use one.
- **Surface Loft** and **Surface Fill** have kernel tests but have not been exercised by hand.
-- Two kernel tests are known-broken and excluded from the suite: a tangent-constraint case
- that aborts inside the vendored solver, and an internal-thread groove volume below its
- asserted threshold. Both are tracked, neither is fixed.
+- Card wiring for 9 of the 16 late-wired tools has never been click-tested.
- Mate resolves by composing transforms directly. There is no 3D assembly solver, so mates
are applied in order rather than solved simultaneously, and mate limits are not implemented.
- Move-face and replace-face are not implemented — OCCT offers no clean primitive for them.
+- There is no automated GUI test in CI. Every behaviour above is traced to code and to a
+ hand pass, not to a synthetic click.
---
diff --git a/docs/design_tab_pr_description.md b/docs/design_tab_pr_description.md
new file mode 100644
index 0000000000..941d3bdac0
--- /dev/null
+++ b/docs/design_tab_pr_description.md
@@ -0,0 +1,73 @@
+# Design (CAD) tab — upstream pull request
+
+## What this adds
+
+A sketch-first parametric CAD tab inside the slicer. The workflow is direct:
+sketch → constrain → solid features → commit to plate. The whole feature recipe is
+persisted inside the 3MF, so reopening restores an editable model rather than a frozen mesh.
+
+- Kernel: OCCT, which upstream already links for STEP import — see
+ [cad_dependency_weight.md](docs/cad_dependency_weight.md)
+- Constraint solver: vendored SolveSpace `libslvs` subset
+- Interaction model: object-driven — point at geometry, the geometry offers the verbs that
+ apply to it; see [cad_ux_guidelines.md](docs/cad_ux_guidelines.md)
+- Full user-facing documentation: [design_tab.md](docs/design_tab.md)
+
+## Why it belongs in the slicer
+
+Every round trip through an external CAD tool costs a file export, a re-import, and the
+design intent that both steps discard. A part modified after slicing should return to its
+feature history, not to a mesh. Keeping the CAD model inside the slicer preserves that
+loop — the nozzle diameter, the build volume and the material are known at design time.
+
+For the integration case in full: [design_tab_upstream_portability.md](docs/design_tab_upstream_portability.md).
+
+## How it is built
+
+The `SLIC3R_CAD` CMake flag (default ON) gates the entire tab. With it OFF the tab is not
+compiled and the deps prefix matches upstream exactly — the dependency diff is one line in
+OCCT's CMake: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`.
+
+Measured cost table: [cad_dependency_weight.md](docs/cad_dependency_weight.md).
+
+## Diff shape
+
+
+
+Against merge-base `d6cb667b894f`:
+
+ 306 files changed, 83032 insertions(+), 777 deletions(-)
+
+350 commits, of which 284 are new files and 37 modify upstream files. 99.3 % of the diff
+is new code. The negotiable surface is the 37 modified files.
+
+## Tests
+
+205 `TEST_CASE` blocks across 6 new test source files. This counts assertions written, not
+assertions passed — a run needs a build.
+
+`scripts/kernel-test.sh` is the headless verification contract: it builds only
+`libslic3r_tests` (not the GUI app), needs no display, and exit 0 means the CAD suite
+passed. It now runs with **no exclusions** — both cases that used to be quarantined (the
+circle-line tangency solver abort and the internal-thread reference) are fixed.
+
+## Licensing
+
+The vendored solver in `src/libslic3r/slvs/` is **GPL-3.0** (see `src/libslic3r/slvs/LICENSE`),
+not LGPL. The combined work is distributable under AGPL-3.0. See the Licensing section of
+[design_tab_upstream_portability.md](docs/design_tab_upstream_portability.md) for the
+AGPLv3/GPLv3 compatibility argument; this point should be confirmed with upstream explicitly.
+
+## Not verified
+
+- Card wiring for 9 of the 16 late-wired tools was never click-tested.
+- There is no automated GUI test in CI. A green kernel run says nothing about the GUI —
+ synthetic clicks never drift, so the test suite and the viewport are two separate realities.
+- The click-test defect rate has **not converged**: a second pass found no new defects, but
+ four further days of work found five more. The earlier pass is not evidence of stability.
+
+## Reviewer's map
+
+See the [Where the code lives](docs/design_tab.md#where-the-code-lives) table in the user
+doc for the file-to-role mapping, and [docs/ux/tool_atlas.json](docs/ux/tool_atlas.json) as
+the generated-from source of `src/slic3r/GUI/DesignOffer.hpp`.
diff --git a/docs/rig_build_traps.md b/docs/rig_build_traps.md
new file mode 100644
index 0000000000..365d5f9392
--- /dev/null
+++ b/docs/rig_build_traps.md
@@ -0,0 +1,123 @@
+# Rig build traps
+
+The build rig is two long-lived containers, `snaporca-gui` and `orcacad-gui`, one per fork. Each
+mounts only its fork's build volume (`snaporca_buildcache` / `orcacad_buildcache`) at
+`/OrcaSlicer/build`, its fork's `resources/`, and a shots directory — nothing else. They run the
+binary; they do not build it. Rebuild with `scripts/rig-build.sh`.
+
+| fork repo | project() | deps image | build volume | GUI container | binary |
+|---|---|---|---|---|---|
+| `snaporca` | `Snapmaker_Orca` | `snaporca-deps` | `snaporca_buildcache` | `snaporca-gui` | `snapmaker-orca` |
+| `orca_cad` | `OrcaSlicer` | `orcacad-deps` | `orcacad_buildcache` | `orcacad-gui` | `orca-slicer` |
+
+`scripts/rig-build.sh` exists alongside `scripts/docker-iter-build.sh` for one reason: it does a
+target-only `ninja` into the volume the GUI rig launches from, so a session can test a single
+change without a full repackage, whereas `docker-iter-build.sh` runs the full packaged build.
+Both start a throwaway container from the deps image with the live repo mounted over the baked
+tree — never build inside the GUI container (Trap 1).
+
+Every trap below has already cost about a session to re-derive, once each. They are recorded now
+so no fresh session pays them again. Symptoms, causes, and exact recovery commands follow.
+
+---
+
+## Trap 1 — never configure inside the GUI container
+
+**Symptom.** After building inside the GUI container, the fork's targets no longer exist; ninja
+reports an unknown target, and `orca-slicer` / `OrcaSlicer` have been replaced by
+`snapmaker-orca` / `Snapmaker_Orca`.
+
+**Cause.** The GUI image's baked `/OrcaSlicer` tree is the Jun-13 Snapmaker-derived source
+(`project(Snapmaker_Orca)`, executable `snapmaker-orca`). `orcacad-deps` is layered on
+`snaporca-deps`, so even on the mainline fork the baked tree is the other fork's. A `cmake .`
+there reconfigures the shared build dir under the wrong project name.
+
+**Fix.** Build only via `scripts/rig-build.sh`, which starts a throwaway container from the deps
+image with the live repo mounted over the baked tree — `src`, `resources`, `cmake`, `deps_src`,
+`localization`, `CMakeLists.txt`, `version.inc` — and writes into the same volume the rig
+launches from.
+
+---
+
+## Trap 2 — stale `NLopt_DIR` in CMakeCache
+
+**Symptom.** Configure fails with `Cannot find NLopt library 'nlopt_cxx' in '/lib'`.
+
+**Cause.** `cmake/modules/FindNLopt.cmake:26` is `set(NLopt_DIR $ENV{NLOPT})`. With `NLOPT`
+unset that expands to `set(NLopt_DIR)` — zero arguments — which *unsets the normal variable* and
+lets a leftover CACHE entry of the same name (e.g. `/lib/cmake/nlopt`) show through the
+following `if(NOT NLopt_DIR)`. The `else()` branch then searches for `nlopt_cxx` under
+`${NLopt_DIR}/lib` with `NO_DEFAULT_PATH`, while the deps prefix ships plain `nlopt`.
+
+**Fix.** From inside the build dir:
+
+ cmake -U NLopt_DIR -U NLopt_LIBS .
+
+Do **not** `sed` the entry out of `CMakeCache.txt` — deleting a line breaks the cache parser.
+
+---
+
+## Trap 3 — the image lacks `deps_src/pybind11`
+
+**Symptom.** Configure aborts with `pybind11 headers not found in /OrcaSlicer/deps_src/pybind11.
+Did you initialize submodules?` (the `FATAL_ERROR` guarding `PYBIND11_SOURCE_DIR` in the mainline
+fork's root `CMakeLists.txt`, near line 948).
+
+**Cause.** The deps image predates that requirement. Only the mainline (`orca_cad`) fork has
+`deps_src/pybind11` and the requirement; snaporca has neither.
+
+**Fix.** Mount `deps_src` over the baked tree — `scripts/rig-build.sh` does. Corollary: mounting a
+snaporca tree into an `orcacad-deps` build reproduces this error exactly.
+
+---
+
+## Trap 4 — `OCCT_LIBS` lags one configure
+
+**Symptom.** A wall of undefined references to `TopOpeBRepBuild` symbols. It reads as a broken
+OCCT installation. It is not.
+
+**Cause.** `src/libslic3r/CMakeLists.txt:603` does
+`set(OCCT_LIBS "${OCCT_LIBS}" CACHE INTERNAL "OCCT toolkits linked by libslic3r")` at the END of
+its own configure, while the consumer in the root `CMakeLists.txt` (`if (NOT OCCT_LIBS)` …
+`foreach (_tk IN LISTS OCCT_LIBS)`) reads whatever is already in the cache. The first reconfigure
+after the `TKFillet TKOffset` prepend (`src/libslic3r/CMakeLists.txt:599`) therefore links the
+previous list and drops `TKBool`/`TKOffset`.
+
+**Fix.** Configure twice. `scripts/rig-build.sh` runs `cmake .` twice for exactly this reason; if
+you ever configure by hand, run it twice.
+
+---
+
+## Trap 5 — `SLIC3R_CAD=ON` in the cache, macro never defined
+
+**Symptom.** The build succeeds and links, but the Design tab is simply absent — or it fails with
+`class GLCanvas3D has no member named set_design_sketch_tool`.
+
+**Cause.** The cache carries `SLIC3R_CAD=ON`, but the root `CMakeLists.txt` actually configured is
+a stale baked copy that predates the gate and never runs `add_definitions(-DSLIC3R_CAD)` (the
+gate is `if (SLIC3R_CAD)` / `add_definitions(-DSLIC3R_CAD)` in the root list — line 179/180 in
+snaporca, 319/320 in orca_cad). Every `#ifdef SLIC3R_CAD` block therefore compiles out while the
+option still reads ON.
+
+**Fix.** Always mount the live `CMakeLists.txt` and `cmake/` — never inherit them from the image.
+This is why `scripts/docker-iter-build.sh`, `scripts/kernel-test.sh` and `scripts/rig-build.sh`
+all mount both.
+
+---
+
+## The binary the rig actually launches
+
+`ninja ` writes `/OrcaSlicer/build/src/Release/`; only `build_linux.sh`
+additionally packages to `/OrcaSlicer/build/package/bin/`. `orca_cad`'s
+`scripts/gui-session.sh` defaults `BIN` to `src/Release/orca-slicer`, but snaporca's defaults to
+`package/bin/snapmaker-orca`. So after a target-only rebuild on snaporca, launching
+`gui-session.sh` with its default runs the **stale packaged** binary — the change under test is
+invisible and the session hunts a phantom. Pass `BIN` explicitly:
+
+ docker exec -e BIN=/OrcaSlicer/build/src/Release/snapmaker-orca snaporca-gui /OrcaSlicer/scripts/gui-session.sh
+
+`scripts/rig-build.sh` prints the correct line for the current fork when it finishes.
+
+Note also that the GUI containers do **not** mount `scripts/`: `/OrcaSlicer/scripts` inside them
+is the baked copy, so a local edit to `gui-session.sh` has no effect until you
+`docker cp scripts/gui-session.sh :/OrcaSlicer/scripts/`.
diff --git a/docs/ux/offer_atlas.html b/docs/ux/offer_atlas.html
index c92f694f28..35c4f5860c 100644
--- a/docs/ux/offer_atlas.html
+++ b/docs/ux/offer_atlas.html
@@ -32,7 +32,7 @@
73secondary rings
113states total
3.5/8mean slots filled
-
6verbs with no GUI yet
+
5verbs with no GUI yet
@@ -71,7 +71,7 @@
Not in the offer
Import STEP, Import mesh, Text, SVG, Export STEP, Commit to Plate, Undo, Redo, Variables, Section view, Origin planes, World axes — these act on the document, not on a selection,
so they stay in chrome. Verbs with kernel support but no GUI today, which still hold an address:
- Split, Pattern on Curve, Align to, Measure, Mass, Interference.
+ Split, Pattern on Curve, Align to, Measure, Interference.
All states
Nothing selected — Working documentNothing selected — Reference ringPlanar face — Working documentPlanar face — Add material ringPlanar face — Remove ringPlanar face — Dress-up ringPlanar face — Transform ringPlanar face — Reference ringPlanar face — Modify ringCylindrical face — Working documentCylindrical face — Reference ringCylindrical face — Modify ringCurved face — Working documentCurved face — Modify ringStraight edge — Working documentStraight edge — Dress-up ringStraight edge — Reference ringCircular edge — Working documentCircular edge — Dress-up ringVertex — Working documentVertex — Reference ringSolid body — Working documentSolid body — Remove ringSolid body — Dress-up ringSolid body — Repeat ringSolid body — Reference ringSolid body — Modify ringSheet body — Working documentSheet body — Modify ringTwo bodies — Working documentTwo bodies — Reference ringDatum plane — Working documentDatum plane — Remove ringDatum plane — Reference ringDatum plane — Modify ringDatum axis — Working documentDatum axis — Modify ringCoordinate system — Working documentCoordinate system — Modify ringText / imported art — Working documentText / imported art — Modify ringClosed sketch loop — Working documentClosed sketch loop — Add material ringClosed sketch loop — Modify ringSketch, nothing picked — Working documentSketch, nothing picked — Create ringSketch, nothing picked — Reference ringSketch line — Working documentSketch line — Create ringSketch line — Dress-up ringSketch line — Reference ringSketch line — Modify ringSketch arc or circle — Working documentSketch arc or circle — Create ringSketch arc or circle — Reference ringSketch arc or circle — Modify ringSketch point — Working documentSketch point — Create ringSketch point — Reference ringTwo sketch entities — Working documentTwo sketch entities — Create ringTwo sketch entities — Dress-up ringTwo sketch entities — Reference ringTwo sketch entities — Modify ringNothing selected — Fresh documentNothing selected — Reference ringPlanar face — Fresh documentPlanar face — Reference ringCylindrical face — Fresh documentCylindrical face — Reference ringCurved face — Fresh documentStraight edge — Fresh documentStraight edge — Reference ringCircular edge — Fresh documentVertex — Fresh documentVertex — Reference ringSolid body — Fresh documentSolid body — Modify ringSheet body — Fresh documentSheet body — Modify ringTwo bodies — Fresh documentDatum plane — Fresh documentDatum plane — Reference ringDatum plane — Modify ringDatum axis — Fresh documentDatum axis — Modify ringCoordinate system — Fresh documentCoordinate system — Modify ringText / imported art — Fresh documentText / imported art — Modify ringClosed sketch loop — Fresh documentClosed sketch loop — Add material ringClosed sketch loop — Modify ringSketch, nothing picked — Fresh documentSketch, nothing picked — Create ringSketch, nothing picked — Reference ringSketch line — Fresh documentSketch line — Create ringSketch line — Dress-up ringSketch line — Reference ringSketch line — Modify ringSketch arc or circle — Fresh documentSketch arc or circle — Create ringSketch arc or circle — Reference ringSketch arc or circle — Modify ringSketch point — Fresh documentSketch point — Create ringSketch point — Reference ringTwo sketch entities — Fresh documentTwo sketch entities — Create ringTwo sketch entities — Dress-up ringTwo sketch entities — Reference ringTwo sketch entities — Modify ring
Import STEP, Import mesh, Text, SVG, Export STEP, Commit to Plate, Undo, Redo, Variables, Section view, Origin planes, World axes — these act on the document, not on a selection,
so they stay in chrome. Verbs with kernel support but no GUI today, which still hold an address:
- Split, Pattern on Curve, Align to, Measure, Mass, Interference.
+ Split, Pattern on Curve, Align to, Measure, Interference.
The states
Fresh document, nothing selected — the first-run pictureLIST · fresh document — every family present, the unavailable ones say whyRING · the same state — an empty slot cannot say anythingLIST · planar faceRING · planar faceLIST · sketch Create submenu — all 9 primitives fit, no overflowRING · the same submenu — 2 verbs pushed behind “More”LIST · planar face, Add material submenuRING · planar face, Add material sub-ringLIST · solid bodyLIST · straight edgeLIST · sketch lineRING · Nothing selectedRING · Planar faceRING · Cylindrical faceRING · Curved faceRING · Straight edgeRING · Circular edgeRING · VertexRING · Solid bodyRING · Sheet bodyRING · Two bodiesRING · Datum planeRING · Datum axisRING · Coordinate systemRING · Text / imported artRING · Closed sketch loopRING · Sketch, nothing pickedRING · Sketch lineRING · Sketch arc or circleRING · Sketch pointRING · Two sketch entitiesPlanar face · Add material sub-ringPlanar face · Reference sub-ringSketch · Create sub-ring (the overflow case)Solid body · Remove sub-ring8 slots — planar face12 slots — planar face8 slots — sketch line12 slots — sketch line
diff --git a/docs/ux/tool_atlas.json b/docs/ux/tool_atlas.json
index 02f0960c8a..600d3eee62 100644
--- a/docs/ux/tool_atlas.json
+++ b/docs/ux/tool_atlas.json
@@ -829,8 +829,8 @@
"bodies": 1
},
"refusal": null,
- "gui": false,
- "action": null
+ "gui": true,
+ "action": "btn:mass"
},
{
"id": "interference",
@@ -871,7 +871,7 @@
"needs": {},
"refusal": null,
"gui": true,
- "action": null
+ "action": "btn:edit"
},
{
"id": "delete_face",
diff --git a/scripts/docker-iter-build.sh b/scripts/docker-iter-build.sh
index af1665c9c3..bd89e8c0b5 100755
--- a/scripts/docker-iter-build.sh
+++ b/scripts/docker-iter-build.sh
@@ -15,6 +15,7 @@
# On success the binary is inside the persistent volume at
# /OrcaSlicer/build/package/bin/orca-slicer (copy it out with a follow-up
# `docker run --rm -v orcacad_buildcache:/b alpine cp ...` or via this script's tail).
+# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): docs/rig_build_traps.md
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
diff --git a/scripts/kernel-test.sh b/scripts/kernel-test.sh
index 4176c620fc..7c2d2873f6 100755
--- a/scripts/kernel-test.sh
+++ b/scripts/kernel-test.sh
@@ -19,6 +19,7 @@
# working tree to a per-volume staging dir on that host and re-runs this same script
# there, so the verification contract is identical either way. Drop --host once the image
# is present locally.
+# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): docs/rig_build_traps.md
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
diff --git a/scripts/rig-build.sh b/scripts/rig-build.sh
new file mode 100755
index 0000000000..352c82d6bb
--- /dev/null
+++ b/scripts/rig-build.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+# Rebuild the GUI binary the design rig launches — in a THROWAWAY container, writing into the
+# same build-cache volume the rig's long-lived GUI container reads from.
+#
+# NEVER build inside the GUI container (snaporca-gui / orcacad-gui). Its baked /OrcaSlicer tree
+# is the Jun-13 Snapmaker-derived source, so a `cmake .` in there silently reconfigures the
+# shared build dir as project(Snapmaker_Orca) and this fork's targets vanish. That is Trap 1 of
+# five; all of them, with symptoms and exact recovery commands, are in docs/rig_build_traps.md.
+# Read that file before debugging a configure or link failure this script reports.
+#
+# Usage:
+# scripts/rig-build.sh # configure + build the fork's GUI target
+# DRY_RUN=1 scripts/rig-build.sh # print the resolved fork identity and exit, no container
+set -euo pipefail
+
+REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+# Fork identity is DERIVED from the repo, never hardcoded, so this file is byte-identical in
+# both forks and cannot be mirrored into the wrong one. Pointing a fork at the other fork's
+# image or volume is not a slow failure: with the wrong image CMake dies at configure, and with
+# the wrong volume the two forks silently trade build artefacts.
+PROJECT="$(sed -n 's/^project(\([A-Za-z_0-9]*\)).*/\1/p' "$REPO/CMakeLists.txt" | head -1)"
+case "$PROJECT" in
+ Snapmaker_Orca) PREFIX=snaporca; BIN=snapmaker-orca ;;
+ OrcaSlicer) PREFIX=orcacad; BIN=orca-slicer ;;
+ *) echo "FATAL: unrecognised project($PROJECT) in $REPO/CMakeLists.txt" >&2; exit 2 ;;
+esac
+TARGET="$PROJECT"
+IMAGE="${PREFIX}-deps"
+BUILD_VOL="${PREFIX}_buildcache"
+
+echo "REPO=$REPO PROJECT=$PROJECT IMAGE=$IMAGE BUILD_VOL=$BUILD_VOL TARGET=$TARGET BIN=$BIN"
+
+if [ -n "${DRY_RUN:-}" ]; then
+ echo "DRY_RUN: resolution only, no container started"
+ exit 0
+fi
+
+# Every one of these mounts covers a trap, none is decorative:
+# CMakeLists.txt + cmake/ carry the SLIC3R_CAD gate — inherit the baked copies and the cache
+# says SLIC3R_CAD=ON while -DSLIC3R_CAD is never defined, so every #ifdef block compiles out.
+# deps_src/ carries pybind11, which the image predates.
+# src/, resources/, localization/, version.inc are the code under test.
+rc=0
+docker run --rm \
+ -v "$REPO/src":/OrcaSlicer/src \
+ -v "$REPO/resources":/OrcaSlicer/resources \
+ -v "$REPO/cmake":/OrcaSlicer/cmake \
+ -v "$REPO/deps_src":/OrcaSlicer/deps_src \
+ -v "$REPO/localization":/OrcaSlicer/localization \
+ -v "$REPO/CMakeLists.txt":/OrcaSlicer/CMakeLists.txt \
+ -v "$REPO/version.inc":/OrcaSlicer/version.inc \
+ -v "$BUILD_VOL":/OrcaSlicer/build \
+ "$IMAGE" bash -lc "
+ cd /OrcaSlicer/build || exit 1
+ cmake . > /tmp/cfg.log 2>&1 || { echo 'CONFIGURE FAILED'; tail -25 /tmp/cfg.log; exit 1; }
+ # Twice, deliberately. src/libslic3r/CMakeLists.txt publishes OCCT_LIBS as CACHE INTERNAL
+ # at the END of its own configure, so a first pass after that list changes links the
+ # PREVIOUS one and drops TKBool/TKOffset — a wall of TopOpeBRepBuild undefined references
+ # that reads as a broken OCCT install and is not. Trap 4.
+ cmake . > /tmp/cfg2.log 2>&1 || { echo 'RECONFIGURE FAILED'; tail -25 /tmp/cfg2.log; exit 1; }
+ ninja -f build-Release.ninja -j\"$(nproc)\" $TARGET > /tmp/bld.log 2>&1
+ rc=\$?
+ echo \"EXIT=\$rc\"
+ grep -n 'error:' /tmp/bld.log | head -20
+ tail -4 /tmp/bld.log
+ ls -la /OrcaSlicer/build/src/Release/$BIN 2>/dev/null
+ exit \$rc
+ " || rc=$?
+
+# A target-only build writes src/Release/, but this fork's gui-session.sh may default BIN to the
+# PACKAGED path that only build_linux.sh refreshes — launching with the default would then run a
+# stale binary. Pass BIN explicitly. See docs/rig_build_traps.md.
+echo "=== launch the rig on the binary just built ==="
+echo " docker exec -e BIN=/OrcaSlicer/build/src/Release/$BIN ${PREFIX}-gui /OrcaSlicer/scripts/gui-session.sh"
+exit "$rc"
diff --git a/src/slic3r/GUI/DesignCanvas.hpp b/src/slic3r/GUI/DesignCanvas.hpp
index 21fefa5a21..bc6f1a6b8b 100644
--- a/src/slic3r/GUI/DesignCanvas.hpp
+++ b/src/slic3r/GUI/DesignCanvas.hpp
@@ -51,7 +51,7 @@ 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 the Plane dropdown changes
+ 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);
void set_sketch_polygon_sides(int n);
void set_sketch_polygon_circumscribed(bool c);
diff --git a/src/slic3r/GUI/DesignOffer.hpp b/src/slic3r/GUI/DesignOffer.hpp
index af65b7710e..a0155a91f7 100644
--- a/src/slic3r/GUI/DesignOffer.hpp
+++ b/src/slic3r/GUI/DesignOffer.hpp
@@ -108,9 +108,9 @@ static const OfferVerb kOfferVerbs[] = {
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false},
{"project", "Project", 6, nullptr, "fly:plane#4", "Project needs a body — add or import one first", 0x00000482u, 1, 0, false, false},
{"measure", "Measure", 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false},
- {"mass_props", "Mass", 6, nullptr, nullptr, nullptr, 0x00000080u, 1, 0, false, false},
+ {"mass_props", "Mass", 6, nullptr, "btn:mass", nullptr, 0x00000080u, 1, 0, false, false},
{"interference", "Interference", 6, nullptr, nullptr, nullptr, 0x00000200u, 2, 0, false, false},
- {"edit_feature", "Edit", 7, nullptr, nullptr, nullptr, 0x00007d8eu, 0, 0, false, false},
+ {"edit_feature", "Edit", 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false},
{"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false},
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x00000180u, 1, 0, false, false},
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7f80u, 0, 0, false, false},
diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp
index 4f5061c221..3c9cab6bc8 100644
--- a/src/slic3r/GUI/DesignPanel.cpp
+++ b/src/slic3r/GUI/DesignPanel.cpp
@@ -375,7 +375,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners"));
sk_key('C', DesignSketchTool::Mode::CenterCircle, _L("Circle — click center, then radius"));
sk_key('A', DesignSketchTool::Mode::ThreePointArc,_L("Arc — click start, end, then a point"));
- sk_key('S', DesignSketchTool::Mode::Slot, _L("Slot — two centerline ends, then width"));
+ sk_key('S', DesignSketchTool::Mode::Slot, _L("Slot — two centerline ends, then end radius"));
sk_key('E', DesignSketchTool::Mode::Ellipse, _L("Ellipse — center, major end, minor point"));
sk_key('B', DesignSketchTool::Mode::BSpline, _L("Spline — click control points"));
sk_key('P', DesignSketchTool::Mode::Point, _L("Point — click to place"));
@@ -533,7 +533,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_status->Refresh();
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("Drawing on %s.\nPick a tool, or right-click for the list."), where)
+ ? wxString::Format(_L("Drawing on %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();
@@ -542,7 +542,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// 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(wxGetMousePosition()); });
+ CallAfter([this] { show_offer_menu(offer_anchor()); });
};
b_sketch->Bind(wxEVT_BUTTON, [act_sketch](wxCommandEvent&) { act_sketch(); });
m_keys_feature[SHIFT('S')] = act_sketch;
@@ -898,6 +898,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
fadd("color", b_color);
m_verb_actions["btn:colour"] = [this] { on_set_body_color(); };
m_verb_actions["btn:delete"] = [this] { on_delete_feature(); };
+ m_verb_actions["btn:edit"] = [this] { on_edit_feature(); };
+ m_verb_actions["btn:mass"] = [this] { on_mass_properties(); };
// Dress-up: finishing operations on the faces and edges of an existing solid — nothing
// that moves a body (see the Placement drawer) and nothing that creates geometry.
@@ -1132,7 +1134,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
{"design_tangentarc", DesignSketchTool::Mode::TangentArc, _L("Tangent arc"), _L("Click start (on the last entity) then end")},
{"design_arc_center", DesignSketchTool::Mode::CenterArc, _L("Center-point arc"), _L("Click center, then start, then a point for the end angle")} });
dropdown("design_slot", _L("Slot"), {
- {"design_slot", DesignSketchTool::Mode::Slot, _L("Slot"), _L("Click two centerline ends, then a point for width")},
+ {"design_slot", DesignSketchTool::Mode::Slot, _L("Slot"), _L("Click two centerline ends, then a point for the end radius")},
{"design_slot_arc", DesignSketchTool::Mode::ArcSlot, _L("Arc slot"), _L("Click center, start, end, then a point for the width")} });
dropdown("design_ellipse", _L("Ellipse"), {
{"design_ellipse", DesignSketchTool::Mode::Ellipse, _L("Ellipse"), _L("Click center, a major-axis end, then a point for the minor axis")},
@@ -1419,13 +1421,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Shape")), 0, wxALIGN_CENTER_VERTICAL);
form->Add(m_shape, 0, wxEXPAND);
- m_plane = make_combo(m_cards);
- m_plane->Append(_L("XY"));
- m_plane->Append(_L("XZ"));
- m_plane->Append(_L("YZ"));
- m_plane->SetSelection(0);
- form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
- form->Add(m_plane, 0, wxEXPAND);
+ // NO plane row. A sketch takes its plane from what is picked in the VIEWPORT — a planar face
+ // on a solid, or one of the reference-plane ghosts clicked in 3D — resolved by
+ // sketch_plane_from_selection(). A three-row XY/XZ/YZ combo could not express either of those
+ // targets, so it displayed a value that was at best redundant and at worst false. snaporca-e1p.
m_width = make_spin(m_cards, 20);
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Width / X")), 0, wxALIGN_CENTER_VERTICAL);
@@ -3473,6 +3472,20 @@ DesignPanel::DesignPanel(wxWindow* parent)
const bool dismissable = m_active != Tool::None || (m_viewport && m_viewport->moving_body());
if (key == WXK_ESCAPE && dismissable) { tool_cancel(); return; }
+ // The offer from the keyboard (charter 4.1): the Menu key, or Shift+F10 for keyboards that
+ // do not have one. Same menu the right-click opens — show_offer_menu already decides which
+ // half of the map applies via sketch_map_applies(), so nothing about the content is decided
+ // here. With no press to anchor it, offer_anchor() puts it on the viewport.
+ // WXK_MENU is the GTK code for the physical Menu key (GDK_KEY_Menu); wxMSW instead sends
+ // WXK_WINDOWS_MENU for VK_APPS and maps Alt to WXK_ALT, so accepting both is safe
+ // everywhere. CallAfter because the menu is modal: let the key event finish dispatching
+ // before a nested loop takes the queue, the same reason the Sketch entry does it.
+ if (!in_text && !ctrl
+ && (key == WXK_MENU || key == WXK_WINDOWS_MENU || (key == WXK_F10 && e.ShiftDown()))) {
+ CallAfter([this] { show_offer_menu(offer_anchor()); });
+ return;
+ }
+
// Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y — undo/redo handled here (not only in the GL canvas) so
// it works even when the canvas lost keyboard focus. In a sketch, undo drops the last entity.
if (!in_text && ctrl && (key == 'Z' || key == 'z' || key == WXK_CONTROL_Z ||
@@ -4089,13 +4102,14 @@ void DesignPanel::on_add_sketch()
{
SketchShape shape = (m_shape->GetSelection() == 1) ? SketchShape::Circle
: SketchShape::Rectangle;
- SketchPlane plane = plane_from_choice(m_plane->GetSelection());
+ wxString where; // named for the status line
+ SketchPlane plane = sketch_plane_from_selection(where); // picked face, else the 3D plane click
m_feature_counter++;
m_doc.add_sketch(shape, plane, m_width->GetValue(), m_height->GetValue(),
m_radius->GetValue(), "Sketch" + std::to_string(m_feature_counter));
m_doc.recompute(); // a lone sketch yields an empty body; that is expected
m_status->SetForegroundColour(wxNullColour);
- m_status->SetLabel(_L("Sketch added — select it and Extrude"));
+ m_status->SetLabel(wxString::Format(_L("Sketch added on %s — select it and Extrude"), where));
refresh_tree();
}
@@ -4712,6 +4726,31 @@ void DesignPanel::on_check_interference()
wxMessageBox(msg, _L("Interference"), wxOK, this);
}
+// Mass properties of the selected solid. A report, not a feature: it never checkpoints, never
+// recomputes and never opens a card, which is why it sits beside the interference check rather
+// than in the on_add_* family. The caller only reaches us with m_sel_solid_body in range.
+void DesignPanel::on_mass_properties()
+{
+ const auto mp = GeometryEngine::mass_properties(m_doc.bodies[m_sel_solid_body].shape);
+ if (!mp.valid) {
+ m_status->SetForegroundColour(wxColour(235, 110, 110));
+ m_status->SetLabel(_L("Mass properties could not be computed for this body"));
+ m_status->Refresh();
+ return;
+ }
+ // 1-based, and the body's own name when it has one — the same wording the parts list uses.
+ wxString name = wxString::Format(_L("Body %d"), m_sel_solid_body + 1);
+ if (!m_doc.bodies[m_sel_solid_body].name.empty())
+ name = wxString::FromUTF8(m_doc.bodies[m_sel_solid_body].name);
+ m_status->SetForegroundColour(wxNullColour);
+ m_status->SetLabel(wxString::Format(_L("%s: %.3f cm³, %.2f cm²"),
+ name, mp.volume / 1000.0, mp.surface_area / 100.0));
+ m_status->Refresh();
+ wxMessageBox(wxString::Format(_L("%s\n\nVolume: %.3f cm³\nSurface area: %.2f cm²"),
+ name, mp.volume / 1000.0, mp.surface_area / 100.0),
+ _L("Mass properties"), wxOK, this);
+}
+
// The rows are only the SHEET bodies, so a row index is NOT a body index — with a solid at 0
// and a sheet at 1 the single row is row 0 but body 1. Every caller must therefore read the
// real body index out of the client data (3-arg Append; the 2-arg form takes a bitmap), never
@@ -5016,6 +5055,17 @@ void DesignPanel::run_offer_action(const char* action)
it->second();
}
+wxPoint DesignPanel::offer_anchor() const
+{
+ const wxPoint mouse = wxGetMousePosition();
+ if (!m_viewport)
+ return mouse;
+ const wxRect r = m_viewport->GetScreenRect();
+ if (r.Contains(mouse))
+ return mouse;
+ return wxPoint(r.x + r.width / 2, r.y + r.height / 2);
+}
+
void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
{
const int kind = offer_selection_kind();
@@ -7167,7 +7217,6 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
switch (f.type) {
case CadFeatureType::Sketch:
m_shape->SetSelection(f.shape == SketchShape::Circle ? 1 : 0);
- m_plane->SetSelection(index_from_plane(f.plane));
m_width->SetValue(f.width);
m_height->SetValue(f.height);
m_radius->SetValue(f.radius);
@@ -7770,7 +7819,11 @@ CadFeature DesignPanel::build_candidate(Tool t) const
case Tool::Sketch:
f.type = CadFeatureType::Sketch;
f.shape = (m_shape->GetSelection() == 0) ? SketchShape::Rectangle : SketchShape::Circle;
- f.plane = plane_from_choice(m_plane->GetSelection());
+ // The plane is STRUCTURAL, like Extrude's profile source. While EDITING it is preserved
+ // from the seeded original — the card carries no plane control and the old combo silently
+ // collapsed a face plane to a base plane through the modeling origin. While ADDING it
+ // comes from what is picked in the viewport. snaporca-e1p.
+ if (!editing) { wxString where; f.plane = sketch_plane_from_selection(where); }
f.width = m_width->GetValue();
f.height = m_height->GetValue();
f.radius = m_radius->GetValue();
diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp
index bdaf60f2c5..d4fd56c77f 100644
--- a/src/slic3r/GUI/DesignPanel.hpp
+++ b/src/slic3r/GUI/DesignPanel.hpp
@@ -127,6 +127,7 @@ private:
void on_add_helix();
void on_add_mate();
void on_check_interference();
+ void on_mass_properties(); // read-only report on the selected solid; edits nothing
// Fill m_bool_target / m_bool_tool / m_cut_target. as_of_feature < 0 = current bodies (add);
// >= 0 = the bodies as they existed just before that feature index (Boolean re-edit, so a
// consumed tool body still appears and its saved selection round-trips).
@@ -405,7 +406,6 @@ private:
// from "nobody has chosen anything yet". This does.
bool m_plane_picked{false};
ComboBox* m_shape{nullptr};
- ComboBox* m_plane{nullptr};
ComboBox* m_mode{nullptr};
wxSpinCtrlDouble* m_width{nullptr};
wxSpinCtrlDouble* m_height{nullptr};
@@ -617,6 +617,11 @@ private:
// the verbs that have no keyboard shortcut to route through.
std::map> m_verb_actions;
void show_offer_menu(const wxPoint& screen_pos);
+ // Where the offer opens when no mouse press anchors it: the keyboard route, and the automatic
+ // open on entering Sketch. The pointer if it is over the viewport, else the viewport's centre.
+ // A raw wxGetMousePosition() can be sitting on the toolbar, on the card column or on another
+ // monitor, and the menu would map there — detached from the geometry it is about.
+ wxPoint offer_anchor() const;
int offer_selection_kind() const; // an OfferSel, as int to keep the header light
// Does the SKETCH half of the map apply? A mode question, not a session one: begin_sketch
// does not run until the first tool is armed, so between "press Sketch" and "pick a tool"
diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp
index 65d1e91212..be217fc934 100644
--- a/src/slic3r/GUI/DesignSketchTool.cpp
+++ b/src/slic3r/GUI/DesignSketchTool.cpp
@@ -1256,7 +1256,7 @@ void DesignSketchTool::open_primary_autoedit()
const double Lc = d.norm();
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
m_autoedit_dims.push_back({ m_live_slot_len_label, Lc, [this, fi](double v){ const Feature& g = m_features[fi]; set_slot(fi, v, g.param); }, span(fi), "Length" });
- m_autoedit_dims.push_back({ m_live_slot_w_label, f.param, [this, fi](double v){ const Feature& g = m_features[fi]; set_slot(fi, (g.c1-g.c0).norm(), std::max(1e-3, v)); }, span(fi), "Width" });
+ m_autoedit_dims.push_back({ m_live_slot_w_label, f.param, [this, fi](double v){ const Feature& g = m_features[fi]; set_slot(fi, (g.c1-g.c0).norm(), std::max(1e-3, v)); }, span(fi), "Radius" });
m_autoedit_dims.push_back({ m_live_slot_angle_label, deg, [this, fi](double v){ set_slot_angle(fi, v); }, span(fi), "Angle" });
}
if (m_live_arc_ei >= 0) { // arc Radius is already a scalar step above; add its sweep angle
@@ -1838,7 +1838,7 @@ void DesignSketchTool::open_slot_editor(int fi, int which)
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
const double v = (which == 0) ? d.norm() : (which == 1) ? f.param : deg;
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
- on_inline_edit(px, v, which == 0 ? "Length" : which == 1 ? "Width" : "Angle",
+ on_inline_edit(px, v, which == 0 ? "Length" : which == 1 ? "Radius" : "Angle",
[this, fi, which](double nv) {
const Feature& g = m_features[fi];
if (which == 0) set_slot(fi, nv, g.param);
diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp
index cd8dc8ef59..ed65970c8b 100644
--- a/src/slic3r/GUI/DesignSketchTool.hpp
+++ b/src/slic3r/GUI/DesignSketchTool.hpp
@@ -80,7 +80,7 @@ public:
const std::vector& constraints,
const SketchPlane& plane);
void set_tool(Mode mode); // switch tool, keep accumulated entities
- void set_plane(const SketchPlane& plane) { m_plane = plane; } // re-plane a live sketch (Plane dropdown changed mid-session); entities are 2D, re-lifted through the new plane
+ void set_plane(const SketchPlane& plane) { m_plane = plane; } // re-plane a live sketch (a reference plane was clicked mid-session); entities are 2D, re-lifted through the new plane
void set_construction(bool c) { m_construction = c; }
void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); }
void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; }