mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 10:51:22 +00:00
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:
co-authored by
Claude Opus 5
parent
6e9910303b
commit
fd1bc092d8
+138
-14
@@ -1,8 +1,9 @@
|
|||||||
# The Design tab
|
# The Design tab
|
||||||
|
|
||||||
A parametric CAD modeller inside the slicer. Draw a sketch, turn it into a solid, refine it,
|
Object-driven parametric CAD inside the slicer. Point at geometry; the geometry offers the
|
||||||
and send it straight to Prepare — without leaving for another application and coming back
|
verbs that apply to it. Selection comes first and the tool consumes it. Draw a sketch,
|
||||||
through an STL.
|
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
|
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
|
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
|
## Getting started
|
||||||
|
|
||||||
1. Open the **Design** tab.
|
1. Open the **Design** tab.
|
||||||
2. Pick a plane — XY, XZ or YZ — and press **Sketch**.
|
2. Click a face or a reference plane in the viewport, then press `Shift+S` (Sketch). The offer
|
||||||
3. Draw a closed profile, then press **✓** to finish the sketch.
|
opens with the sketch tools on it.
|
||||||
4. Select the sketch and press **Extrude** (`Shift+E`).
|
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.
|
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
|
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
|
## Sketching
|
||||||
|
|
||||||
A sketch is a closed (or open) 2D profile on a plane or on a flat face of an existing body.
|
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
|
**Entities:** line, polyline, rectangle (corner / centre / oblique / rounded), circle
|
||||||
(centre-radius / 2-point / 3-point), arc (centre-point / 3-point / tangent), ellipse and
|
(centre-radius / 2-point / 3-point), arc (centre-point / 3-point / tangent), ellipse and
|
||||||
@@ -168,10 +292,10 @@ mesh.
|
|||||||
|
|
||||||
## View controls
|
## View controls
|
||||||
|
|
||||||
**Section view** hides half the model so you can see inside — `PageUp`/`PageDown` move the
|
**Section view** (`X`) hides half the model so you can see inside — `PageUp`/`PageDown` move
|
||||||
plane, Flip shows the other half, `Delete` removes it. **Place on Face** (`F`) lays a picked
|
the plane, `F` flips which half is kept. **Place on Face** (`F`, when section is off) lays a
|
||||||
face flat on the bed. The bed and its grid can be toggled off when they get in the way, and
|
picked face flat on the bed. Origin planes (`P`) and world axes (`A`) can be toggled on while
|
||||||
origin planes and world axes can be shown while you orient yourself.
|
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
|
- **Rib** needs a sketch containing an explicit open line. A parametric rectangle sketch
|
||||||
carries no individual entities, so Rib cannot use one.
|
carries no individual entities, so Rib cannot use one.
|
||||||
- **Surface Loft** and **Surface Fill** have kernel tests but have not been exercised by hand.
|
- **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
|
- Card wiring for 9 of the 16 late-wired tools has never been click-tested.
|
||||||
that aborts inside the vendored solver, and an internal-thread groove volume below its
|
|
||||||
asserted threshold. Both are tracked, neither is fixed.
|
|
||||||
- Mate resolves by composing transforms directly. There is no 3D assembly solver, so mates
|
- 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.
|
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.
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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
@@ -829,8 +829,8 @@
|
|||||||
"bodies": 1
|
"bodies": 1
|
||||||
},
|
},
|
||||||
"refusal": null,
|
"refusal": null,
|
||||||
"gui": false,
|
"gui": true,
|
||||||
"action": null
|
"action": "btn:mass"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "interference",
|
"id": "interference",
|
||||||
@@ -871,7 +871,7 @@
|
|||||||
"needs": {},
|
"needs": {},
|
||||||
"refusal": null,
|
"refusal": null,
|
||||||
"gui": true,
|
"gui": true,
|
||||||
"action": null
|
"action": "btn:edit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "delete_face",
|
"id": "delete_face",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
# On success the binary is inside the persistent volume at
|
# On success the binary is inside the persistent volume at
|
||||||
# /OrcaSlicer/build/package/bin/orca-slicer (copy it out with a follow-up
|
# /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).
|
# `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
|
set -euo pipefail
|
||||||
|
|
||||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
# working tree to a per-volume staging dir on that host and re-runs this same script
|
# 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
|
# there, so the verification contract is identical either way. Drop --host once the image
|
||||||
# is present locally.
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|||||||
Executable
+76
@@ -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"
|
||||||
@@ -51,7 +51,7 @@ public:
|
|||||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||||
const SketchPlane& plane);
|
const SketchPlane& plane);
|
||||||
void set_sketch_tool(DesignSketchTool::Mode mode);
|
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_construction(bool c);
|
||||||
void set_sketch_polygon_sides(int n);
|
void set_sketch_polygon_sides(int n);
|
||||||
void set_sketch_polygon_circumscribed(bool c);
|
void set_sketch_polygon_circumscribed(bool c);
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ static const OfferVerb kOfferVerbs[] = {
|
|||||||
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x00000180u, 1, 0, false, false},
|
||||||
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7f80u, 0, 0, false, false},
|
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7f80u, 0, 0, false, false},
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
|||||||
sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners"));
|
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('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('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('E', DesignSketchTool::Mode::Ellipse, _L("Ellipse — center, major end, minor point"));
|
||||||
sk_key('B', DesignSketchTool::Mode::BSpline, _L("Spline — click control points"));
|
sk_key('B', DesignSketchTool::Mode::BSpline, _L("Spline — click control points"));
|
||||||
sk_key('P', DesignSketchTool::Mode::Point, _L("Point — click to place"));
|
sk_key('P', DesignSketchTool::Mode::Point, _L("Point — click to place"));
|
||||||
@@ -533,7 +533,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
|||||||
m_status->Refresh();
|
m_status->Refresh();
|
||||||
if (m_sketch_hint) { // the card must agree with the status line, not argue with it
|
if (m_sketch_hint) { // the card must agree with the status line, not argue with it
|
||||||
m_sketch_hint->SetLabel(have_plane
|
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."));
|
: _L("Click a face or a reference plane, then a sketch tool."));
|
||||||
m_sketch_hint->Refresh();
|
m_sketch_hint->Refresh();
|
||||||
m_cards->Layout();
|
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
|
// 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.
|
// tool's shortcut, so pressing the key instead of picking a row costs nothing.
|
||||||
if (have_plane)
|
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(); });
|
b_sketch->Bind(wxEVT_BUTTON, [act_sketch](wxCommandEvent&) { act_sketch(); });
|
||||||
m_keys_feature[SHIFT('S')] = act_sketch;
|
m_keys_feature[SHIFT('S')] = act_sketch;
|
||||||
@@ -898,6 +898,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
|||||||
fadd("color", b_color);
|
fadd("color", b_color);
|
||||||
m_verb_actions["btn:colour"] = [this] { on_set_body_color(); };
|
m_verb_actions["btn:colour"] = [this] { on_set_body_color(); };
|
||||||
m_verb_actions["btn:delete"] = [this] { on_delete_feature(); };
|
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
|
// 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.
|
// 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_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")} });
|
{"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"), {
|
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")} });
|
{"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"), {
|
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")},
|
{"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(new wxStaticText(m_cards, wxID_ANY, _L("Shape")), 0, wxALIGN_CENTER_VERTICAL);
|
||||||
form->Add(m_shape, 0, wxEXPAND);
|
form->Add(m_shape, 0, wxEXPAND);
|
||||||
|
|
||||||
m_plane = make_combo(m_cards);
|
// NO plane row. A sketch takes its plane from what is picked in the VIEWPORT — a planar face
|
||||||
m_plane->Append(_L("XY"));
|
// on a solid, or one of the reference-plane ghosts clicked in 3D — resolved by
|
||||||
m_plane->Append(_L("XZ"));
|
// sketch_plane_from_selection(). A three-row XY/XZ/YZ combo could not express either of those
|
||||||
m_plane->Append(_L("YZ"));
|
// targets, so it displayed a value that was at best redundant and at worst false. snaporca-e1p.
|
||||||
m_plane->SetSelection(0);
|
|
||||||
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
|
|
||||||
form->Add(m_plane, 0, wxEXPAND);
|
|
||||||
|
|
||||||
m_width = make_spin(m_cards, 20);
|
m_width = make_spin(m_cards, 20);
|
||||||
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Width / X")), 0, wxALIGN_CENTER_VERTICAL);
|
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());
|
const bool dismissable = m_active != Tool::None || (m_viewport && m_viewport->moving_body());
|
||||||
if (key == WXK_ESCAPE && dismissable) { tool_cancel(); return; }
|
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
|
// 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.
|
// 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 ||
|
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 shape = (m_shape->GetSelection() == 1) ? SketchShape::Circle
|
||||||
: SketchShape::Rectangle;
|
: 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_feature_counter++;
|
||||||
m_doc.add_sketch(shape, plane, m_width->GetValue(), m_height->GetValue(),
|
m_doc.add_sketch(shape, plane, m_width->GetValue(), m_height->GetValue(),
|
||||||
m_radius->GetValue(), "Sketch" + std::to_string(m_feature_counter));
|
m_radius->GetValue(), "Sketch" + std::to_string(m_feature_counter));
|
||||||
m_doc.recompute(); // a lone sketch yields an empty body; that is expected
|
m_doc.recompute(); // a lone sketch yields an empty body; that is expected
|
||||||
m_status->SetForegroundColour(wxNullColour);
|
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();
|
refresh_tree();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4712,6 +4726,31 @@ void DesignPanel::on_check_interference()
|
|||||||
wxMessageBox(msg, _L("Interference"), wxOK, this);
|
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
|
// 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
|
// 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
|
// 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();
|
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)
|
void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||||
{
|
{
|
||||||
const int kind = offer_selection_kind();
|
const int kind = offer_selection_kind();
|
||||||
@@ -7167,7 +7217,6 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
|
|||||||
switch (f.type) {
|
switch (f.type) {
|
||||||
case CadFeatureType::Sketch:
|
case CadFeatureType::Sketch:
|
||||||
m_shape->SetSelection(f.shape == SketchShape::Circle ? 1 : 0);
|
m_shape->SetSelection(f.shape == SketchShape::Circle ? 1 : 0);
|
||||||
m_plane->SetSelection(index_from_plane(f.plane));
|
|
||||||
m_width->SetValue(f.width);
|
m_width->SetValue(f.width);
|
||||||
m_height->SetValue(f.height);
|
m_height->SetValue(f.height);
|
||||||
m_radius->SetValue(f.radius);
|
m_radius->SetValue(f.radius);
|
||||||
@@ -7770,7 +7819,11 @@ CadFeature DesignPanel::build_candidate(Tool t) const
|
|||||||
case Tool::Sketch:
|
case Tool::Sketch:
|
||||||
f.type = CadFeatureType::Sketch;
|
f.type = CadFeatureType::Sketch;
|
||||||
f.shape = (m_shape->GetSelection() == 0) ? SketchShape::Rectangle : SketchShape::Circle;
|
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.width = m_width->GetValue();
|
||||||
f.height = m_height->GetValue();
|
f.height = m_height->GetValue();
|
||||||
f.radius = m_radius->GetValue();
|
f.radius = m_radius->GetValue();
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ private:
|
|||||||
void on_add_helix();
|
void on_add_helix();
|
||||||
void on_add_mate();
|
void on_add_mate();
|
||||||
void on_check_interference();
|
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);
|
// 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
|
// >= 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).
|
// consumed tool body still appears and its saved selection round-trips).
|
||||||
@@ -405,7 +406,6 @@ private:
|
|||||||
// from "nobody has chosen anything yet". This does.
|
// from "nobody has chosen anything yet". This does.
|
||||||
bool m_plane_picked{false};
|
bool m_plane_picked{false};
|
||||||
ComboBox* m_shape{nullptr};
|
ComboBox* m_shape{nullptr};
|
||||||
ComboBox* m_plane{nullptr};
|
|
||||||
ComboBox* m_mode{nullptr};
|
ComboBox* m_mode{nullptr};
|
||||||
wxSpinCtrlDouble* m_width{nullptr};
|
wxSpinCtrlDouble* m_width{nullptr};
|
||||||
wxSpinCtrlDouble* m_height{nullptr};
|
wxSpinCtrlDouble* m_height{nullptr};
|
||||||
@@ -617,6 +617,11 @@ private:
|
|||||||
// the verbs that have no keyboard shortcut to route through.
|
// the verbs that have no keyboard shortcut to route through.
|
||||||
std::map<std::string, std::function<void()>> m_verb_actions;
|
std::map<std::string, std::function<void()>> m_verb_actions;
|
||||||
void show_offer_menu(const wxPoint& screen_pos);
|
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
|
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 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"
|
// does not run until the first tool is armed, so between "press Sketch" and "pick a tool"
|
||||||
|
|||||||
@@ -1256,7 +1256,7 @@ void DesignSketchTool::open_primary_autoedit()
|
|||||||
const double Lc = d.norm();
|
const double Lc = d.norm();
|
||||||
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
|
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_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" });
|
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
|
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;
|
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 double v = (which == 0) ? d.norm() : (which == 1) ? f.param : deg;
|
||||||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
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) {
|
[this, fi, which](double nv) {
|
||||||
const Feature& g = m_features[fi];
|
const Feature& g = m_features[fi];
|
||||||
if (which == 0) set_slot(fi, nv, g.param);
|
if (which == 0) set_slot(fi, nv, g.param);
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public:
|
|||||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||||
const SketchPlane& plane);
|
const SketchPlane& plane);
|
||||||
void set_tool(Mode mode); // switch tool, keep accumulated entities
|
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_construction(bool c) { m_construction = c; }
|
||||||
void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); }
|
void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); }
|
||||||
void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; }
|
void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; }
|
||||||
|
|||||||
Reference in New Issue
Block a user