Design: slot Radius caption, keyboard offer, plane combo removal, mass props, docs

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
This commit is contained in:
Tommaso Bianchi
2026-08-01 06:21:08 +02:00
co-authored by Claude Opus 5
parent 6e9910303b
commit fd1bc092d8
15 changed files with 499 additions and 43 deletions
+138 -14
View File
@@ -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 <face> — 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.
---
+73
View File
@@ -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
<!-- fork-specific: measured against this fork's upstream base; re-run the commands above after mirroring -->
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`.
+123
View File
@@ -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 '<prefix>/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. `<prefix>/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 <target>` writes `/OrcaSlicer/build/src/Release/<binary>`; only `build_linux.sh`
additionally packages to `/OrcaSlicer/build/package/bin/<binary>`. `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 <container>:/OrcaSlicer/scripts/`.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -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",
+1
View File
@@ -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)"
+1
View File
@@ -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)"
+76
View File
@@ -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"
+1 -1
View File
@@ -51,7 +51,7 @@ public:
const std::vector<SketchEntityConstraintDef>& 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);
+2 -2
View File
@@ -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},
+68 -15
View File
@@ -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();
+6 -1
View File
@@ -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<std::string, std::function<void()>> 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"
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -80,7 +80,7 @@ public:
const std::vector<SketchEntityConstraintDef>& 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; }